协议层
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_output
将 udp_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 的清理工作。