>,
,现在的服务器上的网卡一般都是支持多队列的。每一个队列都是由一个RingBuffer表示的,开启了多队列以后的网卡就会有多个RingBuffer。,网卡启动时最重要的任务就是分配和初始化RingBuffer,在网卡启动的时候会调用到__igb_open函数,RingBuffer就是在这里分配的。,igb_setup_tx_resources内部也是申请了两个数组,igb_tx_buffer数组和e1000_adv_tx_desc数组,
一个供内核使用,一个供网卡硬件使用。,在这个时候它们之间还没什么关系,
将来在发送数据的时候这两个数组的指针都指向同一个skb,这样内核和硬件就能共同访问同样的数据了。,
内核往skb写数据,网卡硬件负责发送。,
硬中断的处理函数igb_msix_ring也是在__igb_open函数中注册的。,send系统调用内部真正使用的是sendto系统调用,主要做了两件事:,sock_sendmsg经过一系列调用,最终来到__sock_sendmsg_nosec中调用sock->ops->sendmsg,
对于AF_INET协议族的socket,sendmsg的实现统一为inet_sendmsg,
1. 传输层拷贝,在进入协议栈inet_sendmsg以后,内核接着会找到sock中具体的协议处理函数,对于TCP协议而言,sk_prot操作函数集实例为tcp_prot,其中.sendmsg的实现为tcp_sendmsg(对于UDP而言中的为udp_sendmsg)。,这个函数的实现逻辑比较复杂,代码总只显示了skb拷贝的相关部分,总体逻辑如下:,
2. 传输层发送,上面的发送数据步骤,不论是调用__tcp_push_pending_frames还是tcp_push_one,最终都会执行到
tcp_write_xmit(在网络协议中学到滑动窗口、拥塞控制就是在这个函数中完成的),函数的主要逻辑如下:,这里我们只关注发送的主过程,其他部分不过多展开,即来到
tcp_transmit_skb函数,第一件事就是先
克隆一个新的skb,因为skb后续在调用网络层,最后到达网卡发送完成的时候,这个skb会被释放掉。而
TCP协议是支持丢失重传的,在收到对方的ACK之前,这个skb不能被删除掉。所以内核的做法就是每次调用网卡发送的时候,实际上传递出去的是skb的一个拷贝。等收到ACK再真正删除。,第二件事是修
改skb的TCP头,根据实际情况把TCP头设置好。实际上
skb内部包含了网络协议中所有的头,在设置TCP头的时候,只是把指针指向skb合适的位置。后面设置IP头的时候,再把指针挪动一下即可,避免了频繁的内存申请和拷贝,提高效率。,tcp_transmit_skb是发送数据位于传输层的最后一步,调用了网络层提供的发送接口icsk->icsk_Af_ops->queue_xmit()之后就可以进入网络层进行下一层的操作了。,在tcp_ipv4中,queue_xmit指向的是ip_queue_xmit,具体实现如下:,这个函数主要做的就是
找到该把这个包发往哪,并构造好IP包头。它会去查询socket中是否有缓存的路由表,如果有则直接构造包头,如果没有就去查询并缓存到sokect,然后为skb设置路由表,最后封装ip头,发往ip_local_out函数。,ip_local_out中主要会经过__ip_local_out => nf_hook 的过程
进行netfilter的过滤。如果使用iptables配置了一些规则,那么这里将检测到是否命中规则,然后进行相应的操作,如网络地址转换、数据包内容修改、数据包过滤等。如果设置了非常复杂的netfilter规则,则在这个函数会导致进程CPU的开销大增。经过netfilter处理之后,(忽略其他部分)调用dst_output(skb)函数。,dst_output会去调用skb_dst(skb)->output(skb),即
找到skb的路由表(dst条目),然后
调用路由表的output方法。这里是个函数指针,指向的是ip_output方法。,在ip_output方法中首先会进行一些
简单的统计工作,随后
再次执行netfilter过滤。过滤通过之后
回调ip_finish_output。,在ip_finish_output中,会
校验数据包的长度,如果大于MTU,就会执行分片。MTU的大小是通过MTU发现机制确定,在以太网中为1500字节。分片会带来两个问题:,如果不需要分片则调用ip_finish_output2函数,根据下一跳的IP地址查找邻居项,找不到就创建一个,然后发给下一层——邻居子系统。,总体过程如下:,
邻居子系统是位于网络层和数据链路层中间的一个系统,其作用是为网络层提供一个下层的封装,让网络层不用关心下层的地址信息,让下层来决定发送到哪个MAC地址。,在邻居子系统中主要
查找或者创建邻居项,在创建邻居项时有可能会发出实际的arp请求。然后
封装MAC头,将发生过程再
传递给更下层的网络设备子系统。,ip_finish_output2的实现逻辑大致流程如下:,邻居子系统通过dev_queue_xmit进入网络设备子系统,dev_queue_xmit的工作逻辑如下,在前面讲过,网卡是有多个发送队列的,所以首先需要选择一个队列进行发送。队列的选择首先是通过获取用户的XPS配置(为队列绑核),如果没有配置则调用skb_tx_hash去计算出选择的队列。接着会根据与此队列关联的qdisc得到该队列的排队规则。,最后会根据是否存在队列(如果是发给回环设备或者隧道设备则没有队列)来决定后续数据包流向。对于存在队列的设备会进入__dev_xmit_skb函数。,__dev_xmit_skb分为三种情况:,从上述代码中可以看到,
while循环不断地从队列中取出skb并进行发送,这个时候其实占用的都是用户进程系统态时间sy,只有当quota用尽或者其他进程需要CPU的时候才触发软中断进行发送。,这就是为什么服务器上查看/proc/softirqs,一般NET_RX要比NET_TX大得多的原因。
对于接收来说,都要经过NET_RX软中断,而对于发送来说,只有系统配额用尽才让软中断上。,这里我们聚焦于qdisc_restart函数上,这个函数用于从qdisc队列中取包并发给网络驱动,首先调用 dequeue_skb()
从 qdisc 中取出要发送的 skb。如果队列为空,返回 0, 这将导致上层的 qdisc_restart() 返回 false,继而退出 while 循环。,如果拿到了skb则
调用sch_direct_xmit继续发送,该函数会
调用dev_hard_start_xmit,进入驱动程序发包,如果无法发送则重新入队。,即整个__qdisc_run的整体逻辑为:while 循环调用 qdisc_restart(),后者取出一个 skb,然后尝试通过 sch_direct_xmit() 来发送;sch_direct_xmit 调用 dev_hard_start_xmit 来向驱动程序进行实际发送。任何无法发送的 skb 都重新入队,将在 NET_TX softirq 中进行发送。,上一部分中如果发送网络包的时候CPU耗尽了,会调用进入__netif_schedule,该函数会进入__netif_reschedule,
将发送队列设置到softnet_data上,并最终发出一个NET_TX_SOFTIRQ类型的软中断。软中断是由内核进程运行的,该进程会进入net_tx_action函数,在该函数中能
获得发送队列,并最终也调用到驱动程序的入口函数dev_hard_start_xmit。,从触发软中断开始以后发送数据消耗的CPU就都显示在si中,而不会消耗用户进程的系统时间,可以看到软中断的处理中,最后和前面一样都是调用了__qdisc_run。也就是说不管是在qdisc_restart中直接处理,还是软中断来处理,最终实际都会来到dev_hard_start_xmit(__qdisc_run => qdisc_restart => dev_hard_start_xmit)。,通过前面的介绍可知,
无论对于用户进程的内核态,还是对于软中断上下文,都会调用网络设备子系统的dev_hard_start_xmit函数,在这个函数中,会
调用驱动里的发送函数igb_xmit_frame。在驱动函数里,会
将skb挂到RingBuffer上,驱动调用完毕,
数据包真正从网卡发送出去。,这里ndo_start_xmit是网卡驱动要实现的函数,igb网卡驱动中的实现是igb_xmit_frame(在网卡驱动程序初始化的时候赋值的)。igb_xmit_frame主要会去调用igb_xmit_frame_ring函数,
在这里从网卡的发送队列的RingBuffer上取下来一个元素,并将skb挂到元素上。然后使用igb_tx_map函数将skb数据映射到网卡可访问的内存DMA区域。,当所有需要的描述符都建好,且skb的所有数据都映射到DMA地址后,驱动就会进入到它的最后一步,触发真实的发送。,当数据发送完以后,其实工作并没有结束,因为内存还没有清理。
当发送完成的时候,网卡设备会触发一个硬中断(硬中断会去触发软中断)来释放内存。,这里需要注意的就是,
虽然是数据发送完成通知,但是硬中断触发的软中断是NET_RX_SOFTIRQ,这也就是为什么软中断统计中RX要高于TX的另一个原因。,硬中断中会向softnet_data添加poll_list,
软中断中轮询后调用其poll回调函数,具体实现是igb_poll,其会
在q_vector->tx.ring存在时去调用igb_clean_tx_irq。,其实逻辑无非就是清理了skb(其中data保存的数据包没有释放),解决了DMA映射等,到了这一步传输才算基本完成。,当然因为传输层需要保证可靠性,所以数据包还没有删除,此时还有前面的拷贝过的skb指向它,它得等到收到对方的ACK之后才会真正删除。,
,以上就是深入理解Linux网络之内核是如何发送网络包的的详细内容,更多关于Linux 内核发送网络包的资料请关注其它相关文章!,>