在上述代码中,我们看到 while 循环不断地从队列中取出 skb 并进行发送。注意,这个时候其实都占用的是用户进程的系统态时间(sy)。只有当 quota 用尽或者其它进程需要 CPU 的时候才触发软中断进行发送。
所以这就是为什么一般服务器上查看 /proc/softirqs,一般 NET_RX 都要比 NET_TX 大的多的第二个原因。对于读来说,都是要经过 NET_RX 软中断,而对于发送来说,只有系统态配额用尽才让软中断上。
我们来把精力在放到 qdisc_restart 上,继续看发送过程。
static inline int qdisc_restart(struct Qdisc *q)
{
//从 qdisc 中取出要发送的 skb
skb = dequeue_skb(q);
...
return sch_direct_xmit(skb, q, dev, txq, root_lock);
}
qdisc_restart 从队列中取出一个 skb,并调用 sch_direct_xmit 继续发送。
//file: net/sched/sch_generic.c
int sch_direct_xmit(struct sk_buff *skb, struct Qdisc *q,
struct net_device *dev, struct netdev_queue *txq,
spinlock_t *root_lock)
{
//调用驱动程序来发送数据
ret = dev_hard_start_xmit(skb, dev, txq);
}
4.6 软中断调度
在 4.5 咱们看到了如果系统态 CPU 发送网络包不够用的时候,会调用 __netif_schedule 触发一个软中断。该函数会进入到 __netif_reschedule,由它来实际发出 NET_TX_SOFTIRQ 类型软中断。
软中断是由内核线程来运行的,该线程会进入到 net_tx_action 函数,在该函数中能获取到发送队列,并也最终调用到驱动程序里的入口函数 dev_hard_start_xmit。

//file: net/core/dev.c
static inline void __netif_reschedule(struct Qdisc *q)
{
sd = &__get_cpu_var(softnet_data);
q->next_sched = NULL;
*sd->output_queue_tailp = q;
sd->output_queue_tailp = &q->next_sched;
......
raise_softirq_irqoff(NET_TX_SOFTIRQ);
}
在该函数里在软中断能访问到的 softnet_data 里设置了要发送的数据队列,添加到了 output_queue 里了。紧接着触发了 NET_TX_SOFTIRQ 类型的软中断。(T 代表 transmit 传输)
软中断的入口代码我这里也不详细扒了,感兴趣的同学《图解Linux网络包接收过程》参考一文中的 3.2 小节 - ksoftirqd内核线程处理软中断。
我们直接从 NET_TX_SOFTIRQ softirq 注册的回调函数 net_tx_action讲起。用户态进程触发完软中断之后,会有一个软中断内核线程会执行到 net_tx_action。
牢记,这以后发送数据消耗的 CPU 就都显示在 si 这里了,不会消耗用户进程的系统时间了。
//file: net/core/dev.c
static void net_tx_action(struct softirq_action *h)
{
//通过 softnet_data 获取发送队列
struct softnet_data *sd = &__get_cpu_var(softnet_data);
// 如果 output queue 上有 qdisc
if (sd->output_queue) {
// 将 head 指向第一个 qdisc
head = sd->output_queue;
//遍历 qdsics 列表
while (head) {
struct Qdisc *q = head;
head = head->next_sched;
//发送数据
qdisc_run(q);
}
}
}
软中断这里会获取 softnet_data。前面我们看到进程内核态在调用 __netif_reschedule 的时候把发送队列写到 softnet_data 的 output_queue 里了。软中断循环遍历 sd->output_queue 发送数据帧。
来看 qdisc_run,它和进程用户态一样,也会调用到 __qdisc_run。
//file: include/net/pkt_sched.h
static inline void qdisc_run(struct Qdisc *q)
{
if (qdisc_run_begin(q))
__qdisc_run(q);
}
然后一样就是进入 qdisc_restart => sch_direct_xmit,直到驱动程序函数 dev_hard_start_xmit。
4.7 igb 网卡驱动发送
我们前面看到,无论是对于用户进程的内核态,还是对于软中断上下文,都会调用到网络设备子系统中的 dev_hard_start_xmit 函数。在这个函数中,会调用到驱动里的发送函数 igb_xmit_frame。