目录
12.7 网络设备层
12.7.2 接收分组
12.7.3 发送分组
本专栏文章将有70篇左右,欢迎+关注,查看后续文章。
接收时,使用中断通知分组到达。几乎所有网卡都支持DMA。
一个 skb如何找到对应网络设备?
skb->dev
一个socket 如何关联对应 sk_buff ?
socket fd
-> struct socket
-> struct sock
-> struct sock.sk_receive_queue //socket的接收缓存队列。
-> struct sk_buff
收到一个sk_buff,如何知道查找哪个 struct sock?
tcp为例:
int tcp_v4_rcv(struct sk_buff *skb)
{
const struct tcphdr *th;
th = tcp_hdr(skb);
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
//通过源目端口和IP的hash值,找到struct sock。
}
网卡接收环形缓冲区:
网卡内的一块内存,存储收到的数据帧。
缓冲区大小及其数量由网卡硬件决定。
传统收包:
每收到一个数据包产生一个中断。
缺点:
数据包过多时,中断上下文切换频繁,性能降低。
因此引进 NAPI,即 New API。结合了IRQ + 轮询。
先介绍一个重要结构体:
struct softnet_data { //代表一个CPU收包队列。
struct list_head poll_list:
//NAPI 轮询的设备列表。
//list_add_tail(&napi->poll_list, &sd->poll_list);
struct sk_buff_head process_queue;
struct sk_buff_head input_pkt_queue; // 收包队列
struct napi_struct backlog;
// 为兼容NAPI模式,传统方法收包也用一个napi,用于处理积压队列中报文。
// sd->backlog.poll = process_backlog;
}
下面介绍传统和 NAPI 两种方法的接收分组。
传统网卡使用传统方式。
高吞吐、高性能网卡使用NAPI方式。
在Linux-3.10中,为兼容NAPI方式,将传统方式当做一种特殊NAPI。
即 struct napi_struct backlog。
所以先讲解 NAPI 方式。
1. NAPI
NAPI原理:
1. 收到第一个分组导致网卡发出中断,然后关闭中断。将设备放到轮询表中。
2. 只要设备接收缓存区有分组需处理,就一直对设备轮询。
3. 设备缓存中分组处理完毕后,开启RX中断。
网卡驱动中probe函数中执行netif_napi_add,就是使用 NAPI 模式。
void netif_napi_add(struct net_device *dev, struct napi_struct *napi, int (*poll) (*, ), int weight)
poll 函数:驱动自定义。用于 IRQ 禁用后被 NAPI 调度执行。以接收分组。
weight:一次轮询中处理多少分组。(不超过RX缓冲区容量)。
int (*poll) (struct napi_struct *, int budget)
budget:预算/权重,一次轮询最多允许处理的分组数。
网卡速率越高,weight可设更高。
struct napi_struct { //一个网卡的轮询结构体。
struct list_head poll_list;
unsigned long state;
int weight; //一次轮询可处理的包数量。
unsigned int gro_count;
int (*poll)(struct napi_struct *, int);
...
};
成员介绍:
struct list_head poll_list;
用于将 NAPI 实例链接到 CPU 的软中断列表中。
使用举例:
struct softnet_data *sd;
list_add_tail(&napi->poll_list, &sd->poll_list);
state:
跟踪 NAPI 实例的状态。值有:
1. NAPI_STATE_SCHED:
该 NAPI 实例已加入 softnet_data->poll_list 轮询队列中。
等待CPU调度执行该 NAPI 的poll函数。
2. NAPI_STATE_DISABLE:
禁用该 NAPI 实例。
该 NAPI 实例不会被加入softnet_data->poll_list轮询队列中。
实现 poll 函数
网卡驱动自定义底层收包。
intel e100网卡为例:
int e100_poll(struct napi_struct *napi, int budget)
{
struct nic *nic = container_of(napi, struct nic, napi);
unsigned int work_done = 0;
e100_rx_clean(nic, &work_done, budget);
if (work_done < budget) {
//没有接收足够数量的报文,表示接收完毕,可退出poll模式,并开启中断。
napi_complete(napi);
e100_enable_irq(nic);
}
return work_done;
}
实现 IRQ 处理程序
e100网卡为例:
request_irq(irq, e100_intr, IRQF_SHARED, netdev_name, nic->netdev);
irqreturn_t e100_intr(int irq, void *dev_id)
{
struct net_device *netdev = dev_id;
struct nic *nic = netdev_priv(netdev);
if (napi_schedule_prep( &nic->napi )) {
e100_disable_irq(nic);
__napi_schedule(&nic->napi);
}
return IRQ_HANDLED;
}
napi_schedule_prep:
设置NAPI state为NAPI_STATE_SCHED。(即将将设备放到轮询表中)
e100_disable_irq:
禁止中断,因为即将调用poll函数,轮询收包。
__napi_schedule:
进行NAPI调度。
void ____napi_schedule(struct softnet_data *sd, struct napi_struct *napi)
{
list_add_tail(&napi->poll_list, &sd->poll_list); // 加入本CPU的轮询表中。
__raise_softirq_irqoff( NET_RX_SOFTIRQ );
//触发软中断。
}
处理RX软中断
void net_rx_action(struct softirq_action *h)
{
struct softnet_data *sd = &__get_cpu_var(softnet_data);
unsigned long time_limit = jiffies + 2;
int budget = netdev_budget; // 对应 proc/net/sys/net/core/netdev_budget
struct softnet_data *sd = &__get_cpu_var(softnet_data);
while ( &sd->poll_list )) { // 遍历该CPU上所有napi
if (unlikely(budget <= 0 || time_after_eq(jiffies, time_limit)))
goto softnet_break; // 预算用尽,或执行执行太久。
n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);
if (test_bit(NAPI_STATE_SCHED, &n->state)) {
work = n->poll(n, weight);
// 遍历执行,不同驱动定义的poll函数。
//包括传统方法的struct napi_struct backlog
}
if (work == weight) {
napi_complete( n ); // 接收到足够报文,将该napi从CPU中移除。
}
}
}
总结:
1. 第一个分组导致网卡发出IRQ,触发中断处理函数。
2. 中断处理函数中,将网卡的napi 放在CPU的轮询表上,最后触发NET_RX_SOFTIRQ软中断。
3. 软中断中执行各个napi的轮询函数。
/proc/sys/net/core/netdev_budget:
该CPU中所有 napi 预算总和,每个NAPI设备轮询后,从总数中减去。
budget为0后或超时,退出软中断处理函数。
2. 传统方法
request_irq(dev->irq, net_interrupt, irqflags, dev->name, dev);
net_interrupt :
网卡驱动中自定义。
通常进一步调用 netif_rx。
net/core/dev.c
netif_rx:是传统方式的收包,而不是NAPI方式的收包。
int netif_rx( struct sk_buff *skb)
{
enqueue_to_backlog(skb, get_cpu(), &qtail);
}
int enqueue_to_backlog(struct sk_buff *skb, int cpu, unsigned int *qtail)
{
struct softnet_data *sd;
sd = &per_cpu(softnet_data, cpu);
if (skb_queue_len( &sd->input_pkt_queue) <= netdev_max_backlog) {
if (skb_queue_len(&sd->input_pkt_queue)) {
enqueue:
__skb_queue_tail(&sd->input_pkt_queue, skb);
//将收到的skb 放到 CPU 的等待队列 input_pkt_queue中。
return NET_RX_SUCCESS;
}
if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state)) {
____napi_schedule(sd, &sd->backlog);
}
goto enqueue;
}
return NET_RX_DROP;
}
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); // 触发NET_RX_SOFTIRQ 软中断。
}
3. 执行软中断处理函数 net_rx_action
NAPI也在软中断中调用 net_rx_action。
net_rx_action 执行所有napi中poll函数,接收报文。
包括:
1. 传统方式:sd->backlog.poll = process_backlog;
2. NAPI方式:网卡驱动自定义 poll 函数。
process_backlog:
拓展
GRO技术:
原理:
将多个数据合并成一个大包,减少中断次数和内存拷贝,提高性能。
使用场景:
高吞吐量场景,如大文件、高并发。
高速网络环境,如10GbE网络。
长连接场景,如TCP长连接,GRO合并多个TCP包。
大量小数据包场景。
不适合场景:
HTTP短连接、直播、在线游戏等(GRO会增加延迟)。
数据包长度频繁变化,如VoIP流(容易合并错误)
加密或压缩包。
发包的注意事项:
1. 特定协议的首部校验和。
2. 确定路由。
3. 确定目的MAC。
net/core/dev.c 中 dev_queue_xmit:
作用:把分组放置到发送队列中。
然后调用 网卡驱动自定义 net_device -> hard_start_xmit 函数指针:
作用:完成发送动作。