上一篇介绍了数据包接收流程后,本文将介绍数据包在Linux系统中是如何一步步从应用程序发送到网卡,最后发送出去的。如果英文没问题,强烈建议看后面参考文献里的文章,里面介绍的比较详细。本文只讨论以太网的物理网卡,以一个UDP包的发送过程为例。由于本人对协议栈的代码不熟悉,所以有些地方可能理解有误。欢迎指正socket层+--------------+|申请|+------------+||↓+------------------------------------+|套接字(AF_INET,SOCK_DGRAM,IPPROTO_UDP)|+------------------------------------------+||↓+--------------------+|发送到(袜子,...)|+--------------------+||↓+------------+|inet_sendmsg|+--------------+||↓+----------------+|inet_autobind|+--------------+||↓+------------+|UDP层|+------------+socket(...):创建一个socket结构体,并初始化相应的操作函数,由于我们定义了UDP的socket,所以里面存储的是UDP相关的函数sendto(sock,...):应用层程序(Application)调用此函数开始发送数据包,此函数会调用如下,调用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)|+------------------------+||↓+----------+|IP层|+------------+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的sendbuffer是否已经在这个函数中被Light使用,如果是用完,返回ENOBUFSudp_send_skb(skb,fl4)主要是将UDP头填充到skb中,同时处理校验和,然后调用IP层的相应函数。IP层||↓+------------+|ip_send_skb|+------------+||↓+-----------------++-----------------++---------------+|__ip_local_out_sk|------>|NF_INET_LOCAL_OUT|------>|dst_output_sk|+--------------------++--------------------++------------+||↓+----------------++--------------------++----------+|ip_finish_output|<--------|NF_INET_POST_ROUTING|<------|ip_输出|+----------------++--------------------++-----------+||↓+----------------++----------------++----------------------+|ip_finish_output2|----->|dst_neigh_output|------>|内gh_resolve_output|+--------------------++----------------++----------------------+||↓+----------------+|dev_queue_xmit|+----------------+i??p_send_skb:IP模块发送数据包的入口。这个函数简单的调用了下面的函数__ip_local_out_sk:设置IP包头的长度和校验和,然后调用下面的netfilterhookNF_INET_LOCAL_OUT:netfilter的hook可以配置如何通过iptables处理数据包。如果数据包没有被丢弃,继续往下走dst_output_sk:该函数根据skb中的信息调用相应的输出函数。在我们的UDPIPv4这样的情况下,ip_output会调用ip_output:将上面udp_sendmsg获取的网卡信息写入skb,然后调用NF_INET_POST_ROUTING的hookNF_INET_POST_ROUTING:这里用户可能会配置SNAT,会导致路由信息oftheskbtochangeip_finish_output:这里会判断经过上一步之后,路由信息是否发生了变化,如果有,需要再次调用dst_output_sk(再次调用该函数时,可能不会走到ip_output,而是走到netfilter指定的输出函数,可能是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直接地。在这个函数中,将neigh信息中的mac地址填入skb,然后调用dev_queue_xmit发送数据包neigh_resolve_output:这个函数会发送arp请求,得到下一跳的mac地址,然后填入mac地址进入skb并调用dev_queue_xmitnetdevice子系统||↓+----------------++--------------|dev_queue_xmit||+----------------+|||||↓|+------------------+||交通管制||+----------------+|环回||或+--------------------------------------------------------------+|IP隧道↓||↓||+--------------------+失败+--------------------++---------------++------------>|dev_hard_start_xmit|------------>|提高NET_TX_SOFTIRQ|---->|net_tx_action|+--------------------++--------------------++----------------+|+--------------------------------+||↓↓+-------------++---------------------+|ndo_start_xmit||数据包水龙头(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或者IP隧道,会有失败后无重试逻辑)ndo_start_xmit:这是一个函数指针,会指向具体驱动发送数据的函数DeviceDriverndo_start_xmit会绑定到具体网卡驱动的对应函数。这一步之后,就会由网卡驱动来管理了。不同的网卡驱动有不同的处理方式。这里就不详细介绍了。大体流程如下:将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会不会抓到两个包就不确定了,应该不合理,可能是我没看懂。请参阅监视和调整Linux网络堆栈:在linux网络堆栈中发送数据队列
