核心概念与数据结构
在了解流程之前,必须了解几个核心概念:
struct sk_buff
(Socket Buffer): 这是网络协议栈的“血液”。无论在协议栈的哪一层,数据包都以sk_buff
的形式存在。它是一个复杂的数据结构,包含了指向数据包内容(header和payload)的指针,以及大量的元数据(如所属设备、协议类型、时间戳、校验和状态等)。skb->data
: 指向当前协议层头部的开始。随着数据包在协议栈中逐层处理(剥离头部),这个指针会向前移动。skb->head
,skb->end
: 指向已分配数据区的开始和结束。skb->mac_header
,skb->network_header
,skb->transport_header
: 分别指向L2, L3, L4头的偏移量。skb->dev
: 指向与此包关联的网络设备 (struct net_device
)。skb->sk
: 指向与此包关联的套接字 (struct sock
)。
struct net_device
: 内核中代表一个网络接口(如eth0
)的数据结构。它包含了设备的状态、属性以及一组操作函数指针net_device_ops
,用于驱动与协议栈的交互。- NAPI (New API): 一种高效处理网络包的机制,旨在解决传统纯中断方式在高流量下的“中断风暴”问题。它结合了中断和轮询,在有数据到达时通过中断触发,然后进入无中断的轮询模式(poll)批量收包,处理完后再重新开启中断。
第一部分:数据包接收流程 (从网卡到应用)
我们将以一个最常见的场景为例:一个以太网帧(包含了IP包,IP包内是TCP段)到达网卡。
阶段一:硬件与驱动层 (L1/L2) – 数据包的到来
- 物理接收: 网卡(NIC)在其物理层(L1)接收到电信号,解码成数字比特流,并根据以太网帧(L2)的格式(前导码、SFD)识别出一个完整的帧。
- DMA传输: 网卡通过DMA(Direct Memory Access)将接收到的帧数据直接写入到内存中的一个预先分配好的
Ring Buffer
(环形缓冲区)中。这个过程不需要CPU的参与。Ring Buffer是由网卡驱动在初始化时分配的。 - 触发中断: 数据写入完成后,NIC向CPU发起一个硬件中断(IRQ),通知CPU有新的数据包到达。
- 中断处理: CPU收到中断后,会暂停当前任务,跳转到内核预设的中断处理程序。
- 中断处理程序首先会调用驱动注册的顶半部(Top Half)中断处理函数。这个函数的目标是 尽可能快地完成,通常只做少量工作,如禁用网卡的中断,然后调度软中断(SoftIRQ)来处理真正耗时的数据包。
- 这里就是 NAPI 机制发挥作用的地方。
源码分析:中断处理与NAPI调度 (以Intel igb
驱动为例)
- 文件:
drivers/net/ethernet/intel/igb/igb_main.c
驱动在初始化时会注册中断处理函数,例如 igb_msix_ring
或 igb_intr
。
// drivers/net/ethernet/intel/igb/igb_main.c
static irqreturn_t igb_msix_ring(int irq, void *data)
{
struct igb_q_vector *q_vector = data;
/* Write register to clear interrupt */
igb_write_itr(q_vector); // 清除中断状态,避免重复触发
// 关键:这里并不直接处理数据包,而是调度NAPI
if (likely(napi_schedule_prep(&q_vector->napi))) {
__napi_schedule(&q_vector->napi); // 将该NAPI实例加入轮询列表,并触发软中断
}
return IRQ_HANDLED;
}
napi_schedule()
将此网卡队列的napi_struct
挂入当前CPU的softnet_data->poll_list
链表。- 它会触发一个
NET_RX_SOFTIRQ
类型的软中断。这意味着真正的数据包处理被推迟到软中断上下文中执行,从而大大缩短了硬件中断处理程序的执行时间。
阶段二:软中断与NAPI轮询 (L2 -> L3 的桥梁)
内核线程 ksoftirqd
(每个CPU一个) 会周期性地检查待处理的软中断。当它发现 NET_RX_SOFTIRQ
被触发时,会执行 net_rx_action()
函数。
源码分析:net_rx_action
和 NAPI poll
- 文件:
net/core/dev.c
net_rx_action()
是所有网络接收软中断的入口。
// net/core/dev.c
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 + 2;
int budget = netdev_budget; // 定义了单次轮询处理包数量的预算
LIST_HEAD(list);
LIST_HEAD(repoll);
// ...
while (!list_empty(&sd->poll_list)) { // 遍历 poll_list 链表
struct napi_struct *n;
// ...
n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);
// ...
// 调用驱动注册的 poll 方法
work = n->poll(n, weight);
}
// ...
}
net_rx_action
会遍历poll_list
,并调用每个napi_struct
中注册的poll
函数。这个poll
函数是网卡驱动自己实现的,比如igb
驱动的igb_poll
。- 文件:
drivers/net/ethernet/intel/igb/igb_main.c
igb_poll
是收包的核心,它会批量地从Ring Buffer中取出数据包,封装成sk_buff
,然后送往协议栈上层。
// drivers/net/ethernet/intel/igb/igb_main.c
static int igb_poll(struct napi_struct *napi, int budget)
{
// ...
int cleaned = 0;
while (cleaned < budget) {
// ... 从 Ring Buffer 中取出描述符
// 分配一个新的 skb 用于接收数据
skb = igb_alloc_rx_skb(rx_ring, dma);
if (!skb)
break;
// 将网卡DMA内存中的数据(或其引用)与skb关联起来
// ...
// 将数据包传递给上层协议栈
napi_gro_receive(&rx_ring->q_vector->napi, skb);
cleaned++;
}
// ...
return cleaned;
}
napi_gro_receive()
是一个重要的函数。它会尝试进行 GRO (Generic Receive Offload),即如果连续收到多个属于同一数据流的小包,它会尝试在驱动层将它们聚合成一个大的sk_buff
,从而减少上层协议栈处理的次数,提升性能。- 最终,数据包会通过
netif_receive_skb()
或其变体被送入协议栈的核心接收函数。
阶段三:网络层处理 (L3 – IP协议)
netif_receive_skb_core()
是协议栈处理的入口。它的主要任务是:根据以太网帧头中的协议类型字段(ethhdr->h_proto
),将sk_buff
分发给正确的网络层协议处理函数(如IP、ARP、IPv6)。
源码分析: __netif_receive_skb_core
和 ip_rcv
- 文件:
net/core/dev.c
// net/core/dev.c
static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
// ...
// 从以太网头中提取协议类型 (例如 0x0800 代表 IPv4)
type = skb->protocol;
// ptype_base 是一个哈希表,存储了所有L3协议处理函数的钩子
// key 是协议类型 (eth_type_trans),value 是处理函数(ip_rcv, arp_rcv等)
list_for_each_entry_rcu(pt, &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {
if (pt->type == type) {
// 找到匹配的协议,调用其处理函数
pt->func(skb, skb->dev, pt, orig_dev);
// ...
}
}
// ...
}
- 对于IPv4报文,
pt->func
就是ip_rcv
。 - 文件:
net/ipv4/ip_input.c
ip_rcv
是IPv4协议的入口点。它负责对IP包进行初步的校验和处理。
// net/ipv4/ip_input.c
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
struct iphdr *iph;
// ... 各种检查
iph = ip_hdr(skb);
if (iph->ihl < 5 || iph->version != 4) // 检查IP头长度和版本
goto inhdr_error;
if (ip_fast_csum((u8 *)iph, iph->ihl)) // 检查IP头校验和
goto inhdr_error;
// ...
// 关键:调用 ip_rcv_finish 进行路由决策
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
net, NULL, skb, dev, NULL,
ip_rcv_finish);
}
NF_HOOK
是 Netfilter 的钩子点,允许防火墙等模块在这里对数据包进行处理。如果包未被丢弃,最终会调用ip_rcv_finish
。ip_rcv_finish
的核心任务是进行路由决策:这个包是发给本机的,还是需要转发出去?- 它会调用
ip_route_input_noref()
查找路由表。 - 查找结果会存放在
skb->dst
(struct dst_entry
) 中。 - 如果目标地址是本机,
skb->dst->input
会被设置为ip_local_deliver
。 - 如果需要转发,则设置为
ip_forward
。
- 它会调用
我们假设包是发给本机的,所以接下来会调用 ip_local_deliver
。
阶段四:传输层处理 (L4 – TCP/UDP协议)
ip_local_deliver
的任务是剥离IP头,然后根据IP头中的协议字段(iph->protocol
),将包分发给正确的传输层协议处理函数(如TCP、UDP、ICMP)。
源码分析: ip_local_deliver
和 tcp_v4_rcv
- 文件:
net/ipv4/ip_input.c
// net/ipv4/ip_input.c
int ip_local_deliver(struct sk_buff *skb)
{
// ...
// skb->dst->input 指向 ip_local_deliver
return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
net, NULL, skb, skb->dev, NULL,
ip_local_deliver_finish);
}
static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
// ...
// 从IP头获取L4协议号
int protocol = ip_hdr(skb)->protocol;
// inet_protos 是一个数组,存储了传输层协议的处理句柄
// 例如,IPPROTO_TCP -> tcp_protocol, IPPROTO_UDP -> udp_protocol
const struct net_protocol *ipprot;
ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot) {
// 调用传输层协议的处理函数,例如 tcp_v4_rcv
ret = ipprot->handler(skb);
}
// ...
}
- 对于TCP报文,
ipprot->handler
就是tcp_v4_rcv
。 - 文件:
net/ipv4/tcp_ipv4.c
tcp_v4_rcv
是TCP协议栈的入口。这是整个流程中最复杂的部分之一。其核心任务是:根据 四元组(源IP,目的IP,源端口,目的端口) 找到对应的 socket
。
// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
// ...
// 查找与此skb匹配的socket
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, &refcounted);
if (!sk) // 如果没找到已建立的连接
goto no_tcp_socket;
process:
// 如果是 ESTABLISHED 状态的连接
if (sk->sk_state == TCP_ESTABLISHED) {
// ...
// 调用 tcp_rcv_established 处理数据
if (tcp_rcv_established(sk, skb, th, len)) {
// ...
}
return 0;
}
// 如果是 LISTEN 状态,说明是新的连接请求(SYN)
if (sk->sk_state == TCP_LISTEN) {
// ...
// 处理SYN包,创建新的socket等
struct sock *nsk = tcp_v4_syn_recv_sock(sk, skb, th, len);
// ...
}
// ...
}
- 找到
sock
后,数据包会根据TCP状态机的状态进行处理。我们假设这是一个已建立连接的数据包。 tcp_rcv_established()
会处理TCP序列号、ACK、滑动窗口等,然后将有效数据 (payload
) 添加到 socket的接收队列 (sk->sk_receive_queue
) 中。- 最后,它会调用
sk_data_ready()
。
阶段五:Socket层与用户空间
sk_data_ready()
的作用是唤醒正在等待该 socket
上数据的用户进程。
当用户程序调用 read()
或 recvfrom()
等系统调用来读取数据时,如果 socket
的接收队列为空,进程会进入睡眠状态,并被放入 socket
的等待队列 (wq
) 中。
源码分析:tcp_recvmsg
和数据拷贝
- 用户调用
recv(sockfd, buf, len, 0)
。 - 进入内核态,调用链为:
sys_recv
->sock_recvmsg
->inet_recvmsg
->tcp_recvmsg
。
- 文件:
net/ipv4/tcp.c
// net/ipv4/tcp.c
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
int flags, int *addr_len)
{
// ...
// skb_peek() 从接收队列的头部查看skb,但并不取出
skb = skb_peek(&sk->sk_receive_queue);
while(skb) {
// ...
// copy_datagram_iovec() 将数据从内核空间的skb拷贝到用户空间的buffer (msg->msg_iov)
err = skb_copy_datagram_iovec(skb, offset, msg->msg_iov, used);
// ...
}
// 如果没有数据并且是非阻塞模式,直接返回EAGAIN
if (!copied) {
if (nonblock)
return -EAGAIN;
// 阻塞模式下,进入睡眠等待,直到被 sk_data_ready 唤醒
sk_wait_data(sk, &timeo, &last);
}
// ...
}
tcp_recvmsg
从sk->sk_receive_queue
中取出sk_buff
。- 使用
skb_copy_datagram_iovec
将sk_buff
中的数据负载安全地拷贝到用户程序提供的缓冲区。 - 至此,数据包的接收之旅圆满完成。
第二部分:数据包发送流程 (从应用到网卡)
发送过程在概念上是接收过程的逆序,但实现细节有所不同。
阶段一:用户空间与Socket层
- 系统调用: 用户程序调用
send()
,write()
,sendto()
等系统调用。 - 进入内核: 陷入内核态,调用链
sys_send
->sock_sendmsg
->inet_sendmsg
->tcp_sendmsg
。
源码分析:tcp_sendmsg
- 文件:
net/ipv4/tcp.c
tcp_sendmsg
的主要工作是:将用户数据拷贝到内核空间,并将其挂到 socket的发送队列 (sk->sk_write_queue
)。
// net/ipv4/tcp.c
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
// ...
while (msg_data_left(msg)) { // 循环处理用户数据
// ...
// skb_copy_to_page_nocache() 将用户数据拷贝到内核页
// 内核会分配skb,并将数据组织起来
err = skb_copy_to_page_nocache(sk, &msg->msg_iter, skb,
pfrag, copied);
// ...
// 将包含数据的 skb 添加到发送队列
__skb_queue_tail(&sk->sk_write_queue, skb);
}
// ...
// 尝试推送数据包,触发发送流程
__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
return copied;
}
阶段二:传输层 (L4 – TCP)
__tcp_push_pending_frames
会检查是否可以发送数据(根据Nagle算法、拥塞窗口等)。如果可以,它会调用 tcp_write_xmit
来构建TCP段。
源码分析:tcp_write_xmit
- 文件:
net/ipv4/tcp.c
该函数会从 sk_write_queue
取出 sk_buff
,为其添加TCP头部,并计算校验和。
// net/ipv4/tcp.c
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle)
{
// ...
while ((skb = tcp_send_head(sk))) { // 获取发送队列的第一个skb
// ...
// tcp_transmit_skb() 是实际构建和发送skb的核心函数
tcp_transmit_skb(sk, skb, 1, gfp);
}
// ...
}
// net/ipv4/tcp_output.c
static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask)
{
// ...
// 构建TCP头部
th = (struct tcphdr *)skb_push(skb, tcp_header_len);
// ...填充TCP头字段: 端口、序列号、标志位等...
// 计算TCP校验和(如果硬件不支持校验和卸载)
// ...
// 将包交给IP层发送
err = icsk->icsk_af_ops->queue_xmit(sk, skb, &fl);
return err;
}
icsk->icsk_af_ops->queue_xmit
对于IPv4来说,就是ip_queue_xmit
。
阶段三:网络层 (L3 – IP)
ip_queue_xmit
负责添加IP头部。它会从socket上缓存的路由信息 (dst_entry
) 中获取下一跳地址等信息。
源码分析:ip_queue_xmit
和 ip_output
- 文件:
net/ipv4/ip_output.c
// net/ipv4/ip_output.c
int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
{
// ...
// sk_setup_caps() 检查 GSO/TSO 等硬件卸载能力
// TSO (TCP Segmentation Offload) 允许TCP层构建一个大的TCP包,由网卡来切分成小的TCP segment
// ...
// 调用 dst_output,它最终会指向 ip_output
return dst_output(net, sk, skb);
}
int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct net_device *dev = skb_dst(skb)->dev;
// ...
// 添加IP头
iph = ip_hdr(skb);
iph->version = 4;
//... 填充IP头其他字段: ttl, protocol等...
// 计算IP头校验和
ip_send_check(iph);
// ...
// 将包交给邻居子系统(ARP)处理
return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,
net, sk, skb, NULL, dev,
ip_finish_output,
!(IPCB(skb)->flags & IPSKB_REROUTED));
}
ip_finish_output
是POST_ROUTING
之后的最终发送函数。
阶段四:邻居子系统与驱动层 (L2)
ip_finish_output
会调用 dst_neigh_output
,它会查找下一跳IP地址对应的MAC地址(即查询ARP缓存)。
- ARP查询:
- 如果ARP缓存中有对应的MAC地址,就用它来填充以太网头部。
- 如果缓存中没有,系统会发送一个ARP请求,并将当前的
sk_buff
放入一个队列中等待。当ARP响应回来后,再将此sk_buff
取出并发送。
- 排队与发送:
dev_queue_xmit
是将sk_buff
交给网络设备的通用接口。- 它会首先经过Linux的 Qdisc (Queueing Discipline),即流量控制和队列规则。
- Qdisc决定了数据包的发送顺序(比如
pfifo_fast
队列)。
源码分析:dev_queue_xmit
和 驱动 ndo_start_xmit
- 文件:
net/core/dev.c
// net/core/dev.c
int dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev)
{
// ...
// 获取设备的发送队列
txq = netdev_get_tx_queue(dev, skb_get_queue_mapping(skb));
// ...
// 将 skb 送入 Qdisc
rc = q->enqueue(skb, q, &to_free, &ret);
// ...
// __qdisc_run() 会最终触发发送
}
- Qdisc处理完后,会调用网络设备驱动注册的发送函数,即
net_device_ops->ndo_start_xmit
。 - 文件:
drivers/net/ethernet/intel/igb/igb_main.c
// drivers/net/ethernet/intel/igb/igb_main.c
static netdev_tx_t igb_xmit_frame(struct sk_buff *skb,
struct net_device *netdev)
{
// ...
// 将 skb 的数据映射到DMA可以访问的地址
dma = dma_map_single(dev, skb->data, skb_headlen(skb), DMA_TO_DEVICE);
// ...
// 准备发送描述符,填入DMA地址和长度
tx_desc->buffer_addr = cpu_to_le64(dma);
// ...
// 更新环形缓冲区的尾指针,这会“告诉”网卡有一个新的包需要发送
writel(i, tx_ring->tail);
// 网卡硬件会检测到指针变化,通过DMA从内存中读取数据,并将其发送到网络上
return NETDEV_TX_OK;
}
- 驱动程序将
sk_buff
信息填入发送 Ring Buffer,并更新硬件寄存器。 - 网卡硬件接管,通过DMA读取数据,并将其按以太网帧格式发送出去。
- 发送完成后,网卡可能会再次触发一个中断,通知驱动发送完成,以便驱动可以释放相关的
sk_buff
和Ring Buffer空间。
总结
Linux网络协议栈是一个高度模块化、分层清晰且充满优化的复杂系统。
- 接收流程:
中断 -> NAPI轮询(igb_poll) -> netif_receive_skb -> ip_rcv -> ip_local_deliver -> tcp_v4_rcv -> socket接收队列 -> 用户程序recv()
- 发送流程:
用户程序send() -> tcp_sendmsg -> socket发送队列 -> tcp_write_xmit -> ip_queue_xmit -> dev_queue_xmit -> 驱动ndo_start_xmit(igb_xmit_frame) -> 硬件发送
整个过程以sk_buff
为核心载体,在各层之间传递,每层通过添加/剥离头部并调用下一层/上一层的函数指针来完成自己的任务。同时,像NAPI、GRO/TSO、Netfilter、Qdisc等机制穿插其中,共同构成了一个功能强大且性能卓越的网络处理框架。