系列文章:
前面的章节深度分析了网络包的接收,也拆分了网络包的发送,总之收发流程算是闭环了。不过还有一种特殊的情况没有讨论,那就是接收和发送都在本机进行。而且实践中这种本机网络IO出现的场景还不少,而且还有越来越多的趋势。例如LNMP技术栈中的nginx和php-fpm进程就是通过本机来通信的,还有流行的微服务中sidecar模式也是本机网络IO。
在开始讲述本机通信过程之前,先回顾前面的跨机网络通信。
应用层:send/sendto
系统调用:(send=>)sendto
协议栈:inet_sendmsg(AF_INET协议族对socck->ops->sendmsg的实现)
传输层
网络层
邻居子系统
rt_nexthop:获取路由下一跳的IP信息
__ipv4_neigh_lookup_noref:根据下一条IP信息在arp缓存中查找邻居项
__neigh_create:创建一个邻居项,并加入邻居哈希表
dst_neigh_output => neighbour->output(实际指向neigh_resolve_output):
网络设备子系统
驱动程序:igb_xmit_frame
硬件发送
硬件
驱动程序
网络设备子系统:netif_receive_skb
网络协议栈处理:pt_prev->func
网络层
传输层
用户进程
上面主要介绍了跨机时整个网络的发送过程, 而在本机网络IO过程中,会有一些差别。主要的差异有两部分,分别是路由和驱动程序。
发送数据进入协议栈到达网络层的时候,网络层入口函数是ip_queue_xmit。在网络层里会进行路由选择,路由选择完毕再设置一些IP头,进行一些Netfilter的过滤,数据包分片等操作,然后将包交给邻居子系统。
对于本机网络IO来说,特殊之处在于在local路由表中就可以找到路由项,对应的设备都是用loopback网卡,也就是常说的lo设备。
我们重新回到之前网络层查找路由项的部分代码:
int ip_queue_xmit(struct sk_buff *skb, struct flowi *fl)
{
// 检查socket中是否有缓存的路由表
rt = (struct rtable*)__sk_dst_check(sk, 0);
......
if(rt == null) {
// 没有缓存则展开查找路由项并缓存到socket中
rt = ip_route_output_ports(...);
sk_setup_caps(sk, &rt->dst);
}
}
查找路由项的函数时ip_route_output_ports,它经过层层调用,来到关键的部分——fib_lookup
static inline int fib_lookup(struct net *net, const struct flowi4 *flp, struct fib_result *res)
{
struct fib_table *table;
table = fib_get_table(net, RT_TABLE_LOCAL);
if(!fib_table_lookup(table, flp, res, FIB_LOOKUP_NOREF))
// 查找与给定流(由flp指定)匹配的路由项,并将查找结果存储在res中。FIB_LOOKUP_NOREF是传递给此函数的标志,用于指定查找行为的一些细节。
// 查找成功返回0
return 0;
table = fib_get_table(net, RT_TABLE_MAIN);
if(!fib_table_lookup(table, flp, res, FIB_LOOKUP_NOREF))
return 0;
return -ENETUNREACH;
}
在fib_lookup中将会对local和main两个路由表展开查询,并且先查询local后查询main。我们在Linux上使用ip命令可以查看到这两个路由表,这里只看local路由表(因为本机网络IO查询到整个表就结束了)
#ip route list table local
local 10.143.x.y dev eth0 proto kernel scope host src 10.143.x.y
local 127.0.0.1 dev lo proto kernel host src 127.0.0.1
从上述结果可以看出127.0.0.1的路由在local路由表中就能够找到。
上面路由表中10.143.x.y dev eth0是本机的局域网IP,虽然写的是dev eth0,但是其实内核在初始化local路由表的时候,把local路由表里所有的路由项都设置为了RTN_LOCAL。所以即使本机IP不用环回地址,内核在路由项查找的时候判断类型是RTN_LOCAL,仍然会使用net->loopback_dev,也就是lo虚拟网卡。
此处可以使用tcpdump -i eht0 port 8888以及telnet 10.143.x.y 8888进行验证,telnet后tcpdump并不会收到网络请求,因为发给的是lo。
之后fib_lookup的工作完成,返回上一层__ip_route_output_key函数继续执行。
struct rtable *ip_route_output_key(struct net *net, struct flowi4 *fl4)
{
if(fib_lookup(net, fl4, &res) {
}
if(res.type == RTN_LOCAL) {
dev_out = net->loopback_dev;
......
}
......
}
对于本机的网络请求,设备将全部使用net->loopback_dev,也就是lo虚拟网卡。接下来的网络层仍然和跨机网络IO一样(所以本机网络IO如果skb大于MTU仍然会进行分片,不过lo虚拟网卡(65535)的MTU(1500)比Ethernet大得多),最终会经过ip_finish_output,进入邻居子系统的入口函数dst_neigh_output。
在邻居子系统函数中经过处理后,进入网络设备子系统(入口函数是dev_queue_xmit)
网络设备子系统的入口函数是dev_queue_xmit,其中会判断是否有队列。对于有队列的物理设备,该函数进行了一系列复杂的排队等处理后,才调用dev_hard_start_xmit,从这个函数在进入驱动程序igb_xmit_frame来发送。在这个过程中还可能触发软中断进行发送。
但是对于启动状态的回环设备(q->enqueue判断为false)来说就简单多了,它没有队列的问题,直接进入dev_hard_start_xmit。
int dev_queue_xmit(struct sk_buff *skb)
{
q = rcu_dereference_bh(txq_qdisc);
if(q->enqueue) { // 回环设备这里返回false
rc = __dev_xmit_skb(skb, q, dev, txq);
goto out;
}
// 开始回环设备处理
if(dev->flags & IFF_UP) {
dev_hard_start_xmit(skb, dev, txq, ...);
......
}
}
在dev_hard_start_xmit函数中还将调用设备驱动的操作函数,对于回环设备的而言,其“设备驱动”的操作函数ops->ndo_start_xmit指向的是loopback_xmit(不同于正常网络设备的igb_xmit_frame)。
static netdev_tx_t loopback_xmit(struct sk_buff *skb, struct net_device *dev)
{
// 剥离掉和源socket的联系
skb_orphan(skb);
// 调用netif_rx
if(likely(netif_rx(skb) == NET_RX_SUCCESS) {}
}
loopback_xmit中首先调用skb_orphan先把skb上的socket指针去掉了,接着调用netif_tx,在该方法中最终会执行到enqueue_to_backlog。
在本机IO发送的过程中,传输层下面的skb就不需要释放了,直接给接收方传过去就行。不过传输层的skb就节约不了,还是需要频繁地申请和释放。
static int enqueue_to_backlog(struct sk_buff *skb, int cpu, unsigned int *qtail)
{
sd = &per_cpu(softnet_data, cpu);
......
__skb_queue_tail(&sd->input_pkt_queue, skb);
......
__napi_schedule(sd, &sd->backlog);
}
enqueue_to_backlog函数用于把要发送的skb插入softnet_data->input_pkt_queue队列
具体步骤如下:
这里触发的软中断类型是NET_RX_SOFTIRQ,只有触发完软中断,发送过程才算完成了。
发送过程触发软中断后,会进入软中断处理函数net_rx_action。
在跨机地网络包地接收过程中,需要经过硬中断,然后才能触发软中断。而在本机地网络IO过程中,由于并不真的过网卡,所以网卡地发送过程、硬中断就都省去了,直接从软中断开始。
对于igb网卡来说,软中断中轮询调用的poll函数指向的是igb_poll函数。而对于loopback网卡来说,poll函数是process_backlog。
static int process_backlog(struct napi_struct *napi, int quota)
{
while() {
while((skb = __skb_dequeue(&sd->process_queue)) {
__netif_receive_skb(skb);
}
// skb_queue_splice_tail_init()函数用来将链表a(输入队列)的元素链接到链表b(处理队列)上
// 形成一个新的链表b,并将原来a的头变成了空链表
qlen = skb_queue_len(&sd->input_pkt_queue);
if(qlen)
skb_queue_splice_tail_init(&sd->input_pkt_queue, &sd->process_queue);
}
}
}
这个函数用于反复处理队列中的数据包,直到队列为空或者处理的数据包数量达到了指定的配额(quota)。
在内层循环中,它使用 __skb_dequeue() 函数从 process_queue 中取出一个数据包,然后使用 __netif_receive_skb() 函数处理这个数据包。
在内层循环结束后,它检查 input_pkt_queue(输入数据包队列)是否还有剩余的数据包。如果有,它使用 skb_queue_splice_tail_init() 函数将 input_pkt_queue 中的数据包移动到 process_queue 中,然后在下一次内层循环中继续处理这些数据包。
__netif_receive_skb用于将数据送往协议栈,在此之后的调用过程就和跨机网络的IO又一致了:__netif_receive_skb => __netif_receive_skb_core => deliver_skb,然后再将数据送入ip_rcv中进行后续操作。
127.0.0.1本机网络IO需要经过网卡吗
数据包在内核中是什么走向,和外网发送相比流程上有什么差别
访问本机服务时,使用127.0.0.1能比本机IP(例如192.168.x.x)快吗
参考资料:
《深入理解Linux网络》—— 张彦飞