在 ip_queue_xmit 中,也即 IP 层的发送函数里面,有三部分逻辑。
第一部分,选取路由,也即我要发送这个包应该从哪个网卡出去。
这件事情主要由 ip_route_output_ports 函数完成。接下来的调用链为:ip_route_output_ports->ip_route_output_flow->__ip_route_output_key->ip_route_output_key_hash->ip_route_output_key_hash_rcu。
ip_route_output_key_hash_rcu 先会调用 fib_lookup。FIB 全称是 Forwarding Information Base,转发信息表。其实就是咱们常说的路由表。
路由表可以有多个,一般会有一个主表,RT_TABLE_MAIN。然后 fib_table_lookup 函数在这个表里面进行查找。
路由就是在 Linux 服务器上的路由表里面配置的一条一条规则。这些规则大概是这样的:想访问某个网段,从某个网卡出去,下一跳是某个 IP。
一个著名的实现,就是内核模块 ip_tables。在用户态,还有一个客户端程序 iptables,用命令行来干预内核的规则。
iptables 有表和链的概念,最终要的是两个表。filter 表处理过滤功能,主要包含以下三个链。
- INPUT 链:过滤所有目标地址是本机的数据包
- FORWARD 链:过滤所有路过本机的数据包
- OUTPUT 链:过滤所有由本机产生的数据包
nat 表主要处理网络地址转换,可以进行 SNAT(改变源地址)、DNAT(改变目标地址),包含以下三个链。
- PREROUTING 链:可以在数据包到达时改变目标地址
- OUTPUT 链:可以改变本地产生的数据包的目标地址
- POSTROUTING 链:在数据包离开时改变数据包的源地址
在这里,网络包马上就要发出去了,因而是 NF_INET_LOCAL_OUT,也即 ouput 链,如果用户曾经在 iptables 里面写过某些规则,就会在 nf_hook 这个函数里面起作用。
从 ip_finish_output 函数开始,发送网络包的逻辑由第三层到达第二层。ip_finish_output 最终调用 ip_finish_output2。
在 ip_finish_output2 中,先找到 struct rtable 路由表里面的下一跳,下一跳一定和本机在同一个局域网中,可以通过二层进行通信,因而通过 __ipv4_neigh_lookup_noref,查找如何通过二层访问下一跳。
在 __neigh_event_send 中,激活 ARP 分两种情况,第一种情况是马上激活,也即 immediate_probe。另一种情况是延迟激活则仅仅设置一个 timer。然后将 ARP 包放在 arp_queue 上。如果马上激活,就直接调用 neigh_probe;如果延迟激活,则定时器到了就会触发 neigh_timer_handler,在这里面还是会调用 neigh_probe。
qdisc 全称是 queueing discipline,中文叫排队规则。内核如果需要通过某个网络接口发送数据包,都需要按照为这个接口配置的 qdisc(排队规则)把数据包加入队列。
最简单的 qdisc 是 pfifo,它不对进入的数据包做任何的处理,数据包采用先入先出的方式通过队列。pfifo_fast 稍微复杂一些,它的队列包括三个波段(band)。在每个波段里面,使用先进先出规则。
三个波段的优先级也不相同。band 0 的优先级最高,band 2 的最低。如果 band 0 里面有数据包,系统就不会处理 band 1 里面的数据包,band 1 和 band 2 之间也是一样。
数据包是按照服务类型(Type of Service,TOS)被分配到三个波段里面的。TOS 是 IP 头里面的一个字段,代表了当前的包是高优先级的,还是低优先级的。
pfifo_fast 分为三个先入先出的队列,我们能称为三个 Band。根据网络包里面的 TOS,看这个包到底应该进入哪个队列。TOS 总共四位,每一位表示的意思不同,总共十六种类型。
解析了发送一个网络包的过程。
这个过程分成几个层次。
- VFS 层:write 系统调用找到 struct file,根据里面的 file_operations 的定义,调用 sock_write_iter 函数。sock_write_iter 函数调用 sock_sendmsg 函数。
- Socket 层:从 struct file 里面的 private_data 得到 struct socket,根据里面 ops 的定义,调用 inet_sendmsg 函数。
- Sock 层:从 struct socket 里面的 sk 得到 struct sock,根据里面 sk_prot 的定义,调用 tcp_sendmsg 函数。
- TCP 层:tcp_sendmsg 函数会调用 tcp_write_xmit 函数,tcp_write_xmit 函数会调用 tcp_transmit_skb,在这里实现了 TCP 层面向连接的逻辑。
- IP 层:扩展 struct sock,得到 struct inet_connection_sock,根据里面 icsk_af_ops 的定义,调用 ip_queue_xmit 函数。
- IP 层:ip_route_output_ports 函数里面会调用 fib_lookup 查找路由表。FIB 全称是 Forwarding Information Base,转发信息表,也就是路由表。
- 在 IP 层里面要做的另一个事情是填写 IP 层的头。
- 在 IP 层还要做的一件事情就是通过 iptables 规则。
- MAC 层:IP 层调用 ip_finish_output 进行 MAC 层。
- MAC 层需要 ARP 获得 MAC 地址,因而要调用 ___neigh_lookup_noref 查找属于同一个网段的邻居,他会调用 neigh_probe 发送 ARP。
- 有了 MAC 地址,就可以调用 dev_queue_xmit 发送二层网络包了,它会调用 __dev_xmit_skb 会将请求放入队列。
- 设备层:网络包的发送会触发一个软中断 NET_TX_SOFTIRQ 来处理队列中的数据。这个软中断的处理函数是 net_tx_action。
- 在软中断处理函数中,会将网络包从队列上拿下来,调用网络设备的传输函数 ixgb_xmit_frame,将网络包发到设备的队列上去。