標籤:
本文通過學習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線程的建立情況。
[[email protected] 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,
};
可見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進入睡眠的線程。
代碼學習-Linux核心網卡收包過程(NAPI)