![](http://imgq8.q578.com/ef/0602/2b6f0e597f3299a8.jpg)
再通过 ip_local_out 进入到下一步的处理。
//file: net/ipv4/ip_output.c
int ip_local_out(struct sk_buff *skb)
{
//执行 netfilter 过滤
err = __ip_local_out(skb);
//开始发送数据
if (likely(err == 1))
&nbsnbsp;err = dst_output(skb);
......
在 ip_local_out => __ip_local_out => nf_hook 会执行 netfilter 过滤。如果你使用 iptables 配置了一些规则,那么这里将检测是否命中规则。如果你设置了非常复杂的 netfilter 规则,在这个函数这里将会导致你的进程 CPU 开销会极大增加。
还是不多展开说,继续只聊和发送有关的过程 dst_output。
//file: include/net/dst.h
static inline int dst_output(struct sk_buff *skb)
{
return skb_dst(skb)->output(skb);
}
此函数找到到这个 skb 的路由表(dst 条目) ,然后调用路由表的 output 方法。这又是一个函数指针,指向的是 ip_output 方法。
//file: net/ipv4/ip_output.c
int ip_output(struct sk_buff *skb)
{
//统计
.....
//再次交给 netfilter,完毕后回调 ip_finish_output
return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING, skb, NULL, dev,
ip_finish_output,
!(IPCB(skb)->flags & IPSKB_REROUTED));
}
在 ip_output 中进行一些简单的,统计工作,再次执行 netfilter 过滤。过滤通过之后回调 ip_finish_output。
//file: net/ipv4/ip_output.c
static int ip_finish_output(struct sk_buff *skb)
{
//大于 mtu 的话就要进行分片了
if (skb->len > ip_skb_dst_mtu(skb) && !skb_is_gso(skb))
return ip_fragment(skb, ip_finish_output2);
else
return ip_finish_output2(skb);
}
在 ip_finish_output 中我们看到,如果数据大于 MTU 的话,是会执行分片的。
实际 MTU 大小确定依赖 MTU 发现,以太网帧为 1500 字节。之前 QQ 团队在早期的时候,会尽量控制自己数据包尺寸小于 MTU,通过这种方式来优化网络性能。因为分片会带来两个问题:1、需要进行额外的切分处理,有额外性能开销。2、只要一个分片丢失,整个包都得重传。所以避免分片既杜绝了分片开销,也大大降低了重传率。
在 ip_finish_output2 中,终于发送过程会进入到下一层,邻居子系统中。
//file: net/ipv4/ip_output.c
static inline int ip_finish_output2(struct sk_buff *skb)
{
//根据下一跳 IP 地址查找邻居项,找不到就创建一个
nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)->daddr);
neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
if (unlikely(!neigh))
neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);
//继续向下层传递
int res = dst_neigh_output(dst, neigh, skb);
}
4.4 邻居子系统
邻居子系统是位于网络层和数据链路层中间的一个系统,其作用是对网络层提供一个封装,让网络层不必关心下层的地址信息,让下层来决定发送到哪个 MAC 地址。
而且这个邻居子系统并不位于协议栈 net/ipv4/ 目录内,而是位于 net/core/neighbour.c。因为无论是对于 IPv4 还是 IPv6 ,都需要使用该模块。
![](http://imgq8.q578.com/ef/0602/f88e189b2c219f79.jpg)
在邻居子系统里主要是查找或者创建邻居项,在创造邻居项的时候,有可能会发出实际的 arp 请求。然后封装一下 MAC 头,将发送过程再传递到更下层的网络设备子系统。大致流程如图。
![](http://imgq8.q578.com/ef/0602/774e46d087d08a88.jpg)
理解了大致流程,我们再回头看源码。在上面小节 ip_finish_output2 源码中调用了 __ipv4_neigh_lookup_noref。它是在 arp 缓存中进行查找,其第二个参数传入的是路由下一跳 IP 信息。
//file: include/net/arp.h
extern struct neigh_table arp_tbl;
static inline struct neighbour *__ipv4_neigh_lookup_noref(
struct net_device *dev, u32 key)
{
struct neigh_hash_table *nht = rcu_dereference_bh(arp_tbl.nht);
//计算 hash 值,加速查找
hash_val = arp_hashfn(......);
for (n = rcu_dereference_bh(nht->hash_buckets[hash_val]);
n != NULL;
n = rcu_dereference_bh(n->next)) {
if (n->dev == dev && *(u32 *)n->primary_key == key)
return n;
}
}