<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Linux 網(wǎng)絡(luò)子系統(tǒng)

          共 32570字,需瀏覽 66分鐘

           ·

          2021-09-22 18:45

          在下方公眾號(hào)后臺(tái)回復(fù):面試手冊(cè),可獲取杰哥匯總的 3 份面試 PDF 手冊(cè)。

          今天分享一篇經(jīng)典Linux協(xié)議棧文章,主要講解Linux網(wǎng)絡(luò)子系統(tǒng),看完相信大家對(duì)協(xié)議棧又會(huì)加深不少,不光可以了解協(xié)議棧處理流程,方便定位問(wèn)題,還可以學(xué)習(xí)一下怎么去設(shè)計(jì)一個(gè)可擴(kuò)展的子系統(tǒng),屏蔽不同層次的差異。

          目錄

          Linux網(wǎng)絡(luò)子系統(tǒng)的分層

          • Linux網(wǎng)絡(luò)子系統(tǒng)實(shí)現(xiàn)需要:

          • 支持不同的協(xié)議族 ( INET, INET6, UNIX, NETLINK…)

          • 支持不同的網(wǎng)絡(luò)設(shè)備

          • 支持統(tǒng)一的BSD socket API

          • 需要屏蔽協(xié)議、硬件、平臺(tái)(API)的差異,因而采用分層結(jié)構(gòu):

          系統(tǒng)調(diào)用提供用戶(hù)的應(yīng)用程序訪問(wèn)內(nèi)核的唯一途徑。協(xié)議無(wú)關(guān)接口由socket layer來(lái)實(shí)現(xiàn)的,其提供一組通用功能,以支持各種不同的協(xié)議。網(wǎng)絡(luò)協(xié)議層為socket層提供具體協(xié)議接口——proto{},實(shí)現(xiàn)具體的協(xié)議細(xì)節(jié)。設(shè)備無(wú)關(guān)接口,提供一組通用函數(shù)供底層網(wǎng)絡(luò)設(shè)備驅(qū)動(dòng)程序使用。設(shè)備驅(qū)動(dòng)與特定網(wǎng)卡設(shè)備相關(guān),定義了具體的協(xié)議細(xì)節(jié),會(huì)分配一個(gè)net_device結(jié)構(gòu),然后用其必需的例程進(jìn)行初始化。

          TCP/IP分層模型

          在TCP/IP網(wǎng)絡(luò)分層模型里,整個(gè)協(xié)議棧被分成了物理層、鏈路層、網(wǎng)絡(luò)層,傳輸層和應(yīng)用層。物理層對(duì)應(yīng)的是網(wǎng)卡和網(wǎng)線,應(yīng)用層對(duì)應(yīng)的是我們常見(jiàn)的Nginx,F(xiàn)TP等等各種應(yīng)用。Linux實(shí)現(xiàn)的是鏈路層、網(wǎng)絡(luò)層和傳輸層這三層。

          在Linux內(nèi)核實(shí)現(xiàn)中,鏈路層協(xié)議靠網(wǎng)卡驅(qū)動(dòng)來(lái)實(shí)現(xiàn),內(nèi)核協(xié)議棧來(lái)實(shí)現(xiàn)網(wǎng)絡(luò)層和傳輸層。內(nèi)核對(duì)更上層的應(yīng)用層提供socket接口來(lái)供用戶(hù)進(jìn)程訪問(wèn)。我們用Linux的視角來(lái)看到的TCP/IP網(wǎng)絡(luò)分層模型應(yīng)該是下面這個(gè)樣子的。

          首先我們梳理一下每層模型的職責(zé):

          鏈路層:對(duì)0和1進(jìn)行分組,定義數(shù)據(jù)幀,確認(rèn)主機(jī)的物理地址,傳輸數(shù)據(jù);

          網(wǎng)絡(luò)層:定義IP地址,確認(rèn)主機(jī)所在的網(wǎng)絡(luò)位置,并通過(guò)IP進(jìn)行MAC尋址,對(duì)外網(wǎng)數(shù)據(jù)包進(jìn)行路由轉(zhuǎn)發(fā);

          傳輸層:定義端口,確認(rèn)主機(jī)上應(yīng)用程序的身份,并將數(shù)據(jù)包交給對(duì)應(yīng)的應(yīng)用程序;

          應(yīng)用層:定義數(shù)據(jù)格式,并按照對(duì)應(yīng)的格式解讀數(shù)據(jù)。

          然后再把每層模型的職責(zé)串聯(lián)起來(lái),用一句通俗易懂的話講就是:

          當(dāng)你輸入一個(gè)網(wǎng)址并按下回車(chē)鍵的時(shí)候,首先,應(yīng)用層協(xié)議對(duì)該請(qǐng)求包做了格式定義;緊接著傳輸層協(xié)議加上了雙方的端口號(hào),確認(rèn)了雙方通信的應(yīng)用程序;然后網(wǎng)絡(luò)協(xié)議加上了雙方的IP地址,確認(rèn)了雙方的網(wǎng)絡(luò)位置;最后鏈路層協(xié)議加上了雙方的MAC地址,確認(rèn)了雙方的物理位置,同時(shí)將數(shù)據(jù)進(jìn)行分組,形成數(shù)據(jù)幀,采用廣播方式,通過(guò)傳輸介質(zhì)發(fā)送給對(duì)方主機(jī)。而對(duì)于不同網(wǎng)段,該數(shù)據(jù)包首先會(huì)轉(zhuǎn)發(fā)給網(wǎng)關(guān)路由器,經(jīng)過(guò)多次轉(zhuǎn)發(fā)后,最終被發(fā)送到目標(biāo)主機(jī)。目標(biāo)機(jī)接收到數(shù)據(jù)包后,采用對(duì)應(yīng)的協(xié)議,對(duì)幀數(shù)據(jù)進(jìn)行組裝,然后再通過(guò)一層一層的協(xié)議進(jìn)行解析,最終被應(yīng)用層的協(xié)議解析并交給服務(wù)器處理。

          Linux 網(wǎng)絡(luò)協(xié)議棧

          基于TCP/IP協(xié)議棧的send/recv在應(yīng)用層,傳輸層,網(wǎng)絡(luò)層和鏈路層中具體函數(shù)調(diào)用過(guò)程已經(jīng)有很多人研究,本文引用一張比較完善的圖如下:

          以上說(shuō)明基本大致說(shuō)明了TCP/IP中TCP,UDP協(xié)議包在網(wǎng)絡(luò)子系統(tǒng)中的實(shí)現(xiàn)流程。本文主要在鏈路層中,即關(guān)于網(wǎng)卡收?qǐng)?bào)觸發(fā)中斷到進(jìn)入網(wǎng)絡(luò)層之間的過(guò)程探究。

          Linux 網(wǎng)卡收包時(shí)的中斷處理問(wèn)題

          中斷,一般指硬件中斷,多由系統(tǒng)自身或與之鏈接的外設(shè)(如鍵盤(pán)、鼠標(biāo)、網(wǎng)卡等)產(chǎn)生。中斷首先是處理器提供的一種響應(yīng)外設(shè)請(qǐng)求的機(jī)制,是處理器硬件支持的特性。一個(gè)外設(shè)通過(guò)產(chǎn)生一種電信號(hào)通知中斷控制器,中斷控制器再向處理器發(fā)送相應(yīng)的信號(hào)。處理器檢測(cè)到了這個(gè)信號(hào)后就會(huì)打斷自己當(dāng)前正在做的工作,轉(zhuǎn)而去處理這次中斷(所以才叫中斷)。當(dāng)然在轉(zhuǎn)去處理中斷和中斷返回時(shí)都有保護(hù)現(xiàn)場(chǎng)和返回現(xiàn)場(chǎng)的操作,這里不贅述。

          那軟中斷又是什么呢?我們知道在中斷處理時(shí)CPU沒(méi)法處理其它事物,對(duì)于網(wǎng)卡來(lái)說(shuō),如果每次網(wǎng)卡收包時(shí)中斷的時(shí)間都過(guò)長(zhǎng),那很可能造成丟包的可能性。當(dāng)然我們不能完全避免丟包的可能性,以太包的傳輸是沒(méi)有100%保證的,所以網(wǎng)絡(luò)才有協(xié)議棧,通過(guò)高層的協(xié)議來(lái)保證連續(xù)數(shù)據(jù)傳輸?shù)臄?shù)據(jù)完整性(比如在協(xié)議發(fā)現(xiàn)丟包時(shí)要求重傳)。但是即使有協(xié)議保證,那我們也不能肆無(wú)忌憚的使用中斷,中斷的時(shí)間越短越好,盡快放開(kāi)處理器,讓它可以去響應(yīng)下次中斷甚至進(jìn)行調(diào)度工作。基于這樣的考慮,我們將中斷分成了上下兩部分,上半部分就是上面說(shuō)的中斷部分,需要快速及時(shí)響應(yīng),同時(shí)需要越快結(jié)束越好。而下半部分就是完成一些可以推后執(zhí)行的工作。對(duì)于網(wǎng)卡收包來(lái)說(shuō),網(wǎng)卡收到數(shù)據(jù)包,通知內(nèi)核數(shù)據(jù)包到了,中斷處理將數(shù)據(jù)包存入內(nèi)存這些都是急切需要完成的工作,放到上半部完成。而解析處理數(shù)據(jù)包的工作則可以放到下半部去執(zhí)行。

          軟中斷就是下半部使用的一種機(jī)制,它通過(guò)軟件模仿硬件中斷的處理過(guò)程,但是和硬件沒(méi)有關(guān)系,單純的通過(guò)軟件達(dá)到一種異步處理的方式。其它下半部的處理機(jī)制還包括tasklet,工作隊(duì)列等。依據(jù)所處理的場(chǎng)合不同,選擇不同的機(jī)制,網(wǎng)卡收包一般使用軟中斷。對(duì)應(yīng)NET_RX_SOFTIRQ這個(gè)軟中斷,軟中斷的類(lèi)型如下:

          enum
          {
                  HI_SOFTIRQ=0,
                  TIMER_SOFTIRQ,
                  NET_TX_SOFTIRQ,
                  NET_RX_SOFTIRQ,
                  BLOCK_SOFTIRQ,
                  IRQ_POLL_SOFTIRQ,
                  TASKLET_SOFTIRQ,
                  SCHED_SOFTIRQ,
                  HRTIMER_SOFTIRQ,
                  RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */
                  NR_SOFTIRQS
          };

          通過(guò)以上可以了解到,Linux中斷注冊(cè)顯然應(yīng)該包括網(wǎng)卡的硬中斷,包處理的軟中斷兩個(gè)步驟。

          注冊(cè)網(wǎng)卡中斷

          我們以一個(gè)具體的網(wǎng)卡驅(qū)動(dòng)為例,比如e1000。其模塊初始化函數(shù)就是:

          static int __init e1000_init_module(void)
          {
                  int ret;
                  pr_info("%s - version %s\n", e1000_driver_string, e1000_driver_version);
                  pr_info("%s\n", e1000_copyright);
                  ret = pci_register_driver(&e1000_driver);
          ...
                  return ret;

          }

          其中e1000_driver這個(gè)結(jié)構(gòu)體是一個(gè)關(guān)鍵,這個(gè)結(jié)構(gòu)體中很主要的一個(gè)方法就是.probe方法,也就是e1000_probe():

          /**                                                  

           * e1000_probe - Device Initialization Routine         
           * @pdev: PCI device information struct                    
           * @ent: entry in e1000_pci_tbl     
           *                                
           * Returns 0 on success, negative on failure                                                                               
           *                                                                                                               
           * e1000_probe initializes an adapter identified by a pci_dev structure.                                                               
           * The OS initialization, configuring of the adapter private structure,                                                                  
           * and a hardware reset occur.                                                      
           **/

          static int e1000_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
          {
          ...
          ...
                  netdev->netdev_ops = &e1000_netdev_ops;
                  e1000_set_ethtool_ops(netdev);
          ...
          ...
          }

          這個(gè)函數(shù)很長(zhǎng),我們不都列出來(lái),這是e1000主要的初始化函數(shù),即使從注釋都能看出來(lái)。我們留意其注冊(cè)了netdev的netdev_ops,用的是e1000_netdev_ops這個(gè)結(jié)構(gòu)體:

          static const struct net_device_ops e1000_netdev_ops = {
                  .ndo_open               = e1000_open,
                  .ndo_stop               = e1000_close,
                  .ndo_start_xmit         = e1000_xmit_frame,
                  .ndo_set_rx_mode        = e1000_set_rx_mode,
                  .ndo_set_mac_address    = e1000_set_mac,
                  .ndo_tx_timeout         = e1000_tx_timeout,
          ...
          ...
          };

          這個(gè)e1000的方法集里有一個(gè)重要的方法,e1000_open,我們要說(shuō)的中斷的注冊(cè)就從這里開(kāi)始:

          /**           
           * e1000_open - Called when a network interface is made active  
           * @netdev: network interface device structure            
           *                                                 
           * Returns 0 on success, negative value on failure     
           *     
           * The open entry point is called when a network interface is made                                                                                                    
           * active by the system (IFF_UP).  At this point all resources needed                                                                            
           * for transmit and receive operations are allocated, the interrupt                                                     
           * handler is registered with the OS, the watchdog task is started,                                                                                                     
           * and the stack is notified that the interface is ready.                                                                                                             
           **/

          int e1000_open(struct net_device *netdev)
          {
                  struct e1000_adapter *adapter = netdev_priv(netdev);
                  struct e1000_hw *hw = &adapter->hw;
          ...
          ...
                  err = e1000_request_irq(adapter);
          ...
          }

          e1000在這里注冊(cè)了中斷:

          static int e1000_request_irq(struct e1000_adapter *adapter)
          {
                  struct net_device *netdev = adapter->netdev;
                  irq_handler_t handler = e1000_intr;
                  int irq_flags = IRQF_SHARED;
                  int err;
                  err = request_irq(adapter->pdev->irq, handler, irq_flags, netdev->name,
          ...
          ...
          }

          如上所示,這個(gè)被注冊(cè)的中斷處理函數(shù),也就是handler,就是e1000_intr()。我們不展開(kāi)這個(gè)中斷處理函數(shù)看了,我們知道中斷處理函數(shù)在這里被注冊(cè)了,在網(wǎng)絡(luò)包來(lái)的時(shí)候會(huì)觸發(fā)這個(gè)中斷函數(shù)。

          注冊(cè)軟中斷

          內(nèi)核初始化期間,softirq_init會(huì)注冊(cè)TASKLET_SOFTIRQ以及HI_SOFTIRQ相關(guān)聯(lián)的處理函數(shù)。

          void __init softirq_init(void)
          {
              ......
              open_softirq(TASKLET_SOFTIRQ, tasklet_action);
              open_softirq(HI_SOFTIRQ, tasklet_hi_action);

          }

          網(wǎng)絡(luò)子系統(tǒng)分兩種soft IRQ。NET_TX_SOFTIRQ和NET_RX_SOFTIRQ,分別處理發(fā)送數(shù)據(jù)包和接收數(shù)據(jù)包。這兩個(gè)soft IRQ在net_dev_init函數(shù)(net/core/dev.c)中注冊(cè):

          open_softirq(NET_TX_SOFTIRQ, net_tx_action);

          open_softirq(NET_RX_SOFTIRQ, net_rx_action);

          收發(fā)數(shù)據(jù)包的軟中斷處理函數(shù)被注冊(cè)為net_rx_action和net_tx_action。其中open_softirq實(shí)現(xiàn)為:

          void open_softirq(int nr, void (*action)(struct softirq_action *))
          {
              softirq_vec[nr].action = action;
          }

          從硬中斷到軟中斷

          Linux 網(wǎng)絡(luò)啟動(dòng)的準(zhǔn)備工作

          首先在開(kāi)始收包之前,Linux要做許多的準(zhǔn)備工作:

          1.創(chuàng)建ksoftirqd線程,為它設(shè)置好它自己的線程函數(shù),后面就指望著它來(lái)處理軟中斷呢。

          2.協(xié)議棧注冊(cè),linux要實(shí)現(xiàn)許多協(xié)議,比如arp,icmp,ip,udp,tcp,每一個(gè)協(xié)議都會(huì)將自己的處理函數(shù)注冊(cè)一下,方便包來(lái)了迅速找到對(duì)應(yīng)的處理函數(shù)

          3.網(wǎng)卡驅(qū)動(dòng)初始化,每個(gè)驅(qū)動(dòng)都有一個(gè)初始化函數(shù),內(nèi)核會(huì)讓驅(qū)動(dòng)也初始化一下。在這個(gè)初始化過(guò)程中,把自己的DMA準(zhǔn)備好,把NAPI的poll函數(shù)地址告訴內(nèi)核

          4.啟動(dòng)網(wǎng)卡,分配RX,TX隊(duì)列,注冊(cè)中斷對(duì)應(yīng)的處理函數(shù)

          創(chuàng)建ksoftirqd內(nèi)核線程

          Linux的軟中斷都是在專(zhuān)門(mén)的內(nèi)核線程(ksoftirqd)中進(jìn)行的,因此我們非常有必要看一下這些進(jìn)程是怎么初始化的,這樣我們才能在后面更準(zhǔn)確地了解收包過(guò)程。該進(jìn)程數(shù)量不是1個(gè),而是N個(gè),其中N等于你的機(jī)器的核數(shù)。

          系統(tǒng)初始化的時(shí)候在kernel/smpboot.c中調(diào)用了smpboot_register_percpu_thread, 該函數(shù)進(jìn)一步會(huì)執(zhí)行到spawn_ksoftirqd(位于kernel/softirq.c)來(lái)創(chuàng)建出softirqd進(jìn)程。

          相關(guān)代碼如下:

          //file: kernel/softirq.c

          static struct smp_hotplug_thread softirq_threads = {
              .store          = &ksoftirqd,
              .thread_should_run  = ksoftirqd_should_run,
              .thread_fn      = run_ksoftirqd,
              .thread_comm        = "ksoftirqd/%u",
          };


          當(dāng)ksoftirqd被創(chuàng)建出來(lái)以后,它就會(huì)進(jìn)入自己的線程循環(huán)函數(shù)ksoftirqd_should_run和run_ksoftirqd了。不停地判斷有沒(méi)有軟中斷需要被處理。這里需要注意的一點(diǎn)是,軟中斷不僅僅只有網(wǎng)絡(luò)軟中斷,還有其它類(lèi)型。

          創(chuàng)建ksoftirqd內(nèi)核線程

          linux內(nèi)核通過(guò)調(diào)用subsys_initcall來(lái)初始化各個(gè)子系統(tǒng),在源代碼目錄里你可以grep出許多對(duì)這個(gè)函數(shù)的調(diào)用。這里我們要說(shuō)的是網(wǎng)絡(luò)子系統(tǒng)的初始化,會(huì)執(zhí)行到net_dev_init函數(shù)。

          在這個(gè)函數(shù)里,會(huì)為每個(gè)CPU都申請(qǐng)一個(gè)softnet_data數(shù)據(jù)結(jié)構(gòu),在這個(gè)數(shù)據(jù)結(jié)構(gòu)里的poll_list是等待驅(qū)動(dòng)程序?qū)⑵鋚oll函數(shù)注冊(cè)進(jìn)來(lái),稍后網(wǎng)卡驅(qū)動(dòng)初始化的時(shí)候我們可以看到這一過(guò)程。

          另外open_softirq注冊(cè)了每一種軟中斷都注冊(cè)一個(gè)處理函數(shù)。NET_TX_SOFTIRQ的處理函數(shù)為net_tx_action,NET_RX_SOFTIRQ的為net_rx_action。繼續(xù)跟蹤open_softirq后發(fā)現(xiàn)這個(gè)注冊(cè)的方式是記錄在softirq_vec變量里的。后面ksoftirqd線程收到軟中斷的時(shí)候,也會(huì)使用這個(gè)變量來(lái)找到每一種軟中斷對(duì)應(yīng)的處理函數(shù)。

          協(xié)議棧注冊(cè)

          內(nèi)核實(shí)現(xiàn)了網(wǎng)絡(luò)層的ip協(xié)議,也實(shí)現(xiàn)了傳輸層的tcp協(xié)議和udp協(xié)議。這些協(xié)議對(duì)應(yīng)的實(shí)現(xiàn)函數(shù)分別是ip_rcv(),tcp_v4_rcv()和udp_rcv()。和我們平時(shí)寫(xiě)代碼的方式不一樣的是,內(nèi)核是通過(guò)注冊(cè)的方式來(lái)實(shí)現(xiàn)的。Linux內(nèi)核中的fs_initcall和subsys_initcall類(lèi)似,也是初始化模塊的入口。fs_initcall調(diào)用inet_init后開(kāi)始網(wǎng)絡(luò)協(xié)議棧注冊(cè)。通過(guò)inet_init,將這些函數(shù)注冊(cè)到了inet_protos和ptype_base數(shù)據(jù)結(jié)構(gòu)中

          相關(guān)代碼如下

          //file: net/ipv4/af_inet.c

          static struct packet_type ip_packet_type __read_mostly = {
              .type = cpu_to_be16(ETH_P_IP),
              .func = ip_rcv,
          };

          static const struct net_protocol udp_protocol = {
              .handler =  udp_rcv,
              .err_handler =  udp_err,
              .no_policy =    1,
              .netns_ok = 1,
          };

          static const struct net_protocol tcp_protocol = {
              .early_demux    =   tcp_v4_early_demux,
              .handler    =   tcp_v4_rcv,
              .err_handler    =   tcp_v4_err,
              .no_policy  =   1,
              .netns_ok   =   1,
          };


          擴(kuò)展一下,如果看一下ip_rcv和udp_rcv等函數(shù)的代碼能看到很多協(xié)議的處理過(guò)程。例如,ip_rcv中會(huì)處理netfilter和iptable過(guò)濾,如果你有很多或者很復(fù)雜的 netfilter 或 iptables 規(guī)則,這些規(guī)則都是在軟中斷的上下文中執(zhí)行的,會(huì)加大網(wǎng)絡(luò)延遲。再例如,udp_rcv中會(huì)判斷socket接收隊(duì)列是否滿了。對(duì)應(yīng)的相關(guān)內(nèi)核參數(shù)是net.core.rmem_max和net.core.rmem_default。如果有興趣,建議大家好好讀一下inet_init這個(gè)函數(shù)的代碼。

          網(wǎng)卡驅(qū)動(dòng)初始化

          每一個(gè)驅(qū)動(dòng)程序(不僅僅只是網(wǎng)卡驅(qū)動(dòng))會(huì)使用 module_init 向內(nèi)核注冊(cè)一個(gè)初始化函數(shù),當(dāng)驅(qū)動(dòng)被加載時(shí),內(nèi)核會(huì)調(diào)用這個(gè)函數(shù)。比如igb網(wǎng)卡驅(qū)動(dòng)的代碼位于drivers/net/ethernet/intel/igb/igb_main.c

          驅(qū)動(dòng)的pci_register_driver調(diào)用完成后,Linux內(nèi)核就知道了該驅(qū)動(dòng)的相關(guān)信息,比如igb網(wǎng)卡驅(qū)動(dòng)的igb_driver_name和igb_probe函數(shù)地址等等。當(dāng)網(wǎng)卡設(shè)備被識(shí)別以后,內(nèi)核會(huì)調(diào)用其驅(qū)動(dòng)的probe方法(igb_driver的probe方法是igb_probe)。驅(qū)動(dòng)probe方法執(zhí)行的目的就是讓設(shè)備ready,對(duì)于igb網(wǎng)卡,其igb_probe位于drivers/net/ethernet/intel/igb/igb_main.c下。主要執(zhí)行的操作如下:

          第5步中我們看到,網(wǎng)卡驅(qū)動(dòng)實(shí)現(xiàn)了ethtool所需要的接口,也在這里注冊(cè)完成函數(shù)地址的注冊(cè)。當(dāng) ethtool 發(fā)起一個(gè)系統(tǒng)調(diào)用之后,內(nèi)核會(huì)找到對(duì)應(yīng)操作的回調(diào)函數(shù)。對(duì)于igb網(wǎng)卡來(lái)說(shuō),其實(shí)現(xiàn)函數(shù)都在drivers/net/ethernet/intel/igb/igb_ethtool.c下。相信你這次能徹底理解ethtool的工作原理了吧?這個(gè)命令之所以能查看網(wǎng)卡收發(fā)包統(tǒng)計(jì)、能修改網(wǎng)卡自適應(yīng)模式、能調(diào)整RX 隊(duì)列的數(shù)量和大小,是因?yàn)閑thtool命令最終調(diào)用到了網(wǎng)卡驅(qū)動(dòng)的相應(yīng)方法,而不是ethtool本身有這個(gè)超能力。

          第6步注冊(cè)的igb_netdev_ops中包含的是igb_open等函數(shù),該函數(shù)在網(wǎng)卡被啟動(dòng)的時(shí)候會(huì)被調(diào)用。

          //file: drivers/net/ethernet/intel/igb/igb_main.
          ......
          static const struct net_device_ops igb_netdev_ops = {
            .ndo_open               = igb_open,
            .ndo_stop               = igb_close,
            .ndo_start_xmit         = igb_xmit_frame,
            .ndo_get_stats64        = igb_get_stats64,
            .ndo_set_rx_mode        = igb_set_rx_mode,
            .ndo_set_mac_address    = igb_set_mac,
            .ndo_change_mtu         = igb_change_mtu,
            .ndo_do_ioctl           = igb_ioctl,......
          }

          第7步中,在igb_probe初始化過(guò)程中,還調(diào)用到了igb_alloc_q_vector。他注冊(cè)了一個(gè)NAPI機(jī)制所必須的poll函數(shù),對(duì)于igb網(wǎng)卡驅(qū)動(dòng)來(lái)說(shuō),這個(gè)函數(shù)就是igb_poll,如下代碼所示。

          static int igb_alloc_q_vector(struct igb_adapter *adapter,
                            int v_count, int v_idx,
                            int txr_count, int txr_idx,
                            int rxr_count, int rxr_idx
          )
          {
              ......
              /* initialize NAPI */
              netif_napi_add(adapter->netdev, &q_vector->napi,
                         igb_poll, 64);
          }

          啟動(dòng)網(wǎng)卡

          當(dāng)上面的初始化都完成以后,就可以啟動(dòng)網(wǎng)卡了。回憶前面網(wǎng)卡驅(qū)動(dòng)初始化時(shí),我們提到了驅(qū)動(dòng)向內(nèi)核注冊(cè)了 structure net_device_ops 變量,它包含著網(wǎng)卡啟用、發(fā)包、設(shè)置mac 地址等回調(diào)函數(shù)(函數(shù)指針)。當(dāng)啟用一個(gè)網(wǎng)卡時(shí)(例如,通過(guò) ifconfig eth0 up),net_device_ops 中的 igb_open方法會(huì)被調(diào)用。它通常會(huì)做以下事情:

          //file: drivers/net/ethernet/intel/igb/igb_main.c
          static int __igb_open(struct net_device *netdev, bool resuming)
          {
              /* allocate transmit descriptors */
              err = igb_setup_all_tx_resources(adapter);
              /* allocate receive descriptors */
              err = igb_setup_all_rx_resources(adapter);
              /* 注冊(cè)中斷處理函數(shù) */
              err = igb_request_irq(adapter);
              if (err)
                  goto err_req_irq;
              /* 啟用NAPI */
              for (i = 0; i < adapter->num_q_vectors; i++)
                  napi_enable(&(adapter->q_vector[i]->napi));
              ......
          }

          在上面__igb_open函數(shù)調(diào)用了igb_setup_all_tx_resources,和igb_setup_all_rx_resources。在igb_setup_all_rx_resources這一步操作中,分配了RingBuffer,并建立內(nèi)存和Rx隊(duì)列的映射關(guān)系。(Rx Tx 隊(duì)列的數(shù)量和大小可以通過(guò) ethtool 進(jìn)行配置)。我們?cè)俳又粗袛嗪瘮?shù)注冊(cè)igb_request_irq:

          static int igb_request_irq(struct igb_adapter *adapter)
          {
              if (adapter->msix_entries) {
                  err = igb_request_msix(adapter);
                  if (!err)
                      goto request_done;
                  ......
              }
          }

          static int igb_request_msix(struct igb_adapter *adapter)
          {
              ......
              for (i = 0; i < adapter->num_q_vectors; i++) {
                  ...
                  err = request_irq(adapter->msix_entries[vector].vector,
                            igb_msix_ring, 0, q_vector->name,
              }

          在上面的代碼中跟蹤函數(shù)調(diào)用, __igb_open => igb_request_irq => igb_request_msix, 在igb_request_msix中我們看到了,對(duì)于多隊(duì)列的網(wǎng)卡,為每一個(gè)隊(duì)列都注冊(cè)了中斷,其對(duì)應(yīng)的中斷處理函數(shù)是igb_msix_ring(該函數(shù)也在drivers/net/ethernet/intel/igb/igb_main.c下)。我們也可以看到,msix方式下,每個(gè) RX 隊(duì)列有獨(dú)立的MSI-X 中斷,從網(wǎng)卡硬件中斷的層面就可以設(shè)置讓收到的包被不同的 CPU處理。(可以通過(guò) irqbalance ,或者修改 /proc/irq/IRQ_NUMBER/smp_affinity能夠修改和CPU的綁定行為)。

          到此準(zhǔn)備工作完成。

          Linux網(wǎng)絡(luò)包:中斷到網(wǎng)絡(luò)層接收

          網(wǎng)卡收包從整體上是網(wǎng)線中的高低電平轉(zhuǎn)換到網(wǎng)卡FIFO存儲(chǔ)再拷貝到系統(tǒng)主內(nèi)存(DDR3)的過(guò)程,其中涉及到網(wǎng)卡控制器,CPU,DMA,驅(qū)動(dòng)程序,在OSI模型中屬于物理層和鏈路層,如下圖所示。

          中斷上半文

          物理網(wǎng)卡收到數(shù)據(jù)包的處理流程如上圖左半部分所示,詳細(xì)步驟如下:

          1.網(wǎng)卡收到數(shù)據(jù)包,先將高低電平轉(zhuǎn)換到網(wǎng)卡fifo存儲(chǔ),網(wǎng)卡申請(qǐng)ring buffer的描述,根據(jù)描述找到具體的物理地址,從fifo隊(duì)列物理網(wǎng)卡會(huì)使用DMA將數(shù)據(jù)包寫(xiě)到了該物理地址,,其實(shí)就是skb_buffer中.

          2.這個(gè)時(shí)候數(shù)據(jù)包已經(jīng)被轉(zhuǎn)移到skb_buffer中,因?yàn)槭荄MA寫(xiě)入,內(nèi)核并沒(méi)有監(jiān)控?cái)?shù)據(jù)包寫(xiě)入情況,這時(shí)候NIC觸發(fā)一個(gè)硬中斷,每一個(gè)硬件中斷會(huì)對(duì)應(yīng)一個(gè)中斷號(hào),且指定一個(gè)vCPU來(lái)處理,如上圖vcpu2收到了該硬件中斷.

          3.硬件中斷的中斷處理程序,調(diào)用驅(qū)動(dòng)程序完成,a.啟動(dòng)軟中斷

          4.硬中斷觸發(fā)的驅(qū)動(dòng)程序會(huì)禁用網(wǎng)卡硬中斷,其實(shí)這時(shí)候意思是告訴NIC,再來(lái)數(shù)據(jù)不用觸發(fā)硬中斷了,把數(shù)據(jù)DMA拷入系統(tǒng)內(nèi)存即可

          5.硬中斷觸發(fā)的驅(qū)動(dòng)程序會(huì)啟動(dòng)軟中斷,啟用軟中斷目的是將數(shù)據(jù)包后續(xù)處理流程交給軟中斷慢慢處理,這個(gè)時(shí)候退出硬件中斷了,但是注意和網(wǎng)絡(luò)有關(guān)的硬中斷,要等到后續(xù)開(kāi)啟硬中斷后,才有機(jī)會(huì)再次被觸發(fā)

          6.NAPI觸發(fā)軟中斷,觸發(fā)napi系統(tǒng)

          7.消耗ringbuffer指向的skb_buffer

          8.NAPI循環(huán)處理ringbuffer數(shù)據(jù),處理完成

          9.啟動(dòng)網(wǎng)絡(luò)硬件中斷,有數(shù)據(jù)來(lái)時(shí)候就可以繼續(xù)觸發(fā)硬件中斷,繼續(xù)通知CPU來(lái)消耗數(shù)據(jù)包.

          其實(shí)上述過(guò)程過(guò)程簡(jiǎn)單描述為:網(wǎng)卡收到數(shù)據(jù)包,DMA到內(nèi)核內(nèi)存,中斷通知內(nèi)核數(shù)據(jù)有了,內(nèi)核按輪次處理消耗數(shù)據(jù)包,一輪處理完成后,開(kāi)啟硬中斷。其核心就是網(wǎng)卡和內(nèi)核其實(shí)是生產(chǎn)和消費(fèi)模型,網(wǎng)卡生產(chǎn),內(nèi)核負(fù)責(zé)消費(fèi),生產(chǎn)者需要通知消費(fèi)者消費(fèi);如果生產(chǎn)過(guò)快會(huì)產(chǎn)生丟包,如果消費(fèi)過(guò)慢也會(huì)產(chǎn)生問(wèn)題。也就說(shuō)在高流量壓力情況下,只有生產(chǎn)消費(fèi)優(yōu)化后,消費(fèi)能力夠快,此生產(chǎn)消費(fèi)關(guān)系才可以正常維持,所以如果物理接口有丟包計(jì)數(shù)時(shí)候,未必是網(wǎng)卡存在問(wèn)題,也可能是內(nèi)核消費(fèi)的太慢。

          關(guān)于CPU與ksoftirqd的關(guān)系可以描述如下:

          網(wǎng)卡收到的數(shù)據(jù)寫(xiě)入到內(nèi)核內(nèi)存

          NIC在接收到數(shù)據(jù)包之后,首先需要將數(shù)據(jù)同步到內(nèi)核中,這中間的橋梁是rx ring buffer。它是由NIC和驅(qū)動(dòng)程序共享的一片區(qū)域,事實(shí)上,rx ring buffer存儲(chǔ)的并不是實(shí)際的packet數(shù)據(jù),而是一個(gè)描述符,這個(gè)描述符指向了它真正的存儲(chǔ)地址,具體流程如下:

          1.驅(qū)動(dòng)在內(nèi)存中分配一片緩沖區(qū)用來(lái)接收數(shù)據(jù)包,叫做sk_buffer;

          2.將上述緩沖區(qū)的地址和大小(即接收描述符),加入到rx ring buffer。描述符中的緩沖區(qū)地址是DMA使用的物理地址;

          3.驅(qū)動(dòng)通知網(wǎng)卡有一個(gè)新的描述符;

          4.網(wǎng)卡從rx ring buffer中取出描述符,從而獲知緩沖區(qū)的地址和大小;

          5.網(wǎng)卡收到新的數(shù)據(jù)包;

          6.網(wǎng)卡將新數(shù)據(jù)包通過(guò)DMA直接寫(xiě)到sk_buffer中。

          當(dāng)驅(qū)動(dòng)處理速度跟不上網(wǎng)卡收包速度時(shí),驅(qū)動(dòng)來(lái)不及分配緩沖區(qū),NIC接收到的數(shù)據(jù)包無(wú)法及時(shí)寫(xiě)到sk_buffer,就會(huì)產(chǎn)生堆積,當(dāng)NIC內(nèi)部緩沖區(qū)寫(xiě)滿后,就會(huì)丟棄部分?jǐn)?shù)據(jù),引起丟包。這部分丟包為rx_fifo_errors,在 /proc/net/dev中體現(xiàn)為fifo字段增長(zhǎng),在ifconfig中體現(xiàn)為overruns指標(biāo)增長(zhǎng)。

          中斷下半文

          ksoftirqd內(nèi)核線程處理軟中斷,即中斷下半部分軟中斷處理過(guò)程:

          1.NAPI(以e1000網(wǎng)卡為例):net_rx_action() -> e1000_clean() -> e1000_clean_rx_irq() -> e1000_receive_skb() -> netif_receive_skb()

          2.非NAPI(以dm9000網(wǎng)卡為例):net_rx_action() -> process_backlog() -> netif_receive_skb()

          最后網(wǎng)卡驅(qū)動(dòng)通過(guò)netif_receive_skb()將sk_buff上送協(xié)議棧。

          內(nèi)核線程初始化的時(shí)候,我們介紹了ksoftirqd中兩個(gè)線程函數(shù)ksoftirqd_should_run和run_ksoftirqd。其中ksoftirqd_should_run代碼如下:

          #define local_softirq_pending() \

          __IRQ_STAT(smp_processor_id(), __softirq_pending)

          這里看到和硬中斷中調(diào)用了同一個(gè)函數(shù)local_softirq_pending。使用方式不同的是硬中斷位置是為了寫(xiě)入標(biāo)記,這里僅僅只是讀取。如果硬中斷中設(shè)置了NET_RX_SOFTIRQ,這里自然能讀取的到。接下來(lái)會(huì)真正進(jìn)入線程函數(shù)中run_ksoftirqd處理:

          static void run_ksoftirqd(unsigned int cpu)
          {
              local_irq_disable();
              if (local_softirq_pending()) {
                  __do_softirq();
                  rcu_note_context_switch(cpu);
                  local_irq_enable();
                  cond_resched();
                  return;
              }
              local_irq_enable();
          }

          在__do_softirq中,判斷根據(jù)當(dāng)前CPU的軟中斷類(lèi)型,調(diào)用其注冊(cè)的action方法。

          asmlinkage void __do_softirq(void)

          在網(wǎng)絡(luò)子系統(tǒng)初始化小節(jié),我們看到我們?yōu)镹ET_RX_SOFTIRQ注冊(cè)了處理函數(shù)net_rx_action。所以net_rx_action函數(shù)就會(huì)被執(zhí)行到了。

          這里需要注意一個(gè)細(xì)節(jié),硬中斷中設(shè)置軟中斷標(biāo)記,和ksoftirq的判斷是否有軟中斷到達(dá),都是基于smp_processor_id()的。這意味著只要硬中斷在哪個(gè)CPU上被響應(yīng),那么軟中斷也是在這個(gè)CPU上處理的。所以說(shuō),如果你發(fā)現(xiàn)你的Linux軟中斷CPU消耗都集中在一個(gè)核上的話,做法是要把調(diào)整硬中斷的CPU親和性,來(lái)將硬中斷打散到不通的CPU核上去。

          我們?cè)賮?lái)把精力集中到這個(gè)核心函數(shù)net_rx_action上來(lái)。

          static void net_rx_action(struct softirq_action *h)
          {
              struct softnet_data *sd = &__get_cpu_var(softnet_data);
              unsigned long time_limit = jiffies + 2;
              int budget = netdev_budget;
              void *have;
              local_irq_disable();
              while (!list_empty(&sd->poll_list)) {
                  ......
                  n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);
                  work = 0;
                  if (test_bit(NAPI_STATE_SCHED, &n->state)) {
                      work = n->poll(n, weight);
                      trace_napi_poll(n);
                  }
                  budget -= work;
              }
          }

          函數(shù)開(kāi)頭的time_limit和budget是用來(lái)控制net_rx_action函數(shù)主動(dòng)退出的,目的是保證網(wǎng)絡(luò)包的接收不霸占CPU不放。等下次網(wǎng)卡再有硬中斷過(guò)來(lái)的時(shí)候再處理剩下的接收數(shù)據(jù)包。其中budget可以通過(guò)內(nèi)核參數(shù)調(diào)整。這個(gè)函數(shù)中剩下的核心邏輯是獲取到當(dāng)前CPU變量softnet_data,對(duì)其poll_list進(jìn)行遍歷, 然后執(zhí)行到網(wǎng)卡驅(qū)動(dòng)注冊(cè)到的poll函數(shù)。對(duì)于igb網(wǎng)卡來(lái)說(shuō),就是igb驅(qū)動(dòng)力的igb_poll函數(shù)了。

          /**
           *  igb_poll - NAPI Rx polling callback
           *  @napi: napi polling structure
           *  @budget: count of how many packets we should handle
           **/

          static int igb_poll(struct napi_struct *napi, int budget)
          {
              ...
              if (q_vector->tx.ring)
                  clean_complete = igb_clean_tx_irq(q_vector);
              if (q_vector->rx.ring)
                  clean_complete &= igb_clean_rx_irq(q_vector, budget);
              ...
          }

          在讀取操作中,igb_poll的重點(diǎn)工作是對(duì)igb_clean_rx_irq的調(diào)用。

          static bool igb_clean_rx_irq(struct igb_q_vector *q_vector, const int budget)
          {
              ...
              do {
                  /* retrieve a buffer from the ring */
                  skb = igb_fetch_rx_buffer(rx_ring, rx_desc, skb);
                  /* fetch next buffer in frame if non-eop */
                  if (igb_is_non_eop(rx_ring, rx_desc))
                      continue;
                  }
                  /* verify the packet layout is correct */
                  if (igb_cleanup_headers(rx_ring, rx_desc, skb)) {
                      skb = NULL;
                      continue;
                  }
                  /* populate checksum, timestamp, VLAN, and protocol */
                  igb_process_skb_fields(rx_ring, rx_desc, skb);
                  napi_gro_receive(&q_vector->napi, skb);
          }

          igb_fetch_rx_buffer和igb_is_non_eop的作用就是把數(shù)據(jù)幀從RingBuffer上取下來(lái)。為什么需要兩個(gè)函數(shù)呢?因?yàn)橛锌赡軒级喽鄠€(gè)RingBuffer,所以是在一個(gè)循環(huán)中獲取的,直到幀尾部。獲取下來(lái)的一個(gè)數(shù)據(jù)幀用一個(gè)sk_buff來(lái)表示。收取完數(shù)據(jù)以后,對(duì)其進(jìn)行一些校驗(yàn),然后開(kāi)始設(shè)置sbk變量的timestamp, VLAN id, protocol等字段。接下來(lái)進(jìn)入到napi_gro_receive中:

          //file: net/core/dev.c
          gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
          {
              skb_gro_reset_offset(skb);
              return napi_skb_finish(dev_gro_receive(napi, skb), skb);
          }

          dev_gro_receive這個(gè)函數(shù)代表的是網(wǎng)卡GRO特性,可以簡(jiǎn)單理解成把相關(guān)的小包合并成一個(gè)大包就行,目的是減少傳送給網(wǎng)絡(luò)棧的包數(shù),這有助于減少 CPU 的使用量。我們暫且忽略,直接看napi_skb_finish, 這個(gè)函數(shù)主要就是調(diào)用了netif_receive_skb。

          //file: net/core/dev.c
          static gro_result_t napi_skb_finish(gro_result_t ret, struct sk_buff *skb)
          {
              switch (ret) {
              case GRO_NORMAL:
                  if (netif_receive_skb(skb))
                      ret = GRO_DROP;
                  break;
              ......
          }

          在netif_receive_skb中,數(shù)據(jù)包將被送到協(xié)議棧中,接下來(lái)在網(wǎng)絡(luò)層協(xié)議層的處理流程便不再贅述。

          總結(jié)

          send發(fā)包過(guò)程

          1、網(wǎng)卡驅(qū)動(dòng)創(chuàng)建tx descriptor ring(一致性DMA內(nèi)存),將tx descriptor ring的總線地址寫(xiě)入網(wǎng)卡寄存器TDBA

          2、協(xié)議棧通過(guò)dev_queue_xmit()將sk_buff下送網(wǎng)卡驅(qū)動(dòng)

          3、網(wǎng)卡驅(qū)動(dòng)將sk_buff放入tx descriptor ring,更新TDT

          4、DMA感知到TDT的改變后,找到tx descriptor ring中下一個(gè)將要使用的descriptor

          5、DMA通過(guò)PCI總線將descriptor的數(shù)據(jù)緩存區(qū)復(fù)制到Tx FIFO

          6、復(fù)制完后,通過(guò)MAC芯片將數(shù)據(jù)包發(fā)送出去

          7、發(fā)送完后,網(wǎng)卡更新TDH,啟動(dòng)硬中斷通知CPU釋放數(shù)據(jù)緩存區(qū)中的數(shù)據(jù)包

          recv收包過(guò)程

          1、網(wǎng)卡驅(qū)動(dòng)創(chuàng)建rx descriptor ring(一致性DMA內(nèi)存),將rx descriptor ring的總線地址寫(xiě)入網(wǎng)卡寄存器RDBA

          2、網(wǎng)卡驅(qū)動(dòng)為每個(gè)descriptor分配sk_buff和數(shù)據(jù)緩存區(qū),流式DMA映射數(shù)據(jù)緩存區(qū),將數(shù)據(jù)緩存區(qū)的總線地址保存到descriptor

          3、網(wǎng)卡接收數(shù)據(jù)包,將數(shù)據(jù)包寫(xiě)入Rx FIFO

          4、DMA找到rx descriptor ring中下一個(gè)將要使用的descriptor

          5、整個(gè)數(shù)據(jù)包寫(xiě)入Rx FIFO后,DMA通過(guò)PCI總線將Rx FIFO中的數(shù)據(jù)包復(fù)制到descriptor的數(shù)據(jù)緩存區(qū)

          6、復(fù)制完后,網(wǎng)卡啟動(dòng)硬中斷通知CPU數(shù)據(jù)緩存區(qū)中已經(jīng)有新的數(shù)據(jù)包了,CPU執(zhí)行硬中斷函數(shù):

          NAPI(以e1000網(wǎng)卡為例):e1000_intr() -> __napi_schedule() -> __raise_softirq_irqoff(NET_RX_SOFTIRQ)

          非NAPI(以dm9000網(wǎng)卡為例):dm9000_interrupt() -> dm9000_rx() -> netif_rx() -> napi_schedule() -> __napi_schedule() -> __raise_softirq_irqoff(NET_RX_SOFTIRQ)

          7、ksoftirqd執(zhí)行軟中斷函數(shù)net_rx_action():

          NAPI(以e1000網(wǎng)卡為例):net_rx_action() -> e1000_clean() -> e1000_clean_rx_irq() -> e1000_receive_skb() -> netif_receive_skb()

          非NAPI(以dm9000網(wǎng)卡為例):net_rx_action() -> process_backlog() -> netif_receive_skb()

          8、網(wǎng)卡驅(qū)動(dòng)通過(guò)netif_receive_skb()將sk_buff上送協(xié)議棧

          Linux網(wǎng)絡(luò)子系統(tǒng)的分層

          Linux網(wǎng)絡(luò)子系統(tǒng)實(shí)現(xiàn)需要:

          • 支持不同的協(xié)議族 ( INET, INET6, UNIX, NETLINK…)

          • 支持不同的網(wǎng)絡(luò)設(shè)備

          • 支持統(tǒng)一的BSD socket API

          需要屏蔽協(xié)議、硬件、平臺(tái)(API)的差異,因而采用分層結(jié)構(gòu):

          系統(tǒng)調(diào)用提供用戶(hù)的應(yīng)用程序訪問(wèn)內(nèi)核的唯一途徑。協(xié)議無(wú)關(guān)接口由socket layer來(lái)實(shí)現(xiàn)的,其提供一組通用功能,以支持各種不同的協(xié)議。網(wǎng)絡(luò)協(xié)議層為socket層提供具體協(xié)議接口——proto{},實(shí)現(xiàn)具體的協(xié)議細(xì)節(jié)。設(shè)備無(wú)關(guān)接口,提供一組通用函數(shù)供底層網(wǎng)絡(luò)設(shè)備驅(qū)動(dòng)程序使用。設(shè)備驅(qū)動(dòng)與特定網(wǎng)卡設(shè)備相關(guān),定義了具體的協(xié)議細(xì)節(jié),會(huì)分配一個(gè)net_device結(jié)構(gòu),然后用其必需的例程進(jìn)行初始化。

          來(lái)源:https://www.cnblogs.com/ypholic/p/14337328.html

          推薦閱讀

          Linux 虛擬網(wǎng)絡(luò)設(shè)備之 bridge


          利用 Linux 查找重復(fù)文件


          20 個(gè)提高生產(chǎn)力的 Linux 命令與技巧!


          如何有效的在 60 秒內(nèi)進(jìn)行 Linux 服務(wù)器性能故障分析


          你不好奇Linux文件系統(tǒng)是怎么工作的?

          瀏覽 235
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  非洲婬乱a一级毛片多女 | 色婷婷在线视频播放 | 精品国产乱码一区二区三区小黄书 | 亚洲日韩中文在线观看 | 成人做爱网站视频|