如果查找不到,则调用 __neigh_create 创建一个邻居。
//file: net/core/neighbour.c
struct neighbour *__neigh_create(......)
{
//申请邻居表项
struct neighbour *n1, *rc, *n = neigh_alloc(tbl, dev);
//构造赋值
memcpy(n->primary_key, pkey, key_len);
n->dev = dev;
n->parms->neigh_setup(n);
//最后添加到邻居 hashtable 中
rcu_assign_pointer(nht->hash_buckets[hash_val], n);
......
有了邻居项以后,此时仍然还不具备发送 IP 报文的能力,因为目的 MAC 地址还未获取。调用 dst_neigh_output 继续传递 skb。
//file: include/net/dst.h
static inline int dst_neigh_output(struct dst_entry *dst,
struct neighbour *n, struct sk_buff *skb)
{
......
return n->output(n, skb);
}
调用 output,实际指向的是 neigh_resolve_output。在这个函数内部有可能会发出 arp 网络请求。
//file: net/core/neighbour.c
int neigh_resolve_output(){
//注意:这里可能会触发 arp 请求
if (!neigh_event_send(neigh, skb)) {
//neigh->ha 是 MAC 地址
dev_hard_header(skb, dev, ntohs(skb->protocol),
neigh->ha, NULL, skb->len);
//发送
dev_queue_xmit(skb);
}
}
当获取到硬件 MAC 地址以后,就可以封装 skb 的 MAC 头了。最后调用 dev_queue_xmit 将 skb 传递给 Linux 网络设备子系统。
4.5 网络设备子系统
![](http://imgq8.q578.com/ef/0602/73a60a5b9300818b.jpg)
邻居子系统通过 dev_queue_xmit 进入到网络设备子系统中来。
//file: net/core/dev.c
int dev_queue_xmit(struct sk_buff *skb)
{
//选择发送队列
txq = netdev_pick_tx(dev, skb);
//获取与此队列关联的排队规则
q = rcu_dereference_bh(txq->qdisc);
//如果有队列,则调用__dev_xmit_skb 继续处理数据
if (q->enqueue) {
rc = __dev_xmit_skb(skb, q, dev, txq);
goto out;
}
//没有队列的是回环设备和隧道设备
......
}
开篇第二节网卡启动准备里我们说过,网卡是有多个发送队列的(尤其是现在的网卡)。上面对 netdev_pick_tx 函数的调用就是选择一个队列进行发送。
netdev_pick_tx 发送队列的选择受 XPS 等配置的影响,而且还有缓存,也是一套小复杂的逻辑。这里我们只关注两个逻辑,首先会获取用户的 XPS 配置,否则就自动计算了。代码见 netdev_pick_tx => __netdev_pick_tx。
//file: net/core/flow_dissector.c
u16 __netdev_pick_tx(struct net_device *dev, struct sk_buff *skb)
{
//获取 XPS 配置
int new_index = get_xps_queue(dev, skb);
//自动计算队列
if (new_index < 0)
new_index = skb_tx_hash(dev, skb);}
然后获取与此队列关联的 qdisc。在 linux 上通过 tc 命令可以看到 qdisc 类型,例如对于我的某台多队列网卡机器上是 mq disc。
#tc qdisc
qdisc mq 0: dev eth0 root
大部分的设备都有队列(回环设备和隧道设备除外),所以现在我们进入到 __dev_xmit_skb。
//file: net/core/dev.c
static inline int __dev_xmit_skb(struct sk_buff *skb, struct Qdisc *q,
struct net_device *dev,
struct netdev_queue *txq)
{
//1.如果可以绕开排队系统
if ((q->flags & TCQ_F_CAN_BYPASS) && !qdisc_qlen(q) &&
qdisc_run_begin(q)) {
......
}
//2.正常排队
else {
//入队
q->enqueue(skb, q)
//开始发送
__qdisc_run(q);
}
}
上述代码中分两种情况,1 是可以 bypass(绕过)排队系统的,另外一种是正常排队。我们只看第二种情况。
先调用 q->enqueue 把 skb 添加到队列里。然后调用 __qdisc_run 开始发送。
//file: net/sched/sch_generic.c
void __qdisc_run(struct Qdisc *q)
{
int quota = weight_p;
//循环从队列取出一个 skb 并发送
while (qdisc_restart(q)) {
// 如果发生下面情况之一,则延后处理:
// 1. quota 用尽
// 2. 其他进程需要 CPU
if (--quota <= 0 || need_resched()) {
//将触发一次 NET_TX_SOFTIRQ 类型 softirq
__netif_schedule(q);
break;
}
}
}