image

协议层

socket

创建 socket 结构体,并初始化相应的操作函数。

sendto

sendto 代码会先将数据整理成底层可以处理的格式,然后调用 sock_sendmsg

sock_sendmsg

sock_sendmsg 做一些错误检查,然后调用 __sock_sendmsg

__sock_sendmsg

__sock_sendmsg 调用 __sock_sendmsg_nosec

__sock_sendmsg_nosec 将数据传递到 socket 子系统的更深处。

sendmsg 方法就是 inet_sendmsg

inet_sendmsg

AF_INET 协议族提供的通用函数。

调用 sock_rps_record_flow 来记录最后一个处理该 flow 的 CPU。 Receive Packet Steering 会用到这个信息。

检查当前 socket 有没有绑定源端口,如果没有的话,调用 inet_autobind 分配一个。

调用 socket 的协议类型对应的 sendmsg 方法。

sendmsg 函数作为分界点,处理逻辑从 AF_INET 协议族通用处理转移到具体的 UDP 协议的处理。

inet_autobind

调用 socket 上绑定的 get_port 函数获取一个可用的端口。

UDP 层

udp_sendmsg

UDP 模块发送数据包的入口。

获取目的 IP 地址和端口
  • 如果之前 socket 已经建立连接,那 socket 本身就存储了目标地址。
  • 地址通过辅助结构 struct msghdr 传入.

UDP socket 的状态使用了 TCP 状态来描述。

如果没有 struct msghdr 变量,内核函数到达 udp_sendmsg 函数时,会从 socket 本身检索目标地址和端口,并将 socket 标记为"已连接"。

socket 发送,bookkeeping 和打时间戳

获取存储在 socket 上的源地址、设备索引(device index)和时间戳选项。

ipc.addr = inet->inet_saddr;
ipc.oif = sk->sk_bound_dev_if;
sock_tx_timestamp(sk, &ipc.tx_flags);

路由

调用 ip_route_output_flow 获取路由信息,主要包括源 IP 和网卡。

根据路由表和目的 IP,找到这个数据包应该从哪个设备发送出去。

如果该 socket 没有绑定源 IP,还会根据路由表找到一个最合适的源 IP 给它。

如果该 socket 已经绑定了源 IP,但根据路由表,从这个源 IP 对应的网卡没法到达目的地址,则该包会被丢弃,于是数据发送失败,sendto 函数将返回错误。

最后会将找到的设备和源 IP 塞进 flowi4 结构体并返回给 udp_sendmsg

准备待发送数据

调用 ip_make_skb 构造 skb 结构体。 将网卡的信息和 skb 关联。

构造 skb:

  • MTU
  • UDP corking 如果启用
  • UDP Fragmentation Offloading(UFO)
  • Fragmentation(分片),如果硬件不支持 UFO,但是要传输的数据大于 MTU,需要软件做分片

构造好的 skb 里面已经分配了 IP 包头,并且初始化了部分信息。 IP 包头的源 IP 就在这里被设置进去。

调用 __ip_append_dat,如果需要分片的话,会在 __ip_append_data 函数中进行分片。

检查 socket 的 send buffer 是否已经用光,如果被用光的话,返回 ENOBUFS。

udp_send_skb

  • 向 skb 添加 UDP 头
  • 处理校验和 checksum
  • 调用 ip_send_skb 将 skb 发送到 IP 协议层
  • 更新发送成功或失败的统计计数器

IP 层

ip_send_skb

IP 模块发送数据包的入口。 只是简单的调用一下后面的函数。

ip_local_out 只需调用 __ip_local_out,如果返回值为 1,则调用路由层 dst_output 发送数据包。

__ip_local_out_sk 设置 IP 报文头的校验和 checksum,然后调用 netfilter 的钩子。

NF_INET_LOCAL_OUT 是 netfilter 的钩子,可以通过 iptables 来配置怎么处理该数据包。 如果该数据包没被丢弃,则继续往下走。

dst_output_sk 根据 skb 里面的信息,调用相应的 output 函数。 IPv4 这种情况下,会调用 ip_output

ip_outputudp_sendmsg 得到的网卡信息写入 skb,然后调用 NF_INET_POST_ROUTING 的钩子。

NF_INET_POST_ROUTING 中有可能配置了 SNAT,从而导致该 skb 的路由信息发生变化。

ip_finish_output

判断经过了上一步后,路由信息是否发生变化。 如果发生变化的话,需要重新调用 dst_output_sk,否则往下走。

重新调用 dst_output_sk 时,可能就不会再走到 ip_output,而是走到被 netfilter 指定的 output 函数里,这里有可能是 xfrm4_transport_output

ip_finish_output2

根据目的 IP 到路由表里面找到下一跳 nexthop 的地址。 调用 __ipv4_neigh_lookup_noref 去 arp 表里面找下一跳的 neigh 信息。 没找到的话会调用 __neigh_create 构造一个空的 neigh 结构体。

将包发送到邻居缓存之前处理各种统计计数器。

如果 ip_finish_output2 没得到 neigh 信息,那么将会走到函数 neigh_resolve_output 中。

dst_neigh_output

将 neigh 信息里面的 MAC 地址填到 skb 中,然后调用 dev_queue_xmit 发送数据包。

neigh_resolve_output

发送 arp 请求,得到下一跳的 MAC 地址。 将 MAC 地址填到 skb 中并调用 dev_queue_xmit

Linux 网络设备子系统

dev_queue_xmit 简单封装了 __dev_queue_xmit

netdevice 子系统的入口函数。

获取设备对应的 qdisc。 如果没有,直接调用 dev_hard_start_xmit。 如果有,数据包将经过 Traffic Control 模块进行处理。

dev_hard_start_xmit

如果 dev_hard_start_xmit 返回错误的话(大部分情况可能是 NETDEV_TX_BUSY),调用它的函数会把 skb 放到一个地方,然后抛出软中断 NET_TX_SOFTIRQ,交给软中断处理程序 net_tx_action 稍后重试。

拷贝一份 skb 给 taps。 tcpdump 就是从这里得到数据的。

调用 ndo_start_xmit

Traffic Control

进行过滤和优先级处理。

如果队列满了的话,数据包会被丢掉。

网络设备驱动

ndo_start_xmit 会绑定到具体网卡驱动的相应函数。 由网卡驱动接管,不同的网卡驱动有不同的处理方式。

igb_tx_map 函数处理将 skb 数据映射到 RAM 的 DMA 区域的细节。

将 skb 放入网卡自己的发送队列 tx ring buffer,通知网卡发送数据包。

更新设备 TX Queue 的尾部指针,从而触发设备 被唤醒,从 RAM 获取数据并开始发送。

网卡发送完成后发送中断给 CPU。

收到中断后进行 skb 的清理工作。