Linux網(wǎng)絡數(shù)據(jù)包接收:原理、流程與優(yōu)化策略
在當今數(shù)字化時代,網(wǎng)絡已成為計算機系統(tǒng)不可或缺的部分。無論是日常的網(wǎng)頁瀏覽、文件傳輸,還是大規(guī)模數(shù)據(jù)中心的高效通信,網(wǎng)絡數(shù)據(jù)包的收發(fā)都在其中扮演著重要角色。對于 Linux 系統(tǒng)而言,深入理解網(wǎng)絡數(shù)據(jù)包的接收過程,是優(yōu)化網(wǎng)絡性能、解決網(wǎng)絡問題的關鍵。
想象一下,你正在進行在線視頻會議,突然畫面卡頓、聲音中斷;或者在進行大規(guī)模數(shù)據(jù)傳輸時,速度遠遠低于預期。這些令人困擾的網(wǎng)絡問題,很多都與 Linux 系統(tǒng)對網(wǎng)絡數(shù)據(jù)包的接收處理過程緊密相關。而這一過程,涉及到從硬件層面的網(wǎng)卡,到軟件層面的內核協(xié)議棧、中斷處理機制等多個復雜環(huán)節(jié)。
通過深入剖析 Linux 下網(wǎng)絡數(shù)據(jù)包的接收過程,我們不僅能夠洞悉網(wǎng)絡數(shù)據(jù)在系統(tǒng)內部的流轉路徑,還能明確各環(huán)節(jié)對網(wǎng)絡性能的影響。這對于開發(fā)者、系統(tǒng)管理員來說,無疑是優(yōu)化網(wǎng)絡性能、解決網(wǎng)絡故障的有力武器。它可以幫助我們在面對網(wǎng)絡問題時,迅速定位問題根源,采取有效的解決措施,如調整網(wǎng)絡配置、優(yōu)化內核參數(shù)等,從而保障網(wǎng)絡的穩(wěn)定高效運行。
一、引言
這里深度理解一下在Linux下網(wǎng)絡包的接收過程,為了簡單起見,我們用udp來舉例,如下:
int main(){
int serverSocketFd = socket(AF_INET, SOCK_DGRAM, 0);
bind(serverSocketFd, ...);
char buff[BUFFSIZE];
int readCount = recvfrom(serverSocketFd, buff, BUFFSIZE, 0, ...);
buff[readCount] = '\0';
printf("Receive from client:%s\n", buff);
}
上面代碼是一段udp server接收收據(jù)的邏輯。只要客戶端有對應的數(shù)據(jù)發(fā)送過來,服務器端執(zhí)行recv_from后就能收到它,并把它打印出來。那么當網(wǎng)絡包達到網(wǎng)卡,直到recvfrom收到數(shù)據(jù),這中間究竟都發(fā)生過什么?
二、Linux網(wǎng)絡基礎
2.1 網(wǎng)絡協(xié)議棧
在Linux內核實現(xiàn)中,鏈路層協(xié)議靠網(wǎng)卡驅動來實現(xiàn),內核協(xié)議棧來實現(xiàn)網(wǎng)絡層和傳輸層。內核對更上層的應用層提供socket接口來供用戶進程訪問。我們用Linux的視角來看到的TCP/IP網(wǎng)絡分層模型應該是下面這個樣子的。
圖片
鏈路層,作為網(wǎng)絡協(xié)議棧的最底層,主要負責將網(wǎng)絡層傳來的數(shù)據(jù)轉換為物理信號,通過物理介質進行傳輸,并處理物理介質上的數(shù)據(jù)接收。在以太網(wǎng)環(huán)境中,鏈路層使用以太網(wǎng)協(xié)議,它定義了數(shù)據(jù)幀的格式,包括源 MAC 地址、目的 MAC 地址、類型字段以及數(shù)據(jù)部分等。
例如,當你的計算機向同一局域網(wǎng)內的另一臺計算機發(fā)送數(shù)據(jù)時,鏈路層會在數(shù)據(jù)幀中填入對方的 MAC 地址,以便數(shù)據(jù)能夠準確地發(fā)送到目標設備。ARP(地址解析協(xié)議)也工作在這一層,它用于將 IP 地址解析為對應的 MAC 地址。當主機需要向某個 IP 地址發(fā)送數(shù)據(jù)時,如果不知道該 IP 地址對應的 MAC 地址,就會發(fā)送 ARP 請求廣播,目標主機收到后會回復其 MAC 地址,從而建立起 IP 地址與 MAC 地址的映射關系。
網(wǎng)絡層,其主要職責是實現(xiàn)數(shù)據(jù)包的路由和轉發(fā),使數(shù)據(jù)能夠在不同的網(wǎng)絡之間傳輸。IP 協(xié)議是網(wǎng)絡層的核心協(xié)議,它為每個網(wǎng)絡設備分配唯一的 IP 地址,并負責將數(shù)據(jù)包從源 IP 地址發(fā)送到目的 IP 地址。IP 協(xié)議還支持數(shù)據(jù)包的分片與重組功能,當數(shù)據(jù)包的大小超過鏈路層的最大傳輸單元(MTU)時,IP 協(xié)議會將數(shù)據(jù)包進行分片,在到達目標主機后再進行重組。例如,當你訪問外網(wǎng)的網(wǎng)站時,數(shù)據(jù)包會經(jīng)過多個路由器的轉發(fā),每個路由器根據(jù) IP 地址來決定數(shù)據(jù)包的下一跳路徑,最終將數(shù)據(jù)包送達目標服務器。ICMP(互聯(lián)網(wǎng)控制消息協(xié)議)也是網(wǎng)絡層的重要協(xié)議,它用于在網(wǎng)絡設備之間傳遞控制消息,如 ping 命令就是利用 ICMP 協(xié)議來測試網(wǎng)絡的連通性。
傳輸層,主要負責為應用層提供端到端的通信服務,確保數(shù)據(jù)的可靠傳輸或高效傳輸。TCP(傳輸控制協(xié)議)和 UDP(用戶數(shù)據(jù)報協(xié)議)是傳輸層的兩個主要協(xié)議。TCP 是一種面向連接的、可靠的協(xié)議,它通過三次握手建立連接,在數(shù)據(jù)傳輸過程中使用序列號、確認號和重傳機制來確保數(shù)據(jù)的完整性和順序性。例如,在進行文件傳輸時,TCP 協(xié)議能夠保證文件的每個字節(jié)都準確無誤地到達對方。UDP 則是一種無連接的、不可靠的協(xié)議,它不保證數(shù)據(jù)的可靠傳輸,但具有傳輸速度快、開銷小的特點,常用于對實時性要求較高的應用場景,如視頻流、音頻流等。比如,在觀看在線視頻時,UDP 協(xié)議可以快速地將視頻數(shù)據(jù)傳輸給用戶,即使偶爾有數(shù)據(jù)包丟失,也不會對用戶體驗造成太大影響。
這些不同層次的協(xié)議相互協(xié)作,共同完成了網(wǎng)絡數(shù)據(jù)包的接收與發(fā)送過程。從物理介質上接收到的數(shù)據(jù),會依次經(jīng)過鏈路層、網(wǎng)絡層和傳輸層的處理,最終被傳遞到應用層,供應用程序使用。
2.2 中斷機制
在 Linux 網(wǎng)絡數(shù)據(jù)處理中,中斷機制起著至關重要的作用,它主要包括硬中斷和軟中斷 。
硬中斷,是由硬件設備產(chǎn)生的,用于通知 CPU 有緊急事件需要處理。當網(wǎng)卡接收到網(wǎng)絡數(shù)據(jù)包時,會觸發(fā)一個硬中斷信號,通知 CPU 有新的數(shù)據(jù)到達。這個過程就像是門鈴突然響起,提醒主人有訪客到來。CPU 在接收到硬中斷信號后,會立即暫停當前正在執(zhí)行的任務,保存現(xiàn)場信息,然后跳轉到對應的中斷處理程序。對于網(wǎng)卡的硬中斷處理程序,其主要任務是將接收到的數(shù)據(jù)從網(wǎng)卡的緩沖區(qū)拷貝到內存中,并進行一些初步的處理,如設置數(shù)據(jù)的相關標志位等。由于硬中斷處理需要快速響應,以避免數(shù)據(jù)丟失,因此硬中斷處理程序通常只執(zhí)行一些緊急且耗時較短的操作。
軟中斷,是一種推后執(zhí)行的機制,用于處理那些可以稍微延遲處理的任務。在硬中斷處理完成后,會觸發(fā)相應的軟中斷,將數(shù)據(jù)包的后續(xù)處理工作交給軟中斷來完成。軟中斷的處理函數(shù)通常在一個特定的上下文中執(zhí)行,這個上下文允許執(zhí)行一些相對耗時的操作,而不會影響硬中斷的及時響應。例如,在網(wǎng)絡數(shù)據(jù)處理中,軟中斷會對從網(wǎng)卡拷貝到內存中的數(shù)據(jù)包進行進一步的解析和處理,將其傳遞給相應的網(wǎng)絡協(xié)議棧層進行后續(xù)處理。軟中斷的優(yōu)勢在于可以在合適的時機批量處理多個任務,提高系統(tǒng)的整體效率。與硬中斷相比,軟中斷的觸發(fā)不是由硬件直接產(chǎn)生,而是由軟件在特定條件下觸發(fā),并且軟中斷的處理可以在多個 CPU 核心上并行進行,從而提高處理速度。
硬中斷和軟中斷在 Linux 網(wǎng)絡數(shù)據(jù)處理中緊密配合。硬中斷負責快速響應硬件事件,及時將數(shù)據(jù)從硬件設備傳輸?shù)絻却?;軟中斷則負責對數(shù)據(jù)進行后續(xù)的詳細處理,將數(shù)據(jù)傳遞給相應的協(xié)議棧層進行進一步的分析和處理,確保網(wǎng)絡數(shù)據(jù)能夠被系統(tǒng)有效地接收和處理。
內核實現(xiàn)了網(wǎng)絡層的 ip 協(xié)議,也實現(xiàn)了傳輸層的 tcp 協(xié)議和 udp 協(xié)議。這些協(xié)議對應的實現(xiàn)函數(shù)分別是 ip_rcv(),tcp_v4_rcv()和udp_rcv()。
網(wǎng)絡協(xié)議棧是通過函數(shù) inet_init() 注冊的,通過inet_init,將這些函數(shù)注冊到了inet_protos和ptype_base數(shù)據(jù)結構中了。如下圖:
相關代碼如下:
//net/ipv4/af_inet.c
static struct packet_type ip_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_IP),
.func = ip_rcv,
.list_func = ip_list_rcv,
};
static const struct net_protocol tcp_protocol = {
.handler = tcp_v4_rcv,
.err_handler = tcp_v4_err,
.no_policy = 1,
.icmp_strict_tag_validation = 1,
};
static const struct net_protocol udp_protocol = {
.handler = udp_rcv,
.err_handler = udp_err,
.no_policy = 1,
};
static int __init inet_init(void){
......
if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
pr_crit("%s: Cannot add ICMP protocol\n", __func__);
if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0) //注冊 udp_rcv()
pr_crit("%s: Cannot add UDP protocol\n", __func__);
if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0) //注冊 tcp_v4_rcv()
pr_crit("%s: Cannot add TCP protocol\n", __func__);
......
dev_add_pack(&ip_packet_type); /注冊 ip_rcv()
}
上面的代碼中我們可以看到,udp_protocol結構體中的handler是udp_rcv,tcp_protocol結構體中的handler是tcp_v4_rcv,通過inet_add_protocol被初始化了進來。
int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol){
if (!prot->netns_ok) {
pr_err("Protocol %u is not namespace aware, cannot register.\n",
protocol);
return -EINVAL;
}
return !cmpxchg((const struct net_protocol **)&inet_protos[protocol],
NULL, prot) ? 0 : -1;
}
inet_add_protocol函數(shù)將tcp和udp對應的處理函數(shù)都注冊到了inet_protos數(shù)組中了。
再看dev_add_pack(&ip_packet_type);這一行,ip_packet_type結構體中的type是協(xié)議名,func是ip_rcv函數(shù),在dev_add_pack中會被注冊到ptype_base哈希表中。
//net/core/dev.c
void dev_add_pack(struct packet_type *pt){
struct list_head *head = ptype_head(pt);
......
}
static inline struct list_head *ptype_head(const struct packet_type *pt){
if (pt->type == htons(ETH_P_ALL))
return &ptype_all;
else
return &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}
這里我們需要記住inet_protos記錄著udp,tcp的處理函數(shù)地址,ptype_base存儲著ip_rcv()函數(shù)的處理地址。后面我們會看到軟中斷中會通過ptype_base找到ip_rcv函數(shù)地址,進而將ip包正確地送到ip_rcv()中執(zhí)行。在ip_rcv中將會通過inet_protos找到tcp或者udp的處理函數(shù),再而把包轉發(fā)給udp_rcv()或tcp_v4_rcv()函數(shù)。
三、接收前的準備工作
3.1 網(wǎng)絡子系統(tǒng)初始化
在 Linux 內核啟動的過程中,網(wǎng)絡子系統(tǒng)的初始化是一項關鍵任務,它為后續(xù)的網(wǎng)絡數(shù)據(jù)接收和處理奠定了基礎。這一過程涉及一系列復雜的步驟和眾多關鍵函數(shù)的調用。
在內核啟動階段,start_kernel函數(shù)作為啟動的入口,會有條不紊地調用一系列初始化函數(shù)。其中,net_dev_init函數(shù)在網(wǎng)絡子系統(tǒng)初始化中扮演著核心角色。net_dev_init函數(shù)會對每個可能的 CPU 進行初始化操作,為網(wǎng)絡數(shù)據(jù)的接收和處理準備好相關的數(shù)據(jù)結構。例如,它會初始化每個 CPU 上的softnet_data結構體,該結構體包含了用于存儲網(wǎng)絡數(shù)據(jù)包的隊列,如input_pkt_queue用于接收來自網(wǎng)絡設備的數(shù)據(jù)包,process_queue則用于存放等待進一步處理的數(shù)據(jù)包 。通過skb_queue_head_init函數(shù)對這些隊列進行初始化,確保它們能夠正確地存儲和管理數(shù)據(jù)包。
在網(wǎng)絡子系統(tǒng)初始化過程中,還需要對軟中斷進行注冊。軟中斷是一種用于處理可延遲任務的機制,在網(wǎng)絡數(shù)據(jù)處理中起著至關重要的作用。通過open_softirq函數(shù),分別注冊了用于網(wǎng)絡發(fā)送的NET_TX_SOFTIRQ和用于網(wǎng)絡接收的NET_RX_SOFTIRQ軟中斷。以NET_RX_SOFTIRQ為例,它會關聯(lián)到net_rx_action函數(shù),當網(wǎng)絡設備接收到數(shù)據(jù)包并觸發(fā)相應的軟中斷時,net_rx_action函數(shù)將被調用,從而啟動對接收數(shù)據(jù)包的后續(xù)處理流程。linux內核通過調用subsys_initcall來初始化各個子系統(tǒng),其中網(wǎng)絡子系統(tǒng)的初始化會執(zhí)行到net_dev_init函數(shù):
//net/core/dev.c
static int __init net_dev_init(void){
......
for_each_possible_cpu(i) {
struct softnet_data *sd = &per_cpu(softnet_data, i);
memset(sd, 0, sizeof(*sd));
skb_queue_head_init(&sd->input_pkt_queue);
skb_queue_head_init(&sd->process_queue);
sd->completion_queue = NULL;
INIT_LIST_HEAD(&sd->poll_list);
......
}
......
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
}
subsys_initcall(net_dev_init);
首先為每個CPU都申請一個softnet_data數(shù)據(jù)結構,在這個數(shù)據(jù)結構里的poll_list是等待驅動程序將其poll函數(shù)注冊進來,稍后網(wǎng)卡驅動初始化的時候我們可以看到這一過程。
然后 open_softirq 為每一種軟中斷都注冊一個處理函數(shù)。NET_TX_SOFTIRQ的處理函數(shù)為net_tx_action,NET_RX_SOFTIRQ的為net_rx_action。
//kernel/softirq.c
void open_softirq(int nr, void (*action)(struct softirq_action *)){
softirq_vec[nr].action = action;
}
open_softirq 會把不同的 action 記錄在softirq_vec變量里的。后面ksoftirqd線程收到軟中斷的時候,也會使用這個變量來找到每一種軟中斷對應的處理函數(shù)。
圖片
3.2 網(wǎng)卡驅動初始化
網(wǎng)卡驅動的初始化是網(wǎng)絡數(shù)據(jù)包能夠被正確接收的前提條件,它涉及到驅動的加載與注冊,以及為數(shù)據(jù)接收所做的一系列準備工作。
在 Linux 系統(tǒng)中,大多數(shù)網(wǎng)卡通過 PCI 總線與系統(tǒng)相連,內核通過 PCI 子系統(tǒng)對這些設備進行管理。當系統(tǒng)啟動時,會遍歷 PCI 總線上的設備,并為它們尋找合適的驅動程序。對于網(wǎng)卡驅動而言,首先要使用pci_register_driver函數(shù)向內核注冊驅動程序。在注冊過程中,需要提供一個pci_driver結構體,該結構體包含了驅動的名稱、所支持的 PCI 設備 ID 表、探測函數(shù)(probe)等重要信息。
以常見的igb網(wǎng)卡驅動為例,igb_driver結構體中定義了驅動的名稱為igb_driver_name,并指定了igb_pci_tbl作為所支持的 PCI 設備 ID 表。當內核發(fā)現(xiàn)一個 PCI 設備的 ID 與igb_pci_tbl中的某個條目匹配時,就會調用igb_probe探測函數(shù)。在igb_probe函數(shù)中,會進行一系列的初始化操作,如為網(wǎng)卡分配內存空間、初始化 DMA(直接內存訪問)、設置網(wǎng)卡的相關寄存器等。它還會注冊net_device結構體,這個結構體代表了網(wǎng)絡設備,包含了設備的各種屬性和操作函數(shù),如設備的打開、關閉、數(shù)據(jù)發(fā)送和接收等函數(shù)。通過這些初始化操作,網(wǎng)卡驅動為后續(xù)的數(shù)據(jù)接收做好了充分準備,確保網(wǎng)卡能夠與內核進行有效的通信,將接收到的網(wǎng)絡數(shù)據(jù)包傳遞給內核進行處理。這里以 FSL 系列網(wǎng)卡為例,其驅動位于:drivers/net/ethernet/freescale/fec_main.c
static struct platform_driver fec_driver = {
.driver = {
.name = DRIVER_NAME,
.pm = &fec_pm_ops,
.of_match_table = fec_dt_ids,
.suppress_bind_attrs = true,
},
.id_table = fec_devtype,
.probe = fec_probe,
.remove = fec_drv_remove,
};
static int
fec_probe(struct platform_device *pdev)
{
fec_enet_clk_enable
fec_reset_phy //使用gpio 復位phy 芯片
fec_enet_init //設置netdev_ops、設置ethtool_ops
for (i = 0; i < irq_cnt; i++) {
devm_request_irq(..., irq, fec_enet_interrupt, ...);
}
fec_enet_mii_init //讀取dts mdio節(jié)點下phy子節(jié)點,并注冊phy_device
register_netdev //注冊網(wǎng)絡設備
}
Linux 以太網(wǎng)驅動會向上層提供 net_device_ops ,方便應用層控制網(wǎng)卡,比如網(wǎng)卡被啟動(例如,通過 ifconfig eth0 up)的時候會被調用 fec_enet_open,此外它還包含著網(wǎng)卡發(fā)包、設置 mac 地址等回調函數(shù)。
static const struct net_device_ops fec_netdev_ops = {
.ndo_open = fec_enet_open,
.ndo_stop = fec_enet_close,
.ndo_start_xmit = fec_enet_start_xmit,
.ndo_select_queue = fec_enet_select_queue,
.ndo_set_rx_mode = set_multicast_list,
.ndo_validate_addr = eth_validate_addr,
.ndo_tx_timeout = fec_timeout,
.ndo_set_mac_address = fec_set_mac_address,
.ndo_eth_ioctl = fec_enet_ioctl,
#ifdef CONFIG_NET_POLL_CONTROLLER
.ndo_poll_controller = fec_poll_controller,
#endif
.ndo_set_features = fec_set_features,
.ndo_bpf = fec_enet_bpf,
.ndo_xdp_xmit = fec_enet_xdp_xmit,
};
此外,網(wǎng)卡驅動實現(xiàn)了 ethtool 所需要的接口,當 ethtool 發(fā)起一個系統(tǒng)調用之后,內核會找到對應操作的回調函數(shù)??梢钥吹?ethtool 這個命令之所以能查看網(wǎng)卡收發(fā)包統(tǒng)計、能修改網(wǎng)卡自適應模式、能調整RX 隊列的數(shù)量和大小,是因為 ethtool 命令最終調用到了網(wǎng)卡驅動的相應方法。
static const struct ethtool_ops fec_enet_ethtool_ops = {
.supported_coalesce_params = ETHTOOL_COALESCE_USECS |
ETHTOOL_COALESCE_MAX_FRAMES,
.get_drvinfo = fec_enet_get_drvinfo,
.get_regs_len = fec_enet_get_regs_len,
.get_regs = fec_enet_get_regs,
.nway_reset = phy_ethtool_nway_reset,
.get_link = ethtool_op_get_link,
.get_coalesce = fec_enet_get_coalesce,
.set_coalesce = fec_enet_set_coalesce,
#ifndef CONFIG_M5272
.get_pauseparam = fec_enet_get_pauseparam,
.set_pauseparam = fec_enet_set_pauseparam,
.get_strings = fec_enet_get_strings,
.get_ethtool_stats = fec_enet_get_ethtool_stats,
.get_sset_count = fec_enet_get_sset_count,
#endif
.get_ts_info = fec_enet_get_ts_info,
.get_tunable = fec_enet_get_tunable,
.set_tunable = fec_enet_set_tunable,
.get_wol = fec_enet_get_wol,
.set_wol = fec_enet_set_wol,
.get_eee = fec_enet_get_eee,
.set_eee = fec_enet_set_eee,
.get_link_ksettings = phy_ethtool_get_link_ksettings,
.set_link_ksettings = phy_ethtool_set_link_ksettings,
.self_test = net_selftest,
};
3.3 ksoftirqd 內核線程創(chuàng)建
ksoftirqd內核線程在 Linux 網(wǎng)絡數(shù)據(jù)處理中承擔著處理軟中斷的重要職責,其創(chuàng)建過程與系統(tǒng)的啟動流程緊密相關。
在系統(tǒng)啟動時,start_kernel函數(shù)會創(chuàng)建init線程,隨后init線程調用do_pre_smp_initcalls函數(shù)。在do_pre_smp_initcalls函數(shù)中,會調用spawn_ksoftirqd函數(shù)來創(chuàng)建ksoftirqd內核線程。spawn_ksoftirqd函數(shù)通過兩次調用cpu_callback函數(shù),分別使用CPU_UP_PREPARE和CPU_ONLINE參數(shù)來完成ksoftirqd線程的創(chuàng)建和喚醒。
當使用CPU_UP_PREPARE參數(shù)調用cpu_callback函數(shù)時,會通過kthread_create函數(shù)創(chuàng)建一個新的內核線程,并將其命名為ksoftirqd/%d,其中%d為 CPU 的編號。創(chuàng)建完成后,將該線程的task_struct指針存儲在per_cpu(ksoftirqd, hotcpu)變量中,以便后續(xù)對該線程進行管理和調度。接著,使用CPU_ONLINE參數(shù)再次調用cpu_callback函數(shù),此時的作用是喚醒剛剛創(chuàng)建的ksoftirqd線程,使其能夠開始執(zhí)行任務。
ksoftirqd線程的主要任務是處理系統(tǒng)中的軟中斷。當網(wǎng)絡設備接收到數(shù)據(jù)包并觸發(fā)硬中斷后,硬中斷處理程序會快速完成一些必要的操作,然后觸發(fā)相應的軟中斷。ksoftirqd線程會不斷地檢查是否有軟中斷等待處理,一旦發(fā)現(xiàn)有軟中斷,就會調用相應的軟中斷處理函數(shù),對網(wǎng)絡數(shù)據(jù)包進行進一步的處理,如解析數(shù)據(jù)包、將其傳遞給協(xié)議棧的上層進行處理等。通過這種方式,ksoftirqd線程有效地分擔了硬中斷處理的工作量,避免了硬中斷處理時間過長而影響系統(tǒng)對其他硬件中斷的響應,確保了系統(tǒng)能夠高效、穩(wěn)定地處理網(wǎng)絡數(shù)據(jù)。
四、數(shù)據(jù)包接收流程
4.1 網(wǎng)卡接收數(shù)據(jù)
當網(wǎng)絡數(shù)據(jù)包在網(wǎng)絡中傳輸并抵達網(wǎng)卡時,網(wǎng)卡便開始發(fā)揮其關鍵作用。網(wǎng)卡具備專門的硬件電路,能夠識別和捕獲這些數(shù)據(jù)包。通過直接內存訪問(DMA)技術,網(wǎng)卡得以將接收到的數(shù)據(jù)包高效地存入內存緩沖區(qū)中。
DMA 是一種強大的技術,它允許外部設備(如網(wǎng)卡)直接與內存進行數(shù)據(jù)傳輸,而無需 CPU 的頻繁干預 。在這個過程中,網(wǎng)卡中的 DMA 控制器會負責管理數(shù)據(jù)的傳輸過程,它會在內存中開辟出一塊特定的緩沖區(qū),通常被稱為 Ring Buffer(環(huán)形緩沖區(qū))。Ring Buffer 就像是一個環(huán)形的隊列,數(shù)據(jù)包按照順序依次存入其中。由于其環(huán)形的結構,當緩沖區(qū)的尾部到達末尾時,下一個數(shù)據(jù)包會接著存入緩沖區(qū)的頭部,從而實現(xiàn)了連續(xù)的數(shù)據(jù)存儲。
與傳統(tǒng)的通過 CPU 進行數(shù)據(jù)傳輸?shù)姆绞较啾龋珼MA 具有顯著的優(yōu)勢。傳統(tǒng)方式下,CPU 需要親自參與數(shù)據(jù)的讀取和寫入操作,這會占用大量的 CPU 時間和資源,導致 CPU 無法高效地處理其他任務。而采用 DMA 技術,CPU 可以將精力集中在其他重要的任務上,大大提高了系統(tǒng)的整體性能。例如,在進行大規(guī)模數(shù)據(jù)傳輸時,如果沒有 DMA 技術,CPU 可能會被數(shù)據(jù)傳輸任務所占據(jù),導致系統(tǒng)響應遲緩,其他應用程序無法正常運行。而有了 DMA 技術,網(wǎng)卡可以在后臺默默地將數(shù)據(jù)傳輸?shù)絻却嬷?,CPU 則可以同時處理用戶的其他操作,如運行其他應用程序、處理文件等,極大地提高了系統(tǒng)的并發(fā)處理能力。
4.2 硬件中斷處理
當網(wǎng)卡成功將數(shù)據(jù)包存入內存緩沖區(qū)后,為了及時通知 CPU 有新的數(shù)據(jù)到達,網(wǎng)卡會觸發(fā)一個硬件中斷信號 。這個信號就像是一個緊急通知,告訴 CPU 需要立即處理新到達的數(shù)據(jù)。
CPU 在接收到這個硬件中斷信號時,會迅速做出響應。它會暫停當前正在執(zhí)行的任務,保存當前任務的現(xiàn)場信息,包括寄存器的值、程序計數(shù)器的值等,以便在處理完中斷后能夠恢復到原來的任務執(zhí)行狀態(tài)。之后,CPU 會根據(jù)中斷向量表,找到對應的中斷處理函數(shù)。中斷向量表就像是一個索引表,它記錄了每個中斷源對應的中斷處理函數(shù)的入口地址。通過查詢中斷向量表,CPU 可以快速找到處理網(wǎng)卡中斷的函數(shù)。
對于網(wǎng)卡的中斷處理函數(shù),其主要任務是將接收到的數(shù)據(jù)從網(wǎng)卡的緩沖區(qū)拷貝到內核緩沖區(qū)中,并進行一些初步的處理 。它會對數(shù)據(jù)包進行簡單的校驗,檢查數(shù)據(jù)包是否完整、是否存在錯誤等。如果發(fā)現(xiàn)數(shù)據(jù)包存在問題,可能會采取相應的措施,如丟棄該數(shù)據(jù)包或者發(fā)送錯誤通知。中斷處理函數(shù)還會設置一些與數(shù)據(jù)包相關的標志位,以便后續(xù)的處理程序能夠了解數(shù)據(jù)包的狀態(tài)。由于硬件中斷處理需要快速響應,以避免數(shù)據(jù)丟失或其他硬件事件的延遲處理,因此硬件中斷處理函數(shù)通常只執(zhí)行一些緊急且耗時較短的操作,而將更復雜的處理工作交給后續(xù)的軟中斷處理。
首先當數(shù)據(jù)幀從網(wǎng)線到達網(wǎng)卡,網(wǎng)卡在分配給自己的 ringBuffer 中尋找可用的內存位置,找到后 DMA 會把數(shù)據(jù)拷貝到網(wǎng)卡之前關聯(lián)的內存里。當 DMA 操作完成以后,網(wǎng)卡會向 CPU 發(fā)起一個硬中斷,通知 CPU 有數(shù)據(jù)到達。中斷處理函數(shù)為:
//drivers/net/ethernet/freescale/fec_main.c
static irqreturn_t
fec_enet_interrupt(int irq, void *dev_id)
{
struct net_device *ndev = dev_id;
struct fec_enet_private *fep = netdev_priv(ndev);
irqreturn_t ret = IRQ_NONE;
if (fec_enet_collect_events(fep) && fep->link) {
ret = IRQ_HANDLED;
if (napi_schedule_prep(&fep->napi)) {
/* Disable interrupts */
writel(0, fep->hwp + FEC_IMASK);
__napi_schedule(&fep->napi);
}
}
return ret;
}
//net/core/dev.c
__napi_schedule->____napi_schedule
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);
}
這里我們看到,list_add_tail修改了CPU變量softnet_data里的poll_list,將驅動napi_struct傳過來的poll_list添加了進來。其中softnet_data中的poll_list是一個雙向列表,其中的設備都帶有輸入幀等著被處理。緊接著 __raise_softirq_irqoff 觸發(fā)了一個軟中斷 NET_RX_SOFTIRQ。
圖片
注意:當RingBuffer滿的時候,新來的數(shù)據(jù)包將給丟棄。ifconfig查看網(wǎng)卡的時候,可以里面有個overruns,表示因為環(huán)形隊列滿被丟棄的包。如果發(fā)現(xiàn)有丟包,可能需要通過ethtool命令來加大環(huán)形隊列的長度。
4.3 軟中斷處理
在硬件中斷處理完成后,會觸發(fā)相應的軟中斷,將數(shù)據(jù)包的后續(xù)處理工作交給軟中斷機制來完成 。軟中斷的處理主要由ksoftirqd內核線程負責。
ksoftirqd線程會不斷地檢查是否有軟中斷等待處理。當檢測到網(wǎng)絡接收相關的軟中斷(NET_RX_SOFTIRQ)被觸發(fā)時,它會調用net_rx_action函數(shù)。net_rx_action函數(shù)是軟中斷處理網(wǎng)絡數(shù)據(jù)包的核心函數(shù),它會從 Ring Buffer 中取出數(shù)據(jù)包,并調用網(wǎng)卡驅動注冊的poll函數(shù)。
poll函數(shù)的作用是對數(shù)據(jù)包進行進一步的處理和準備工作 。它會對數(shù)據(jù)包進行合并、整理等操作。有時,一個完整的數(shù)據(jù)包可能會被分成多個部分存儲在 Ring Buffer 中,poll函數(shù)會將這些分散的部分合并成一個完整的數(shù)據(jù)包。poll函數(shù)還會對數(shù)據(jù)包進行一些過濾和篩選,去除不符合要求的數(shù)據(jù)包。經(jīng)過poll函數(shù)處理后的數(shù)據(jù)包,會被傳遞給內核協(xié)議棧的網(wǎng)絡層進行后續(xù)的處理,從而實現(xiàn)了從硬件層面到軟件層面的數(shù)據(jù)交接,確保數(shù)據(jù)包能夠在系統(tǒng)中繼續(xù)流轉和處理。接下來進入軟中斷處理函數(shù):
//kernel/softirq.c
static void run_ksoftirqd(unsigned int cpu){
local_irq_disable();
if (local_softirq_pending()) {
__do_softirq();
rcu_note_context_switch(cpu);
local_irq_enable();
cond_resched();
return;
}
local_irq_enable();
}
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
while ((softirq_bit = ffs(pending))) {
h->action(h);
}
}
在網(wǎng)絡設備子系統(tǒng)初始化中,講到為 NET_RX_SOFTIRQ 注冊了處理函數(shù) net_rx_action。所以 net_rx_action 函數(shù)就會被執(zhí)行到了。
//net/core/dev.c
static __latent_entropy void net_rx_action(struct softirq_action *h)
{
struct softnet_data *sd = this_cpu_ptr(&softnet_data);
list_splice_init(&sd->poll_list, &list);
for (;;) {
...
n = list_first_entry(&list, struct napi_struct, poll_list);
budget -= napi_poll(n, &repoll);
...
}
...
}
napi_poll->__napi_poll->work = n->poll(n, weight)
首先獲取到當前CPU變量softnet_data,對其poll_list進行遍歷, 然后執(zhí)行到網(wǎng)卡驅動注冊到的 poll 函數(shù)。對于 FSL 網(wǎng)卡來說,其驅動對應的 poll 函數(shù)就是 fec_enet_rx_napi。
//drivers/net/ethernet/freescale/fec_main.c
static int fec_enet_rx_napi(struct napi_struct *napi, int budget)
{
struct net_device *ndev = napi->dev;
struct fec_enet_private *fep = netdev_priv(ndev);
int done = 0;
do {
done += fec_enet_rx(ndev, budget - done);
fec_enet_tx(ndev);
} while ((done < budget) && fec_enet_collect_events(fep));
if (done < budget) {
napi_complete_done(napi, done);
writel(FEC_DEFAULT_IMASK, fep->hwp + FEC_IMASK);
}
return done;
}
fec_enet_rx->fec_enet_rx_queue
圖片
然后進入 GRO 處理,流程如下:
napi_gro_receive->napi_skb_finish->gro_normal_one->gro_normal_list->netif_receive_skb_list_internal
圖片
最終通過函數(shù) netif_receive_skb_list_internal() 進入內核協(xié)議棧。
4.4 協(xié)議棧逐層處理
⑴網(wǎng)絡接口層
當數(shù)據(jù)包從軟中斷處理環(huán)節(jié)傳遞到內核協(xié)議棧的網(wǎng)絡接口層時,網(wǎng)絡接口層首先會對數(shù)據(jù)包的合法性進行嚴格檢查 。它會驗證數(shù)據(jù)包的格式是否符合網(wǎng)絡接口層協(xié)議的規(guī)定,例如以太網(wǎng)協(xié)議規(guī)定了數(shù)據(jù)幀的特定格式,包括前導碼、幀起始定界符、源 MAC 地址、目的 MAC 地址、類型字段、數(shù)據(jù)字段和幀校驗序列(FCS)等。網(wǎng)絡接口層會檢查這些字段是否完整、正確,F(xiàn)CS 是否匹配,以確保數(shù)據(jù)包在傳輸過程中沒有出現(xiàn)錯誤。
網(wǎng)絡接口層還會仔細判斷數(shù)據(jù)包所使用的協(xié)議類型 。在以太網(wǎng)環(huán)境中,類型字段用于標識上層協(xié)議,常見的類型值如 0x0800 表示 IP 協(xié)議,0x0806 表示 ARP 協(xié)議等。通過識別這個類型字段,網(wǎng)絡接口層能夠確定該數(shù)據(jù)包應該被傳遞給哪個上層協(xié)議進行進一步處理。如果是 IP 協(xié)議的數(shù)據(jù)包,網(wǎng)絡接口層會去掉以太網(wǎng)幀頭和幀尾,將剩余的 IP 數(shù)據(jù)包傳遞給網(wǎng)絡層;如果是 ARP 協(xié)議的數(shù)據(jù)包,則會在網(wǎng)絡接口層進行相應的 ARP 處理,如更新 ARP 緩存表等。
⑵網(wǎng)絡層
在網(wǎng)絡接口層完成處理后,IP 數(shù)據(jù)包被傳遞到網(wǎng)絡層。網(wǎng)絡層的首要任務是對 IP 包進行深入的路由判斷 。它會檢查 IP 包的目的 IP 地址,然后查詢系統(tǒng)的路由表,以確定該數(shù)據(jù)包應該被發(fā)送到哪里。路由表中存儲了一系列的路由規(guī)則,這些規(guī)則根據(jù)目的 IP 地址的網(wǎng)絡部分來確定數(shù)據(jù)包的下一跳地址。例如,如果目的 IP 地址屬于本地網(wǎng)絡,網(wǎng)絡層會直接將數(shù)據(jù)包發(fā)送到本地的目標主機;如果目的 IP 地址屬于其他網(wǎng)絡,網(wǎng)絡層會根據(jù)路由表將數(shù)據(jù)包發(fā)送到合適的路由器,由路由器繼續(xù)轉發(fā)數(shù)據(jù)包。
網(wǎng)絡層還負責將數(shù)據(jù)包分發(fā)給正確的上層協(xié)議 。IP 包的首部中有一個協(xié)議字段,該字段用于標識上層協(xié)議的類型,如 6 表示 TCP 協(xié)議,17 表示 UDP 協(xié)議等。根據(jù)這個協(xié)議字段的值,網(wǎng)絡層能夠確定將數(shù)據(jù)包傳遞給傳輸層的哪個協(xié)議模塊進行后續(xù)處理。當確定上層協(xié)議為 TCP 時,網(wǎng)絡層會去掉 IP 頭,將剩余的 TCP 數(shù)據(jù)段傳遞給傳輸層的 TCP 模塊;若上層協(xié)議為 UDP,則將 UDP 數(shù)據(jù)報傳遞給傳輸層的 UDP 模塊。
⑶傳輸層
傳輸層在接收到網(wǎng)絡層傳遞過來的 TCP 數(shù)據(jù)段或 UDP 數(shù)據(jù)報后,會依據(jù)端口號來準確地將數(shù)據(jù)送到對應的 Socket 。每個 Socket 都與一個特定的端口號相關聯(lián),而端口號則用于區(qū)分不同的應用程序或服務。
對于 TCP 協(xié)議,傳輸層會根據(jù) TCP 數(shù)據(jù)段中的源端口號、目的端口號、源 IP 地址和目的 IP 地址這四元組,在系統(tǒng)中查找與之匹配的 Socket 。這個四元組就像是一個唯一的標識,能夠準確地確定一個 TCP 連接。一旦找到對應的 Socket,傳輸層會將 TCP 數(shù)據(jù)段中的數(shù)據(jù)拷貝到該 Socket 的接收緩沖區(qū)中,等待應用程序通過 Socket 接口來讀取數(shù)據(jù)。例如,當你在瀏覽器中訪問一個網(wǎng)站時,瀏覽器會與網(wǎng)站服務器建立一個 TCP 連接,傳輸層會根據(jù)這個連接的四元組信息,將接收到的網(wǎng)頁數(shù)據(jù)準確地送到對應的 Socket 接收緩沖區(qū),供瀏覽器讀取并顯示網(wǎng)頁內容。
對于 UDP 協(xié)議,傳輸層同樣根據(jù) UDP 數(shù)據(jù)報中的源端口號和目的端口號,查找對應的 Socket 。由于 UDP 是無連接的協(xié)議,它不需要像 TCP 那樣進行復雜的連接建立和管理過程。一旦找到匹配的 Socket,傳輸層就會將 UDP 數(shù)據(jù)報中的數(shù)據(jù)拷貝到該 Socket 的接收緩沖區(qū)中。例如,在進行實時視頻聊天時,視頻數(shù)據(jù)通常通過 UDP 協(xié)議傳輸,傳輸層會快速地將接收到的 UDP 視頻數(shù)據(jù)報送到對應的 Socket 接收緩沖區(qū),以便視頻播放程序能夠及時讀取并顯示視頻畫面。
五、數(shù)據(jù)包到達應用層
經(jīng)過一系列復雜的處理,數(shù)據(jù)包終于抵達了應用層,這是數(shù)據(jù)旅程的最后一站,也是數(shù)據(jù)能夠被應用程序實際使用的關鍵環(huán)節(jié)。應用程序通過 Socket 接口來讀取這些數(shù)據(jù),從而實現(xiàn)各種網(wǎng)絡功能。
在 Linux 系統(tǒng)中,Socket 是應用程序與網(wǎng)絡協(xié)議棧進行交互的重要接口。它為應用程序提供了一種統(tǒng)一的方式來發(fā)送和接收網(wǎng)絡數(shù)據(jù),隱藏了底層網(wǎng)絡協(xié)議的復雜性。當數(shù)據(jù)包到達傳輸層并被存入對應的 Socket 接收緩沖區(qū)后,應用程序便可以通過系統(tǒng)調用,如recv或recvfrom函數(shù),從 Socket 緩沖區(qū)中讀取數(shù)據(jù)。
以一個簡單的網(wǎng)絡聊天程序為例,當用戶發(fā)送消息時,消息會被封裝成數(shù)據(jù)包,經(jīng)過網(wǎng)絡協(xié)議棧的層層處理發(fā)送出去;而當接收方收到數(shù)據(jù)包時,數(shù)據(jù)包會沿著協(xié)議棧層層向上傳遞,最終到達應用層。接收方的聊天程序通過 Socket 接口調用recv函數(shù),從 Socket 接收緩沖區(qū)中讀取數(shù)據(jù),將其解析為用戶能夠理解的消息內容,并顯示在聊天窗口中。
在實際應用中,應用程序可能會根據(jù)自身的需求,對讀取到的數(shù)據(jù)進行進一步的處理和解析。例如,對于一個 HTTP 服務器應用程序,它接收到的數(shù)據(jù)包可能包含了 HTTP 請求信息,服務器會解析這些請求,提取出請求的資源路徑、請求方法等信息,然后根據(jù)這些信息返回相應的 HTTP 響應。