上一篇介绍了数据包接收流程后,本文将介绍数据包在Linux系统中是如何一步步从应用程序发送到网卡,最后发送出去的。如果英文没问题,强烈建议看后面参考文献里的文章,里面介绍的比较详细。本文只讨论以太网的物理网卡,以一个UDP包的发送过程为例。由于本人对协议栈的代码不熟悉,所以有些地方可能理解有误。欢迎指正socket层+--------------+|Application|+------------+||↓+-------------------------------------+|socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP)|+--------------------------------------+||↓+-------------------+|sendto(sock,...)|+----------------+||↓+------------+|inet_sendmsg|+-------------+||↓+-------------+|inet_autobind|+---------------+||↓+------------+|UDPlayer|+----------+socket(...):创建一个socket结构,并初始化相应的操作函数,由于我们定义了UDP的socket,所有与UDP相关的函数都存放在里面sendto(sock,...):应用层程序(Application)调用这个函数开始发送数据包,这个函数会调用下面的inet_sendmsginet_sendmsg:这个函数主要是检查当前socket是否绑定了源端口,如果没有,调用inet_autobind分配一个,然后调用UDP层的函数inet_autobind:这个函数会调用socket上面绑定的get_port函数获取一个可用的端口。由于socket是UDPsocket,所以会把get_port函数调用到UDP代码中对应的函数中。UDP层||↓+------------+|udp_sendmsg|+------------+||↓+-------------------+|ip_route_output_flow|+--------------------+||↓+-----------+|ip_make_skb|+------------+||↓+-----------------------+|udp_send_skb(skb,fl4)|+------------------------+||↓+----------+|IPlayer|+------------+udp_sendmsg:udp模块发送数据包的入口。这个函数比较长。该函数中会先调用ip_route_output_flow获取路由信息(主要包括源IP和网卡),然后调用ip_make_skb构造skb结构体,最后将网卡信息与skb相关联。ip_route_output_flow:这个函数会根据路由表和目的IP找到数据包应该从哪个设备发出。如果socket没有绑定源IP,这个函数也会根据路由表为它找到最合适的源IP。如果socket已经绑定了源IP,但是根据路由表,源IP对应的网卡无法到达目的地址,那么这个包就会被丢弃,所以数据传输失败,sendto函数就会返回一个错误。该函数会将找到的设备和源IP插入到flowi4结构体中,返回给udp_sendmsgip_make_skb:该函数的作用是构造skb包,其中已经分配了IP头,并初始化了一些信息(源IP的IP头在这里设置),同时函数会调用__ip_append_dat。如果需要分片,会在__ip_append_data函数中进行分片,同时在该函数中也会检查socket的发送缓冲区是否已经用完,如果用完了,则返回ENOBUFSudp_send_skb(skb,fl4)is主要是在skb中填充UDP头,同时处理checksum,然后调用IP层的相应函数。IP层||↓+------------+|ip_send_skb|+------------+||↓+-------------------++--------------------++----------------+|__ip_local_out_sk|----->|NF_INET_LOCAL_OUT|------>|dst_output_sk|+--------------------++-------------------++----------------+||↓+----------------++--------------------++------------+|ip_finish_output|<-------|NF_INET_POST_ROUTING|<------|ip_output|+----------------++--------------------++----------+||↓+----------------++-----------------++--------------------+|ip_finish_output2|----->|dst_neigh_output|------>|neigh_resolve_output|+--------------------++-------------------++----------------------+||↓+----------------+|dev_queue_xmit|+----------------+ip_send_skb:IP模块发送数据包的入口,该函数简单调用以下函数__ip_local_out_sk:设置IP包头长度和校验和,然后调用下面的netfilterhookNF_INET_LOCAL_OUT:netfilterhook,可以配置如何通过iptables处理数据包,如果数据包没有被丢弃,则继续往下走dst_output_sk:这个函数根据in中的信息调用skb对应的输出函数,在我们的UD中PIPv4情况下,ip_output会调用ip_output:将上面udp_sendmsg获取的网卡信息写入skb,然后调用NF_INET_POST_ROUTING的hookNF_INET_POST_ROUTING:这里用户可能配置SNAT,会导致skbtochangeip_finish_output:这个会判断上一步之后路由信息是否发生变化。如果有变化,需要再次调用dst_output_sk(再次调用该函数时,可能不会再去ip_output,而是去netfilter指定的output函数中,这里可能是xfrm4_transport_output),否则往下走ip_finish_output2:根据目的IP在路由表中查找下一跳(nexthop)的地址,然后调用__ipv4_neigh_lookup_noref在arp表中查找下一跳的neigh信息,没有找到则将调用__neigh_create构造一个空的neigh结构体dst_neigh_output:在这个函数中,如果上一步ip_finish_output2没有得到neigh信息,那么就会去函数neigh_resolve_output,否则直接调用neigh_hh_output,在这个函数中,会把mac地址填入将neigh信息放入skb,然后调用dev_queue_xmit发送数据包neigh_resolve_output:该函数会发送arp请求获取下一个的mac地址hop,然后将mac地址填入skb,调用dev_queue_xmitnetdevice子系统||↓+----------------++----------------|dev_queue_xmit||+--------------+|||||↓|+----------------+||TrafficControl||+----------------+|环回||或+----------------------------------------------------------+|IP隧道↓||↓||+--------------------+失败+---------------------++----------------++------------>|dev_hard_start_xmit|------------>|raiseNET_TX_SOFTIRQ|---->|net_tx_action|+--------------------++-----------------------++----------------+|+-------------------------------+||↓↓+----------------++-------------------------+|ndo_start_xmit||packettaps(AF_PACKET)|+----------------++------------------------+dev_queue_xmit:netdevice子系统的入口函数。在这个函数中,会先获取设备对应的qdisc。如果没有(比如loopback或者IPtunnels),会直接调用dev_hard_start_xmit,否则数据包会通过TrafficControl模块进行处理。TrafficControl:这个主要是做一些过滤和优先级处理。这里,如果队列已满,数据包将被丢弃。详情请参考文档。这一步完成后,也会去dev_hard_start_xmitdev_hard_start_xmit:在这个函数中,先拷贝一个skb到“packettaps”,tcpdump从这里获取数据,然后调用ndo_start_xmit如果dev_hard_start_xmit返回错误(多数情况下可能是NETDEV_TX_BUSY),调用它的函数会将skb放在一个地方,然后抛出一个软中断NET_TX_SOFTIRQ,交给软中断处理函数net_tx_action稍后重试(如果是是loopback或IPtunnels,失败后不会有重试逻辑)ndo_start_xmit:这是一个函数指针,会指向从具体driver发送数据的函数DeviceDriverndo_start_xmit会绑定到具体driver对应的函数网卡驱动。经过这一步,就属于网卡驱动负责了。不同的网卡驱动有不同的处理方式。我不会在这里详细介绍它们。大致流程如下:将skb放入网卡自己的发送队列,通知网卡发送数据包。网卡向CPU发送中断后,接收到中断。最后,清理skb。在网卡驱动发送数据的过程中,会有一些地方需要和netdevice子系统打交道。然后通知上层发送数据。其他SO_SNDBUF:从上面的流程可以看出,对于UDP来说,是没有对应的发送缓冲区的。SO_SNDBUF只是一个限制。当这个socket分配的skb占用的内存超过这个值时,就会返回ENOBUFS,所以只要不出现ENOBUFS错误,增加这个值是没有意义的。从sendto函数的帮助文件中,看到了这样一句话:(Linux下一般不会出现这种情况,设备队列溢出时,数据包会被静默丢弃。)。这里的设备队列应该是指TrafficControl中的队列,说明在Linux中,队列默认的SO_SNDBUF值就足够了。问题是队列的长度和数量是可以配置的。如果配置太大,按理说应该出现ENOBUFS的情况。txqueuelen:很多地方都说这个是控制qdisc中队列的长度,但是好像只有部分类型的qdisc使用这个配置,比如linux中默认的pfifo_fast。hardwareRX:一般网卡都有自己的ringqueue。这个队列的大小可以通过ethtool配置。当驱动程序收到一个发送请求时,通常会把它放到这个队列中,然后通知网卡发送数据。当队列满的时候,会返回NETDEV_TX_BUSYpackettaps(AF_PACKET)给上层调用:第一次发送数据包和重试发送数据包时,都会经过这里。如果有retry的话,tcpdump会不会抓到两个Subpackage就不确定了,应该不合理,可能是我没看懂。请参阅监视和调整Linux网络堆栈:在linux网络堆栈中发送数据队列
