前言:在Linux網(wǎng)絡(luò)編程中,網(wǎng)絡(luò)包接收指的是主機(jī)從網(wǎng)絡(luò)上接收到一個數(shù)據(jù)包。它可以是來自其他計算機(jī)或設(shè)備發(fā)送的數(shù)據(jù)包,也可以是回環(huán)地址(localhost)上本地發(fā)送的數(shù)據(jù)包。
當(dāng)一個網(wǎng)絡(luò)包被接收時, 它經(jīng)過了多個層次的處理:
首先,在物理層,網(wǎng)卡會檢測到數(shù)據(jù)包,并將其傳遞給操作系統(tǒng)內(nèi)核。
然后,在網(wǎng)絡(luò)協(xié)議棧中,內(nèi)核會對數(shù)據(jù)包進(jìn)行解析和處理。它可能會檢查目標(biāo)IP地址、端口號等信息,并根據(jù)規(guī)則進(jìn)行路由、過濾或轉(zhuǎn)發(fā)操作。
最終,當(dāng)數(shù)據(jù)包成功被接收并處理后,應(yīng)用程序可以通過讀取套接字(socket)來獲取其中的數(shù)據(jù)內(nèi)容。通過監(jiān)聽和接收網(wǎng)絡(luò)包,我們可以實現(xiàn)各種功能,如實時通信、網(wǎng)絡(luò)監(jiān)控、報文分析等。
-----------------------------零聲白金卡限時活動 ---------------------------------
我自己學(xué)C++,填了一個坑又一個坑,深知新手學(xué)習(xí)C/C++的重要性和疑難問題,因此特地給C/C++開發(fā)的同學(xué)精心準(zhǔn)備了一份優(yōu)惠優(yōu)質(zhì)學(xué)習(xí)卡——零聲白金卡(https:// xxetb.xet.tech/s/3wrN44 購買地址) ,6個項目分別是:基礎(chǔ)架構(gòu)-KV存儲項目、spdk文件系統(tǒng)實現(xiàn)項目、Linux內(nèi)核內(nèi)存管理實戰(zhàn)案例分析、golang云原生、FFmpeg+SDL播放器開發(fā)實站QtMP3音樂播放器搜索引擎實戰(zhàn),提供項目源碼下載,同時這份資料也包括 C/C++學(xué)習(xí)路線、簡歷指導(dǎo)和求職技巧等。
一、網(wǎng)卡接收 當(dāng)網(wǎng)絡(luò)包到達(dá)網(wǎng)卡時,網(wǎng)卡會將數(shù)據(jù)包存儲到接收緩沖區(qū)中。網(wǎng)卡通常使用DMA(Direct Memory Access)來直接將數(shù)據(jù)復(fù)制到主內(nèi)存,減少CPU的參與。
網(wǎng)卡本身是有內(nèi)存的,每個網(wǎng)卡一般都有4K以上的內(nèi)存,用來發(fā)送,接收數(shù)據(jù)。
數(shù)據(jù)在從主內(nèi)存搬到網(wǎng)卡之后,不是立即就能被發(fā)送出去的,而是要先在網(wǎng)卡自身的內(nèi)存中排隊,再按照先后順序發(fā)送;同樣的,數(shù)據(jù)從以太網(wǎng)傳遞到網(wǎng)卡時,網(wǎng)卡也是先把數(shù)據(jù)存儲到自身的內(nèi)存中,等到收到一幀數(shù)據(jù)了,再經(jīng)過中斷的方式,告訴主CPU(不是網(wǎng)卡本身的微處理器)把網(wǎng)卡內(nèi)存的數(shù)據(jù)讀走,而讀走后的內(nèi)存,又被清空,再次被使用,用來接收新的數(shù)據(jù),如此循環(huán)往復(fù)。
而網(wǎng)卡本身的內(nèi)存,又多是按照256字節(jié)為1頁的方式,把所有內(nèi)存分頁,之后把這些頁組成隊列,大致的結(jié)構(gòu)如圖:
藍(lán)色部分為發(fā)送數(shù)據(jù)用的頁面總和,總共只有6個頁面用于發(fā)送數(shù)據(jù)(40h~45h);剩余的46h~80h都是接收數(shù)據(jù)用的,而在接收數(shù)據(jù)內(nèi)存中,只有紅色部分是有數(shù)據(jù)的,當(dāng)接收新的數(shù)據(jù)時,是向紅色部分前面的綠色中的256字節(jié)寫入數(shù)據(jù),同時“把當(dāng)前指針”移動到+256字節(jié)的后面(網(wǎng)卡自動完成),而現(xiàn)在要讀的數(shù)據(jù),是在“邊界指針”那里開始的256字節(jié)(紫色部分),下一個要讀的數(shù)據(jù),是在“下一包指針”的位置開始的256字節(jié),當(dāng)256字節(jié)被讀出來了,就變成了重新可以使用的內(nèi)存,即綠色所表示,而接收數(shù)據(jù),就是把可用的內(nèi)存拿來用,即變成了紅色,當(dāng)數(shù)據(jù)寫到了0x80h后,又從0x46h開始寫數(shù)據(jù),這樣循環(huán),如果數(shù)據(jù)滿了,則網(wǎng)卡就不能再接收數(shù)據(jù),必須等待數(shù)據(jù)被讀出去了,才能再繼續(xù)接收。
下面是一些網(wǎng)卡常用的寄存器:
CR(command register)---命令寄存器
TSR(transmit state register)---發(fā)送狀態(tài)寄存器
ISR(interrupt state register)----中斷狀態(tài)寄存器
RSR(receive state register)---接收狀態(tài)寄存器
RCR(receive configure register)---接收配置寄存器
TCR(transmit configure register)---發(fā)送配置寄存器
DCR(data configure register)---數(shù)據(jù)配置寄存器
IMR(interrupt mask register)---中斷屏蔽寄存器
NCR(non-coding region)---包發(fā)送期間碰撞次數(shù)
FIFO(first in first out)
CNTR0(counter register)--- 幀同步錯總計數(shù)器
CNTR1---CRC錯總計數(shù)器
CNTR2---丟包總計數(shù)器
PAR0~5(physical address register)---本地MAC地址
MAR0~7(multiple address register)---多播地址匹配
PSTOP(page stop register)---結(jié)束頁面寄存器
PSTART(page start register)---開始頁面寄存器
BNRY(boundary register)----邊界頁寄存器
CURR(current page register)---當(dāng)前頁面寄存器
CLDA0,1(Current Local DMA Address)---當(dāng)前本地DMA寄存器
TPSR(Transmit page start register)---傳送頁面開始寄存器
TBCR0,1(transmit byte counter register)---傳送字節(jié)計數(shù)寄存器
CRDA0,1(current remote DMA address)---當(dāng)前遠(yuǎn)程DMA寄存器
RSAR0,1(remote start address register)---遠(yuǎn)程DMA起始地址寄存器
RBCR0,1(remote byte counter register)---遠(yuǎn)程字節(jié)計數(shù)寄存器
BPAGE(BROM page register)---BROM頁面寄存器
1.1框架 網(wǎng)絡(luò)子系統(tǒng)中,在本文中我們關(guān)注的是驅(qū)動和內(nèi)核的交互。也就是網(wǎng)卡收到數(shù)據(jù)包后怎么交給內(nèi)核,內(nèi)核收到數(shù)據(jù)包后怎么交給協(xié)議棧處理。
在內(nèi)核中,網(wǎng)卡設(shè)備是被net_device結(jié)構(gòu)體描述的。驅(qū)動需要通過net_device向內(nèi)核注冊一組操作網(wǎng)卡硬件的函數(shù),這樣內(nèi)核便可以使用網(wǎng)卡了。而所有的數(shù)據(jù)包在內(nèi)核空間都是使用sk_buff結(jié)構(gòu)體來表示,所以將網(wǎng)卡硬件收到的數(shù)據(jù)轉(zhuǎn)換成內(nèi)核認(rèn)可的skb_buff也是驅(qū)動的工作。
在這之后,還有兩個結(jié)構(gòu)體也發(fā)揮了非常重要的作用。一個是為struct softnet_data,另一個是struct napi_struct。為軟中斷的方式處理數(shù)據(jù)包提供了支持。
1.2初始化 一切的起源都是上電那一刻,當(dāng)系統(tǒng)初始化完畢后,我們的系統(tǒng)就應(yīng)該是可用的了。網(wǎng)絡(luò)子模塊的初始化也是在Linux啟動經(jīng)歷兩階段的混沌boost自舉后,進(jìn)入的第一個C函數(shù)start_kernel。在這之前是Bootloader和Linux的故事,在這之后,便是Linux的單人秀了。
網(wǎng)絡(luò)子設(shè)備初始化調(diào)用鏈:start_kernel->rest_init->kernel_init->kernel_init_freeable->do_basic_setup->do_initcalls->do_initcalls->net_dev_init。
上面調(diào)用關(guān)系中的kernel_init是一個內(nèi)核子線程中調(diào)用的:
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
然后再一個問題就是當(dāng)進(jìn)入do_initcalls
后我們會發(fā)現(xiàn)畫風(fēng)突變:
static void __init do_initcalls(void) { int level; for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) do_initcall_level(level); }
我是誰,我來自哪,我要到哪去。
如果do_initcalls
還給了我們一絲看下去的希望,點開do_initcall_level
可能就真的絕望了。
static void __init do_initcall_level(int level) { initcall_t *fn; strcpy(initcall_command_line, saved_command_line); parse_args(initcall_level_names[level], initcall_command_line, __start___param, __stop___param - __start___param, level, level, NULL, &repair_env_string); trace_initcall_level(initcall_level_names[level]); for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++) do_one_initcall(*fn); }
全局一個fn指針,實現(xiàn)調(diào)用全靠猜。反正我不管,我說調(diào)用了net_dev_init就是調(diào)用了。偉大的google告訴我只要被下面這些宏定義包裹的函數(shù)就會被do_one_initcall調(diào)用,用了什么黑科技,先不管:
#file:include/linux/init.h #define pure_initcall(fn) __define_initcall(fn, 0) #define core_initcall(fn) __define_initcall(fn, 1) #define core_initcall_sync(fn) __define_initcall(fn, 1s) #define postcore_initcall(fn) __define_initcall(fn, 2) #define postcore_initcall_sync(fn) __define_initcall(fn, 2s) #define arch_initcall(fn) __define_initcall(fn, 3) #define arch_initcall_sync(fn) __define_initcall(fn, 3s) #define subsys_initcall(fn) __define_initcall(fn, 4) #define subsys_initcall_sync(fn) __define_initcall(fn, 4s) #define fs_initcall(fn) __define_initcall(fn, 5) #define fs_initcall_sync(fn) __define_initcall(fn, 5s) #define rootfs_initcall(fn) __define_initcall(fn, rootfs) #define device_initcall(fn) __define_initcall(fn, 6) #define device_initcall_sync(fn) __define_initcall(fn, 6s) #define late_initcall(fn) __define_initcall(fn, 7) #define late_initcall_sync(fn) __define_initcall(fn, 7s)
在net_dev_init的定義下面,我們可以找到subsys_initcall(net_dev_init);。Ok,網(wǎng)絡(luò)子系統(tǒng)的初始化入口已找到到。
static int __init net_dev_init(void) { int i, rc = -ENOMEM; BUG_ON(!dev_boot_phase); if (dev_proc_init()) goto out; if (netdev_kobject_init()) goto out; INIT_LIST_HEAD(&ptype_all); for (i = 0; i < PTYPE_HASH_SIZE; i++) INIT_LIST_HEAD(&ptype_base[i]); INIT_LIST_HEAD(&offload_base); if (register_pernet_subsys(&netdev_net_ops)) goto out; /* * Initialise the packet receive queues. */ for_each_possible_cpu(i) { struct work_struct *flush = per_cpu_ptr(&flush_works, i); struct softnet_data *sd = &per_cpu(softnet_data, i); INIT_WORK(flush, flush_backlog); skb_queue_head_init(&sd->input_pkt_queue); skb_queue_head_init(&sd->process_queue); #ifdef CONFIG_XFRM_OFFLOAD skb_queue_head_init(&sd->xfrm_backlog); #endif INIT_LIST_HEAD(&sd->poll_list); sd->output_queue_tailp = &sd->output_queue; #ifdef CONFIG_RPS sd->csd.func = rps_trigger_softirq; sd->csd.info = sd; sd->cpu = i; #endif sd->backlog.poll = process_backlog; sd->backlog.weight = weight_p; } dev_boot_phase = 0; /* The loopback device is special if any other network devices * is present in a network namespace the loopback device must * be present. Since we now dynamically allocate and free the * loopback device ensure this invariant is maintained by * keeping the loopback device as the first device on the * list of network devices. Ensuring the loopback devices * is the first device that appears and the last network device * that disappears. */ if (register_pernet_device(&loopback_net_ops)) goto out; if (register_pernet_device(&default_device_ops)) goto out; open_softirq(NET_TX_SOFTIRQ, net_tx_action); open_softirq(NET_RX_SOFTIRQ, net_rx_action); rc = cpuhp_setup_state_nocalls(CPUHP_NET_DEV_DEAD, "net/dev:dead", NULL, dev_cpu_dead); WARN_ON(rc < 0); rc = 0; out: return rc; }
在net_dev_init中,初始化了內(nèi)核收發(fā)包隊列,開啟了對應(yīng)的軟中斷NET_TX_SOFTIRQ和NET_RX_SOFTIRQ。在其中,該函數(shù)為每個CPU初始化了一個softnet_data來掛載需要處理設(shè)備的napi_struct。這個結(jié)構(gòu)非常重要,軟中斷的處理就是從這個鏈表上取napi_struct,然后收包的。這也是內(nèi)核和驅(qū)動的接口之一。
再就是開啟的兩個軟中斷,當(dāng)驅(qū)動在硬終端完成必要的上半部工作后,就會拉起對應(yīng)的軟中斷。讓數(shù)據(jù)包下半部軟中斷中處理。
net_dev_init執(zhí)行完后,我們內(nèi)核就有了處理數(shù)據(jù)包的能力,只要驅(qū)動能向softnet_data掛載需要收包設(shè)備的napi_struct。內(nèi)核子線程ksoftirqd便會做后續(xù)的處理。接下來就是網(wǎng)卡驅(qū)動的初始化了。
各種網(wǎng)卡肯定有不同的驅(qū)動,各驅(qū)動封裝各自硬件的差異,給內(nèi)核提供一個統(tǒng)一的接口。我們這不關(guān)心,網(wǎng)卡驅(qū)動是怎么把數(shù)據(jù)發(fā)出去的,如何收回來的。而是探究網(wǎng)卡收到數(shù)據(jù)了,要怎么交給內(nèi)核,內(nèi)核如何將要發(fā)的數(shù)據(jù)給網(wǎng)卡??傊?,驅(qū)動需要給內(nèi)核提供哪些接口,內(nèi)核又需要給網(wǎng)卡哪些支持。我們以e1000網(wǎng)卡為例子??纯此蛢?nèi)核的纏綿故事。
e1000網(wǎng)卡是一塊PCI設(shè)備。所以它首先得要讓內(nèi)核能通過PCI總線探測到,需要向內(nèi)核注冊一個pci_driver結(jié)構(gòu),PCI設(shè)備的使用是另一個話題,這里不會探究,我也不知道:
static struct pci_driver e1000_driver = { .name = e1000_driver_name, .id_table = e1000_pci_tbl, .probe = e1000_probe, .remove = e1000_remove, #ifdef CONFIG_PM /* Power Management Hooks */ .suspend = e1000_suspend, .resume = e1000_resume, #endif .shutdown = e1000_shutdown, .err_handler = &e1000_err_handler };
其中e1000_probe就是給內(nèi)核的探測回調(diào)函數(shù),算是網(wǎng)卡的初始化函數(shù)吧,驅(qū)動需要在這里初始化網(wǎng)卡設(shè)備。去掉總線相關(guān)的代碼,錯誤處理的代碼,硬件相關(guān)的代碼:
static int e1000_probe(struct pci_dev *pdev, const struct pci_device_id *ent) { struct net_device *netdev; netdev = alloc_etherdev(sizeof(struct e1000_adapter));//申請net_device設(shè)備 netdev->netdev_ops = &e1000_netdev_ops; //注冊操作設(shè)備的回調(diào)函數(shù) e1000_set_ethtool_ops(netdev); netdev->watchdog_timeo = 5 * HZ; netif_napi_add(netdev, &adapter->napi, e1000_clean, 64);//軟中斷里會調(diào)用poll鉤子函數(shù) strncpy(netdev->name, pci_name(pdev), sizeof(netdev->name) - 1); err = register_netdev(netdev); }
每一個網(wǎng)絡(luò)設(shè)備都有一個對應(yīng)的net_devie結(jié)構(gòu)體來描述。其中像設(shè)備文件操作一樣,保存了一種操作設(shè)備的接口函數(shù)netdev_ops,對e1000網(wǎng)卡是e1000_netdev_ops。當(dāng)通過終端輸入ifup,ifdowm命令操作網(wǎng)卡時,對應(yīng)的open,close函數(shù)就會被調(diào)用。這段代碼最重要的還是netif_napi_add的調(diào)用,它向內(nèi)核注冊了e1000_clean函數(shù),用來給上面的CPU收包隊列調(diào)用。
通過初始化,驅(qū)動注冊了網(wǎng)卡描述net_device, 內(nèi)核可以通過它操作到網(wǎng)卡設(shè)備。通過e1000_clean函數(shù)內(nèi)核軟中斷也可以收包了。
1.3驅(qū)動收包 前面有一個內(nèi)核軟中斷來收包,但這個軟中斷怎么觸發(fā)呢?硬中斷。當(dāng)有數(shù)據(jù)到網(wǎng)卡時,會產(chǎn)生一個硬中斷。這中斷的注冊是上面,e1000_netdev_ops中的e1000_up函數(shù)調(diào)用的。也就是網(wǎng)卡up時會注冊這個硬中斷處理函數(shù)e1000_intr。
/** * e1000_intr - Interrupt Handler * @irq: interrupt number * @data: pointer to a network interface device structure **/ static irqreturn_t e1000_intr(int irq, void *data) { struct net_device *netdev = data; struct e1000_adapter *adapter = netdev_priv(netdev); struct e1000_hw *hw = &adapter->hw; u32 icr = er32(ICR); /* disable interrupts, without the synchronize_irq bit */ ew32(IMC, ~0); E1000_WRITE_FLUSH(); if (likely(napi_schedule_prep(&adapter->napi))) { adapter->total_tx_bytes = 0; adapter->total_tx_packets = 0; adapter->total_rx_bytes = 0; adapter->total_rx_packets = 0; __napi_schedule(&adapter->napi); } else { /* this really should not happen! if it does it is basically a * bug, but not a hard error, so enable ints and continue */ if (!test_bit(__E1000_DOWN, &adapter->flags)) e1000_irq_enable(adapter); } return IRQ_HANDLED; }
去掉unlikely的代碼,其中通過if (likely(napi_schedule_prep(&adapter->napi)))測試,網(wǎng)卡設(shè)備自己的napi是否正在被CPU使用。沒有就調(diào)用__napi_schedule將自己的napi掛載到CPU的softnet_data上。這樣軟中斷的內(nèi)核線程就能輪詢到這個軟中斷。
/** * __napi_schedule - schedule for receive * @n: entry to schedule * * The entry's receive function will be scheduled to run. * Consider using __napi_schedule_irqoff() if hard irqs are masked. */ void __napi_schedule(struct napi_struct *n) { unsigned long flags; local_irq_save(flags); ____napi_schedule(this_cpu_ptr(&softnet_data), n); local_irq_restore(flags); } /* Called with irq disabled */ static inline void ____napi_schedule(struct softnet_data *sd, struct napi_struct *napi) { list_add_tail(&napi->poll_list, &sd->poll_list); __raise_softirq_irqoff(NET_RX_SOFTIRQ); //設(shè)置軟中斷標(biāo)志位NET_RX_SOFTIRQ }
這里的softnet_data就是前面net_dev_init函數(shù)為每個CPU初始化的。到這里硬件中斷就處理完了,但我們依然沒有發(fā)現(xiàn)任何有關(guān)數(shù)據(jù)包的處理,只知道了有一個napi被掛載。這是因為硬件中斷不能顯然太長,的確不會去做數(shù)據(jù)的處理工作。這些都交給軟中斷的內(nèi)核線程來處理的。
1.4內(nèi)核處理 硬中斷將一個napi結(jié)構(gòu)體甩給了內(nèi)核,內(nèi)核要怎么根據(jù)它來接收數(shù)據(jù)呢?前面說到,內(nèi)核為每個CPU核心都運行了一個內(nèi)核線程ksoftirqd。軟中斷也就是在這線程中處理的。上面的硬件中斷函數(shù)設(shè)置了NET_RX_SOFTIRQ軟中斷標(biāo)志,這個字段處理函數(shù)還記得在哪注冊的么?是的,net_dev_init中。
open_softirq(NET_TX_SOFTIRQ, net_tx_action); open_softirq(NET_RX_SOFTIRQ, net_rx_action);
顯然,后續(xù)處理肯定是由net_rx_action
來完成。
static __latent_entropy void net_rx_action(struct softirq_action *h) { struct softnet_data *sd = this_cpu_ptr(&softnet_data); unsigned long time_limit = jiffies + usecs_to_jiffies(netdev_budget_usecs); int budget = netdev_budget; LIST_HEAD(list); LIST_HEAD(repoll); local_irq_disable(); list_splice_init(&sd->poll_list, &list); local_irq_enable(); for (;;) { struct napi_struct *n; if (list_empty(&list)) { if (!sd_has_rps_ipi_waiting(sd) && list_empty(&repoll)) goto out; break; } n = list_first_entry(&list, struct napi_struct, poll_list); budget -= napi_poll(n, &repoll); //在這回調(diào)驅(qū)動的poll函數(shù),這個函數(shù)在napi中 /* If softirq window is exhausted then punt. * Allow this to run for 2 jiffies since which will allow * an average latency of 1.5/HZ. */ if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit))) { sd->time_squeeze++; break; } } local_irq_disable(); list_splice_tail_init(&sd->poll_list, &list); list_splice_tail(&repoll, &list); list_splice(&list, &sd->poll_list); if (!list_empty(&sd->poll_list)) __raise_softirq_irqoff(NET_RX_SOFTIRQ); net_rps_action_and_irq_enable(sd); out: __kfree_skb_flush(); }
上面看到budget -= napi_poll(n, &repoll);他會去調(diào)用我們驅(qū)動初始化時注冊的poll函數(shù),在e1000網(wǎng)卡中就是e1000_clean函數(shù)。
/** * e1000_clean - NAPI Rx polling callback * @adapter: board private structure **/ static int e1000_clean(struct napi_struct *napi, int budget) { struct e1000_adapter *adapter = container_of(napi, struct e1000_adapter, napi); int tx_clean_complete = 0, work_done = 0; tx_clean_complete = e1000_clean_tx_irq(adapter, &adapter->tx_ring[0]); adapter->clean_rx(adapter, &adapter->rx_ring[0], &work_done, budget);//將數(shù)據(jù)發(fā)給協(xié)議棧來處理。 if (!tx_clean_complete) work_done = budget; /* If budget not fully consumed, exit the polling mode */ if (work_done < budget) { if (likely(adapter->itr_setting & 3)) e1000_set_itr(adapter); napi_complete_done(napi, work_done); if (!test_bit(__E1000_DOWN, &adapter->flags)) e1000_irq_enable(adapter); } return work_done; }
e1000_clean
函數(shù)通過調(diào)用clean_rx函數(shù)指針來處理數(shù)據(jù)包。
/** * e1000_clean_jumbo_rx_irq - Send received data up the network stack; legacy * @adapter: board private structure * @rx_ring: ring to clean * @work_done: amount of napi work completed this call * @work_to_do: max amount of work allowed for this call to do * * the return value indicates whether actual cleaning was done, there * is no guarantee that everything was cleaned */ static bool e1000_clean_jumbo_rx_irq(struct e1000_adapter *adapter, struct e1000_rx_ring *rx_ring, int *work_done, int work_to_do) { struct net_device *netdev = adapter->netdev; struct pci_dev *pdev = adapter->pdev; struct e1000_rx_desc *rx_desc, *next_rxd; struct e1000_rx_buffer *buffer_info, *next_buffer; u32 length; unsigned int i; int cleaned_count = 0; bool cleaned = false; unsigned int total_rx_bytes = 0, total_rx_packets = 0; i = rx_ring->next_to_clean; rx_desc = E1000_RX_DESC(*rx_ring, i); buffer_info = &rx_ring->buffer_info[i]; e1000_receive_skb(adapter, status, rx_desc->special, skb); napi_gro_frags(&adapter->napi); return cleaned; } /** * e1000_receive_skb - helper function to handle rx indications * @adapter: board private structure * @status: descriptor status field as written by hardware * @vlan: descriptor vlan field as written by hardware (no le/be conversion) * @skb: pointer to sk_buff to be indicated to stack */ static void e1000_receive_skb(struct e1000_adapter *adapter, u8 status, __le16 vlan, struct sk_buff *skb) { skb->protocol = eth_type_trans(skb, adapter->netdev); if (status & E1000_RXD_STAT_VP) { u16 vid = le16_to_cpu(vlan) & E1000_RXD_SPC_VLAN_MASK; __vlan_hwaccel_put_tag(skb, htons(ETH_P_8021Q), vid); } napi_gro_receive(&adapter->napi, skb); }
這個函數(shù)太長,我就保留了e1000_receive_skb函數(shù)的調(diào)用,它調(diào)用了napi_gro_receive,這個函數(shù)同樣是NAPI提供的函數(shù),我們的skb從這里調(diào)用到netif_receive_skb協(xié)議棧的入口函數(shù)。調(diào)用路徑是napi_gro_receive->napi_frags_finish->netif_receive_skb_internal->__netif_receive_skb。具體的流程先放放。
畢竟NAPI是內(nèi)核為了提高網(wǎng)卡收包性能而設(shè)計的一套框架。這就可以讓我先挖個坑以后在分析NAPI的時候在填上??傊辛薔API后的收包流程和之前的區(qū)別如圖:
到這里,網(wǎng)卡驅(qū)動到協(xié)議棧入口的處理過程就寫完了。
1.5接收網(wǎng)絡(luò)包 硬件網(wǎng)卡接收到網(wǎng)絡(luò)包之后,通過 DMA 技術(shù),將網(wǎng)絡(luò)包放入 Ring Buffer。
硬件網(wǎng)卡通過中斷通知 CPU 新的網(wǎng)絡(luò)包的到來。網(wǎng)卡驅(qū)動程序會注冊中斷處理函數(shù) ixgb_intr。
中斷處理函數(shù)處理完需要暫時屏蔽中斷的核心流程之后,通過軟中斷 NET_RX_SOFTIRQ 觸發(fā)接下來的處理過程。 NET_RX_SOFTIRQ 軟中斷處理函數(shù) net_rx_action,net_rx_action 會調(diào)用 napi_poll,進(jìn)而調(diào)用 ixgb_clean_rx_irq,從 Ring Buffer 中讀取數(shù)據(jù)到內(nèi)核 struct sk_buff。
調(diào)用 netif_receive_skb 進(jìn)入內(nèi)核網(wǎng)絡(luò)協(xié)議棧,進(jìn)行一些關(guān)于 VLAN 的二層邏輯處理后,調(diào)用 ip_rcv 進(jìn)入三層 IP 層。在 IP 層,會處理 iptables 規(guī)則,然后調(diào)用 ip_local_deliver,交給更上層 TCP 層。在 TCP 層調(diào)用 tcp_v4_rcv。
NAPI:,就是當(dāng)一些網(wǎng)絡(luò)包到來觸發(fā)了中斷,內(nèi)核處理完這些網(wǎng)絡(luò)包之后,我們可以先進(jìn)入主動輪詢 poll 網(wǎng)卡的方式,主動去接收到來的網(wǎng)絡(luò)包。如果一直有,就一直處理,等處理告一段落,就返回干其他的事情。當(dāng)再有下一批網(wǎng)絡(luò)包到來的時候,再中斷,再輪詢 poll。這樣就會大大減少中斷的數(shù)量,提升網(wǎng)絡(luò)處理的效率。 硬件網(wǎng)卡接收到網(wǎng)絡(luò)包之后,通過 DMA 技術(shù),將網(wǎng)絡(luò)包放入 Ring Buffer; 硬件網(wǎng)卡通過中斷通知 CPU 新的網(wǎng)絡(luò)包的到來; 網(wǎng)卡驅(qū)動程序會注冊中斷處理函數(shù) ixgb_intr; 中斷處理函數(shù)處理完需要暫時屏蔽中斷的核心流程之后,通過軟中斷 NET_RX_SOFTIRQ 觸發(fā)接下來的處理過程; NET_RX_SOFTIRQ 軟中斷處理函數(shù) net_rx_action,net_rx_action 會調(diào)用 napi_poll,進(jìn)而調(diào)用 ixgb_clean_rx_irq,從 Ring Buffer 中讀取數(shù)據(jù)到內(nèi)核 struct sk_buff; 調(diào)用 netif_receive_skb 進(jìn)入內(nèi)核網(wǎng)絡(luò)協(xié)議棧,進(jìn)行一些關(guān)于 VLAN 的二層邏輯處理后,調(diào)用 ip_rcv 進(jìn)入三層 IP 層;
在 IP 層,會處理 iptables 規(guī)則,然后調(diào)用 ip_local_deliver 交給更上層 TCP 層;
在 TCP 層調(diào)用 tcp_v4_rcv,這里面有三個隊列需要處理,如果當(dāng)前的 Socket 不是正在被讀;取,則放入 backlog 隊列,如果正在被讀取,不需要很實時的話,則放入 prequeue 隊列,其他情況調(diào)用 tcp_v4_do_rcv; 在 tcp_v4_do_rcv 中,如果是處于 TCP_ESTABLISHED 狀態(tài),調(diào)用 tcp_rcv_established,其他的狀態(tài),調(diào)用 tcp_rcv_state_process;
在 tcp_rcv_established 中,調(diào)用 tcp_data_queue,如果序列號能夠接的上,則放入 sk_receive_queue 隊列; 如果序列號接不上,則暫時放入 out_of_order_queue 隊列,等序列號能夠接上的時候,再放入 sk_receive_queue 隊列。至此內(nèi)核接收網(wǎng)絡(luò)包的過程到此結(jié)束,接下來就是用戶態(tài)讀取網(wǎng)絡(luò)包的過程,這個過程分成幾個層次。
VFS 層:read 系統(tǒng)調(diào)用找到 struct file,根據(jù)里面的 file_operations 的定義,調(diào)用 sock_read_iter 函數(shù)。sock_read_iter 函數(shù)調(diào)用 sock_recvmsg 函數(shù)。
Socket 層:從 struct file 里面的 private_data 得到 struct socket,根據(jù)里面 ops 的定義,調(diào)用 inet_recvmsg 函數(shù)。
Sock 層:從 struct socket 里面的 sk 得到 struct sock,根據(jù)里面 sk_prot 的定義,調(diào)用 tcp_recvmsg 函數(shù)。 TCP 層:tcp_recvmsg 函數(shù)會依次讀取 receive_queue 隊列、prequeue 隊列和 backlog 隊列。
二、中斷處理 一旦網(wǎng)卡接收完成,它會向CPU發(fā)送一個中斷信號以通知數(shù)據(jù)包的到達(dá)。操作系統(tǒng)內(nèi)核會相應(yīng)地觸發(fā)一個中斷處理程序,并暫停當(dāng)前正在執(zhí)行的任務(wù)。
Linux內(nèi)核網(wǎng)絡(luò)收包過程函數(shù)調(diào)用分析
數(shù)據(jù)幀首先到達(dá)網(wǎng)卡的接收隊列,分配RingBuffer
DMA把數(shù)據(jù)搬運到網(wǎng)卡關(guān)聯(lián)的內(nèi)存
網(wǎng)卡向CPU發(fā)起硬中斷,通知CPU有數(shù)據(jù)
調(diào)用驅(qū)動注冊的硬中斷處理函數(shù)
啟動NAPI,觸發(fā)軟中斷
2.1網(wǎng)卡驅(qū)動注冊硬中斷處理函數(shù) 網(wǎng)卡驅(qū)動注冊中斷處理函數(shù)igb_msix_ring()。
igb_open() - drivers/net/ethernet/intel/igb/igb_main.c igb_request_irq - drivers/net/ethernet/intel/igb/igb_main.c igb_request_msix - drivers/net/ethernet/intel/igb/igb_main.c igb_msix_ring() - drivers/net/ethernet/intel/igb/igb_main.c
系統(tǒng)啟動時注冊軟中斷處理函數(shù)
NET_RX_SOFTIRQ的軟中斷處理函數(shù)為net_rx_action()。
subsys_initcall(net_dev_init) - net/core/dev.c net_dev_init() - net/core/dev.c open_softirq(NET_RX_SOFTIRQ, net_rx_action) - net/core/dev.c
系統(tǒng)啟動時注冊協(xié)議棧處理函數(shù)
在網(wǎng)絡(luò)層,以IPv4為例,注冊的協(xié)議處理函數(shù)為ip_rcv()。在傳輸層,根據(jù)協(xié)議注冊其處理函數(shù)upd_rcv()、tcp_v4_rcv()、icmp_rcv()等。
fs_initcall(inet_init) - net/ipv4/af_inet.c inet_init() - net/ipv4/af_inet.c inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) - net/ipv4/af_inet.c inet_add_protocol(&udp_protocol, IPPROTO_UDP) - net/ipv4/af_inet.c inet_add_protocol(&tcp_protocol, IPPROTO_TCP) - net/ipv4/af_inet.c dev_add_pack(&ip_packet_type) - net/ipv4/af_inet.c
2.2硬中斷處理函數(shù) 當(dāng)有數(shù)據(jù)包到達(dá)網(wǎng)卡時,DMA把數(shù)據(jù)映射到內(nèi)存,通知CPU硬中斷,執(zhí)行注冊的硬中斷處理函數(shù)igb_msix_ring(),簡單處理后,發(fā)出軟中斷NET_RX_SOFTIRQ。
igb_msix_ring() - drivers/net/ethernet/intel/igb/igb_main.c __napi_schedule() - net/core/dev.c ____napi_schedule() - net/core/dev.c __raise_softirq_irqoff(NET_RX_SOFTIRQ) - net/core/dev.c
2.3軟中斷處理函數(shù) ksoftirqd為軟中斷處理進(jìn)程,ksoftirqd收到NET_RX_SOFTIRQ軟中斷后,執(zhí)行軟中斷處理函數(shù)net_rx_action(),調(diào)用網(wǎng)卡驅(qū)動poll()函數(shù)收包。最后通過調(diào)用注冊的ip協(xié)議處理函數(shù)ip_rcv()將數(shù)據(jù)包送往協(xié)議棧。
run_ksoftirqd() - kernel/softirqd.c __do_softirq() - kernel/softirqd.c h->action(h) - kernel/softirqd.c net_rx_action() - net/core/dev.c napi_poll() - net/core/dev.c __napi_poll - net/core/dev.c work = n->poll(n, weight) - net/core/dev.c igb_poll() - drivers/net/ethernet/intel/igb/igb_main.c igb_clean_rx_irq() - drivers/net/ethernet/intel/igb/igb_main.c napi_gro_receive() - net/core/gro.c napi_skb_finish() - net/core/gro.c netif_receive_skb_list_internal() - net/core/dev.c __netif_receive_skb_list() - net/core/dev.c __netif_receive_skb_list_core - net/core/dev.c __netif_receive_skb_core - net/core/dev.c deliver_skb() - net/core/dev.c pt_prev->func(skb, skb->dev, pt_prev, orig_dev)
協(xié)議棧處理函數(shù)-L3
在軟中斷處理的最后,調(diào)用的pt_prev->func()函數(shù)即為協(xié)議棧注冊ipv4處理函數(shù)ip_rcv()。網(wǎng)絡(luò)層處理完成之后,根據(jù)傳輸協(xié)議執(zhí)行注冊的傳輸層處理函數(shù)tcp_v4_rcv或者udp_rcv()。
ip_rcv() - net/ipv4/ip_input.c ip_rcv_finish() - net/ipv4/ip_input.c dst_input() - include/net/dst.h ip_local_deliver() - net/ipv4/ip_input.c ip_local_deliver_finish() - net/ipv4/ip_input.c ip_protocol_deliver_rcu() - net/ipv4/ip_input.c ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv, skb)
協(xié)議棧處理函數(shù)-L4
這里以udp協(xié)議為例說明處理過程,tcp協(xié)議處理過程更復(fù)雜一些。最后將數(shù)據(jù)包添加到socket的接收隊列。然后進(jìn)入用戶空間應(yīng)用層面處理。
udp_rcv() - net/ipv4/udp.c udp_unicast_rcv_skb() - net/ipv4/udp.c udp_queue_rcv_skb() - net/ipv4/udp.c udp_queue_rcv_one_skb() - net/ipv4/udp.c __udp_queue_rcv_skb() - net/ipv4/udp.c __udp_enqueue_schedule_skb() - net/ipv4/udp.c __skb_queue_tail() - net/ipv4/udp.c
最終調(diào)用 gro_normal_list將數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)協(xié)議棧 。
三、包分類 中斷處理程序開始運行后,它會根據(jù)網(wǎng)絡(luò)包的協(xié)議類型(如TCP、UDP等)和目標(biāo)IP地址進(jìn)行分類。這樣可以確保每個數(shù)據(jù)包被傳送給正確的協(xié)議棧。
四、協(xié)議棧處理 對于需要進(jìn)一步處理的數(shù)據(jù)包,操作系統(tǒng)內(nèi)核將其傳遞給相應(yīng)的網(wǎng)絡(luò)層、傳輸層和應(yīng)用層協(xié)議棧。例如,在IPv4上運行TCP/IP時,數(shù)據(jù)包將經(jīng)過IPv4模塊、TCP模塊等依次處理。
4.1圖解以IPv4分組為例 4.2處理過程 1) ip_rcv()
skb被送到ip_rcv()函數(shù)進(jìn)行處理。首先ip_rcv函數(shù)驗證IP分組。比如目的地是否是本機(jī)地址,校驗和是否正確等等。若正確,則交給netfilter的NF_IP_ROUTING;否則,丟棄
2)ip_rcv_finish()
隨后將分組發(fā)送到ip_rcv_finish()函數(shù)處理。根據(jù)skb結(jié)構(gòu)的目的或路由信息發(fā)送到不同的處理函數(shù)。
ip_rcv_finish()函數(shù)的具體處理過程如下:
從 skb->nh ( IP 頭,由 netif_receive_skb 初始化)結(jié)構(gòu)得到 IP 地址
struct net_device *dev = skb->dev; struct iphdr *iph = skb->nh.iph;
而 skb->dst 或許包含了數(shù)據(jù)分組到達(dá)目的地的路由信息,如果沒有,則需要查找路由,如果最后結(jié)果顯示目的地不可達(dá),那么就丟棄該數(shù)據(jù)包:
if (skb->dst == NULL) { if (ip_route_input(skb, iph->daddr, iph->saddr, iph->tos, dev)) goto drop; //丟棄 }
ip_rcv_finish() 函數(shù)最后執(zhí)行 dst_input ,決定數(shù)據(jù)包的下一步的處理。
本機(jī)分組則由ip_local_deliver處理; 需要轉(zhuǎn)發(fā)的數(shù)據(jù)則由ip_forward()函數(shù)處理; 組播數(shù)據(jù)包則由ip_mr_input()函數(shù)處理。
4.3 ip_forward()轉(zhuǎn)發(fā)數(shù)據(jù)包 處理IP頭選項。如果需要,會記錄本地IP地址和時間戳;
確認(rèn)分組可以被轉(zhuǎn)發(fā)
將TTL減1,如果TTL為0,則丟棄分組,TTL是 Time To Live的縮寫,該字段指定IP包被路由器丟棄之前允許通過的最大網(wǎng)段數(shù)量。TTL是IPv4報頭的一個8 bit字段。
根據(jù)MTU大小和路由信息,對數(shù)據(jù)分組進(jìn)行分片。MTU即最大傳輸單元(Maximum Transmission Unit,MTU)用來通知對方所能接受數(shù)據(jù)服務(wù)單元的最大尺寸,說明發(fā)送方能夠接受的有效載荷大小。
將數(shù)據(jù)分組送往外出設(shè)備。如果因為某種原因分組轉(zhuǎn)發(fā)失敗,則回應(yīng)ICMP消息,來回復(fù)不能轉(zhuǎn)發(fā)的原因。如果對轉(zhuǎn)發(fā)的分組進(jìn)行各種檢查無誤后。
執(zhí)行ip_forward_finish()函數(shù),準(zhǔn)備發(fā)送。然后執(zhí)行dst_output(skb)將分組發(fā)到轉(zhuǎn)發(fā)的目的主機(jī)或本地主機(jī)。dst_output(skb) 函數(shù)要執(zhí)行虛函數(shù) output (單播的話為 ip_output ,多播為 ip_mc_output )。
最后, 調(diào)用ip_finish_output() 進(jìn)入鄰居子系統(tǒng)。鄰居子系統(tǒng):在數(shù)據(jù)鏈接層,必須要獲取發(fā)送方和接收方的MAC地址,這樣數(shù)據(jù)才能正確到達(dá)接收方。鄰居子系統(tǒng)的作用就是把IP地址轉(zhuǎn)換成對應(yīng)的MAC地址。如果目的主機(jī)不是和發(fā)送發(fā)位于同一局域網(wǎng)時,解析的MAC地址就是下一跳網(wǎng)關(guān)地址
4.4ip_local_deliver本地處理 ip_local_deliver中對ip分片進(jìn)行重組 ,經(jīng)過LOCAL_IN鉤子點,然后調(diào)用ip_local_deliver_finish;
/* * Deliver IP Packets to the higher protocol layers. */ int ip_local_deliver(struct sk_buff *skb) { /* * 重組 IP fragments. */ struct net *net = dev_net(skb->dev); /* 分片重組 */ if (ip_is_fragment(ip_hdr(skb))) { if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER)) return 0; } /* 經(jīng)過LOCAL_IN鉤子點 */ return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, net, NULL, skb, skb->dev, NULL, ip_local_deliver_finish); }
最后調(diào)用ip_local_deliver_finish()函數(shù):ip_local_deliver_finish函數(shù)處理原始套接字的數(shù)據(jù)接收,并調(diào)用上層協(xié)議的包接收函數(shù),將數(shù)據(jù)包傳遞到傳輸層;
4.5傳輸層處理 TCP處理過程如圖:
接收到的分組由ip_local_deliver進(jìn)入。
發(fā)送分組或者響應(yīng)分組有ip_queue_xmit()函數(shù)出口出去:
發(fā)送時,ip_queue_xmit()函數(shù)檢查socket結(jié)構(gòu)體中是否有路由信息,如果沒有則執(zhí)行ip_route_flow()查找,并存儲到skb數(shù)據(jù)結(jié)構(gòu)中。如果找不到,則丟棄。
五、應(yīng)用程序處理 協(xié)議處理程序處理完成后,會將數(shù)據(jù)包存儲到應(yīng)用層緩沖區(qū)中,等待應(yīng)用程序處理。應(yīng)用程序可以從應(yīng)用層緩沖區(qū)中讀取數(shù)據(jù)包,并進(jìn)行相應(yīng)的處理。
六、發(fā)送響應(yīng)數(shù)據(jù) 當(dāng)應(yīng)用程序處理完數(shù)據(jù)包后,會將響應(yīng)數(shù)據(jù)返回給協(xié)議棧。協(xié)議棧會將響應(yīng)數(shù)據(jù)封裝成數(shù)據(jù)包,并通過網(wǎng)卡發(fā)送出去。