轉自: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
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!
