Deep Dive into Linux Network Packet Reception Process
This article provides a comprehensive, illustrated explanation of how Linux receives network packets—from hardware DMA and interrupt handling through soft‑interrupt processing, NAPI polling, protocol stack registration, and finally delivery to user‑space via recvfrom—detailing each kernel component and relevant source code.
因为要对百万、千万、甚至是过亿的用户提供各种网络服务,所以在一线互联网企业里面试和晋升后端开发同学的其中一个重点要求就是要能支撑高并发,要理解性能开销,会进行性能优化。而很多时候,如果你对Linux底层的理解不深的话,遇到很多线上性能瓶颈你会觉得狗拿刺猬,无从下手。
我们今天用图解的方式,来深度理解一下在Linux下网络包的接收过程。还是按照惯例来借用一段最简单的代码开始思考。为了简单起见,我们用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接收收据的逻辑。当在开发视角看的时候,只要客户端有对应的数据发送过来,服务器端执行 recv_from 后就能收到它,并把它打印出来。我们现在想知道的是,当网络包达到网卡,直到我们的 recvfrom 收到数据,这中间,究竟都发生过什么?
通过本文,你将深入理解Linux网络系统内部是如何实现的,以及各个部分之间如何交互。相信这对你的工作将会有非常大的帮助。本文基于Linux 3.10,源代码参见https://mirrors.edge.kernel.org/pub/linux/kernel/v3.x/,网卡驱动采用Intel的igb网卡举例。
友情提示,本文略长,可以先Mark后看!
一
Linux网络收包总览
在TCP/IP网络分层模型里,整个协议栈被分成了物理层、链路层、网络层,传输层和应用层。物理层对应的是网卡和网线,应用层对应的是我们常见的Nginx,FTP等等各种应用。Linux实现的是链路层、网络层和传输层这三层。
在Linux内核实现中,链路层协议靠网卡驱动来实现,内核协议栈来实现网络层和传输层。内核对更上层的应用层提供socket接口来供用户进程访问。我们用Linux的视角来看到的TCP/IP网络分层模型应该是下面这个样子的。
图1 Linux视角的网络协议栈
在Linux的源代码中,网络设备驱动对应的逻辑位于 driver/net/ethernet , 其中intel系列网卡的驱动在 driver/net/ethernet/intel 目录下。协议栈模块代码位于 kernel 和 net 目录。
内核和网络设备驱动是通过中断的方式来处理的。当设备上有数据到达的时候,会给CPU的相关引脚上触发一个电压变化,以通知CPU来处理数据。对于网络模块来说,由于处理过程比较复杂和耗时,如果在中断函数中完成所有的处理,将会导致中断处理函数(优先级过高)将过度占据CPU,将导致CPU无法响应其它设备,例如鼠标和键盘的消息。因此Linux中断处理函数是分上半部和下半部的。上半部是只进行最简单的工作,快速处理然后释放CPU,接着CPU就可以允许其它中断进来。剩下将绝大部分的工作都放到下半部中,可以慢慢从容处理。2.4以后的内核版本采用的下半部实现方式是软中断,由ksoftirqd内核线程全权处理。和硬中断不同的是,硬中断是通过给CPU物理引脚施加电压变化,而软中断是通过给内存中的一个变量的二进制值以通知软中断处理程序。
static
struct
smp_hotplug_thread softirq_threads
=
{
.store
=
&
ksoftirqd,
.thread_should_run
=
ksoftirqd_should_run,
.thread_fn
=
run_ksoftirqd,
.thread_comm
=
"ksoftirqd/%u"
,};在Linux的视角,我们说过网卡驱动的硬中断只完成最必要的工作,随后把大部分工作交给软中断处理。硬中断处理函数示例(以igb网卡为例):
static
irqreturn_t
igb_msix_ring
(
int
irq,
void
*
data){
struct
igb_q_vector
*
q_vector
=
data;
/* Write the ITR value calculated from the previous interrupt. */
igb_write_itr(q_vector);
napi_schedule(
&
q_vector
->
napi);
return
IRQ_HANDLED;
}该硬中断函数只记录中断频率并调度对应的NAPI poll函数,随后软中断线程ksoftirqd会在适当时机调用该poll函数来实际拉取数据包。
void
__raise_softirq_irqoff
(
unsigned
int
nr){
trace_softirq_raise(nr);
or_softirq_pending(
1UL
<<
nr);
}软中断被ksoftirqd线程处理,核心函数如下:
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();
}__do_softirq 会遍历已注册的软中断向量并调用对应的处理函数,例如网络接收对应的 NET_RX_SOFTIRQ 会调用 net_rx_action。
void
net_rx_action
(
struct
softirq_action
*
h){
struct
softnet_data
*
sd
=
&
__get_cpu_var(softnet_data);
...
while
(!list_empty(
&
sd
->
poll_list)) {
n
=
list_first_entry(
&
sd
->
poll_list,
struct
napi_struct, poll_list);
work
=
0
;
if
(test_bit(NAPI_STATE_SCHED,
&
n
->
state)) {
work
=
n
->
poll(n, weight);
trace_napi_poll(n);
}
budget
-=
work;
}
}对于igb网卡,poll函数是 igb_poll,核心代码片段如下(省略部分实现细节):
static
int
igb_poll
(
struct
napi_struct
*
napi,
int
budget){
...
if (q_vector
->
rx.ring)
clean_complete
&=
igb_clean_rx_irq(q_vector, budget);
...
}igb_clean_rx_irq 会把网卡环形缓冲区中的帧拷贝到 sk_buff 并交给网络层处理,关键调用 napi_gro_receive 将数据递交给上层协议栈。
gro_result_t
napi_gro_receive
(
struct
napi_struct
*
napi,
struct
sk_buff
*
skb){
skb_gro_reset_offset(skb);
return
napi_skb_finish(dev_gro_receive(napi, skb), skb);
}dev_gro_receive 最终会调用 netif_receive_skb 将数据包送入 IP 协议层。
int
netif_receive_skb
(
struct
sk_buff
*
skb){
...
return
__netif_receive_skb(skb);
}__netif_receive_skb 进一步调用 dst_input,最终根据协议把数据递交给 ip_rcv、udp_rcv 或 tcp_v4_rcv 等处理函数。
int
ip_rcv
(
struct
sk_buff
*
skb,
struct
net_device
*
dev,
struct
packet_type
*
pt,
struct
net_device
*
orig_dev){
...
return
NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev,
NULL
, ip_rcv_finish);
}ip_rcv_finish 完成路由查找后调用 dst_input,最终进入 ip_local_deliver,进而根据协议号调用相应的处理函数(如 udp_rcv)。
int
udp_rcv
(
struct
sk_buff
*
skb){
return
__udp4_lib_rcv(skb,
&
udp_table, IPPROTO_UDP);
}__udp4_lib_rcv 会查找对应的 socket(通过端口号),若找到则把 skb 放入该 socket 的接收队列,否则发送 ICMP 不可达报文。
int
udp_queue_rcv_skb
(
struct
sock
*
sk,
struct
sk_buff
*
skb){
...
if
(sk_rcvqueues_full(sk, skb, sk
->
sk_rcvbuf))
goto
drop;
bh_lock_sock(sk);
if
(
!
sock_owned_by_user(sk))
rc
=
__udp_queue_rcv_skb(sk, skb);
else
if
(sk_add_backlog(sk, skb, sk
->
sk_rcvbuf)) {
bh_unlock_sock(sk);
goto
drop;
}
bh_unlock_sock(sk);
return
rc;
}当用户进程调用 recvfrom(glibc 包装的 sys_recvfrom)时,最终会走到 inet_recvmsg → udp_recvmsg → __skb_recv_datagram,读取 sk_receive_queue 中的 skb。如果队列为空且用户允许阻塞,进程会被挂起等待数据到达。
五
总结
通过本文的图解与源码追踪,你可以看到从网卡硬件触发中断、软中断调度、NAPI 轮询、协议栈分发到最终用户态 recvfrom 的完整路径。每一步都涉及内核子系统的协同工作,理解这些细节有助于定位网络性能瓶颈、调优中断亲和性以及合理配置 socket 缓冲区。
Refining Core Development Skills
Fei has over 10 years of development experience at Tencent and Sogou. Through this account, he shares his deep insights on performance.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.