代碼學習-Linux內核網卡收包過程(NAPI)【轉】


轉自:https://blog.csdn.net/crazycoder8848/article/details/46333761

版權聲明:本文沒有任何版權限制,任何人可以以任何方式使用本文。 https://blog.csdn.net/crazycoder8848/article/details/46333761
 本文通過學習RealTek8169/8168/8101網卡的驅動代碼(drivers/net/r8169.c),梳理一下Linux下網卡的收包過程。

在下水平相當有限,有不當之處,還請大家斧正^_^

 

驅動的初始化
如下的rtl8169_init_module函數是此驅動的初始化代碼,此函數只干了一件事,就是向內核注冊一個pci驅動rtl8169_pci_driver。

static int __init rtl8169_init_module(void)

{

        returnpci_register_driver(&rtl8169_pci_driver);

}

 

rtl8169_pci_driver驅動的定義如下。

static struct pci_driver rtl8169_pci_driver= {

        .name               = MODULENAME,

        .id_table  = rtl8169_pci_tbl,

        .probe              = rtl8169_init_one,

        .remove            = __devexit_p(rtl8169_remove_one),

        .shutdown       = rtl_shutdown,

        .driver.pm        = RTL8169_PM_OPS,

};

 

.id_table成員是一個驅動程序支持的全部設備列表。對於rtl8169_pci_driver,id_tabl就是b rtl8169_pci_tbl了,其內容如下。可見此驅動支持多種不同型號的網卡芯片。

 

static struct pci_device_idrtl8169_pci_tbl[] = {

        {PCI_DEVICE(PCI_VENDOR_ID_REALTEK, 0x8129),0, 0, RTL_CFG_0 },

        {PCI_DEVICE(PCI_VENDOR_ID_REALTEK, 0x8136),0, 0, RTL_CFG_2 },

        {PCI_DEVICE(PCI_VENDOR_ID_REALTEK, 0x8167),0, 0, RTL_CFG_0 },

        {PCI_DEVICE(PCI_VENDOR_ID_REALTEK, 0x8168),0, 0, RTL_CFG_1 },

        {PCI_DEVICE(PCI_VENDOR_ID_REALTEK, 0x8169),0, 0, RTL_CFG_0 },

        {PCI_DEVICE(PCI_VENDOR_ID_DLINK,       0x4300),0, 0, RTL_CFG_0 },

        {PCI_DEVICE(PCI_VENDOR_ID_AT,               0xc107),0, 0, RTL_CFG_0 },

        {PCI_DEVICE(0x16ec,                         0x0116),0, 0, RTL_CFG_0 },

        {PCI_VENDOR_ID_LINKSYS,  0x1032,   PCI_ANY_ID, 0x0024, 0, 0, RTL_CFG_0 },

        {0x0001,        0x8168,         PCI_ANY_ID, 0x2410, 0, 0, RTL_CFG_2 },

        {0,},

};

 

需要注意到,驅動中還有如下一行代碼。

MODULE_DEVICE_TABLE(pci, rtl8169_pci_tbl);

這個宏貌似是給rtl8169_pci_tbl變量起了一個別名__mod_pci_device_table。可見__mod_pci_device_table是pci設備驅動中的一個統一的符號名。他包含了此驅動支持的全部設備的列表。

這是干什么的呢?這時解釋一下。

如果此驅動被編譯到了內核中,或者此驅動已經被加載到內核中。那么這一句話就沒什么作用了。因為內核隨時可以根據rtl8169_pci_driver中的信息,來判斷某一設備是否匹配此驅動。代碼見pci_match_device函數。

但是,如果此驅動被編譯成了一個模塊文件r8169.ko,並且沒有被加載到內核中(正常情況下,大量的設備驅動都應該是被編譯成模塊的,並且都是不加載到內核中的。機器上電時,根據掃描到的設備,動態加載相應的驅動模塊。不然的話,如果各種驅動都加載到內核中,那內核就太臃腫了)。此時,如果內核掃描到了一個pci設備,就得加載相應的驅動模塊文件。但內核只掌握了此設備的類似Vendorand device ID這樣的信息,如何將這種信息對應到具體的驅動模塊文件r8169.ko呢。這時候MODULE_DEVICE_TABLE這句話就發揮作用了。具體細節,可以參考udev與modprobe等相關知識。

 

這里順便多說兩句,當一個pci驅動被加載到內核中時(見調用鏈pci_register_driver ->__pci_register_driver -> driver_register ->bus_add_driver ->driver_attach),或者當內核發現一個新設備時(見調用鏈device_add->bus_probe_device->device_attach),都會做一次驅動與設備的匹配操作。

 

probe一塊網卡rtl8169_init_one
當某一塊網卡匹配了rtl8169_pci_driver時,rtl8169_pci_driver. probe函數(即rtl8169_init_one)即被調用,此函數針對此網卡做一些初始化操作,然后此網卡就可用了。

這里順便說一下,一個設備,如何與業務流程關聯起來。不同的設備,可能是不一樣的。

例如,有些設備(如看門狗設備,塊設備),是在文件系統中創建一個文件(如/dev/ watchdog)。業務通過打開設備文件,操作/讀/寫設備文件,就將設備用起來了。

而網卡設備,則不是這樣。網卡設備是向內核注冊一個struct  net_device結構。注冊以后,ifconfig命令就能看到此網卡了。內核協議棧及路由系統也就與此net_device結構關聯起來了。struct  net_device結構,是內核對網絡設備的一種抽象,他使得內核可以用統一的方式操作一切網絡設備。

 

下面看看rtl8169_init_one的主要任務:

l  將網卡配置寄存器區間映射到內核虛存空間

l  執行硬件初始化

l  構建一個net_device結構,注冊到內核中

 

 

這里需要多說的是net_device結構的構建。net_device結構類似於面向對象編程中的多態。前面說過,struct  net_device結構,是內核對網絡設備的一種抽象,他使得內核可以用統一的方式操作一切網絡設備。具體的網卡驅動,如何各自以不同的方法實現自己的功能呢。每個net_device結構上,除了通用的內容外,還有一片私有空間用於保存各個網卡的私有數據。通過netdev_priv函數即可得到一個net_device結構的私有空間。R8169驅動就在這個私有空間中保存了一個struct rtl8169_private結構,用於保存R8169系列網卡的私有數據。這里就不詳細說明了,但后面會根據需要提到其中的某些成分。

 

net_device結構中包含一個指針netdev_ops,指向一個struct net_device_ops結構,此結構中包含了指向網卡的各種操作的函數指針。這種設計使得內核可以對於任何網卡,看到一個統一的操作界面。不同的網卡驅動,將自己實現的各種操作的函數指針填到一個net_device_ops結構中,然后將此結構的地址填到net_device結構的netdev_ops指針中即可。

對於R8169驅動,這個net_device_ops結構就是rtl8169_netdev_ops。

 

打開網卡rtl8169_open
當用戶執行ifconfig  eth0 up命令啟動一個網卡時,網卡對應的net_device的netdev_ops->ndo_open函數被調用(調用鏈:sys_ioctl->do_vfs_ioctl->vfs_ioctl->sock_ioctl->dev_ioctl->dev_ifsioc ->dev_change_flags->dev_open),對於R8169驅動來說就是rtl8169_open函數。

 

這里為理解收包過程,列出rtl8169_open中的部分操作:

 

1)    申請一個struct RxDesc類型的數組空間,地址保存到rtl8169_private結構的RxDescArray成員中。

struct RxDesc結構用於描述一個buffer,主要是包含一個buffer的物理地址與長度。

rtl8169_private結構的RxDescArray成員就存放了RxDesc數組的起始物理地址。接下來,代碼會預先申請一些buffer(rtl8169_rx_fill函數中實現,最終是調用__alloc_skb分配的buffer。Tcp發送數據時,最終也是調用__alloc_skb分配buffer的,可以參考tcp_sendmsg函數),然后將這些buffer的物理地址及長度記錄到RxDesc數組中,以供硬件收包使用。

 

2)    申請一個struct sk_buff *類型的數組空間,地址保存到rtl8169_private.Rx_skbuff成員中。

上面提到的預先申請的那么buffer,其內核態虛擬地址均記錄到此數組中。這樣的話,硬件將報文輸出到buffer中后,驅動能夠獲取到相應的buffer地址,將報文傳入內核協議棧。從代碼來看,buffer存放一個報文。

 

3)    注冊中斷處理函數rtl8169_interrupt

當網卡收到報文時,內核的框架代碼最終會調用到這里注冊的中斷處理函數。

 

4)    enable網卡的napi

 

5)    啟動網卡

這里涉及諸多硬件操作,我們的主要關注點是,1)中提到的物理地址通過rtl_set_rx_tx_desc_registers函數(調用鏈rtl8169_open->rtl_hw_start->rtl_hw_start_8169->rtl_set_rx_tx_desc_registers)寫給了硬件。

這樣一來,這就等於通過RxDesc數組,等於向硬件提供了一組buffer的信息。從而讓硬件將收到的報文輸出到這些buffer中

 

中斷處理
當中斷發生時,硬件已經將報文輸出到了前面所說的預先申請的buffer中了。此時,系統的中斷處理機制最終會調用rtl8169_interrupt進行中斷處理。

這里為理解收包過程,列出rtl8169_interrupt所做的部分操作:

l  處理中斷硬件層面相關工作

l  調用__napi_schedule將網卡的rtl8169_private.napi結構掛入__get_cpu_var(softnet_data).poll_list鏈表。

l  調用__raise_softirq_irqoff(NET_RX_SOFTIRQ);讓軟中斷處理線程ksoftirqd被調度執行,此線程將負責完成報文的接收。

 

軟中斷處理線程ksoftirqd
前面說了,網卡中斷發生后,會觸發軟中斷處理線程ksoftirqd被調度執行,而此線程將會負責完成報文的接收。那么此線程是個什么東東呢?這里先簡單介紹一下。

每個核上,都會創建一個ksoftirqd線程,專門負責處理軟中斷。

如果沒有配置CONFIG_PREEMPT_SOFTIRQS,則ksoftirqd 線程是在cpu_callback中通過如下代碼創建的。可見這種情況下,線程的處理函數就是ksoftirqd。

 

kthread_create(ksoftirqd, hcpu,"ksoftirqd/%d", hotcpu);

 

通過如下命令,可以查看當前機器上ksoftirqd線程的創建情況。

 

[root@A22770684 VMB]# ps -ef | grep irq

root        4     2  0 May18 ?        00:00:00 [ksoftirqd/0]

root        9     2  0 May18 ?        00:00:00 [ksoftirqd/1]

 

ksoftirqd軟中斷處理線程並不是專門負責網卡設備的軟中斷處理,他還負責其他各種設備的軟中斷處理。

 

內核的各個子系統,通過open_softirq注冊相應的軟中斷處理條目。

下面是網絡系統與塊設備系統注冊軟中斷處理條目的代碼。

open_softirq(BLOCK_IOPOLL_SOFTIRQ,blk_iopoll_softirq);

open_softirq(BLOCK_SOFTIRQ,blk_done_softirq);

open_softirq(NET_TX_SOFTIRQ,net_tx_action);

open_softirq(NET_RX_SOFTIRQ, net_rx_action);

 

open_softirq的代碼如下。由此可見,每個條目,其實就是一個軟中斷處理函數。那么網卡收包軟中斷就對應net_rx_action函數了。

void open_softirq(int nr, void(*action)(struct softirq_action *))

{

        softirq_vec[nr].action= action;

}

 

Ksoftirqd最終調用__do_softirq中完成各種軟中斷任務的處理。

__do_softirq 遍歷softirq_vec數組,執行每個條目的action。

對網卡收包來說,action就是net_rx_action函數了。

 

網卡收包
前面說了,網卡中斷發生后,會觸發軟中斷處理線程ksoftirqd被調度執行,而此線程將會負責完成報文的接收。具體如何接收呢,從前面的介紹可以知道,對ksoftirqd來說,其實就是調用net_rx_action函數而已。

下面看看net_rx_action函數的工作:

前面說過,中斷來了,網卡驅動將自己的napi結構掛到了__get_cpu_var(softnet_data).poll_list鏈表中。那么net_rx_action的核心工作,就是從鏈表中一一取出其中的napi結構,執行napi結構中的poll成員所指向的函數。為什么是一一取區呢?因為可能不止一塊網卡在發生了中斷后,將自己的napi結構掛進了鏈表。

對於R8169驅動來說,其napi結構poll成員指向的函數就是rtl8169_poll。這是在rtl8169_init_one中設置好的。實際上,rtl8169_poll中既做報文接收工作又做報文發送完成后的善后工作。從代碼來看,rtl8169_start_xmit負責發送工作,代碼中將要發送的報文的buffer信息填入rtl8169_private.TxDescArray數組中,然后寫寄存器(RTL_W8(TxPoll, NPQ);)通知硬件發包。硬件完成發送后,同樣會上報中斷。然后rtl8169_poll調用rtl8169_tx_interrupt對rtl8169_private.TxDescArray中的buffer描述信息置空,以供未來新的報文發送使用。因此,不管是收,還是發,最終都是產生中斷,然后由rtl8169_interrupt中斷處理將流程轉入軟中斷處理線程ksoftirqd,再由軟中斷進入rtl8169_poll函數處理。

這里,我們只看接收相關的代碼。很明顯,接收工作是由rtl8169_rx_interrupt函數完成的。

我們這里不看硬件相關的代碼,只分析純粹的收包相關的代碼。

前面提到,為了收包,預先申請了一批buffer。這些buffer的信息,存在了如下兩個數組中。

第一個是給硬件看的,第二個是給驅動看的。

rtl8169_private.RxDescArray

rtl8169_private.Rx_skbuff

 

rtl8169_private.Rx_skbuff就是一個環型數組,每個元素就是一個指向struct sk_buff結構的指針。rtl8169_rx_interrupt遍歷此數組,取出其中的一個個報文,調用協議棧報文接收函數netif_receive_skb即可。

從代碼實現來看,驅動總是先嘗試重新申請一個sk_buff,將硬件接收buffer中的報文拷出來。但是,如果拷貝失敗,那就不拷了,直接將硬件接收buffer中的報文轉入協議棧接收流程。代碼這樣做,可能是不想重新申請buffer給硬件接收使用。

當拷貝失敗時,由於代碼直接將硬件接收buffer中的報文轉入協議棧接收流程。這樣的話,這個buffer就不能再繼續用作硬件接收buffer了。因此對於這種情況,代碼就將rtl8169_private.Rx_skbuff[idx]置成NULL。這樣的話,可用的硬件接收buffer就變少了。為了應對這種情況,rtl8169_rx_interrupt函數尾部會調用rtl8169_rx_fill嘗試重新將接收buffer補滿。

 

內核協議棧對報文的接收
前面看到,網卡驅動調用netif_receive_skb,將處理流程轉入內核協議棧。

netif_receive_skb先跳過一些簡單的和不用關心的代碼,從下面的地方開始看。

可見,如果接收端口是一個bond的成員口,則skb中的接收端口skb->dev需要換成接口端口的master,即bond口。但也未必總是會換,因為有時候成員口還未起來,但是收到一些雜包,這時候這些雜包不屬於bond口的流量,因此不換。

 

        null_or_orig= NULL;

        orig_dev= skb->dev;

        if(orig_dev->master) {

                 if(skb_bond_should_drop(skb))

                         null_or_orig = orig_dev; /*deliver only exact match */

                 else

                         skb->dev= orig_dev->master;

        }

 

接下來,先通過如下代碼將報文送達可能存在的raw socket(PF_PACKET協議族)。

        list_for_each_entry_rcu(ptype,&ptype_all, list) {

                 if(ptype->dev == null_or_orig || ptype->dev == skb->dev ||

                     ptype->dev == orig_dev) {

                         if(pt_prev)

                                  ret= deliver_skb(skb, pt_prev, orig_dev);

                         pt_prev= ptype;

                 }

        }

 

這些報文接收條目是通過dev_add_pack注冊的。

 

接下來,將報文傳遞給bridge處理。如果這里一步返回了0,報文就不往下走了。

        skb= handle_bridge(skb, &pt_prev, &ret, orig_dev);

        if(!skb)

                 gotoout;

 

否則,通過如下代碼,將報文傳達給各個協議處理。

        type= skb->protocol;

 

        list_for_each_entry_rcu(ptype,

                         &ptype_base[ntohs(type)& PTYPE_HASH_MASK], list) {

                 if(ptype->type == type &&

                     (ptype->dev == null_or_orig ||ptype->dev == skb->dev ||

                      ptype->dev == orig_dev)) {

                         if(pt_prev)

                                  ret= deliver_skb(skb, pt_prev, orig_dev);

                         pt_prev= ptype;

                 }

        }

 

這里的各個接收條目,也是通過dev_add_pack注冊的。看看其代碼,報文接收條目有兩種,一種是全接收,一種是單收。

 

void dev_add_pack(struct packet_type *pt)

{

        inthash;

 

        spin_lock_bh(&ptype_lock);

        if(pt->type == htons(ETH_P_ALL))

                 list_add_rcu(&pt->list,&ptype_all);

        else{

                 hash= ntohs(pt->type) & PTYPE_HASH_MASK;

                 list_add_rcu(&pt->list,&ptype_base[hash]);

        }

        spin_unlock_bh(&ptype_lock);

}

 

來看看IP協議的接收條目的定義:

static struct packet_type ip_packet_type__read_mostly = {

        .type= cpu_to_be16(ETH_P_IP),

        .func= ip_rcv,

        .gso_send_check= inet_gso_send_check,

        .gso_segment= inet_gso_segment,

        .gro_receive= inet_gro_receive,

        .gro_complete= inet_gro_complete,

};

 

順便也看看arp協議的接收條目定義(arp的學習就是通過arp_rcv完成的, arp的查找則是通過neigh_lookup接口):

static struct packet_type arp_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_ARP),
.func = arp_rcv,
};

 

從ip_packet_type可知,IP報文接收的入口是ip_rcv

如果是本機接收,主處理調用鏈如下:

ip_rcv->ip_rcv_finish-> dst_input->skb_dst(skb)->input (即ip_local_deliver)

ip_local_deliver主要是根據協議,選擇一個協議來處理。

 

hash = protocol & (MAX_INET_PROTOS -1);

ipprot =rcu_dereference(inet_protos[hash]);

ipprot->handler(skb);

 

這些協議是通過inet_add_protocol注冊的。例如,UDP協議的注冊通過如下代碼。

inet_add_protocol(&udp_protocol,IPPROTO_UDP)

 

udp_protocol的定義如下:

static const struct net_protocoludp_protocol = {

        .handler=        udp_rcv,

        .err_handler= udp_err,

        .gso_send_check= udp4_ufo_send_check,

        .gso_segment= udp4_ufo_fragment,

        .no_policy=   1,

        .netns_ok=     1,

};

 

可見UDP的接收函數是udp_rcv

如果是一般的UDP,接收過程如下:

sock_queue_rcv_skb

調用udp_rcv ->__udp4_lib_rcv->udp_queue_rcv_skb ->__udp_queue_rcv_skb ->sock_queue_rcv_skb ->sk->sk_data_ready(即sock_def_readable)

最后一個函數sock_def_readable用於喚醒因讀取socket進入睡眠的線程。
---------------------
作者:孫明保
來源:CSDN
原文:https://blog.csdn.net/crazycoder8848/article/details/46333761
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM