本文为《深入理解Linux网络》学习笔记,使用的Linux源码版本是3.10,网卡驱动默认采用的都是Intel的igb网卡驱动
Linux源码在线阅读:https://elixir.bootlin.com/linux/v3.10/source
数据包的发送过程如下图:
用户数据被拷贝到内核态,然后经过协议栈处理后进入RingBuffer。随后网卡驱动真正将数据发送了出去。当发送完成的时候,是通过硬中断来通知CPU,然后清理RingBuffer
从代码的视角得到的流程如下图:
等网络发送完毕,网卡会给CPU发送一个硬中断来通知CPU。收到这个硬中断后会释放RingBuffer中使用的内存,如下图所示:
数据包的接收过程如下图:
当网卡收到数据以后,向CPU发起一个中断,以通知CPU有数据到达。当CPU收到中断请求后,会去调用网络驱动注册的中断处理函数,触发软中断。ksoftirqd检测到有软中断请求到达,开始轮询收包,收到后交由各级协议栈处理。当协议栈处理完并把数据放到接收队列之后,唤醒用户进程(假设是阻塞方式)
从内核组件和源码视角来看,流程如下图:
那么汇总起来,一次跨机网络通信的过程如下图所示:
本机网络IO和跨机网络IO有差异的地方总共有两处,分别是路由和驱动程序
发送数据进入协议栈到达网络层的时候,网络层入口函数是ip_queue_xmit。在网络层里会进行路由选择,路由选择完毕,再设置IP头,进行netfilter的过滤,将包交给邻居子系统。网络层工作流程如下图所示:
对于本机网络IO来说,特殊之处在于在local路由表中就能找到路由项,对应的设备都将使用loopback网卡,也就是常说的lo设备
网络层入口函数ip_queue_xmit源码如下:
// net/ipv4/ip_output.c
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(sock_net(sk), fl4, sk,
daddr, inet->inet_saddr,
inet->inet_dport,
inet->inet_sport,
sk->sk_protocol,
RT_CONN_FLAGS(sk),
sk->sk_bound_dev_if);
...
sk_setup_caps(sk, &rt->dst);
}
...
}
查找路由项的函数是ip_route_output_ports,它又依次调用ip_route_output_flow、__ip_route_output_key
、fib_lookup函数。调用过程略过,直接看fib_lookup的关键代码
// include/net/ip_fib.h
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))
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 scope host src 127.0.0.1
从上述结果可以看出,对于目的是127.0.0.1的路由在local路由表中就能够找到。fib_lookup的工作完成,返回__ip_route_output_key
函数继续执行
// net/ipv4/route.c
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;
...
}
...
rth = __mkroute_output(&res, fl4, orig_oif, dev_out, flags);
...
return rth;
}
对于本机的网络请求,设备将全部使用net->loopback_dev,也就是lo虚拟网卡
接下来的网络层仍然和跨机网络IO一样,最终会经过ip_finish_output,进入邻居子系统的入口函数dst_neigh_output
本机网络IO需要进行IP分片吗?
因为和正常的网络层处理过程一样,会经过ip_finish_output函数,在这个函数中,如果skb大于MTU,仍然会进行分片。只不过lo虚拟网卡的MTU比Ethernet要大很多。通过ifconfig命令就可以查到,物理网卡MTU一般为1500,而lo虚拟接口能有65535个
在邻居子系统函数中经过处理后,进入网络设备子系统(入口函数是dev_queue_xmit)
问题:用本机IP(例如192.168.x.x)和用127.0.0.1在性能上有差别吗?
前面讲过,选用哪个设备是路由相关函数__ip_route_output_key
确定的
// net/ipv4/route.c
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;
...
}
...
rth = __mkroute_output(&res, fl4, orig_oif, dev_out, flags);
...
return rth;
}
在fib_lookup函数里会查询到local路由表
$ ip route list table local
local 10.162.*.* dev eth0 proto kernel scope host src 10.162.*.*
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
很多人在看到这个路由表的时候就被它迷惑了,以为上面的10.162.*.*
真的会被路由到eth0(其中10.162.*.*
是我的本机局域网IP,后面两段用*号隐藏起来了)
但其实内核在初始化local路由表的时候,把local路由表里所有的路由项都设置成了RTN_LOCAL,不只是127.0.0.1。这个过程是在设置本机IP的时候,调用fib_inetaddr_event函数完成设置的
// net/ipv4/fib_frontend.c
static int fib_inetaddr_event(struct notifier_block *this, unsigned long event, void *ptr)
{
...
switch (event) {
case NETDEV_UP:
fib_add_ifaddr(ifa);
...
break;
case NETDEV_DOWN:
fib_del_ifaddr(ifa, NULL);
...
break;
}
return NOTIFY_DONE;
}
// net/ipv4/fib_frontend.c
void fib_add_ifaddr(struct in_ifaddr *ifa)
{
...
fib_magic(RTM_NEWROUTE, RTN_LOCAL, addr, 32, prim);
...
}
所以即使本机IP不用127.0.0.1,内核在路由项查找的时候判断类型是RTN_LOCAL,仍然会使用net->loopback_dev,也就是lo虚拟网卡
网络设备子系统的入口函数是dev_hard_start_xmit。之前讲述跨机发送过程时介绍过,对于真的有队列的物理设备,该函数进行了一系列复杂的排队等处理后,才调用dev_hard_start_xmit,从这个函数再进入驱动程序来发送。在这个过程中,甚至还有可能出发软中断进行发送,流程如下图:
但是对于启动状态的回环设备(q->enqueue判断为false)来说,就简单多了。没有队列的问题,直接进入dev_hard_start_xmit。接着进入回环设备的驱动里发送回调函数loopback_xmit,将skb发送出去,如下图所示:
下面来看看详细的过程,从网络设备子系统的入口函数dev_queue_xmit看起
// net/core/dev.c
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) {
...
rc = dev_hard_start_xmit(skb, dev, txq);
...
}
...
}
在dev_queue_xmit函数中还将调用设备驱动的操作函数
// net/core/dev.c
int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev,
struct netdev_queue *txq)
{
// 获取设备驱动的回调函数集合ops
const struct net_device_ops *ops = dev->netdev_ops;
...
// 调用驱动的ndo_start_xmit进行发送
rc = ops->ndo_start_xmit(skb, dev);
...
}
回环设备的驱动程序的工作流程如下图:
loopback(回环)设备的驱动代码在drivers/net/loopback.c文件里
// drivers/net/loopback.c
static const struct net_device_ops loopback_ops = {
.ndo_init = loopback_dev_init,
.ndo_start_xmit= loopback_xmit,
.ndo_get_stats64 = loopback_get_stats64,
};
所以对dev_hard_start_xmit调用实际上执行的是loopback驱动里的loopback_xmit(loopback是一个纯软件性质的虚拟接口,并没有真正意义上对物理设备的驱动)
// drivers/net/loopback.c
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)) {
...
}
return NETDEV_TX_OK;
}
在skb_orphan中先把skb上的socket指针去掉了(剥离出来)
注意,在本机网络IO发送的过程中,传输层下面的skb就不需要释放了,直接给接收方传过去就行,总算是省了一点点开销。不过可惜传输层的skb同样节约不了,还是要频繁地申请和释放
接着调用netif_rx,在该方法中最终会执行到enqueue_to_backlog(netif_rx->enqueue_to_backlog)
// net/core/dev.c
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队列 并调用____napi_schedule
来触发软中断
// net/core/dev.c
static inline void ____napi_schedule(struct softnet_data *sd,
struct napi_struct *napi)
{
list_add_tail(&napi->poll_list, &sd->poll_list);
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
只有触发完软中断,发送过程才算完成了
发送过程触发软中断后,会进入软中断处理函数net_rx_action,如下图所示:
在跨机的网络包的接收过程中,需要经过硬中断,然后才能触发软中断。而在本机的网络IO过程中,由于并不真的过网卡,所以网卡的发送过程、硬中断就都省去了,直接从软中断开始
在软中断被触发以后,会进入NET_RX_SOFTIRQ对应的处理方法net_rx_action中
// net/core/dev.c
static void net_rx_action(struct softirq_action *h)
{
...
while (!list_empty(&sd->poll_list)) {
...
work = n->poll(n, weight);
...
}
...
}
对于igb网卡来说,poll实际调用的是igb_poll函数。那么loopback网卡的poll函数是哪个呢?由于poll_list里面是struct softnet_data对象,在net_dev_init中找到了对应的处理函数
// net/core/dev.c
static int __init net_dev_init(void)
{
...
for_each_possible_cpu(i) {
...
sd->backlog.poll = process_backlog;
...
}
...
}
struct softnet_data默认的poll在初始化的时候设置成了process_backlog函数
// net/core/dev.c
static int process_backlog(struct napi_struct *napi, int quota)
{
...
while (work < quota) {
...
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);
...
}
...
}
skb_queue_splice_tail_init是把sd->input_pkt_queue里的skb链到sd->process_queue链表上去,__skb_dequeue
是从sd->process_queue取下来包进行处理。这样和前面发送过程的结尾处就对上,发送过程是把包放到了input_pkt_queue队列里,如下图所示:
最后调用__netif_receive_skb
将数据送往协议栈。在此之后的调用过程就和跨机网络IO又一致了。送往协议栈的调用链是__netif_receive_skb=>__netif_receive_skb_core=>deliver_skb
,然后将数据包送入ip_rcv中。网络层再往后是传输层,最后唤醒用户进程
本机网络IO的内核总体执行流程如下图:
1)127.0.0.1本机网络IO需要经过网卡吗?
不需要经过网卡。即使把网卡拔了,本机网络还是可以正常使用的
2)数据包在内核中是什么走向,和外网发送相比流程上有什么差别?
总的来说,本机网络IO和跨机网络IO比较起来,确实是节约了驱动上的一些开销。发送数据不需要进RingBuffer的驱动队列,直接把skb传给接收协议栈(经过软中断)。但是在内核其他组件上,可是一点儿都没少,系统调用、协议栈(传输层、网络层等)、设备子系统整个走了一遍。连驱动程序都走了(虽然对于回环设备来说只是一个纯软件的虚拟出来的东西)。所以即使是本机网络IO,切忌误认为没啥开销就滥用
3)访问本机服务时,使用127.0.0.1能比使用本机IP(例如192.168.x.x)更快吗?
使用本机IP和127.0.0.1没有差别,都是走虚拟的回环设备lo。这是因为内核在设置IP的时候,把所有的本机IP都初始化到local路由表里了,类型写死了是RTN_LOCAL。在后面的路由项选择的时候发现类型是RTN_LOCAL就会选择lo设备了