一点声明
原文链接: http://www.ecsl.cs.sunysb.edu/elibrary/linux/Network/LinuxKernel.pdf
http://www.07net01.com/r_s/102569.html
详细请参考:
http://blog.csdn.net/qy532846454?viewmode=contents
http://blog.csdn.net/qy532846454?viewmode=list
http://blog.csdn.net/dog250
http://blog.chinaunix.net/uid/20498361/list/1.html?cid=61524
译者注: 原文写于2003年,文中描述的不少内容已经发生了改变,在不影响愿意的情况下,我擅自增删了一些内容.
翻译过程中找到的好资料:
translated by ripwu, 个人主页: http://godorz.info
sk_buff
结构体定义
struct sk_buff
{
/* These two members must be first. */
struct sk_buff * next; /* Next buffer in list */
struct sk_buff * prev; /* Previous buffer in list */
struct sk_buff_head * list; /* List we are title=sk_buff src="http://img.07net01.com/upload/images/2015/02/09/1425043090431142.png" width=361 height=311>为了实现sk_buff在各层间传输时的零拷贝,采用的是将数据放在内存最后,增减报文头时在内存前面操作,然后重新设置相关指针即可.下图演示了一个UDP数据包在发送时添加UDP报头和IP报头的过程:
sk_buff的实现方式是双向链表.和所有的链表一样,sk_buff含有指向下一元素的next指针和指向前一元素的prev指针,为了更快的知道某个sk_buff元素属于哪一链表,sk_buff还包含一个指向头结点的list指针.
struct sk_buff_head {
/* These two members must be first. */
struct sk_buff * next;
struct sk_buff * prev;
_ _u32 qlen;
spinlock_t lock;
};
如上所示,链表的头结点比较特殊,它包括了链表长度qlen和用于同步的自旋锁lock.
sk_buff元素组成的链表结构图如下:
创建和销毁
alloc_skb()负责sk_buff的创建,从sk_buff结构体的定义可以看到,当创建一个sk_buff时,总共需要申请两块内存,一块内存存储sk_buff本身(通过kmem_cache_alloc()),另一块内存存储sk_buff.data指向的数据区(通过kmalloc()).示意图如下:
其中, Padding的作用是字节对其. skb_shared_info这里不做介绍,详细信息参考.
sk_buff的销毁稍微复杂,但原理很简单,这里也不做介绍.
2. 网络层
本节介绍底层接收和处理packet(数据包)的流程.
接收packet的过程如下:
1. 网卡接收packet.
这些packet由DMA机制存储在最近使用的网卡的rx_ring中.rx_ring是一种工作于内核态的环形队列,其容量大小依赖于具体的网卡.有一种比较老的网卡,工作方式为轮询(PIO)模式: 由CPU将网卡数据读入内存态内存.
2. 网卡产生中断.
CPU开始执行网卡驱动的ISR代码,在2.4.19内核版本后,工作方式有些区别:
对于2.4.19版本之前(包括2.4.19)的内核:
如上图所示,中断处理程序将调用netif_rx()(net/dev/core.c). netif_rx()将接收到的数据压入被中断CPU的backlog队列,然后调度一个软中断(softirq, 一种内核行为,见 http://tldp.org/HOWTO/KernelAnalysis-HOWTO-5.html 和 http://www.netfilter.org/unreliable-guides/kernel-hacking/lk-hacking-guide.html 译注: 这里实际是指”下半段”,与硬中断的”上半段”呼应,见 update[2011-09-06] 译注错误: 软中断是bottom half的扩展,两者不应混为一谈. 操作系统之所以独立分开一个top half的概念,主要是因为中断是在CPU关中断(CLI)的情况下运行的,为了不丢失新到的中断信号,操作系统将中断服务程序一分为二,前者(top half)对其执行时间要求比较严格,一般立刻响应中断请求,为保证原子操作,它在关中断情况下执行,后者(bottom half)对时间要求没那么严格,通常在开中断下执行.但是,bottom half有一个致命的缺点,它只能在一个CPU上运行,也就是严格的串行,为了更充分的利用SMP,Linux2.4内核扩展了bottom half,成果便是所谓的”软中断(softirq)”机制,软中断最大的优点在于它可以在多CPU上运行,它在设计与实现中自始自终都贯彻了一个思想: “谁触发,谁执行(Who marks, Who runs),即触发软中断的那个CPU负责执行它所触发的软中断,而且每个 CPU 都由它自己的软中断触发与控制机制.), 由 软中断来负责packet的进一步处理(比如说: TCP/IP 协议的处理). backlog队列的 长度为300个packet大小(/proc/sys/net/core/netdev_max_backlog).
当backlog队列为满时,它转入throttle状态,在此状态等待数据清空,然后重新回到正常状态,正常状态下,backlog允许packet的入队操作.(netif_rx(), net/dev/core.c). 当backlog队列处于throttle状态时, netif_rx()将丢弃新来的packet. 我们可以通过/proc/net/softnet_stats来查看backlog队列的统计信息: 每一行对应一颗CPU,前两列分别是packet量和丢包量,第三列表示backlog队列有多少次进入了throttle状态.
对于2.4.19版本之后的内核:
如上图所示,内核采用的是NAPI新机制(见http://en.wikipedia.org/wiki/NAPI): 中断处理程序调用netif_rx_schedule()(include/linux/netdevice.h).该函数执行入队的对象不是packet,而是packet的一个引用(reference).(softnet_data>poll_list; include/linux/netdevice.h). 与旧内核机制相同的是,中断处理程序同样会调度一个软中断.为了向后兼容, 在NAPI机制中, backlog被当作像网卡一样可以处理所有到达packet的虚拟设备.内核开发者重写了netif_rx(),该函数将packet压入backlog之后,又将backlog压入CPU的poll_list链表.
3. 软中断执行net_rx_action()(net/core/dev.c).
在这一步中,新老内核处理方式同样有所不同:
对于2.4.19版本之前(包括2.4.19)的内核:
net_rx_action()为backlog队列中的所有packet调用**_rcv()函数(net/ipv4/ip_input.c),这里的**指代不同的packet类型,如ip, arp, bootp等.
对于NAPI:
CPU轮训其poll_list链表中的设备(注: NAPI中backlog为虚拟设备, process_backlog: net/core/dev.c),从设备的rx_ring环形队列中获得全部packet.CPU的轮询是通过 netif_receive_skb()(net/core/dev.c)调用ip_rcv()来完成的.
当一个packet被创建时,内核做了如下工作:
*IP packet由arp_constructor()创建.每个packet都包含dst field信息,dst提供了路由所需的目的地址,它对应一个output方法,对于IP packet而言,此方法为dev_queue_xmit().
*内核提供了多种排队原则(queueing disciplines, 简称qdisc).默认的排队原则使用FIFO队列,其缺省长度为100个packet大小.(ether_setup(): dev->tx_queue_len ; drivers/net/net_init.c),此长度可以通过带txqueuelen选项的ifconfig命令进行设置. 我们无法查看默认FIFO的统计信息,这里提供一个小技巧,通过tc 命令可以设置新的FIFO,以取代缺省的qdisc:
1.对从IP层(IP layer)传入的packet,调用dev_queue_xmit()(net/core/dev.c).该函数将packet压入外发网卡(output interface, 它由路由决定)相应的qdisc.如果网卡驱动正常, qdisc_restart()(net/sched/sch_generic.c)将处理qdisc中的所有packet;
2.调用hard_start_xmit().该方法实现于网卡驱动,它将packet压入tx_ring环形队列,然后网卡驱动将通知网卡有数据可发送.
3. 网卡发送packet并通知CPU. CPU调用net_tx_action()(net/core/dev.c)将packet压入completion_queue,然后调度一个负责释放packet内存的软中断.网卡与CPU之间的通信方式是硬件相关的,这里不作详细介绍.
3. 传输层
IP相关文件有:
还有一些模块负责处理 IP packet fragmentation(ip_fragment.c),IP options(ip_options.c), multicast(ipmr.c) 和 IP over IP (ipip.c)
下图描述了packet在IP层的流转路径. 当一个packet到达主机后,如前文所述种种流程,net_rx_action()将它转给ip_rcv()处理. 在经过first netfilter hook后, ip_rcv_finish()验证该packet的目的地是否就是本机.如果是的话, ip_rcv_finish()将该packet传给ip_local_delivery(), 然后由ip_local_delivery()转发给合适的传输层(transport layer)处理函数(tcp, udp, etc).
如果IP packet的目的地址是其他主机,那么处理该packet的当前主机就起了router的角色(这种场景在小型局域网上很常见). 如果主机允许转发packet(通过/proc/sys/net/ipv4/ip_forward查看或设置),那么该packet将由一系列复杂但高效的函数处理.
如果路由表(它是一种hash表)中存在packet的路由信息的话,该packet的行经路径通过ip_route_input()查找,否则,通过ip_route_input_slow(). ip_route_input_slow()调用fib(Forward informationbase, 路由表)族函数,这些函数定义在fib*.c文件中. FIB结构体非常复杂.
如果packet是多播(multicast)数据包,那么内核通过ip_route_input_mc()计算出packet发往的那些设备(该情况下,packet目的地址不变).
在计算出packet的路由信息之后,内核往IP pcaket插入新的目的地址,并将其所属设备信息插入对应的sk_buff结构. 然后,packet被转发(forwarding)函数(ip_forward() 和 ip_forward_finish()).
一个packet也可以从上层发往ip层(通过 TCP or UDP 传输). 处理该packet的第一个函数ip_queue_xmit(),它通过ip_output()将packet发往output part.
output part对packet做了最后的修改. dev_queue_transmit()将packet压入输出队列(output queue),然后通过q->disc_run()来调用网络调度机制(network scheduler mechanism).该指针指向不同的函数,这取决于内核采用的调度器(默认为FIFO类型,可以通过tc工具进行设置).
调度函数(qdisc_restart() 和 dev_queue_xmit_init())独立于IP层其他函数.
4. TCP
本节介绍Linux内核网络协议栈中最为复杂的TCP.
Introduction
TCP相关文件有:
TCP数据的处理如下两图,上图处理接收,下图处理发送.
TCP Input (mainly tcp_input.c }
TCP input在TCP实现中占了很大一部分.它处理TCP packet的接收,因为TCP实体(TCP entity, 也就是TCP协议栈)可以同时处于接收和发送两种状态,所以这两类代码混杂在了一起.
ip_local_deliver()将packet从IP层发往TCP层.它把packet传给ipproto->handler,在IPv4的实现中该handler就是tcp_v4_rcv().此函数进一步调用tcp_v4_do_rcv().
tcp_v4_do_rcv()会根据TCP连接(connection)的不同状态调用不同的函数.如果连接已建立(TCP_ESTABLISHED),它会调用tcp_rcv_established(),这是我们接下来会重点介绍的部分. 如果连接状态是TIME_WAIT,它会调用tcp_timewait_process().
对于其他的状态, tcp_v4_do_rcv()统一调用tcp_rcv_state_process().对于SYS_ENT状态的连接,该函数进一步调用tcp_rcv_sysent_state_process().
tcp_rcv_state_process()和tcp_timewait_process()必须初始化TCP结构体,这通过tcp_init_buffer_space()和tcp_init_metrics()完成. tcp_init_metrics()调用tcp_init_cwnd()来初始化其拥塞窗口(congestion window).
tcp_rcv_established()
tcp_rcv_established()有两条分支路径.我们首先介绍慢路径(slow path)分支,因为它简单清晰,另一分支留待后文介绍.
slow path
在RFC中,slow path只要有7步操作:
tcp_data_queue()
tcp_data_queue()负责处理packet数据.如果packet顺序到达(所有之前的packet已到达),它将把数据拷贝到tp->ucopy.iov (skb_copy_datagram_iovec(skb, 0, tp->ucopy.iov, chunk)).
如果packet并非顺序到达,那么它将通过tcp_ofo_queue()把packet压入乱序队列(out of order queue).
如果乱序队列满,根据RFC 2581文档4.2节, 协议栈将返回一个ACK(tp->ack.pingpong 被置0,tcp_ack_snd_check()被调用来返回ACK).
packet的到达引起各种反应,这由tcp_event_data_recv()处理. 它首先通过tcp_schedule_ack()调度一个ACK的发回,然后通过tcp_measure_rcv_mss()估算MSS(Maximum Segment Size).在特定的情况下(如: 慢启动), tcp_event_data_recv()选择调用tcp_incr_quickack(),以立刻返回ACK(即不使用延时确认机制).最后, tcp_event_data_recv()通过tcp_grow_window()增大通告窗口(advertised window, 译注: 其实也就是receive window,即接收窗口,更多参数设定见 http://140.116.72.80/~smallko/ns2/TCPparameter.htm).
tcp_data_queue()最终将检查FIN位,如果该位被置位,调用tcp_fin().
tcp_ack()
对发送模块(“sender”)而言,它每收到一个ACK,都会调用tcp_ack(),注意这一点不要和接收模块(“receiver”)通过tcp_send_ack()调用tcp_write_xmit()发送ACK的行为混淆了.
tcp_ack()首先检查ACK是否比先前收到的ACK较老还是较新,如果较老的话,内核通过执行 uninteresting_ack和old_ack无视之.
如果一切正常, tcp_ack()将通过tcp_ack_update_window()和/或tcp_update_wl()更新发送端的拥塞窗口.
如果ACK可疑(dubious) ,那么通过tcp_fastretrans_alert()进入快速重传(fast retransmit).
tcp_fast_retransmit()
tcp_fast_retransmit_alert()只由tcp_ack()在特定条件下调用.为了更好的理解这些特定条件,我们必须对
NewReno/SACK/FACK/ECN状态有深入的认识.注意这个TCP状态机本身没有什么关系,在这里连接状态几乎可以确定是TCP_ESTABLISHED.
拥塞控制状态有:
定时器的超时事件会产生一个软中断,软中断通过timer_bh()调用run_timer_list(). run_timer_list()将调用timer->function函数指针指向的tcp_wite_timer(). tcp_wite_timer()进而调用tcp_retransmit_timer(),最终, tcp_wite_timer()调用tcp_enter_loss(). tcp_enter_loss()将拥塞控制状态设为CA_Loss,然后由fastretransmit_alert()重传packet.
ECN
ECN(Explicit Congestion Notification,即显式拥塞通知)代码比较简单, 几乎所有的代码都在/include/net目录下的tcp_ecn.h文件中. tcp_ack()调用TCP_ECN_rcv_ecn_echo()来处理ECN packet.
TCP output
这部分代码(主要在tcp_output.c)负责packet(包括sender发送的数据packet和receiver发回的ACK)的发送.
5. UDP
本节简单介绍UDP,它很重要,但比起TCP,其拥塞控制却简单多了.
UDP相关文件:
当packet通过ip_local_delivery()从IP层到达时,它被传给udp_rcv()(该函数角色类似于TCP的tcp_v4_rcv()). udp_rcv()调用sock_put()将packet压入socket queue.packet的传递就到此结束了.内核然后调用inet_recvmsg()(通过recvmsg()系统调用), inet_recvmsg()调用udp_recvmsg(), 而udp_recvmsg()进一步调用skb_rcv_datagram(). skb_rcv_datagram()函数从队列中获得packet,然后据此packet填充用户态将读取的结构体.
当一个packet从用户态到达时(译注: 也就是说要发送出去),处理就更加简单了. inet_sendmsg()调用udp_sendmsg(), udp_sendmsg()通过从sk结构体获取的信息(这些信息在socket被创建和被绑定(bind调用)时被设置于sk中)填充UDP数据报(UDP datagram).
当UDP数据报填充完成之后,数据报被传给ip_build_xmit(), ip_build_xmit()通过ip_build_xmit_slow()创建IP packet.
IP packet创建完成以后, packet被传给ip_output(),正如4 - Network Layer 介绍的一样, ip_output()将packet发往更低层.