Linux内核网络部分控制流

英文原文地址:http://www.linuxfoundation.org/collaborate/workgroups/networking/kernelflow#Layer_5:_Session_layer_.28sockets_and_files.29


Linux内核网络部分控制流_第1张图片


这篇文章描述Linux内核网络部分的控制流(以及相关的数据缓冲),上述图片是对网络控制流的一个概括性的总图。这篇文章是基于Linux 2.6.20内核的。


1.前言

大家可以参阅:网络概述(Network Overview)来了解内核中网络部分的各方面:路由、邻居发现协议(ND,Neighbour Discovery)、NAPI、过滤器等。
网络数据是通过 sk_buff 数据结构进行管理的,这最大限度地减少了网络各层之间拷贝数据的开销。学习内核网络部分需要我们对 sk_buff结构体有所了解。
  Linux内核作为一个整体使用了大量的虚拟方法,这些方法都使用函数指针记录在数据结构中。在图中,我们将它们表示成钻石型(菱形)。这篇文章不会列出所有的这中虚拟方法,只是列出一些主要的。
本文只讨论了以太网之上基于IPv4的TCP连接。当然,除此之外还有很多种不同的网络层连接方式,比如:隧道、桥接等。


2.发送路径

 

2.1 第五层:会议层(Socket和文件)

通过网络发送数据有三种系统调用:
> write (写内存数据到一个文件描述符)
> sendto (写内存数据到一个socket)

> sendmsg (写综合信息到一个socket)


这些系统调用最终都调用 __sock_sendmsg()方法,__sock_sendmsg()会先调用security_sock_sendmsg()检查权限然后使用socket的sendmsg方法把这些信息发送到下一层。


2.2 第四层:传输层(TCP)

tcp_sendmsg:对于信息中的每个段
1) 发现一个可用的 sk_buff(如果有剩余空间的话使用最后一个,否则重新分配一个并追加到末尾)
2) 使用 skb_add_data() 从用户空间拷贝数据到sk_buff数据空间(内核空间,很可能是DMA空间)
>> 对于每个socket,缓冲空间是预先分配的。假如缓冲区空间满了,通信会暂停:那些数据还是在用户空间,直到缓冲区可用(假如系统调用被设置成无阻塞,它会立刻返回一个错误)
>> 分配的 sk_buff 空间的大小等于MSS(最大段长)+头部长度(MSS在连接过程中可能会改变,或者被用户操作修改)
>> 在这一层还有分段操作(写的时候则需要合并),在一个 sk_buff 的数据最终都成为一个TCP段(这句解释的不清楚,原文为:Whatever ends up in the same sk_buff will become a single TCP segment.)。尽管如此,这样的段也可能会在IP层被分片。
3) TCP队列被激活,调用tcp_transmit_skb()发送包(假如有许多活动的缓冲区,则会调用多次)。
4) tcp_transmit_skb()创建TCP头部(分配 sk_buff 时已经预留空间了)。它克隆skb信息为了将控制信息传递给网络层。通过socket地址家族的 queue_xmit 方法接通网络层(inet_connection_sock->icsk_af_ops)。


2.3 第三层:网络层(IPv4)

1) ip_queue_xmit() 会负责路由工作(如果需要的话),并创建 IPv4 报头。
2nf_hook() 在一些地方被调用用来做网络过滤器(防火墙,NAT……),它会修改数据报或者直接丢弃。 
3路由会根据目标(dst_entry)进行决策,这个目标地址是数据包中的接收方IP地址。dst_entry 方法被调用来执行实际的输出。
4sk_buff 会被传递到ip_output()(或者别的输出机制,比如隧道)。
5ip_output() 需要做路由后的过滤工作,假如需要过滤则会在一个新的目标上重新输出,将数据报分段,并最终把它发送到输出设备。 
>> 分片时会尽可能重用已有的片缓冲(这通常都是面临已经分片的IP包时需要做的)。片缓冲是特殊的 sk_buff 对象,指向相同数据空间(不需要拷贝数据)。
>> 假如没有片缓冲可用,会分配一个新的 sk_buff 对象,然后拷贝数据。
>> 因为TCP已经确保包肯定比MTU小,所以通常不需要再分片了。
6 特定设备的输出会通过一个方法调用实现输出数据到 dst_entry 的邻居数据结构。这个方法通常是dev_queue_xmit。此外,还有一些对已知目标(hh_cache)的包优化。
    

2.4 第二层:链路层(以太网)

内核中网络的链路层主要功能是调度那些即将被发送的数据包。所以 Linux 用了队列规则(Qdisc 结构体)的抽象。有关详细信息,参见Linux高级路由和流量控制的第九章(带宽管理的排队规则),以及内核中 Documentation//networking/multiqueue.txt文档。


dev_queue_xmit 使用qdisc->enqueue 将 sk_buff 放到设备队列中。
> 当需要时(设备不支持分散的数据),数据是线性放到sk_buff结构中,这需要拷贝数据。
> 没有Qdisc的设备直接调用 dev_hard_start_xmit()。
> 一些Qdisc调用策略已经存在,基本上用的最多的是 pfifo_fast,它有三个优先级。


   设备的输出队列会立刻触发 qdisc_run(), qdisc_run()会调用qdisc_restart(),qdisc_restart()使用qdisc->dequeue方法从队列中取skb信息。具体的排队规则会延迟发送而不返回任何skb信息,然后建立一个 qdisc_watchdog_timer() 。当计时器到时间会调用netif_schedule()来启动传输。
最终,sk_buff被dev_hard_start_xmit()发送并从Qdisc中移除。假如发送失败了,skb会被重新排到队列中,netif_schedule()会被调用重试。


netif_schedule()引发一个软中断,软中断ksoftirqd引发 NET_TX_SOFTIRQ 运行,然后会net_tx_action() 运行。net_tx_action() 调用 qdisc_run() 为每个设备安排一个活跃的队列。
dev_hard_start_xmit()为网络设备调用 hard_start_xmit virtual() 方法。但是,他会先调用 dev_queue_xmit_init(),用来检查是否包处理函数已经注册了 ETH_P_ALL 协议(这对tcpdump有作用)。
设备驱动的 hard_start_xmit 函数将生成一个或者多个命令给网络设备用来调度缓冲区的发送。一会后,网络设备会回复成功。这会触发 sk_buff 的释放。假如 sk_buff 是从中断上下文中释放,使用的是dev_kfree_skb_irq()方法。它通过将skb放在 softnet_data完成队列中,延迟实际的释放直到下一个NET_TX_SOFTIRQ运行。这就避免了中断上下文的释放。


3.接收路径

3.1 第二层:链路层(以太层)

网络设备预先分配一些 sk_buffs,具体多少是由设备设置。通常,这些 sk_buffs 中的数据空间的地址被配置类似于DMA。设备中断处理函数会接受 sk_buff 并处理它。在NAPI之前,这是通过 netif_rx() 实现。在NAPI中,它包含以下两步:
1) 在中断处理函数中,设备驱动仅仅调用 netif_rx_schedule() 并从中断中返回。netif_rx_schedule() 将设备添加到sofnet_data的poll 列表并开启 NET_RX_SOFTIRQ 软中断。
2) ksoftirqd软中断运行 net_rx_action(),而 net_rx_action()会调用设备的 poll 方法。这个poll方法所作的是设备缓冲区管理,并为每一个sk_buff 调用 netif_receive_skb() ,需要的话会分配新的 sk_buff,最终调用netif_rx_complete()。


netif_receive_skb()会查找将 sk_buff 传递到上层的方法:
1) netpoll_rx() 被调用用来支持 Netpoll API
2) 为ETH_P_ALL协议调用包处理函数(同上面所述,ETH_P_ALL是为了tcpdump功能的实现)
3) 为入口队列调用handle_ing()
4) 为桥接调用handle_bridge()
5) 为VLAN调用handle_macvlan() 
6) 调用包处理函数注册包中对应的第三层协议
包处理函数会调用deliver_skb() ,deliver_skb()调用协议函数用来处理包。


3.2 第三层:网络层(IPv4,ARP)

    3.2.1 ARP

ARP包由 arp_rcv() 函数处理,arp_rcv()处理ARP信息,并把它存到邻居缓存,如果需要的话它会发送一个回复。在这之后,会为这个回复分配一个新的sk_buff(有着新的数据空间)。


    3.2.2 IPv4

IPv4数据包是由 ip_rcv()函数处理。它会分析头部,检查有效性,并发送一个ICMP回复或者错误信息。它还会使用 ip_route_input()查询目标地址。目标地址的 input 函数会调用 sk_buff。
> 假如目标地址是多地址,那么ip_mr_input()方法被调用。这个包可能会调用ip_mr_forward() 推送,也可能直接使用ip_local_delivery()局部交付。
> 假如目标地址是不同的,而我们有路由,那么ip_forward() 方法会被调用,它直接调用邻居的输出方法。

> 假如这台机器就是包的目标地址,那么ip_local_deliver() 方法会被调用,数据段在这被收集。


ip_local_deliver() 在第一次连接时使用 raw_local_deliver() 交付一些原始套接字。然后他会调用数据报中的协议对应的第四层协议处理函数。即使当前有原始套接字存在第四层协议也会被调用。
从始至终,xfrm4_policy_check 调用都包含在其中,用来支持IPSec。

3.3 第四层:传输层(TCP)

针对TCP的网络协议处理函数是tcp_v4_rcv()。这里的大多数代码都是用来处理TCP协议的,比如:建立连接、进行流量控制等。
一个TCP包包括对前面发送包的一个确认,它可能会触发后面的包的发送(tcp_data_snd_check()) 或者确认 (tcp_ack_snd_check())。
将接到的数据包传递到上层在 tcp_rcv_established() 和 tcp_data_queue() 中实现。这些函数维持TCP连接的无序 out_of_order_queue 以及套接字的接受队列 sk_receive_queue 和等待队列 sk_async_wait_queue。假如一个用户进程已经在等待数据的接受,会立刻使用 skb_copy_datagram_iovec() 将数据拷贝到用户空间。否则,这个 sk_buff 会被追加到队列末尾并在之后被拷贝到用户空间。
最后,接受函数会调用套接字的 sk_data_ready 方法用来通知数据可用,这会唤醒等待的进程。


3.4 第五层:会议层(Socket和文件)

有三个系统调用可以从网络接收数据:
> read (从文件描述符获取存储数据)
> recvfrom (从套接字获取存储数据)

> recvmsg (从套接字获取综合信息)


这三种系统调用最终都会调用 __sock_recvmsg(),__sock_recvmsg() 会使用 security_sock_recvmsg() 来检查权限然后使用套接字的 recvmsg 方法请求传递消息到下一层。这通常是使用sock_common_recvmsg()调用recvmsg方法完成的。

__sock_recvmsg() 要么使用 skb_copy_datagram_iovec() 从套接字队列中拷贝数据,要么使用 sk_wait_data() 等待数据到达(使用这个方法会阻塞直到被第四层进程唤醒,上面有提到)。



作者注:翻译真的很费脑子,文中有翻译不到之处还望大家允以之处纠正。博客中添加链接也很费时间,拜托转载请注上转载地址



你可能感兴趣的:(Linux内核网络部分控制流)