偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

Linux性能調(diào)優(yōu)之內(nèi)核網(wǎng)絡(luò)棧發(fā)包收包認(rèn)知

系統(tǒng) Linux
在 Linux 系統(tǒng)中,網(wǎng)絡(luò)數(shù)據(jù)包的接收發(fā)送是一個涉及硬件、驅(qū)動、內(nèi)核協(xié)議棧的復(fù)雜協(xié)作過程。很多開發(fā)者可能只關(guān)注應(yīng)用層的網(wǎng)絡(luò)調(diào)用(如 socket 編程),卻對內(nèi)核底層如何 “迎接” 和 “處理”,發(fā)送數(shù)據(jù)包知之甚少。

內(nèi)核網(wǎng)絡(luò)棧的認(rèn)知

在 Linux 系統(tǒng)中,網(wǎng)絡(luò)數(shù)據(jù)包的接收發(fā)送是一個涉及硬件、驅(qū)動、內(nèi)核協(xié)議棧的復(fù)雜協(xié)作過程。很多開發(fā)者可能只關(guān)注應(yīng)用層的網(wǎng)絡(luò)調(diào)用(如 socket 編程),卻對內(nèi)核底層如何 “迎接” 和 “處理”,發(fā)送數(shù)據(jù)包知之甚少。實際上涉及到一些調(diào)優(yōu),解決網(wǎng)絡(luò)指標(biāo)飽和或者異常的情況都需要對底層有一定的了解,同時在使用 ebpf 進(jìn)行網(wǎng)絡(luò)性能觀測的時候,主要是通過一些內(nèi)核態(tài)和用戶的系統(tǒng)調(diào)用進(jìn)行埋點,對數(shù)據(jù)進(jìn)行匯總分析,所以對內(nèi)核的認(rèn)知是必不可少的。

內(nèi)核收包機(jī)制認(rèn)知

這里將從內(nèi)核收包前的準(zhǔn)備工作入手,一步步拆解數(shù)據(jù)包到達(dá)后的完整處理流程,理清 Linux 網(wǎng)絡(luò)棧的核心邏輯。

一、收包前的準(zhǔn)備:內(nèi)核如何 “迎接” 數(shù)據(jù)包?

在網(wǎng)卡正式接收第一個數(shù)據(jù)包之前,Linux 內(nèi)核需要完成一系列初始化和注冊操作,為后續(xù)高效處理數(shù)據(jù)包做好鋪墊。這 4 項準(zhǔn)備工作缺一不可,直接決定了后續(xù)收包的效率和穩(wěn)定性。這部分工作是在系統(tǒng)啟動之后完成的。

1.創(chuàng)建軟中斷處理線程:ksoftirqd

內(nèi)核首先會創(chuàng)建一個特殊的內(nèi)核線程 ——ksoftirqd,每個 CPU 核心對應(yīng)一個實例(比如 CPU0 對應(yīng)的線程是ksoftirqd/0)。創(chuàng)建時會為其綁定 “軟中斷處理” 的核心邏輯,它的核心職責(zé)是:

  • 接管 “耗時的數(shù)據(jù)包處理工作”(如協(xié)議解析、數(shù)據(jù)分發(fā));
  • 避免硬中斷長時間占用 CPU(硬中斷優(yōu)先級高,若處理耗時會阻塞其他任務(wù))。
[root@liruilongs.github.io]-[~]$ ps -ef | grep   ksoftirqd | grep -v grep
root          11       2  0 8月03 ?       00:00:00 [ksoftirqd/0]
root          17       2  0 8月03 ?       00:00:00 [ksoftirqd/1]
root          22       2  0 8月03 ?       00:00:00 [ksoftirqd/2]
root          27       2  0 8月03 ?       00:00:00 [ksoftirqd/3]
[root@liruilongs.github.io]-[~]$

簡單來說,ksoftirqd是內(nèi)核專門為 “網(wǎng)絡(luò)軟中斷” 配備的 “打工人”,后續(xù)數(shù)據(jù)包的核心處理都靠它。這里的軟中斷是什么,后面我們會講。

2.協(xié)議棧注冊:讓內(nèi)核 “認(rèn)識” 所有協(xié)議

Linux 內(nèi)核支持ARP、ICMP、IP、UDP、TCP等多種網(wǎng)絡(luò)協(xié)議,但內(nèi)核不會 “憑空識別” 這些協(xié)議,每個協(xié)議在初始化時,都需要主動將自己的數(shù)據(jù)包處理函數(shù)注冊到內(nèi)核的 協(xié)議鏈表 中。

舉幾個關(guān)鍵協(xié)議的注冊例子:

  • UDP 協(xié)議:注冊udp_rcv()函數(shù),作為 UDP 數(shù)據(jù)包的 “入口處理器”;
  • TCP 協(xié)議(IPv4 場景):注冊tcp_rcv_v4()函數(shù),負(fù)責(zé) TCP 數(shù)據(jù)包的接收邏輯;
  • IP 協(xié)議:注冊ip_rcv()函數(shù),處理 IP 頭解析和協(xié)議分發(fā)(比如判斷數(shù)據(jù)包是 UDP 還是 TCP)。

這個注冊過程的作用很關(guān)鍵:當(dāng)數(shù)據(jù)包到達(dá)時,內(nèi)核只需查看數(shù)據(jù)包的 “協(xié)議類型字段”(如 IP 頭中的protocol字段),就能快速找到對應(yīng)的處理函數(shù),無需遍歷所有協(xié)議,極大提升了處理效率,類似于一個 Hash 路由一樣。

3.網(wǎng)卡驅(qū)動初始化:打通硬件與內(nèi)核的 “通道”

網(wǎng)卡是數(shù)據(jù)包進(jìn)入系統(tǒng)的 “物理入口”,但它需要驅(qū)動程序才能與內(nèi)核協(xié)作。內(nèi)核會調(diào)用網(wǎng)卡對應(yīng)的驅(qū)動初始化函數(shù),完成 3 件核心事:

初始化 DMA(直接內(nèi)存訪問):配置網(wǎng)卡與內(nèi)存的 DMA 映射,讓網(wǎng)卡可以直接將數(shù)據(jù)包寫入內(nèi)存(無需 CPU 中轉(zhuǎn),減少 CPU 開銷);下面為系統(tǒng)啟動對應(yīng)的內(nèi)核日志。

下面是內(nèi)核日志中對應(yīng)的分配邏輯,沒有CPU 中轉(zhuǎn),意味著不需要 MMU 內(nèi)存管理單元參與,分配的是實際的連續(xù)的物理內(nèi)存,而不是虛擬內(nèi)存,不存在超售問題,并且是鎖定狀態(tài),不會發(fā)生頁面換出,分配即占用。

[root@liruilongs.github.io]-[~]$ sudo dmesg -T | grep DMA
[六 9月 13 19:15:28 2025]   DMA      [mem 0x0000000040000000-0x00000000ffffffff] # 內(nèi)核劃分DMA內(nèi)存域(DMA zone),地址范圍1GB-4GB,供網(wǎng)卡等DMA設(shè)備直接訪問,對應(yīng)“網(wǎng)卡驅(qū)動初始化→DMA映射配置”環(huán)節(jié)
[六 9月 13 19:15:28 2025]   DMA32    empty # DMA32內(nèi)存域為空,該內(nèi)存域用于僅支持32位地址的老舊DMA設(shè)備,當(dāng)前系統(tǒng)無此類設(shè)備,對網(wǎng)卡收包無影響
[六 9月 13 19:15:28 2025]   DMA zone: 12288 pages used for memmap # DMA zone中12288個頁(48MB)用于內(nèi)存映射表(memmap),內(nèi)核通過該表管理DMA內(nèi)存使用狀態(tài),是DMA內(nèi)存分配的基礎(chǔ)
[六 9月 13 19:15:28 2025]   DMA zone: 0 pages reserved # DMA zone無預(yù)留內(nèi)存,所有內(nèi)存可用于網(wǎng)卡等DMA設(shè)備的數(shù)據(jù)包緩存,保障收包時內(nèi)存可用性
[六 9月 13 19:15:28 2025]   DMA zone: 786432 pages, LIFO batch:63 # DMA zone共786432個頁(3072MB=3GB)可用,LIFO batch=63表示內(nèi)核批量分配內(nèi)存時一次最多分配63個頁,提升分配效率,為網(wǎng)卡提供充足DMA內(nèi)存
[六 9月 13 19:15:28 2025] DMA: preallocated 1024 KiB GFP_KERNEL pool for atomic allocations # 預(yù)分配1024KB GFP_KERNEL內(nèi)存池,供內(nèi)核常規(guī)場景的DMA原子分配(非緊急數(shù)據(jù)包緩存),原子分配確保不能睡眠的場景(如硬中斷)快速獲內(nèi)存
[六 9月 13 19:15:28 2025] DMA: preallocated 1024 KiB GFP_KERNEL|GFP_DMA pool for atomic allocations # 預(yù)分配1024KB DMA zone專屬原子內(nèi)存池,專門供網(wǎng)卡高并發(fā)收包時快速分配內(nèi)存(如突發(fā)流量),避免數(shù)據(jù)包丟棄
[六 9月 13 19:15:28 2025] DMA: preallocated 1024 KiB GFP_KERNEL|GFP_DMA32 pool for atomic allocations # 預(yù)分配1024KB DMA32 zone原子內(nèi)存池,因DMA32 zone為空,當(dāng)前暫未使用,預(yù)留供老舊DMA設(shè)備使用
[root@liruilongs.github.io]-[~]$

注冊 NAPI 的 poll 函數(shù):將驅(qū)動實現(xiàn)的poll函數(shù)地址告訴內(nèi)核,后續(xù)軟中斷處理時,內(nèi)核會通過這個函數(shù)輪詢從網(wǎng)卡讀取數(shù)據(jù)包;

網(wǎng)卡收到數(shù)據(jù)包后,正常觸發(fā)硬中斷(IRQ);CPU 通知內(nèi)核執(zhí)行中斷處理程序(ISR),讀取數(shù)據(jù)包到內(nèi)存,并喚醒上層協(xié)議棧,若數(shù)據(jù)包密集(如千兆網(wǎng)卡滿速傳輸),會產(chǎn)生大量硬中斷,頻發(fā)的上下文切換會增加 CPU 開銷,同時會使CPU 緩存失效。

NAPI (NET API)是為了在高吞吐量場景下減少硬中斷次數(shù),所以 NAPI通過 “硬中斷觸發(fā) + 軟中斷輪詢” 的混合模式平衡效率與實時性,而 poll 函數(shù)正是輪詢階段的核心執(zhí)行者。poll 函數(shù)由網(wǎng)卡驅(qū)動實現(xiàn),內(nèi)核通過注冊機(jī)制 “記住” 這個函數(shù)的地址,以便在軟中斷中調(diào)用。注冊過程本質(zhì)是將驅(qū)動的硬件操作邏輯接入內(nèi)核的網(wǎng)絡(luò)中斷處理框架。

配置硬件參數(shù):設(shè)置網(wǎng)卡的 MAC 地址、雙工模式(全雙工 / 半雙工)、傳輸速率(如 1Gbps)等基礎(chǔ)參數(shù)。

[root@liruilongs.github.io]-[~]$ ifconfig   enp3s0
enp3s0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.12.191.125  netmask 255.255.240.0  broadcast 10.12.191.255
        inet6 fe80::f816:3eff:fe76:29e7  prefixlen 64  scopeid 0x20<link>
        ether fa:16:3e:76:29:e7  txqueuelen 1000  (Ethernet)
        RX packets 8800  bytes 12120818 (11.5 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1431  bytes 130773 (127.7 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

[root@liruilongs.github.io]-[~]$

這一步相當(dāng)于 “打通網(wǎng)卡與內(nèi)核的通信通道”,讓硬件具備接收數(shù)據(jù)包的能力。

4.啟動網(wǎng)卡:配置隊列與中斷

驅(qū)動初始化完成后,內(nèi)核會將網(wǎng)卡從 “down” 狀態(tài)切換到 “up” 狀態(tài)(即啟動網(wǎng)卡),同時完成兩項關(guān)鍵配置:

分配 RX/TX 隊列:通常為每個 CPU 核心分配獨立的接收隊列(RX 隊列)和發(fā)送隊列(TX 隊列)(即 RSS 隊列技術(shù)),實現(xiàn)數(shù)據(jù)包的負(fù)載均衡,避免單隊列成為性能瓶頸;

查看通道信息,網(wǎng)卡的 “通道” 是硬件層面的數(shù)據(jù)處理隊列,用于將網(wǎng)絡(luò)收發(fā)任務(wù)分配到不同 CPU 核心,實現(xiàn)并行處理以提升吞吐量。

[root@liruilongs.github.io]-[~]$ ethtool -l  enp3s0
Channel parameters for enp3s0:
Pre-set maximums:    # 硬件支持的最大隊列數(shù)(驅(qū)動或固件限制)
RX:             n/a   # 接收隊列最大數(shù):不支持獨立RX隊列(n/a = not applicable)
TX:             n/a   # 發(fā)送隊列最大數(shù):不支持獨立TX隊列
Other:          n/a     # 其他隊列(如管理隊列)不支持
Combined:       1     # 組合隊列(同時處理RX和TX)最大數(shù):1

Current hardware settings:  # 當(dāng)前生效的隊列配置
RX:             n/a   # 當(dāng)前未啟用獨立RX隊列
TX:             n/a   # 當(dāng)前未啟用獨立TX隊列
Other:          n/a   # 當(dāng)前未啟用其他隊列
Combined:       1     # 當(dāng)前啟用1個組合隊列

上面的配置可以看出張網(wǎng)卡(虛擬化環(huán)境中的網(wǎng)卡, KVM 的 virtio-net)不支持獨立的 RX/TX 隊列,而是使用 “組合隊列”(Combined)同時處理接收和發(fā)送數(shù)據(jù),當(dāng)前配置了 1 個組合隊列

通過 sys 偽文件系統(tǒng)查看網(wǎng)卡的 “通道” 信息,可以看到 “通道” 的數(shù)量為 1,即 1 個 RX 隊列和 1 個 TX 隊列。

[root@developer ~]# ls /sys/class/net/enp3s0/queues/
rx-0  tx-0
[root@developer ~]#

確認(rèn)網(wǎng)卡類型:

[root@liruilongs.github.io]-[~]$ ethtool -i  enp3s0 | grep vir
driver: virtio_net
[root@liruilongs.github.io]-[~]$

注冊硬中斷處理函數(shù):將網(wǎng)卡的 “數(shù)據(jù)包到達(dá)中斷” 與內(nèi)核中的中斷處理函數(shù)綁定,當(dāng)網(wǎng)卡收到數(shù)據(jù)時,能觸發(fā) CPU 響應(yīng)硬中斷,網(wǎng)卡中斷的觸發(fā)流程。

  • 硬件層面:網(wǎng)卡收到數(shù)據(jù)包后,通過 PCIe 總線向 CPU 發(fā)送中斷請求(IRQ)信號;
  • CPU 層面:CPU 響應(yīng)中斷,暫停當(dāng)前任務(wù),跳轉(zhuǎn)到內(nèi)核預(yù)設(shè)的中斷處理入口;
  • 內(nèi)核層面:內(nèi)核通過中斷向量號(IRQ 號)找到對應(yīng)的處理函數(shù)(ISR)并執(zhí)行。

實際上就是將網(wǎng)卡的硬件中斷信號與內(nèi)核中的中斷服務(wù)程序(ISR)建立關(guān)聯(lián),讓內(nèi)核知道 “哪個 IRQ 號對應(yīng)哪個網(wǎng)卡的哪個事件(如數(shù)據(jù)包到達(dá))”。

至此,收包前的準(zhǔn)備工作全部完成,內(nèi)核開啟網(wǎng)卡的硬中斷,進(jìn)入 “等待數(shù)據(jù)包到達(dá)” 的狀態(tài)。

二、數(shù)據(jù)包到達(dá)后:內(nèi)核如何 “處理” 數(shù)據(jù)包?

當(dāng)外部數(shù)據(jù)包通過網(wǎng)線到達(dá)網(wǎng)卡時,Linux 內(nèi)核會啟動一套 “流水線式” 的處理流程,從硬件接收到底層協(xié)議解析,再到應(yīng)用層分發(fā),每一步都分工明確。

深入理解Linux網(wǎng)絡(luò): 修煉底層內(nèi)功,掌握高性能原理 (張彥飛)深入理解Linux網(wǎng)絡(luò): 修煉底層內(nèi)功,掌握高性能原理 (張彥飛)

第一步:網(wǎng)卡 DMA 寫入內(nèi)存,觸發(fā)硬中斷

網(wǎng)卡接收到物理層的以太網(wǎng)幀后,不會直接通知 CPU “幫忙”,而是通過之前配置的 DMA 通道,直接將數(shù)據(jù)包寫入內(nèi)存中 RX 隊列對應(yīng)的 RingBuffer(環(huán)形緩沖區(qū)) ,這個過程完全不需要 CPU 參與,極大減少了 CPU 的負(fù)擔(dān)。

ethtool -g 用于查看網(wǎng)卡的硬件環(huán)形緩沖區(qū)(Ring Buffer)參數(shù)。

[root@liruilongs.github.io]-[~]$ ethtool -g  enp3s0
Ring parameters for enp3s0:
Pre-set maximums:    # 硬件支持的最大緩沖區(qū)數(shù)量(驅(qū)動/固件限制)
RX:             2048  # 接收環(huán)形緩沖區(qū)最大可配置數(shù)量:2048個
RX Mini:        n/a   # 小數(shù)據(jù)包接收緩沖區(qū):不支持(n/a = not applicable)
RX Jumbo:       n/a   # 巨型幀接收緩沖區(qū):不支持
TX:             2048  # 發(fā)送環(huán)形緩沖區(qū)最大可配置數(shù)量:2048個

Current hardware settings:  # 當(dāng)前生效的緩沖區(qū)配置
RX:             2048  # 當(dāng)前接收緩沖區(qū)數(shù)量:2048個
TX:             2048  # 當(dāng)前發(fā)送緩沖區(qū)數(shù)量:2048個

該網(wǎng)卡支持獨立的接收(RX)發(fā)送(TX)環(huán)形緩沖區(qū);當(dāng)前接收和發(fā)送緩沖區(qū)均配置為 2048 個(已達(dá)硬件最大限制);并且不支持 小數(shù)據(jù)包,巨型幀 的專用緩沖區(qū)(僅用通用緩沖區(qū)處理所有包)。

單個緩沖區(qū)的大小通常與網(wǎng)卡的 MTU(最大傳輸單元)匹配,計算公式為:單緩沖區(qū)大小 ≈ MTU + 協(xié)議頭部開銷(約 18-42 字節(jié))。

[root@liruilongs.github.io]-[~]$ ip link show enp3s0 | grep mtu
2: enp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc tbf state UP mode DEFAULT group default qlen 1000

MTU 為 1500 ,所以總 DMA 物理內(nèi)存估算,計算該網(wǎng)卡的 DMA 內(nèi)存總占用:

  • 接收緩沖區(qū):2048 個 × 1538 字節(jié) ≈ 3.07 MB;
  • 發(fā)送緩沖區(qū):2048 個 × 1538 字節(jié) ≈ 3.07 MB。

總計:約 6.14 MB(實際可能略大,因驅(qū)動會預(yù)留少量管理內(nèi)存)。

當(dāng) RingBuffer 滿的時候,新來的數(shù)據(jù)包將被去棄。使? iconfig 命令查看?卡的時候,可以看到??有個overruns。

[root@liruilongs.github.io]-[~]$ ifconfig enp3s0 | grep over
        RX errors 0  dropped 0  overruns 0  frame 0
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
[root@liruilongs.github.io]-[~]$

表?因為環(huán)形隊列滿被丟棄的包數(shù)。如果發(fā)現(xiàn)有丟包,可能需要通過ethtool命令來加?環(huán)形隊列的長度。

也可以通過 ethtool -S enp3s0  的  rx_queue_0_drops 指標(biāo)來查看。

[root@liruilongs.github.io]-[~]$ ethtool -S enp3s0 
NIC statistics:
     rx_queue_0_packets: 49137
     rx_queue_0_bytes: 69848865
     rx_queue_0_drops: 0
     rx_queue_0_xdp_packets: 0
     rx_queue_0_xdp_tx: 0
     rx_queue_0_xdp_redirects: 0
     rx_queue_0_xdp_drops: 0
     rx_queue_0_kicks: 0
     tx_queue_0_packets: 13662
     tx_queue_0_bytes: 935165
     tx_queue_0_xdp_tx: 0
     tx_queue_0_xdp_tx_drops: 0
     tx_queue_0_kicks: 0
[root@liruilongs.github.io]-[~]$

通常很少有修改的需求,隊列不是越長越好,過長會增加數(shù)據(jù)延遲(數(shù)據(jù)包在隊列中等待時間變長),通常調(diào)整為 512~2048(根據(jù)業(yè)務(wù)場景:高吞吐量場景可稍大,低延遲場景需偏?。?/code>。

如果是小數(shù)據(jù)包,數(shù)據(jù)包寫入完成后,網(wǎng)卡會向 CPU 發(fā)送一個硬中斷請求(IRQ),相當(dāng)于告訴 CPU:“有新數(shù)據(jù)包到了,快處理!”

如果是大數(shù)據(jù)包,大流量,修改隊列長度 2048 時,可能要積累 670 個包才觸發(fā)一次 NAPI 輪詢;若隊列長度增至 5242,可能要積累 2048 個包才處理,即位于第一個緩存區(qū)的數(shù)據(jù)包和第2048個緩存區(qū)的數(shù)據(jù)包在同一時間被處理,對第一個緩存區(qū)的數(shù)據(jù)就造成了延遲。

這里積累的數(shù)據(jù)觸發(fā)機(jī)制通過 NAPI 權(quán)重(napi weight)來控制,本質(zhì)是 單次輪詢最多處理的數(shù)據(jù)包上限。當(dāng)隊列中積累的數(shù)據(jù)包數(shù)量達(dá)到或接近這個值時,NAPI 會觸發(fā)處理。 默認(rèn)是 64。

[root@developer ~]# cat /sys/module/virtio_net/parameters/napi_weight
64
[root@developer ~]#

需要說明的是 napi_weight 是 “上限”,不是 “觸發(fā)閾值”。NAPI 輪詢的觸發(fā)閾值(即積累多少個數(shù)據(jù)包才會觸發(fā)一次輪詢處理)控制還有一個影響參數(shù),即 內(nèi)核全局預(yù)算(netdev_budget)netdev_budget 限制一次軟中斷中所有 NAPI 實例處理的數(shù)據(jù)包總數(shù),即所有的網(wǎng)卡,避免單個軟中斷占用 CPU 過久。 默認(rèn)值為 300。

[root@developer ~]# sysctl net.core.netdev_budget
net.core.netdev_budget = 300
[root@developer ~]#

權(quán)重是針對單個隊列的,而預(yù)算 netdev_budget限制了一次軟中斷處理周期內(nèi)所有NAPI實例總共能處理的數(shù)據(jù)包數(shù)量,防止軟中斷過長時間占用CPU。

第二步:CPU 響應(yīng)硬中斷,觸發(fā)軟中斷

CPU 收到硬中斷請求后,會暫停當(dāng)前正在執(zhí)行的任務(wù),切換到內(nèi)核態(tài),執(zhí)行之前注冊的硬中斷處理函數(shù)。但這里有個關(guān)鍵設(shè)計:硬中斷處理函數(shù)非常 “輕量化”,只做兩件小事:

  • 告知網(wǎng)卡 “我已收到中斷通知”,避免網(wǎng)卡重復(fù)觸發(fā)中斷;
  • 向內(nèi)核發(fā)起網(wǎng)絡(luò)接收軟中斷請求(NET_RX_SOFTIRQ),將后續(xù)的 “數(shù)據(jù)包解析、協(xié)議處理” 等耗時操作,交給軟中斷處理。

為什么硬中斷處理要 “輕量化”?因為硬中斷的優(yōu)先級最高,若處理耗時會阻塞其他高優(yōu)先級任務(wù)(如系統(tǒng)調(diào)用、其他硬件中斷),影響系統(tǒng)響應(yīng)性。

/proc/softirqs  文件輸出展示了系統(tǒng)中軟中斷(Software Interrupt)在各個 CPU 核心上的觸發(fā)次數(shù)

[root@developer ~]# cat /proc/softirqs 
                    CPU0       CPU1       CPU2       CPU3       
          HI:          0          5          1          0
       TIMER:      19258      14213      16893      24588
      NET_TX:          0          0          2          2
      NET_RX:       3559       1626       1311       2046
       BLOCK:      15548          0          0          0
    IRQ_POLL:          0          0          0          0
     TASKLET:       1681        225        164       1710
       SCHED:      23419      20707      23057      31402
     HRTIMER:          0          0          0          0
         RCU:      33341      32536      31459      34107
[root@developer ~]#

NET_TX 和 NET_RX 分別是 “發(fā)送” 和 “接收” 對應(yīng)在每個CPU核心的軟中斷,分別對應(yīng) “發(fā)送” 和 “接收” 數(shù)據(jù)包的處理。

第三步:ksoftirqd 處理軟中斷,讀取數(shù)據(jù)包

ksoftirqd 線程會周期性檢查內(nèi)核的軟中斷請求隊列,當(dāng)發(fā)現(xiàn) NET_RX_SOFTIRQ 軟中斷時,會立即開始工作:

  • 關(guān)閉硬中斷:避免處理軟中斷時被新的硬中斷打斷(防止數(shù)據(jù)競爭,保證數(shù)據(jù)一致性);
  • 調(diào)用 poll 函數(shù)讀包:通過驅(qū)動注冊的poll函數(shù),從 RX 隊列的 RingBuffer 中讀取數(shù)據(jù)包,并將其封裝成內(nèi)核統(tǒng)一的sk_buff結(jié)構(gòu)體(sk_buff是內(nèi)核中描述數(shù)據(jù)包的 “標(biāo)準(zhǔn)容器”,包含數(shù)據(jù)包的所有信息,如協(xié)議頭、數(shù)據(jù)內(nèi)容、長度等);
  • 開啟硬中斷:數(shù)據(jù)包讀取完成后,重新開啟硬中斷,允許接收新的數(shù)據(jù)包。

軟中斷處理時,單次調(diào)用 NAPI 的 poll 函數(shù),循環(huán)處理數(shù)據(jù)包(數(shù)量不超過 weight 和 netdev_budget 的最小值),若本次讀了閾值內(nèi)的包(packets_read=64),即使 RX_Ring 還有數(shù)據(jù),也會退出循環(huán),避免單次 poll 占用 CPU 過久。

通過 proc/interrupts 可以看到 虛擬網(wǎng)卡(virtio3)的 數(shù)據(jù)請求隊列中斷(req.0 表示第 0 個數(shù)據(jù)隊列,負(fù)責(zé)網(wǎng)卡的數(shù)據(jù)包接收 / 發(fā)送)。

[root@developer ~]# cat /proc/interrupts | grep req
 81:      16363          0          0          0   ITS-MSI 2097153 Edge      virtio3-req.0

對應(yīng)的中斷親和性,即中斷發(fā)生在那個CPU,可以通過中斷號查看,smp_affinity(中斷親和性)控制 “該中斷允許在哪些 CPU 核心上運行”,值為十六進(jìn)制:

[root@developer ~]# cat /proc/irq/81/smp_affinity
f
[root@developer ~]#

十六進(jìn)制 f = 二進(jìn)制 1111,對應(yīng)系統(tǒng)的 4 個 CPU 核心(CPU0~CPU3)。

這一步完成了 “從硬件緩沖區(qū)到內(nèi)核緩沖區(qū)” 的數(shù)據(jù)包轉(zhuǎn)移,為后續(xù)的協(xié)議解析做好準(zhǔn)備。

通過 top 命令可以看到 每個CPU 核心的 軟中斷(si)/硬中斷(hi)的使用率。

top - 16:03:56 up  3:37,  1 user,  load average: 0.00, 0.00, 0.00
Tasks: 228 total,   1 running, 227 sleeping,   0 stopped,   0 zombie
%Cpu0  :  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st 
%Cpu1  :  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st 
%Cpu2  :  0.3 us,  0.3 sy,  0.0 ni, 99.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st 
%Cpu3  :  0.3 us,  0.0 sy,  0.0 ni, 99.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st 
MiB Mem :   6657.9 total,   5398.1 free,    734.6 used,    700.4 buff/cache     
MiB Swap:   4096.0 total,   4096.0 free,      0.0 used.   5923.4 avail Mem

第四步:協(xié)議棧解析,分發(fā)到應(yīng)用層

poll函數(shù)將sk_buff交給內(nèi)核協(xié)議棧后,協(xié)議棧會按 “從下到上” 的順序逐層解析數(shù)據(jù)包,就像 “拆快遞” 一樣,一層一層揭開數(shù)據(jù)包的 “包裝”:

  1. IP 層處理:調(diào)用ip_rcv()函數(shù)解析 IP 頭,檢查 IP 地址、校驗和等信息,然后根據(jù) IP 頭中的protocol字段,判斷數(shù)據(jù)包的上層協(xié)議(如 UDP 對應(yīng) 17,TCP 對應(yīng) 6);
  2. 傳輸層處理:
  • 若為 UDP 包:調(diào)用udp_rcv()函數(shù)解析 UDP 頭,檢查端口號,然后通過 socket 將數(shù)據(jù)分發(fā)給對應(yīng)的應(yīng)用進(jìn)程;
  • 若為 TCP 包:調(diào)用tcp_rcv_v4()函數(shù)解析 TCP 頭,處理 TCP 連接狀態(tài)(如三次握手、重傳、流量控制),最后將數(shù)據(jù)通過 socket 交給應(yīng)用進(jìn)程。

至此,一個數(shù)據(jù)包從 “到達(dá)網(wǎng)卡” 到 “被應(yīng)用進(jìn)程接收” 的完整流程就結(jié)束了。

內(nèi)核發(fā)包機(jī)制認(rèn)知

網(wǎng)絡(luò)發(fā)包的核心邏輯是 用戶進(jìn)程發(fā)起請求,內(nèi)核協(xié)議棧分層處理,最終通過網(wǎng)卡硬件發(fā)送數(shù)據(jù)。

深入理解Linux網(wǎng)絡(luò): 修煉底層內(nèi)功,掌握高性能原理深入理解Linux網(wǎng)絡(luò): 修煉底層內(nèi)功,掌握高性能原理

一、發(fā)包前的準(zhǔn)備工作:發(fā)送隊列 RingBuffer 的構(gòu)建

發(fā)包同樣是一個涉及用戶態(tài)、內(nèi)核態(tài)多層協(xié)作的復(fù)雜過程,在實際的發(fā)包之前,內(nèi)核會在網(wǎng)卡啟動之后也做一些發(fā)包相關(guān)的準(zhǔn)備,主要是前面我們講到的分配傳輸隊列 RingBuifer的過程。

不同的網(wǎng)卡驅(qū)動實現(xiàn)不同,會分配兩個環(huán)形數(shù)組

  • 一個用于內(nèi)核使用(分配虛擬內(nèi)存)
  • 一個用于網(wǎng)卡硬件使用,分配的是連續(xù)的物理內(nèi)存(DMA)

在后面的發(fā)包過程中會進(jìn)行對應(yīng)的地址映射,這兩個環(huán)形數(shù)組中相同位置的指針都將指向同?個skb(數(shù)據(jù)包的內(nèi)核結(jié)構(gòu)體),內(nèi)核往對應(yīng) skb 地址寫數(shù)據(jù),網(wǎng)卡硬件就能共同訪問同樣的數(shù)據(jù),負(fù)責(zé)發(fā)送。

二、用戶進(jìn)程發(fā)起請求,內(nèi)核協(xié)議棧分層處理

1.用戶進(jìn)程:發(fā)起發(fā)送請求

用戶進(jìn)程通過 sendto系統(tǒng)調(diào)用(或 send/write)發(fā)起網(wǎng)絡(luò)發(fā)送,下面的是一個Python 的Demo,從 Python 標(biāo)準(zhǔn)庫的 socket 模塊封裝,最終觸發(fā)操作系統(tǒng)的 send 系統(tǒng)調(diào)用。

import socket
# 創(chuàng)建TCP socket(內(nèi)核創(chuàng)建對應(yīng)的socket對象)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立連接(內(nèi)核完善socket對象的源/目標(biāo)地址信息)
sock.connect(("110.242.69.21", 80))
# 準(zhǔn)備發(fā)送數(shù)據(jù)
request_data = ()
# 調(diào)用send系統(tǒng)調(diào)用(內(nèi)核查找socket對象并構(gòu)造msghdr結(jié)構(gòu)體)
bytes_sent = sock.send(request_data.encode())
# 接收響應(yīng)
response = sock.recv(4096)
# 關(guān)閉連接(內(nèi)核銷毀socket對象)
sock.close()

此時進(jìn)程從用戶態(tài)切換到內(nèi)核態(tài),內(nèi)核會執(zhí)行兩項核心操作:

  • 查找對應(yīng)的 socket 對象(標(biāo)識 TCP/UDP 連接的源 IP、目標(biāo) IP、源端口、目標(biāo)端口);
  • 構(gòu)造 msghdr 結(jié)構(gòu)體(封裝待發(fā)送消息的地址信息、數(shù)據(jù)長度等元數(shù)據(jù))。

之后會進(jìn)入內(nèi)核網(wǎng)絡(luò)協(xié)議棧分層處理。

2.協(xié)議棧分層處理(內(nèi)核態(tài)):從傳輸層到網(wǎng)絡(luò)設(shè)備

內(nèi)核態(tài)的協(xié)議棧處理遵循 TCP/IP 分層模型,從傳輸層到網(wǎng)絡(luò)層、鄰居子系統(tǒng)、網(wǎng)絡(luò)設(shè)備子系統(tǒng),逐步完成數(shù)據(jù)封裝與轉(zhuǎn)發(fā)。

傳輸層(TCP 處理):

首次內(nèi)存拷貝與淺拷貝,內(nèi)核調(diào)用 tcp_sendmsg 函數(shù)處理傳輸層邏輯,核心操作包括:

  • 申請skb(Socket Buffer,內(nèi)核中存儲網(wǎng)絡(luò)數(shù)據(jù)的緩沖區(qū)),并將用戶態(tài)數(shù)據(jù)拷貝到skb;
  • 進(jìn)行滑動窗口管理(控制數(shù)據(jù)發(fā)送速率,避免對端接收緩沖區(qū)溢出);
  • 設(shè)置 TCP 頭部(如源端口、目標(biāo)端口、序列號、確認(rèn)號等,保證 TCP 可靠傳輸)。

這一步會涉及兩次關(guān)鍵的內(nèi)存拷貝操作:

  1. 第一次拷貝(必需,深拷貝):內(nèi)核先申請 skb(Socket Buffer,內(nèi)核中存儲網(wǎng)絡(luò)數(shù)據(jù)的專用緩沖區(qū)),再將用戶態(tài)傳遞的數(shù)據(jù)包拷貝到 skb 中。該拷貝的開銷隨數(shù)據(jù)量增大而顯著增加;
  2. 第二次拷貝(必需,淺拷貝):為保證 TCP 可靠傳輸(當(dāng)對端未返回 ACK 時需重發(fā)數(shù)據(jù)),內(nèi)核會克隆 skb 的 “描述符”(生成新的 skb 副本),但數(shù)據(jù)本身復(fù)用原 skb 的內(nèi)存(僅拷貝元信息,開銷極低)。

網(wǎng)絡(luò)層(IP 處理):

內(nèi)核調(diào)用 ip_queue_xmit 函數(shù)處理網(wǎng)絡(luò)層邏輯,核心操作包括:

  • 路由查找:根據(jù)目標(biāo) IP 地址查詢路由表,確定數(shù)據(jù)發(fā)送的下一跳地址;
  • 網(wǎng)絡(luò)過濾:經(jīng)過 netfilter 框架(如 iptables 規(guī)則),判斷是否允許數(shù)據(jù)發(fā)送;

MTU 檢查與分片:若數(shù)據(jù)包大小超過網(wǎng)絡(luò)設(shè)備的 MTU(最大傳輸單元,默認(rèn) 1500 字節(jié)),則觸發(fā) IP 分片,申請多個新 skb,將原 skb 中的數(shù)據(jù)深拷貝到新 skb 中(第三次拷貝,可選)。

結(jié)合傳輸層處理,內(nèi)核發(fā)包的內(nèi)存拷貝可總結(jié)為 “兩次必需 + 一次可選”:

  • 必需拷貝 1:用戶態(tài)數(shù)據(jù) → 內(nèi)核態(tài) skb(深拷貝);
  • 必需拷貝 2:傳輸層 skb → 網(wǎng)絡(luò)層 skb(淺拷貝,僅描述符);
  • 可選拷貝 3:IP 分片時的深拷貝(僅當(dāng)數(shù)據(jù)包超過 MTU 時觸發(fā))。

鄰居子系統(tǒng):

獲取目標(biāo) MAC 地址,內(nèi)核再次調(diào)用 ip_queue_xmit(邏輯分支不同),通過 ARP 協(xié)議獲取目標(biāo)設(shè)備的 MAC 地址:

  • 緩存命中:若本地 ARP 緩存中存在 “目標(biāo) IP → MAC 地址” 的映射,直接使用該 MAC 地址;
  • 緩存未命中:發(fā)送 ARP 請求包(詢問目標(biāo) IP 對應(yīng)的 MAC 地址),待收到 ARP 響應(yīng)后繼續(xù)傳輸。

網(wǎng)絡(luò)設(shè)備子系統(tǒng):

這里網(wǎng)絡(luò)設(shè)備子系統(tǒng)主要用于隊列選擇和觸發(fā)軟中斷。

隊列選擇:內(nèi)核調(diào)用 dev_queue_xmit,根據(jù)網(wǎng)卡的多隊列配置(若支持),將 skb 放入對應(yīng)的發(fā)送隊列(用于負(fù)載均衡),談后調(diào)用qdise_run 發(fā)送數(shù)據(jù),如果當(dāng)前任然持有CPU時間片,那么會 while循環(huán)不斷地從隊列中取出 skb 并進(jìn)?發(fā)送(qdisc_restar 調(diào)用)。注意,這個時侯其實都占?的是?戶進(jìn)程的內(nèi)核態(tài)時間。只有當(dāng) quota?盡或者其他進(jìn)程需要CPU的時候才觸發(fā) NET_TX_SOFTIRQ類型軟中斷**(由 ksoftirqd 內(nèi)核線程處理)。

在這期間,內(nèi)核會調(diào)用網(wǎng)卡驅(qū)動程序 dev_hard_start_xmit通過網(wǎng)卡發(fā)送數(shù)據(jù)。

所以為什么說 90% 以上的網(wǎng)絡(luò)發(fā)包開銷集中在 “內(nèi)核態(tài)處理階段”(協(xié)議棧解析、內(nèi)存拷貝),從這里可以看到僅當(dāng)內(nèi)核態(tài)進(jìn)程時間片用盡、需由 ksoftirqd 線程繼續(xù)處理發(fā)送隊列時,才會觸發(fā) NET_TX 軟中斷,統(tǒng)計到 si(軟中斷 CPU 時間) 中。

這也是在服務(wù)器上查看/proc/softirgs,?般 NET_RX 都要? NET_TX ?得多的原因之一。對于接收來說,都要經(jīng)過 NET_RX 軟中斷,?對于發(fā)送來說,只有內(nèi)核態(tài)CPU配額?盡才讓軟中斷上。

3.軟中斷與硬中斷:內(nèi)核與硬件的異步協(xié)作

到這里以后發(fā)送數(shù)據(jù)消耗的CPU就都顯?在軟中斷的CPU使用率里面,不會消耗?戶進(jìn)程的內(nèi)核態(tài)時間。

軟中斷處理:ksoftirqd 內(nèi)核線程(每個 CPU 核心對應(yīng)一個)檢測到 NET_TX 軟中斷后,調(diào)用 net_tx_action 函數(shù),再通過 qdisc_run 調(diào)度發(fā)送隊列(如 FIFO、優(yōu)先級調(diào)度),將 skb 提交給網(wǎng)卡驅(qū)動 dev_hard_start_xmit 調(diào)用;

硬中斷處理:網(wǎng)卡完成數(shù)據(jù)發(fā)送后,觸發(fā)硬中斷(如 igb_msix_ring 函數(shù),因網(wǎng)卡驅(qū)動而異),通知內(nèi)核釋放 skb 內(nèi)存、清理 RingBuffer,為下一次發(fā)送做準(zhǔn)備。這里的硬中斷會觸發(fā) NET_RX_SOFTIRQ 軟中斷,即網(wǎng)卡的 “發(fā)送完成通知” 與 “接收數(shù)據(jù)” 觸發(fā)的硬中斷,最終都會調(diào)用 NET_RX_SOFTIRQ(而非 NET_TX_SOFTIRQ),導(dǎo)致 “發(fā)送完成” 的開銷被統(tǒng)計到 NET_RX 中;這也是上面看到 NET_RX 的 CPU 使用率要比 NET_TX 大的原因。

三、網(wǎng)卡驅(qū)動的數(shù)據(jù)包處理

下面我們看下網(wǎng)卡驅(qū)動如何發(fā)送數(shù)據(jù)包,在上面的網(wǎng)絡(luò)設(shè)備子系統(tǒng)中dev_hard_start_xmit調(diào)用驅(qū)動,驅(qū)動銜接硬件

  1. 入口:內(nèi)核調(diào)用驅(qū)動,通過dev_hard_start_xmit函數(shù),調(diào)用驅(qū)動注冊的ndo_start_xmit回調(diào)(如igb網(wǎng)卡的igb_xmit_frame),完成“內(nèi)核到驅(qū)動”的交接。
  2. 驅(qū)動綁定與DMA映射(igb網(wǎng)卡為Demo)igb_xmit_frame先將skb分配到對應(yīng)發(fā)送隊列,再通過igb_xmit_frame_ringskb綁定到RingBuffer(環(huán)形緩沖區(qū))的igb_tx_buffer;隨后igb_tx_map通過dma_map_single,將skb虛擬地址轉(zhuǎn)成硬件可訪問的物理地址,寫入硬件描述符。
  3. 觸發(fā)硬件發(fā)送  驅(qū)動更新網(wǎng)卡寄存器(如E1000_TDT),通知硬件讀取描述符中的物理地址,通過DMA直接讀內(nèi)存數(shù)據(jù)并發(fā)送,無需CPU參與,整個流程核心是“DMA映射”和“RingBuffer銜接”,實現(xiàn)軟件到硬件的高效數(shù)據(jù)傳遞。
責(zé)任編輯:武曉燕 來源: 山河已無恙
相關(guān)推薦

2011-03-18 11:21:48

2023-03-01 23:56:11

2023-03-01 23:53:30

Linuxshutdown進(jìn)程

2023-03-10 14:56:37

Linuxconnect系統(tǒng)

2023-03-28 15:51:20

2025-01-17 09:23:31

2025-03-07 08:30:00

pwruLinux網(wǎng)絡(luò)包追蹤

2021-09-17 11:59:21

tcpdump網(wǎng)絡(luò)包Linux

2021-09-08 10:21:33

內(nèi)核網(wǎng)絡(luò)包Tcpdump

2023-03-06 15:43:56

2009-08-07 10:28:03

2025-05-27 08:20:00

Linux內(nèi)核參數(shù)調(diào)優(yōu)系統(tǒng)

2017-07-21 08:55:13

TomcatJVM容器

2011-03-21 09:35:38

LAMP調(diào)優(yōu)網(wǎng)絡(luò)文件

2013-03-20 17:18:07

Linux系統(tǒng)性能調(diào)優(yōu)

2009-07-16 09:02:38

LINUX 2.4.x網(wǎng)絡(luò)安全LINUX開發(fā)

2024-08-22 14:47:50

開源Linux網(wǎng)絡(luò)抓包工具

2012-06-20 11:05:47

性能調(diào)優(yōu)攻略

2022-01-27 23:32:03

Linux操作系統(tǒng)TCP

2021-03-04 08:39:21

SparkRDD調(diào)優(yōu)
點贊
收藏

51CTO技術(shù)棧公眾號