# Linux E 100 网卡 与 TCP 层原理 二

前面我们看到数据包放入了 input_pkt_queue 队列,并标识了 NET_RX_SOFTIRQ 软中断标志位,那么当内核执行软中断时,将会处理该中断标志位对应的处理代码 net_rx_action。对于软中断的原理,我们在下一篇中进行讲解

# NET_RX_SOFTIRQ 软中断的处理

net_dev_init 函数将随着linux 内核的启动而调用,这里使用 宏定义 subsys_initcall 将其注册为子系统初始化函数,将内核执行子系统初始化时,将会顺序调用注册进入的函数,这里的 net_dev_init 函数便是用于初始化网络设备。

在其中注册的 process_backlog 函数,将会有软中断执行时回调,在其中将处理 input_pkt_queue 队列中的数据。

subsys_initcall(net_dev_init);static int __init net_dev_init(void){
    ...
    // 初始化 per cpu 结构,也即每个CPU一个,此时每个 CPU 自己访问自己的 网络描述符 softnet_data,不需要上锁
    for (i = 0; i < NR_CPUS; i++) {
        struct softnet_data *queue;
        queue = &per_cpu(softnet_data, i);
        skb_queue_head_init(&queue->input_pkt_queue);
        queue->throttle = 0;
        queue->cng_level = 0;
        queue->avg_blog = 10;
        queue->completion_queue = NULL;
        INIT_LIST_HEAD(&queue->poll_list);
        set_bit(__LINK_STATE_START, &queue->backlog_dev.state);
        queue->backlog_dev.weight = weight_p; // int weight_p = 64  权重 默认 64
        queue->backlog_dev.poll = process_backlog; // 兼容旧 API 放置数据包的队列的回调函数,将有软中断来调用
        atomic_set(&queue->backlog_dev.refcnt, 1);
    }
    ...
    open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL); // 设置处理接受数据的软中断函数
    ...
}// 处理设备接收到的数据包
static int process_backlog(struct net_device *backlog_dev, int *budget){
    int work = 0;
    int quota = min(backlog_dev->quota, *budget);
    struct softnet_data *queue = &__get_cpu_var(softnet_data); // 获取当前CPU的 struct softnet_data 结构
    unsigned long start_time = jiffies;for (;;) { // 循环处理所有放入  input_pkt_queue 队列的数据包
        struct sk_buff *skb;
        struct net_device *dev;
        local_irq_disable(); // 关闭当前 CPU 硬中断,保证操作 input_pkt_queue 队列的原子性
        skb = __skb_dequeue(&queue->input_pkt_queue); // 从 input_pkt_queue 列表中移出一个 skb
        if (!skb) // 已经处理完所有的 skb
            goto job_done;
        local_irq_enable(); // 开启硬中断,在后面处理当前移出的 skb 时,允许 e100 中断执行,再次向 input_pkt_queue 队列中放入数据包
        dev = skb->dev;
        netif_receive_skb(skb); // 将数据包递交到上层协议栈处理
        dev_put(dev);
        work++; // 记录当前处理数据包的个数
        if (work >= quota || jiffies - start_time > 1) // 当超过处理配额,或者处理时间超过一个时钟中断周期 默认为100 hz , 也即 10ms,那么退出循环,让出 CPU 执行进程代码,而不是一直执行软中断 
            break;
    }
    // 减少处理配额
    backlog_dev->quota -= work;
    *budget -= work;
    return -1;
​
job_done: // 处理完所有数据包,正常结束
    backlog_dev->quota -= work;
    *budget -= work;
    list_del(&backlog_dev->poll_list); // 将当前设备从poll列表中移除,因为所有数据包都处理完毕,下一次无需处理
    smp_mb__before_clear_bit(); // 屏障保证 netif_poll_enable 函数写数据位不会发生重排序
    netif_poll_enable(backlog_dev); // 设置可以继续轮训处理数据包
    if (queue->throttle) { // 关闭 throttle 以允许继续向队列中添加数据包,前面描述过
        queue->throttle = 0;
    }
    local_irq_enable(); // 开启硬中断
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

# net_rx_action 函数

该函数将在内核检测软中断时,发现设置了 NET_RX_SOFTIRQ 软中断标志位,从而回调该函数处理接收到的数据包。我们关注到这里使用 budget 配额 和 jiffies - start_time 软中断执行时间来约束 CPU 的资源分配,避免 CPU 长时间执行软中断导致进程代码得不到执行。其中核心代码为:dev->poll(dev, &budget),该函数将回调poll list 上设备处理数据包的回调函数,这里对应于 上述的 process_backlog 函数。源码如下。

static void net_rx_action(struct softirq_action *h)
{
    struct softnet_data *queue = &__get_cpu_var(softnet_data);
    unsigned long start_time = jiffies;
    int budget = netdev_max_backlog; // 最大执行配额 int netdev_max_backlog = 300;
    preempt_disable(); // 关闭内核任务抢占 
    local_irq_disable(); // 关闭CPU硬中断while (!list_empty(&queue->poll_list)) { // 遍历设备列表,调用设备的 poll 函数处理其中的数据包
        struct net_device *dev;
        if (budget <= 0 || jiffies - start_time > 1) // 执行超过 10ms (以100hz算,每过 100hz jiffies + 1),那么退出
            goto softnet_break; 
        local_irq_enable(); // 开启 CPU 硬中断
        dev = list_entry(queue->poll_list.next,
                 struct net_device, poll_list);if (dev->quota <= 0 || dev->poll(dev, &budget)) { // 若设备中存在配额可以执行数据包,那么回调设备回调函数 poll 处理盗来的数据包,否则 关闭中断,将当前设备从 poll 列表中移动到列表末尾并重新分配配额,等待下一次调度执行。注意:这里传入了 budget配额的地址,所以内部使用的配额也会算在每次循环中
            local_irq_disable();
             // 移动到 poll list 末尾,并重新分配 配额 
            list_del(&dev->poll_list);
            list_add_tail(&dev->poll_list, &queue->poll_list);
            if (dev->quota < 0) 
                dev->quota += dev->weight;
            else
                dev->quota = dev->weight;
        } else { // 设备配额仍然存在,代表可以继续执行,那么继续循环处理poll list中的其他设备的数据包
            dev_put(dev);
            local_irq_disable();
        }
    }
out:
    local_irq_enable(); // 开启CPU硬中断
    preempt_enable(); // 开启内核抢占
    return;
​
softnet_break: // 超过执行配额,那么代表仍有数据还未处理,所以继续设置 NET_RX_SOFTIRQ 标志位,下一次软中断时继续处理
    __get_cpu_var(netdev_rx_stat).time_squeeze++;
    __raise_softirq_irqoff(NET_RX_SOFTIRQ);
    goto out;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# netif_receive_skb 函数

该函数由 process_backlog 函数调用,当 process_backlog 函数从 input_pkt_queue 队列中取出一个 sk_buff 后,将通过 netif_receive_skb 函数将数据包传递给上层协议栈。

int netif_receive_skb(struct sk_buff *skb)
{
    struct packet_type *ptype, *pt_prev; 
    int ret = NET_RX_DROP;
    unsigned short type = skb->protocol; // 数据帧协议类型if (!skb->stamp.tv_sec) // 初始化接收到包的时间
        do_gettimeofday(&skb->stamp);skb_bond(skb); // 绑定 skb 的设备信息(PCI 的主设备与从设备)__get_cpu_var(netdev_rx_stat).total++; // 递增当前cpu接受数据包数量
​
    skb->h.raw = skb->nh.raw = skb->data; // 将数据指针分别设置到 网络层 ip (skb->nh) 和 运输层 tcp/udp (skb->h) 结构中,以供对应层使用其中的数据
    pt_prev = NULL;
    rcu_read_lock();
    list_for_each_entry_rcu(ptype, &ptype_all, list) { // 遍历所有上层协议栈,找到可以处理当前设备类型的Tap 设备(static struct list_head ptype_all 结构中用于存放 tap设备,我们知道 tun 虚拟设备用于处理 IP层数据,tap 虚拟设备用于处理 数据链路层协议)
        if (!ptype->dev || ptype->dev == skb->dev) {
            if (pt_prev)  // 找到合适的 TAP 设备后,回调其设置的函数(这里了解即可,我们后续文章会结合 OPENVPN 来描述 TAP 与 TUN 设备原理)
                ret = deliver_skb(skb, pt_prev, 0);
            pt_prev = ptype;
        }
    }
    handle_diverter(skb); // 处理 divert 机制(将数据包转向到本机),这里忽略,不支持时,为空操作
    if (__handle_bridge(skb, &pt_prev, &ret)) // 调用网桥模块处理该协议,也即:该内核配置了网桥模块,拥有了网桥的数据包冲突域解决能力,这里忽略即可,感兴趣的读者可以自行研究,功能等同于 计算机网络中的网桥功能:根据 mac 地址转发 port 端口。当然,如果没有在编译内核时引入该模块,那么为空操作
        goto out;// 找到合适的上层协议来处理该数据包 ,注意:此时是根据数据帧类型 hash 运算找到 ptype_base数组 对应的 hash slot,然后变量其冲突链表找到合适的协议 
    list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type)&15], list) {
        if (ptype->type == type &&
            (!ptype->dev || ptype->dev == skb->dev)) {
            if (pt_prev) 
                ret = deliver_skb(skb, pt_prev, 0);
            pt_prev = ptype;
        }
    }
    if (pt_prev) { // 前面循环:当找到第二个时,回调第一个,所以这里将第二个进行回调
        ret = pt_prev->func(skb, skb->dev, pt_prev);
    } else { // 没有上层协议可以处理该数据包,直接丢弃
        kfree_skb(skb);
        ret = NET_RX_DROP;
    }
out:
    rcu_read_unlock();
    return ret;
}// 回调 packet_type 的 func 处理数据包
static __inline__ int deliver_skb(struct sk_buff *skb,
                  struct packet_type *pt_prev, int last)
{
    atomic_inc(&skb->users);
    return pt_prev->func(skb, skb->dev, pt_prev);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

# ptype_base 初始化

前面我们看到将会通过 ptype_base[ntohs(type)&15] 找到处理数据帧的协议,那么我们来看看合适初始化了该数组,对应 IP 协议来说,初始化了什么。

static struct list_head ptype_base[16]; // 16 个 slot 的 hash 表void dev_add_pack(struct packet_type *pt)
{
    int hash;spin_lock_bh(&ptype_lock);
    if (pt->type == htons(ETH_P_ALL)) { // 添加 TAP 设备处理所有 以太网帧
        netdev_nit++;
        list_add_rcu(&pt->list, &ptype_all);
    } else { // 添加其他特定以太网帧协议处理
        hash = ntohs(pt->type) & 15;
        list_add_rcu(&pt->list, &ptype_base[hash]);
    }
    spin_unlock_bh(&ptype_lock);
}// IP协议层初始化时注册
void __init ip_init(void)
{
    dev_add_pack(&ip_packet_type);
    ...
}// 设置只处理 IP 数据包,处理函数为 ip_rcv,至此,当调用该函数时,数据包正式进入 IP 层
static struct packet_type ip_packet_type = {
    .type = __constant_htons(ETH_P_IP),
    .func = ip_rcv,
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29