免费高清特黄a大片,九一h片在线免费看,a免费国产一级特黄aa大,国产精品国产主播在线观看,成人精品一区久久久久,一级特黄aa大片,俄罗斯无遮挡一级毛片

分享

剖析Linux網(wǎng)絡(luò)包接收過程:掌握數(shù)據(jù)如何被捕獲和分發(fā)的全過程

 深度Linux 2023-08-25 發(fā)布于湖南

前言:在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)過了多個層次的處理:

  1. 首先,在物理層,網(wǎng)卡會檢測到數(shù)據(jù)包,并將其傳遞給操作系統(tǒng)內(nèi)核。

  2. 然后,在網(wǎng)絡(luò)協(xié)議棧中,內(nèi)核會對數(shù)據(jù)包進(jìn)行解析和處理。它可能會檢查目標(biāo)IP地址、端口號等信息,并根據(jù)規(guī)則進(jìn)行路由、過濾或轉(zhuǎn)發(fā)操作。

  3. 最終,當(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ù)包

  1. 處理IP頭選項。如果需要,會記錄本地IP地址和時間戳;

  2. 確認(rèn)分組可以被轉(zhuǎn)發(fā)

  3. 將TTL減1,如果TTL為0,則丟棄分組,TTL是 Time To Live的縮寫,該字段指定IP包被路由器丟棄之前允許通過的最大網(wǎng)段數(shù)量。TTL是IPv4報頭的一個8 bit字段。

  4. 根據(jù)MTU大小和路由信息,對數(shù)據(jù)分組進(jìn)行分片。MTU即最大傳輸單元(Maximum Transmission Unit,MTU)用來通知對方所能接受數(shù)據(jù)服務(wù)單元的最大尺寸,說明發(fā)送方能夠接受的有效載荷大小。

  5. 將數(shù)據(jù)分組送往外出設(shè)備。如果因為某種原因分組轉(zhuǎn)發(fā)失敗,則回應(yīng)ICMP消息,來回復(fù)不能轉(zhuǎn)發(fā)的原因。如果對轉(zhuǎn)發(fā)的分組進(jìn)行各種檢查無誤后。

  6. 執(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 )。

  7. 最后, 調(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ā)送出去。

    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多