Linux性能調優之內核網絡棧發包收包認知
內核網絡棧的認知
在 Linux 系統中,網絡數據包的接收發送是一個涉及硬件、驅動、內核協議棧的復雜協作過程。很多開發者可能只關注應用層的網絡調用(如 socket 編程),卻對內核底層如何 “迎接” 和 “處理”,發送數據包知之甚少。實際上涉及到一些調優,解決網絡指標飽和或者異常的情況都需要對底層有一定的了解,同時在使用 ebpf 進行網絡性能觀測的時候,主要是通過一些內核態和用戶的系統調用進行埋點,對數據進行匯總分析,所以對內核的認知是必不可少的。
內核收包機制認知
這里將從內核收包前的準備工作入手,一步步拆解數據包到達后的完整處理流程,理清 Linux 網絡棧的核心邏輯。
一、收包前的準備:內核如何 “迎接” 數據包?
在網卡正式接收第一個數據包之前,Linux 內核需要完成一系列初始化和注冊操作,為后續高效處理數據包做好鋪墊。這 4 項準備工作缺一不可,直接決定了后續收包的效率和穩定性。這部分工作是在系統啟動之后完成的。
1.創建軟中斷處理線程:ksoftirqd
內核首先會創建一個特殊的內核線程 ——ksoftirqd,每個 CPU 核心對應一個實例(比如 CPU0 對應的線程是ksoftirqd/0)。創建時會為其綁定 “軟中斷處理” 的核心邏輯,它的核心職責是:
- 接管 “耗時的數據包處理工作”(如協議解析、數據分發);
- 避免硬中斷長時間占用 CPU(硬中斷優先級高,若處理耗時會阻塞其他任務)。
[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是內核專門為 “網絡軟中斷” 配備的 “打工人”,后續數據包的核心處理都靠它。這里的軟中斷是什么,后面我們會講。
2.協議棧注冊:讓內核 “認識” 所有協議
Linux 內核支持ARP、ICMP、IP、UDP、TCP等多種網絡協議,但內核不會 “憑空識別” 這些協議,每個協議在初始化時,都需要主動將自己的數據包處理函數注冊到內核的 協議鏈表 中。
舉幾個關鍵協議的注冊例子:
- UDP 協議:注冊
udp_rcv()函數,作為 UDP 數據包的 “入口處理器”; - TCP 協議(IPv4 場景):注冊
tcp_rcv_v4()函數,負責 TCP 數據包的接收邏輯; - IP 協議:注冊
ip_rcv()函數,處理 IP 頭解析和協議分發(比如判斷數據包是 UDP 還是 TCP)。
這個注冊過程的作用很關鍵:當數據包到達時,內核只需查看數據包的 “協議類型字段”(如 IP 頭中的protocol字段),就能快速找到對應的處理函數,無需遍歷所有協議,極大提升了處理效率,類似于一個 Hash 路由一樣。
3.網卡驅動初始化:打通硬件與內核的 “通道”
網卡是數據包進入系統的 “物理入口”,但它需要驅動程序才能與內核協作。內核會調用網卡對應的驅動初始化函數,完成 3 件核心事:
初始化 DMA(直接內存訪問):配置網卡與內存的 DMA 映射,讓網卡可以直接將數據包寫入內存(無需 CPU 中轉,減少 CPU 開銷);下面為系統啟動對應的內核日志。
下面是內核日志中對應的分配邏輯,沒有CPU 中轉,意味著不需要 MMU 內存管理單元參與,分配的是實際的連續的物理內存,而不是虛擬內存,不存在超售問題,并且是鎖定狀態,不會發生頁面換出,分配即占用。
[root@liruilongs.github.io]-[~]$ sudo dmesg -T | grep DMA
[六 9月 13 19:15:28 2025] DMA [mem 0x0000000040000000-0x00000000ffffffff] # 內核劃分DMA內存域(DMA zone),地址范圍1GB-4GB,供網卡等DMA設備直接訪問,對應“網卡驅動初始化→DMA映射配置”環節
[六 9月 13 19:15:28 2025] DMA32 empty # DMA32內存域為空,該內存域用于僅支持32位地址的老舊DMA設備,當前系統無此類設備,對網卡收包無影響
[六 9月 13 19:15:28 2025] DMA zone: 12288 pages used for memmap # DMA zone中12288個頁(48MB)用于內存映射表(memmap),內核通過該表管理DMA內存使用狀態,是DMA內存分配的基礎
[六 9月 13 19:15:28 2025] DMA zone: 0 pages reserved # DMA zone無預留內存,所有內存可用于網卡等DMA設備的數據包緩存,保障收包時內存可用性
[六 9月 13 19:15:28 2025] DMA zone: 786432 pages, LIFO batch:63 # DMA zone共786432個頁(3072MB=3GB)可用,LIFO batch=63表示內核批量分配內存時一次最多分配63個頁,提升分配效率,為網卡提供充足DMA內存
[六 9月 13 19:15:28 2025] DMA: preallocated 1024 KiB GFP_KERNEL pool for atomic allocations # 預分配1024KB GFP_KERNEL內存池,供內核常規場景的DMA原子分配(非緊急數據包緩存),原子分配確保不能睡眠的場景(如硬中斷)快速獲內存
[六 9月 13 19:15:28 2025] DMA: preallocated 1024 KiB GFP_KERNEL|GFP_DMA pool for atomic allocations # 預分配1024KB DMA zone專屬原子內存池,專門供網卡高并發收包時快速分配內存(如突發流量),避免數據包丟棄
[六 9月 13 19:15:28 2025] DMA: preallocated 1024 KiB GFP_KERNEL|GFP_DMA32 pool for atomic allocations # 預分配1024KB DMA32 zone原子內存池,因DMA32 zone為空,當前暫未使用,預留供老舊DMA設備使用
[root@liruilongs.github.io]-[~]$注冊 NAPI 的 poll 函數:將驅動實現的poll函數地址告訴內核,后續軟中斷處理時,內核會通過這個函數輪詢從網卡讀取數據包;
網卡收到數據包后,正常觸發硬中斷(IRQ);CPU 通知內核執行中斷處理程序(ISR),讀取數據包到內存,并喚醒上層協議棧,若數據包密集(如千兆網卡滿速傳輸),會產生大量硬中斷,頻發的上下文切換會增加 CPU 開銷,同時會使CPU 緩存失效。
NAPI (NET API)是為了在高吞吐量場景下減少硬中斷次數,所以 NAPI通過 “硬中斷觸發 + 軟中斷輪詢” 的混合模式平衡效率與實時性,而 poll 函數正是輪詢階段的核心執行者。poll 函數由網卡驅動實現,內核通過注冊機制 “記住” 這個函數的地址,以便在軟中斷中調用。注冊過程本質是將驅動的硬件操作邏輯接入內核的網絡中斷處理框架。
配置硬件參數:設置網卡的 MAC 地址、雙工模式(全雙工 / 半雙工)、傳輸速率(如 1Gbps)等基礎參數。
[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]-[~]$這一步相當于 “打通網卡與內核的通信通道”,讓硬件具備接收數據包的能力。
4.啟動網卡:配置隊列與中斷
驅動初始化完成后,內核會將網卡從 “down” 狀態切換到 “up” 狀態(即啟動網卡),同時完成兩項關鍵配置:
分配 RX/TX 隊列:通常為每個 CPU 核心分配獨立的接收隊列(RX 隊列)和發送隊列(TX 隊列)(即 RSS 隊列技術),實現數據包的負載均衡,避免單隊列成為性能瓶頸;
查看通道信息,網卡的 “通道” 是硬件層面的數據處理隊列,用于將網絡收發任務分配到不同 CPU 核心,實現并行處理以提升吞吐量。
[root@liruilongs.github.io]-[~]$ ethtool -l enp3s0
Channel parameters for enp3s0:
Pre-set maximums: # 硬件支持的最大隊列數(驅動或固件限制)
RX: n/a # 接收隊列最大數:不支持獨立RX隊列(n/a = not applicable)
TX: n/a # 發送隊列最大數:不支持獨立TX隊列
Other: n/a # 其他隊列(如管理隊列)不支持
Combined: 1 # 組合隊列(同時處理RX和TX)最大數:1
Current hardware settings: # 當前生效的隊列配置
RX: n/a # 當前未啟用獨立RX隊列
TX: n/a # 當前未啟用獨立TX隊列
Other: n/a # 當前未啟用其他隊列
Combined: 1 # 當前啟用1個組合隊列上面的配置可以看出張網卡(虛擬化環境中的網卡, KVM 的 virtio-net)不支持獨立的 RX/TX 隊列,而是使用 “組合隊列”(Combined)同時處理接收和發送數據,當前配置了 1 個組合隊列
通過 sys 偽文件系統查看網卡的 “通道” 信息,可以看到 “通道” 的數量為 1,即 1 個 RX 隊列和 1 個 TX 隊列。
[root@developer ~]# ls /sys/class/net/enp3s0/queues/
rx-0 tx-0
[root@developer ~]#確認網卡類型:
[root@liruilongs.github.io]-[~]$ ethtool -i enp3s0 | grep vir
driver: virtio_net
[root@liruilongs.github.io]-[~]$注冊硬中斷處理函數:將網卡的 “數據包到達中斷” 與內核中的中斷處理函數綁定,當網卡收到數據時,能觸發 CPU 響應硬中斷,網卡中斷的觸發流程。
硬件層面:網卡收到數據包后,通過 PCIe 總線向 CPU 發送中斷請求(IRQ)信號;CPU 層面:CPU 響應中斷,暫停當前任務,跳轉到內核預設的中斷處理入口;內核層面:內核通過中斷向量號(IRQ 號)找到對應的處理函數(ISR)并執行。
實際上就是將網卡的硬件中斷信號與內核中的中斷服務程序(ISR)建立關聯,讓內核知道 “哪個 IRQ 號對應哪個網卡的哪個事件(如數據包到達)”。
至此,收包前的準備工作全部完成,內核開啟網卡的硬中斷,進入 “等待數據包到達” 的狀態。
二、數據包到達后:內核如何 “處理” 數據包?
當外部數據包通過網線到達網卡時,Linux 內核會啟動一套 “流水線式” 的處理流程,從硬件接收到底層協議解析,再到應用層分發,每一步都分工明確。
深入理解Linux網絡: 修煉底層內功,掌握高性能原理 (張彥飛)
第一步:網卡 DMA 寫入內存,觸發硬中斷
網卡接收到物理層的以太網幀后,不會直接通知 CPU “幫忙”,而是通過之前配置的 DMA 通道,直接將數據包寫入內存中 RX 隊列對應的 RingBuffer(環形緩沖區) ,這個過程完全不需要 CPU 參與,極大減少了 CPU 的負擔。
ethtool -g 用于查看網卡的硬件環形緩沖區(Ring Buffer)參數。
[root@liruilongs.github.io]-[~]$ ethtool -g enp3s0
Ring parameters for enp3s0:
Pre-set maximums: # 硬件支持的最大緩沖區數量(驅動/固件限制)
RX: 2048 # 接收環形緩沖區最大可配置數量:2048個
RX Mini: n/a # 小數據包接收緩沖區:不支持(n/a = not applicable)
RX Jumbo: n/a # 巨型幀接收緩沖區:不支持
TX: 2048 # 發送環形緩沖區最大可配置數量:2048個
Current hardware settings: # 當前生效的緩沖區配置
RX: 2048 # 當前接收緩沖區數量:2048個
TX: 2048 # 當前發送緩沖區數量:2048個該網卡支持獨立的接收(RX)和發送(TX)環形緩沖區;當前接收和發送緩沖區均配置為 2048 個(已達硬件最大限制);并且不支持 小數據包,巨型幀 的專用緩沖區(僅用通用緩沖區處理所有包)。
單個緩沖區的大小通常與網卡的 MTU(最大傳輸單元)匹配,計算公式為:單緩沖區大小 ≈ MTU + 協議頭部開銷(約 18-42 字節)。
[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 1000MTU 為 1500 ,所以總 DMA 物理內存估算,計算該網卡的 DMA 內存總占用:
- 接收緩沖區:2048 個 × 1538 字節 ≈ 3.07 MB;
- 發送緩沖區:2048 個 × 1538 字節 ≈ 3.07 MB。
總計:約 6.14 MB(實際可能略大,因驅動會預留少量管理內存)。
當 RingBuffer 滿的時候,新來的數據包將被去棄。使? 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]-[~]$表?因為環形隊列滿被丟棄的包數。如果發現有丟包,可能需要通過ethtool命令來加?環形隊列的長度。
也可以通過 ethtool -S enp3s0 的 rx_queue_0_drops 指標來查看。
[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]-[~]$通常很少有修改的需求,隊列不是越長越好,過長會增加數據延遲(數據包在隊列中等待時間變長),通常調整為 512~2048(根據業務場景:高吞吐量場景可稍大,低延遲場景需偏小)。
如果是小數據包,數據包寫入完成后,網卡會向 CPU 發送一個硬中斷請求(IRQ),相當于告訴 CPU:“有新數據包到了,快處理!”
如果是大數據包,大流量,修改隊列長度 2048 時,可能要積累 670 個包才觸發一次 NAPI 輪詢;若隊列長度增至 5242,可能要積累 2048 個包才處理,即位于第一個緩存區的數據包和第2048個緩存區的數據包在同一時間被處理,對第一個緩存區的數據就造成了延遲。
這里積累的數據觸發機制通過 NAPI 權重(napi weight)來控制,本質是 單次輪詢最多處理的數據包上限。當隊列中積累的數據包數量達到或接近這個值時,NAPI 會觸發處理。 默認是 64。
[root@developer ~]# cat /sys/module/virtio_net/parameters/napi_weight
64
[root@developer ~]#需要說明的是 napi_weight 是 “上限”,不是 “觸發閾值”。NAPI 輪詢的觸發閾值(即積累多少個數據包才會觸發一次輪詢處理)控制還有一個影響參數,即 內核全局預算(netdev_budget),netdev_budget 限制一次軟中斷中所有 NAPI 實例處理的數據包總數,即所有的網卡,避免單個軟中斷占用 CPU 過久。 默認值為 300。
[root@developer ~]# sysctl net.core.netdev_budget
net.core.netdev_budget = 300
[root@developer ~]#權重是針對單個隊列的,而預算 netdev_budget限制了一次軟中斷處理周期內所有NAPI實例總共能處理的數據包數量,防止軟中斷過長時間占用CPU。
第二步:CPU 響應硬中斷,觸發軟中斷
CPU 收到硬中斷請求后,會暫停當前正在執行的任務,切換到內核態,執行之前注冊的硬中斷處理函數。但這里有個關鍵設計:硬中斷處理函數非常 “輕量化”,只做兩件小事:
- 告知網卡 “我已收到中斷通知”,避免網卡重復觸發中斷;
- 向內核發起網絡接收軟中斷請求(NET_RX_SOFTIRQ),將后續的 “數據包解析、協議處理” 等耗時操作,交給軟中斷處理。
為什么硬中斷處理要 “輕量化”?因為硬中斷的優先級最高,若處理耗時會阻塞其他高優先級任務(如系統調用、其他硬件中斷),影響系統響應性。
/proc/softirqs 文件輸出展示了系統中軟中斷(Software Interrupt)在各個 CPU 核心上的觸發次數。
[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 分別是 “發送” 和 “接收” 對應在每個CPU核心的軟中斷,分別對應 “發送” 和 “接收” 數據包的處理。
第三步:ksoftirqd 處理軟中斷,讀取數據包
ksoftirqd 線程會周期性檢查內核的軟中斷請求隊列,當發現 NET_RX_SOFTIRQ 軟中斷時,會立即開始工作:
- 關閉硬中斷:避免處理軟中斷時被新的硬中斷打斷(防止數據競爭,保證數據一致性);
- 調用 poll 函數讀包:通過驅動注冊的
poll函數,從RX隊列的RingBuffer中讀取數據包,并將其封裝成內核統一的sk_buff結構體(sk_buff是內核中描述數據包的 “標準容器”,包含數據包的所有信息,如協議頭、數據內容、長度等); - 開啟硬中斷:數據包讀取完成后,重新開啟硬中斷,允許接收新的數據包。
軟中斷處理時,單次調用 NAPI 的 poll 函數,循環處理數據包(數量不超過 weight 和 netdev_budget 的最小值),若本次讀了閾值內的包(packets_read=64),即使 RX_Ring 還有數據,也會退出循環,避免單次 poll 占用 CPU 過久。
通過 proc/interrupts 可以看到 虛擬網卡(virtio3)的 數據請求隊列中斷(req.0 表示第 0 個數據隊列,負責網卡的數據包接收 / 發送)。
[root@developer ~]# cat /proc/interrupts | grep req
81: 16363 0 0 0 ITS-MSI 2097153 Edge virtio3-req.0對應的中斷親和性,即中斷發生在那個CPU,可以通過中斷號查看,smp_affinity(中斷親和性)控制 “該中斷允許在哪些 CPU 核心上運行”,值為十六進制:
[root@developer ~]# cat /proc/irq/81/smp_affinity
f
[root@developer ~]#十六進制 f = 二進制 1111,對應系統的 4 個 CPU 核心(CPU0~CPU3)。
這一步完成了 “從硬件緩沖區到內核緩沖區” 的數據包轉移,為后續的協議解析做好準備。
通過 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第四步:協議棧解析,分發到應用層
poll函數將sk_buff交給內核協議棧后,協議棧會按 “從下到上” 的順序逐層解析數據包,就像 “拆快遞” 一樣,一層一層揭開數據包的 “包裝”:
- IP 層處理:調用
ip_rcv()函數解析 IP 頭,檢查 IP 地址、校驗和等信息,然后根據 IP 頭中的protocol字段,判斷數據包的上層協議(如 UDP 對應 17,TCP 對應 6); - 傳輸層處理:
- 若為 UDP 包:調用
udp_rcv()函數解析 UDP 頭,檢查端口號,然后通過 socket 將數據分發給對應的應用進程; - 若為 TCP 包:調用
tcp_rcv_v4()函數解析 TCP 頭,處理 TCP 連接狀態(如三次握手、重傳、流量控制),最后將數據通過 socket 交給應用進程。
至此,一個數據包從 “到達網卡” 到 “被應用進程接收” 的完整流程就結束了。
內核發包機制認知
網絡發包的核心邏輯是 用戶進程發起請求,內核協議棧分層處理,最終通過網卡硬件發送數據。
深入理解Linux網絡: 修煉底層內功,掌握高性能原理
一、發包前的準備工作:發送隊列 RingBuffer 的構建
發包同樣是一個涉及用戶態、內核態多層協作的復雜過程,在實際的發包之前,內核會在網卡啟動之后也做一些發包相關的準備,主要是前面我們講到的分配傳輸隊列 RingBuifer的過程。
不同的網卡驅動實現不同,會分配兩個環形數組
- 一個用于內核使用(分配虛擬內存)
- 一個用于網卡硬件使用,分配的是連續的物理內存(DMA)
在后面的發包過程中會進行對應的地址映射,這兩個環形數組中相同位置的指針都將指向同?個skb(數據包的內核結構體),內核往對應 skb 地址寫數據,網卡硬件就能共同訪問同樣的數據,負責發送。
二、用戶進程發起請求,內核協議棧分層處理
1.用戶進程:發起發送請求
用戶進程通過 sendto系統調用(或 send/write)發起網絡發送,下面的是一個Python 的Demo,從 Python 標準庫的 socket 模塊封裝,最終觸發操作系統的 send 系統調用。
import socket
# 創建TCP socket(內核創建對應的socket對象)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立連接(內核完善socket對象的源/目標地址信息)
sock.connect(("110.242.69.21", 80))
# 準備發送數據
request_data = ()
# 調用send系統調用(內核查找socket對象并構造msghdr結構體)
bytes_sent = sock.send(request_data.encode())
# 接收響應
response = sock.recv(4096)
# 關閉連接(內核銷毀socket對象)
sock.close()此時進程從用戶態切換到內核態,內核會執行兩項核心操作:
- 查找對應的
socket對象(標識 TCP/UDP 連接的源 IP、目標 IP、源端口、目標端口); - 構造
msghdr結構體(封裝待發送消息的地址信息、數據長度等元數據)。
之后會進入內核網絡協議棧分層處理。
2.協議棧分層處理(內核態):從傳輸層到網絡設備
內核態的協議棧處理遵循 TCP/IP 分層模型,從傳輸層到網絡層、鄰居子系統、網絡設備子系統,逐步完成數據封裝與轉發。
傳輸層(TCP 處理):
首次內存拷貝與淺拷貝,內核調用 tcp_sendmsg 函數處理傳輸層邏輯,核心操作包括:
- 申請
skb(Socket Buffer,內核中存儲網絡數據的緩沖區),并將用戶態數據拷貝到skb; - 進行
滑動窗口管理(控制數據發送速率,避免對端接收緩沖區溢出); - 設置
TCP 頭部(如源端口、目標端口、序列號、確認號等,保證 TCP 可靠傳輸)。
這一步會涉及兩次關鍵的內存拷貝操作:
- 第一次拷貝(必需,深拷貝):內核先申請
skb(Socket Buffer,內核中存儲網絡數據的專用緩沖區),再將用戶態傳遞的數據包拷貝到skb中。該拷貝的開銷隨數據量增大而顯著增加; - 第二次拷貝(必需,淺拷貝):為保證 TCP 可靠傳輸(當對端未返回 ACK 時需重發數據),內核會克隆
skb的 “描述符”(生成新的skb副本),但數據本身復用原skb的內存(僅拷貝元信息,開銷極低)。
網絡層(IP 處理):
內核調用 ip_queue_xmit 函數處理網絡層邏輯,核心操作包括:
路由查找:根據目標 IP 地址查詢路由表,確定數據發送的下一跳地址;網絡過濾:經過netfilter框架(如 iptables 規則),判斷是否允許數據發送;
MTU 檢查與分片:若數據包大小超過網絡設備的 MTU(最大傳輸單元,默認 1500 字節),則觸發 IP 分片,申請多個新 skb,將原 skb 中的數據深拷貝到新 skb 中(第三次拷貝,可選)。
結合傳輸層處理,內核發包的內存拷貝可總結為 “兩次必需 + 一次可選”:
- 必需拷貝 1:用戶態數據 → 內核態
skb(深拷貝); - 必需拷貝 2:傳輸層
skb→ 網絡層skb(淺拷貝,僅描述符); - 可選拷貝 3:IP 分片時的深拷貝(僅當數據包超過 MTU 時觸發)。
鄰居子系統:
獲取目標 MAC 地址,內核再次調用 ip_queue_xmit(邏輯分支不同),通過 ARP 協議獲取目標設備的 MAC 地址:
- 緩存命中:若本地 ARP 緩存中存在 “目標 IP → MAC 地址” 的映射,直接使用該 MAC 地址;
- 緩存未命中:發送 ARP 請求包(詢問目標 IP 對應的 MAC 地址),待收到 ARP 響應后繼續傳輸。
網絡設備子系統:
這里網絡設備子系統主要用于隊列選擇和觸發軟中斷。
隊列選擇:內核調用 dev_queue_xmit,根據網卡的多隊列配置(若支持),將 skb 放入對應的發送隊列(用于負載均衡),談后調用qdise_run 發送數據,如果當前任然持有CPU時間片,那么會 while循環不斷地從隊列中取出 skb 并進?發送(qdisc_restar 調用)。注意,這個時侯其實都占?的是?戶進程的內核態時間。只有當 quota?盡或者其他進程需要CPU的時候才觸發 NET_TX_SOFTIRQ類型軟中斷**(由 ksoftirqd 內核線程處理)。
在這期間,內核會調用網卡驅動程序 dev_hard_start_xmit通過網卡發送數據。
所以為什么說 90% 以上的網絡發包開銷集中在 “內核態處理階段”(協議棧解析、內存拷貝),從這里可以看到僅當內核態進程時間片用盡、需由 ksoftirqd 線程繼續處理發送隊列時,才會觸發 NET_TX 軟中斷,統計到 si(軟中斷 CPU 時間) 中。
這也是在服務器上查看/proc/softirgs,?般 NET_RX 都要? NET_TX ?得多的原因之一。對于接收來說,都要經過 NET_RX 軟中斷,?對于發送來說,只有內核態CPU配額?盡才讓軟中斷上。
3.軟中斷與硬中斷:內核與硬件的異步協作
到這里以后發送數據消耗的CPU就都顯?在軟中斷的CPU使用率里面,不會消耗?戶進程的內核態時間。
軟中斷處理:ksoftirqd 內核線程(每個 CPU 核心對應一個)檢測到 NET_TX 軟中斷后,調用 net_tx_action 函數,再通過 qdisc_run 調度發送隊列(如 FIFO、優先級調度),將 skb 提交給網卡驅動 dev_hard_start_xmit 調用;
硬中斷處理:網卡完成數據發送后,觸發硬中斷(如 igb_msix_ring 函數,因網卡驅動而異),通知內核釋放 skb 內存、清理 RingBuffer,為下一次發送做準備。這里的硬中斷會觸發 NET_RX_SOFTIRQ 軟中斷,即網卡的 “發送完成通知” 與 “接收數據” 觸發的硬中斷,最終都會調用 NET_RX_SOFTIRQ(而非 NET_TX_SOFTIRQ),導致 “發送完成” 的開銷被統計到 NET_RX 中;這也是上面看到 NET_RX 的 CPU 使用率要比 NET_TX 大的原因。
三、網卡驅動的數據包處理
下面我們看下網卡驅動如何發送數據包,在上面的網絡設備子系統中dev_hard_start_xmit調用驅動,驅動銜接硬件
- 入口:內核調用驅動,通過
dev_hard_start_xmit函數,調用驅動注冊的ndo_start_xmit回調(如igb網卡的igb_xmit_frame),完成“內核到驅動”的交接。 - 驅動綁定與DMA映射(igb網卡為Demo)
igb_xmit_frame先將skb分配到對應發送隊列,再通過igb_xmit_frame_ring把skb綁定到RingBuffer(環形緩沖區)的igb_tx_buffer;隨后igb_tx_map通過dma_map_single,將skb虛擬地址轉成硬件可訪問的物理地址,寫入硬件描述符。 - 觸發硬件發送 驅動更新網卡寄存器(如
E1000_TDT),通知硬件讀取描述符中的物理地址,通過DMA直接讀內存數據并發送,無需CPU參與,整個流程核心是“DMA映射”和“RingBuffer銜接”,實現軟件到硬件的高效數據傳遞。





























