在Linux系统中,网络数据包的接收是一个复杂而精妙的过程,涉及硬件中断、DMA传输、内核线程调度以及多层次的协议栈处理。理解这个过程对于网络性能调优、问题排查以及开发高性能网络应用都至关重要。
Linux内核接收网络包的完整流程可以概括为以下关键步骤:
在整个收包流程中,几个核心数据结构发挥着重要作用:
在Linux系统能够接收网络包之前,内核需要完成一系列初始化工作,为后续的数据处理搭建好基础设施。
Linux使用专门的ksoftirqd内核线程来处理软中断,包括网络收发包的软中断:
c复制// kernel/softirq.c
static struct smp_hotplug_thread softirq_threads = {
.store = &ksoftirqd,
.thread_should_run = ksoftirqd_should_run,
.thread_fn = run_ksoftirqd,
.thread_comm = "ksoftirqd/%u",
};
static __init int spawn_ksoftirqd(void)
{
cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD, "softirq:dead", NULL,
takeover_tasklets);
BUG_ON(smpboot_register_percpu_thread(&softirq_threads));
return 0;
}
early_initcall(spawn_ksoftirqd);
每个CPU核心都会有自己对应的ksoftirqd线程,如ksoftirqd/0、ksoftirqd/1等。这种设计确保了软中断负载能够均衡地分布到各个CPU核心上。
网络子系统的初始化由net_dev_init函数完成,主要包括:
c复制static int __init net_dev_init(void)
{
for_each_possible_cpu(i) {
struct softnet_data *sd = &per_cpu(softnet_data, i);
skb_queue_head_init(&sd->input_pkt_queue);
skb_queue_head_init(&sd->process_queue);
init_gro_hash(&sd->backlog);
sd->backlog.poll = process_backlog;
sd->backlog.weight = weight_p;
}
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
register_pernet_device(&loopback_net_ops);
register_pernet_device(&default_device_ops);
}
其中,open_softirq函数为网络收发注册了对应的软中断处理函数:
c复制void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
Linux内核支持多种网络协议,每种协议都需要在协议栈中注册自己的处理函数:
c复制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)
pr_crit("%s: Cannot add UDP protocol\n", __func__);
if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
pr_crit("%s: Cannot add TCP protocol\n", __func__);
dev_add_pack(&ip_packet_type);
}
各协议的处理函数结构体定义如下:
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,
};
以igb网卡驱动为例,其初始化过程主要包括:
c复制static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
// 获取MAC地址
if (eth_platform_get_mac_address(&pdev->dev, hw->mac.addr)) {
if (hw->mac.ops.read_mac_addr(hw))
dev_err(&pdev->dev, "NVM Read Error\n");
}
// DMA初始化
err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
// 注册ethtool函数
igb_set_ethtool_ops(netdev);
// 分配net_device结构体
netdev = alloc_etherdev_mq(sizeof(struct igb_adapter), IGB_MAX_TX_QUEUES);
// 注册NAPI
err = igb_alloc_q_vector(adapter);
}
网卡启动时主要完成以下工作:
c复制static int __igb_open(struct net_device *netdev, bool resuming)
{
// 分配发送描述符
err = igb_setup_all_tx_resources(adapter);
// 分配接收描述符
err = igb_setup_all_rx_resources(adapter);
// 注册中断处理函数
err = igb_request_irq(adapter);
// 启用NAPI
for (i = 0; i < adapter->num_q_vectors; i++)
napi_enable(&(adapter->q_vector[i]->napi));
// 打开中断
igb_irq_enable(adapter);
}
其中,接收队列的初始化在igb_setup_rx_resources函数中完成:
c复制int igb_setup_rx_resources(struct igb_ring *rx_ring)
{
// 申请内核使用的rx_buffer_info数组
size = sizeof(struct igb_rx_buffer) * rx_ring->count;
rx_ring->rx_buffer_info = vmalloc(size);
// 申请网卡使用的DMA内存
rx_ring->size = rx_ring->count * sizeof(union e1000_adv_rx_desc);
rx_ring->size = ALIGN(rx_ring->size, 4096);
rx_ring->desc = dma_alloc_coherent(dev, rx_ring->size, &rx_ring->dma, GFP_KERNEL);
// 初始化队列指针
rx_ring->next_to_alloc = 0;
rx_ring->next_to_clean = 0;
rx_ring->next_to_use = 0;
}
这里使用了dma_alloc_coherent分配内存,确保了CPU和设备可以同时访问这块内存而不会出现缓存一致性问题。
当网卡接收到数据包后,Linux内核会经历一系列复杂的处理流程将数据包最终交付给应用程序。这个过程涉及硬件中断、软中断调度、协议栈处理等多个环节。
数据包到达网卡后的第一站是硬中断处理:
c复制static irqreturn_t igb_msix_ring(int irq, void *data)
{
struct igb_q_vector *q_vector = data;
/* 更新中断频率调节器(ITR) */
igb_update_itr(q_vector);
/* 调度NAPI */
napi_schedule(&q_vector->napi);
return IRQ_HANDLED;
}
值得注意的是,现代高性能网卡驱动在硬中断中只做最少量的工作:
这种设计减少了硬中断处理时间,提高了系统整体吞吐量。
硬中断处理完成后,真正的收包工作由软中断继续完成:
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);
local_irq_disable();
list_splice_init(&sd->poll_list, &list);
local_irq_enable();
for (;;) {
struct napi_struct *n;
if (list_empty(&list)) {
if (!sd_has_rps_ipi_waiting(sd) && list_empty(&repoll))
goto end;
break;
}
n = list_first_entry(&list, struct napi_struct, poll_list);
budget -= napi_poll(n, &repoll);
/* 处理时间或处理包数超过限制时退出 */
if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit))) {
sd->time_squeeze++;
break;
}
}
/* 将需要重新poll的NAPI重新加入列表 */
local_irq_disable();
list_splice_tail_init(&repoll, &sd->poll_list);
list_splice(&list, &sd->poll_list);
if (!list_empty(&sd->poll_list))
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
local_irq_enable();
end:;
}
net_rx_action函数的核心逻辑是:
这种设计实现了中断和轮询的混合模式,在高负载时可以减少中断次数,提高处理效率。
在软中断上下文中,驱动注册的poll函数被调用,从Ring Buffer中取出数据包:
c复制static int igb_poll(struct napi_struct *napi, int budget)
{
struct igb_q_vector *q_vector = container_of(napi, struct igb_q_vector, napi);
struct igb_adapter *adapter = q_vector->adapter;
int work_done = 0;
/* 处理TX完成 */
if (q_vector->tx.ring)
igb_clean_tx_irq(q_vector);
/* 处理RX */
if (q_vector->rx.ring) {
int cleaned = igb_clean_rx_irq(q_vector, budget);
work_done += cleaned;
if (cleaned >= budget)
goto out;
}
/* 如果处理包数不足budget,说明处理完成 */
napi_complete_done(napi, work_done);
igb_ring_irq_enable(q_vector);
out:
return work_done;
}
实际的收包工作在igb_clean_rx_irq函数中完成:
c复制static int igb_clean_rx_irq(struct igb_q_vector *q_vector, const int budget)
{
struct igb_ring *rx_ring = q_vector->rx.ring;
struct sk_buff *skb = rx_ring->skb;
unsigned int total_bytes = 0, total_packets = 0;
u16 cleaned_count = igb_desc_unused(rx_ring);
do {
union e1000_adv_rx_desc *rx_desc;
struct igb_rx_buffer *rx_buffer;
unsigned int size;
/* 获取下一个描述符 */
rx_desc = IGB_RX_DESC(rx_ring, rx_ring->next_to_clean);
/* 检查描述符是否已经完成DMA */
if (!(rx_desc->wb.upper.status_error & cpu_to_le32(E1000_RXD_STAT_DD)))
break;
/* 获取对应的缓冲区信息 */
rx_buffer = &rx_ring->rx_buffer_info[rx_ring->next_to_clean];
/* 分配skb并将数据从DMA缓冲区拷贝到skb */
skb = igb_construct_skb(rx_ring, rx_buffer, rx_desc);
/* 将数据包送入协议栈 */
igb_receive_skb(q_vector, skb, rx_desc);
/* 更新统计信息 */
total_bytes += skb->len;
total_packets++;
/* 清理描述符,准备接收新数据 */
igb_put_rx_buffer(rx_ring, rx_buffer);
cleaned_count++;
} while (cleaned_count < budget);
/* 更新队列指针 */
rx_ring->next_to_clean = next_to_clean;
/* 更新统计信息 */
u64_stats_update_begin(&rx_ring->rx_syncp);
rx_ring->rx_stats.packets += total_packets;
rx_ring->rx_stats.bytes += total_bytes;
u64_stats_update_end(&rx_ring->rx_syncp);
return total_packets;
}
驱动层收包的核心步骤包括:
数据包从驱动层进入协议栈后,首先经过GRO(Generic Receive Offload)处理,尝试合并相关数据包:
c复制static void igb_receive_skb(struct igb_q_vector *q_vector,
struct sk_buff *skb,
union e1000_adv_rx_desc *rx_desc)
{
struct igb_adapter *adapter = q_vector->adapter;
/* 处理硬件时间戳 */
if (igb_test_staterr(rx_desc, E1000_RXDADV_STAT_TSIP)) {
igb_ptp_rx_pktstamp(q_vector, skb);
}
/* 调用GRO处理 */
napi_gro_receive(&q_vector->napi, skb);
}
GRO处理完成后,数据包进入netif_receive_skb函数,开始协议栈的正式处理:
c复制static int netif_receive_skb_internal(struct sk_buff *skb)
{
int ret;
/* 处理RPS(Receive Packet Steering) */
if (static_branch_unlikely(&rps_needed)) {
struct rps_dev_flow voidflow, *rflow = &voidflow;
int cpu;
cpu = get_rps_cpu(skb->dev, skb, &rflow);
if (cpu >= 0) {
ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail);
return ret;
}
}
/* 调用__netif_receive_skb */
return __netif_receive_skb(skb);
}
__netif_receive_skb_core是协议栈处理的核心函数:
c复制static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
struct packet_type *ptype, *pt_prev;
rx_handler_func_t *rx_handler;
struct net_device *orig_dev;
bool deliver_exact = false;
int ret = NET_RX_DROP;
orig_dev = skb->dev;
/* 处理tcpdump等抓包点 */
list_for_each_entry_rcu(ptype, &ptype_all, list) {
if (ptype->dev == NULL || ptype->dev == skb->dev) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
}
/* 处理二层协议分发 */
switch (skb->protocol) {
case htons(ETH_P_IP):
ret = ip_rcv(skb, skb->dev, NULL, NULL);
break;
case htons(ETH_P_ARP):
arp_rcv(skb, skb->dev, NULL, NULL);
break;
/* 其他协议处理 */
}
return ret;
}
IP层的处理从ip_rcv函数开始:
c复制int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,
struct net_device *orig_dev)
{
struct iphdr *iph;
u32 len;
/* 确保skb有足够的头部空间 */
if (!pskb_may_pull(skb, sizeof(struct iphdr)))
goto inhdr_error;
iph = ip_hdr(skb);
/* 检查IP头部有效性 */
if (iph->ihl < 5 || iph->version != 4)
goto inhdr_error;
if (!pskb_may_pull(skb, iph->ihl*4))
goto inhdr_error;
iph = ip_hdr(skb);
/* 校验和检查 */
if (ip_fast_csum((u8 *)iph, iph->ihl) != 0)
goto inhdr_error;
len = ntohs(iph->tot_len);
if (skb->len < len || len < (iph->ihl*4))
goto inhdr_error;
/* 调用Netfilter钩子 */
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
skb, dev, NULL,
ip_rcv_finish);
inhdr_error:
IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INHDRERRORS);
kfree_skb(skb);
return NET_RX_DROP;
}
ip_rcv_finish函数完成IP层的核心处理:
c复制static int ip_rcv_finish(struct sk_buff *skb)
{
const struct iphdr *iph = ip_hdr(skb);
struct net_device *dev = skb->dev;
struct rtable *rt;
/* 路由查找 */
if (!skb_valid_dst(skb)) {
int err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
iph->tos, dev);
if (unlikely(err))
goto drop;
}
/* 处理IP选项 */
if (iph->ihl > 5 && ip_rcv_options(skb))
goto drop;
/* 调用dst_input,进入下一阶段处理 */
return dst_input(skb);
drop:
kfree_skb(skb);
return NET_RX_DROP;
}
路由子系统会根据目标地址确定数据包的下一步处理函数,对于发往本机的数据包,会设置为ip_local_deliver:
c复制int ip_local_deliver(struct sk_buff *skb)
{
/* 处理IP分片重组 */
if (ip_is_fragment(ip_hdr(skb))) {
if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
return 0;
}
/* 调用Netfilter钩子 */
return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
skb, skb->dev, NULL,
ip_local_deliver_finish);
}
最终,ip_local_deliver_finish根据协议类型调用对应的传输层处理函数:
c复制static int ip_local_deliver_finish(struct sk_buff *skb)
{
int protocol = ip_hdr(skb)->protocol;
const struct net_protocol *ipprot;
ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot != NULL)
return ipprot->handler(skb);
return 0;
}
以UDP协议为例,udp_rcv是UDP数据包的处理入口:
c复制int udp_rcv(struct sk_buff *skb)
{
return __udp4_lib_rcv(skb, &udp_table, IPPROTO_UDP);
}
int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable,
int proto)
{
struct sock *sk;
struct udphdr *uh;
unsigned short ulen;
struct rtable *rt = skb_rtable(skb);
__be32 saddr, daddr;
/* 验证UDP头部 */
if (!pskb_may_pull(skb, sizeof(struct udphdr)))
goto drop;
uh = udp_hdr(skb);
ulen = ntohs(uh->len);
if (ulen > skb->len)
goto short_packet;
if (proto == IPPROTO_UDP) {
if (ulen < sizeof(*uh) || pskb_trim_rcsum(skb, ulen))
goto short_packet;
uh = udp_hdr(skb);
}
/* 校验和验证 */
if (udp4_csum_init(skb, uh, proto))
goto csum_error;
/* 查找对应的socket */
sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);
if (sk != NULL) {
/* 将数据包放入socket的接收队列 */
int ret = udp_queue_rcv_skb(sk, skb);
sock_put(sk);
/* 返回值>0表示需要重新处理 */
if (ret > 0)
return -ret;
return 0;
}
/* 没有找到对应的socket */
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
goto drop;
/* 校验和验证 */
if (udp_lib_checksum_complete(skb))
goto csum_error;
/* 更新统计信息 */
UDP_INC_STATS_BH(sock_net(sk), UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);
drop:
kfree_skb(skb);
return 0;
}
对于TCP协议,处理入口是tcp_v4_rcv:
c复制int tcp_v4_rcv(struct sk_buff *skb)
{
struct net *net = dev_net(skb->dev);
const struct iphdr *iph;
const struct tcphdr *th;
struct sock *sk;
int ret;
/* 验证TCP头部 */
if (skb->pkt_type != PACKET_HOST)
goto discard_it;
/* 确保skb有完整的TCP头部 */
if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
goto discard_it;
th = tcp_hdr(skb);
/* 检查TCP头部长度 */
if (th->doff < sizeof(struct tcphdr)/4)
goto bad_packet;
if (!pskb_may_pull(skb, th->doff*4))
goto discard_it;
/* 校验和验证 */
if (skb_csum_unnecessary(skb)) {
skb->csum_valid = 1;
} else {
if (tcp_v4_checksum_init(skb))
goto bad_packet;
}
/* 查找对应的socket */
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
if (!sk)
goto no_tcp_socket;
/* 处理TCP状态机 */
ret = tcp_v4_do_rcv(sk, skb);
sock_put(sk);
return ret;
}
最终,数据包会被放入对应socket的接收队列,等待应用程序读取:
c复制static int __sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{
unsigned long flags;
struct sk_buff_head *list = &sk->sk_receive_queue;
if (atomic_read(&sk->sk_rmem_alloc) >= sk->sk_rcvbuf) {
atomic_inc(&sk->sk_drops);
return -ENOMEM;
}
if (!sk_rmem_schedule(sk, skb, skb->truesize)) {
atomic_inc(&sk->sk_drops);
return -ENOBUFS;
}
skb->dev = NULL;
skb_set_owner_r(skb, sk);
/* 将skb放入接收队列 */
spin_lock_irqsave(&list->lock, flags);
__skb_queue_tail(list, skb);
spin_unlock_irqrestore(&list->lock, flags);
/* 唤醒等待的进程 */
if (!sock_flag(sk, SOCK_DEAD))
sk->sk_data_ready(sk);
return 0;
}
当应用程序调用read/recv等系统调用时,内核会从socket的接收队列中取出数据,拷贝到用户空间,完成整个网络收包流程。
在理解了Linux内核接收网络包的整体流程后,我们需要对一些关键问题进行更深入的探讨,这些问题的理解对于网络性能调优和问题排查至关重要。
在网卡驱动初始化阶段,我们看到了dma_alloc_coherent的使用:
c复制rx_ring->desc = dma_alloc_coherent(dev, rx_ring->size, &rx_ring->dma, GFP_KERNEL);
这里使用dma_alloc_coherent而不是普通的kmalloc或vmalloc,主要原因在于:
现代CPU使用多级缓存加速内存访问,而设备DMA操作通常直接访问物理内存,绕过CPU缓存。如果不做特殊处理,会导致以下问题:
dma_alloc_coherent通过以下方式解决这个问题:
在x86架构上,由于硬件支持缓存一致性,dma_alloc_coherent实际上返回的是普通的内核内存,但保证了设备访问时的缓存一致性。而在一些嵌入式架构上,可能会返回特殊的uncached内存。
NAPI(New API)是Linux网络子系统中的一种重要机制,它结合了中断和轮询的优点:
NAPI的核心数据结构是struct napi_struct:
c复制struct napi_struct {
struct list_head poll_list; // 用于加入softnet_data的poll_list
unsigned long state; // 状态标志
int weight; // 每次poll处理的最大数据包数
int (*poll)(struct napi_struct *, int); // 驱动实现的poll函数
/* 其他字段省略 */
};
NAPI的工作流程:
这种设计在高流量时能显著减少中断次数,提高系统吞吐量。典型的weight值为64,表示每次poll最多处理64个数据包。
tcpdump等抓包工具能够在协议栈处理前捕获原始数据包,其实现原理是:
创建PF_PACKET套接字:
c复制fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
注册packet_type:内核在__netif_receive_skb_core中会遍历ptype_all列表,将数据包复制给所有注册的抓包点
抓包点位置:位于__netif_receive_skb_core函数中,在协议处理(ip_rcv/arp_rcv)之前
关键代码路径:
c复制__netif_receive_skb_core
list_for_each_entry_rcu(ptype, &ptype_all, list) {
if (ptype->dev == NULL || ptype->dev == skb->dev) {
deliver_skb(skb, ptype, orig_dev);
}
}
这使得tcpdump能够捕获到最原始的网络数据包,包括:
iptables是基于netfilter框架实现的,netfilter在内核协议栈中设置了多个钩子点:
每个钩子点都通过NF_HOOK宏调用注册的钩子函数:
c复制return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
skb, dev, NULL,
ip_rcv_finish);
钩子函数可以决定数据包的命运:
复杂的iptables规则会导致每个数据包经过多次判断,增加CPU开销和网络延迟。
现代高性能网卡支持多队列,每个队列有独立的中断,可以均衡到不同的CPU核心:
硬件多队列:网卡为每个队列分配独立的Ring Buffer和中断
RPS(Receive Packet Steering):软件实现的多队列
RFS(Receive Flow Steering):基于流的定向
多队列配合RPS/RFS可以充分利用多核CPU,显著提高网络吞吐量。
理解了Linux内核接收网络包的原理后,我们可以针对性地进行性能调优和问题排查。以下是一些实用的技巧和经验。
查看软中断分布:
bash复制watch -n1 'cat /proc/softirqs'
关注NET_RX和NET_TX的变化情况
查看网络设备统计:
bash复制ethtool -S eth0
重点关注rx_dropped, rx_missed_errors等字段
查看协议栈统计:
bash复制cat /proc/net/netstat
cat /proc/net/snmp