一点声明
原文链接: http://www.ecsl.cs.sunysb.edu/elibrary/linux/network/LinuxKernel.pdf


译者注: 原文写于2003年,文中描述的不少内容已经发生了改变,在不影响愿意的情况下,我擅自增删了一些内容.

翻译过程中找到的好资料:

  • How SKBs Work
  • Evaluation of TCP retransmission delays
  • Congestion Control in Linux TCP
  • Anatomy of the Linux networking stack — From sockets to device drivers
  • Guide to IP Layer Network Administration with Linux
  • Linux内核源码剖析 —TCP/IP实现
  • Understand Linux Network Kernel
  • TCP/IP Illustrated, Vol. 2
  • RFC793, RFC1122, RFC1323, RFC2001, RFC2018, RFC2581

translated by ripwu, 个人主页: http://godorz.info


我们首先应该知道的是常用的数据结构,现在介绍数据在内存中存放的结构:sk_buff;

数据包在应用层称为 data,在TCP 层称为segment,在IP 层称为packet,在数据链路层称为frame。
Linux 内核中sk_buff{}结构来存放数据,在不同的层次会使用上面几种术语来称呼,但这没有什么太大区别,本质上都是sk_buff结构的内存存储。

sk_buff是一个动态的增减的内存,可以在数据部分添加数据也可以去除数据,这样非常的适合数据的报头的添加与删除。

如下图是sk_buff存放的关系:


 sk_buff:指向网络报文的指针,也有描述网络报文的变量

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 on */ 
    struct sock *sk;                 /* Socket we are owned by */ 
    struct timeval stamp;          /* Time we arrived */ 
    struct net_device *dev;       /* Device we arrived on/are leaving by */ 

    /* Transport layer(TCP, UDP, ICMP, etc). header */ 
    union 
    { 
        struct tcphdr *th; 
        struct udphdr *uh; 
        struct icmphdr *icmph; 
        struct igmphdr *igmph; 
        struct iphdr *ipiph; 
        struct spxhdr *spxh; 
        unsigned char *raw; 
    } h; 

    /* Network layer header (IPv4, IPv6, arp, raw, etc).  */ 
    union 
    { 
        struct iphdr *iph; 
        struct ipv6hdr *ipv6h; 
        struct arphdr *arph; 
        struct ipxhdr *ipxh; 
        unsigned char *raw; 
    } nh; 

    /* Link layer header */ 
    union  
    {  
        struct ethhdr *ethernet; 
        unsigned char *raw; 
    } mac; 
以上三个union结构依次是传输层,网络层,链路层的头部结构指针。这些指
针在网络报文进入这一层时被赋值,其中raw是一个无结构的字符指针,用于
扩展的协议。
    struct dst_entry *dst; //此报文的路由,路由确定后赋此值

    char cb[48];   //用于在协议栈之间传递参数,参数内容的涵义由使用它的函数确定。

    unsigned int len;              /* Length of actual data */ 
    unsigned int data_len; 
    unsigned int csum;          /* Checksum */ 
    unsigned char __unused, /* Dead field, may be reused */ 
             cloned,                 /* head may be cloned (check refcnt to be sure). */ 
             pkt_type,              /* Packet class */ 
             ip_summed;          /* Driver fed us an IP checksum */ 
    __u32 priority;               /* Packet queueing priority */ 
    atomic_t users;              /* User count – see datagram.c,tcp.c */ 
    unsigned short protocol; /* Packet protocol from driver. */ 
    unsigned short security; /* Security level of packet */ 
    unsigned int truesize;     /* Buffer size */ 
  //真正的内存数据存放位置的指针
    unsigned char *head;    /* Head of buffer */ 
    unsigned char *data;     /* Data head pointer */ 
    unsigned char *tail;       /* Tail pointer */ 
    unsigned char *end;      /* End pointer */ 
}

布局(Layout)

一个sk_buff对应的内存抽象图示为:

为了实现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这里不做介绍,详细信息参考<Understand Linux Network Kernel>.

sk_buff的销毁稍微复杂,但原理很简单,这里也不做介绍.

then we will introdce the operation fun;

首先是skb_put,skb_push,skb_pull以及skb_reserve这几个最长用的操作data指针的函数。 

这里可以看到内核skb_XXX都还有一个__skb_XXX函数,这是因为前一个只是将后一个函数进行了一个包装,加了一些校验。 

先来看__skb_put函数。 
可以看到它只是将tail指针移动len个位置,然后len也相应的增加len个大小。 

Java代码  收藏代码
  1. static inline unsigned char *__skb_put(struct sk_buff *skb, unsigned int len)  
  2. {  
  3.     unsigned char *tmp = skb_tail_pointer(skb);  
  4.     SKB_LINEAR_ASSERT(skb);  
  5. ///改变相应的域。  
  6.     skb->tail += len;  
  7.     skb->len  += len;  
  8.     return tmp;  
  9. }  


然后是__skb_push,它是将data指针向上移动len个位置,对应的len肯定也是增加len大小。 

Java代码  收藏代码
  1. static inline unsigned char *__skb_push(struct sk_buff *skb, unsigned int len)  
  2. {  
  3.     skb->data -= len;  
  4.     skb->len  += len;  
  5.     return skb->data;  
  6. }  


剩下的两个就不贴代码了,都是很简单的函数,__skb_pull是将data指针向下移动len个位置,然后len减小len大小。__skb_reserve是将整个数据区,也就是data以及tail指针一起向下移动len大小。这个函数一般是用来对齐地址用的。 

看下面的图,描述了4个函数的操作: 


 

接着是skb的alloc函数。 

在内核中分配一个skb是在__alloc_skb中实现的,接下来我们就来看这个函数的具体实现。 

这个函数起始可以看作三部分,第一部分是从cache中分配内存,第二部分是初始化分配的skb的相关域。

还有一个要注意的就是这里__alloc_skb是被三个函数包装后才能直接使用的,我们只看前两个,一个是skb_alloc_skb,一个是alloc_skb_fclone函数,这两个函数传递进来的第三个参数,也就是fclone前一个是0,后一个是1. 

那么这个函数是什么意思呢,它和alloc_skb有什么区别的。 

这个函数可以叫做Fast SKB cloning函数,这个函数存在的主要原因是,以前我们每次skb_clone一个skb的时候,都是要调用kmem_cache_alloc从cache中alloc一块新的内存。而现在当我们拥有了fast clone之后,通过调用alloc_skb_fclone函数来分配一块大于sizeof(struct sk_buff)的内存,也就是在这次请求的skb的下方多申请了一些内存,然后返回的时候设置返回的skb的fclone标记为SKB_FCLONE_ORIG,而多申请的那块内存的sk_buff的fclone为SKB_FCLONE_UNAVAILABLE,这样当我们调用skb_clone克隆这个skb的时候看到fclone的标记就可以直接将skb的指针+1,而不需要从cache中取了。这样的话节省了一次内存存取,提高了clone的效率,不过调用flcone 一般都是我们确定接下来这个skb会被clone很多次。 

更详细的fclone的介绍可以看这里: 

http://lwn.net/Articles/140552/ 

这样我们先来看_alloc_skb,然后紧接着看skb_clone,这样就能更好的理解这些。 

这里fclone的多分配的内存部分,没太弄懂从那里多分配的,自己对内核的内存子系统还是不太熟悉。觉得应该是skbuff_fclone_cache中会自动多分配些内存。 



struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
   int fclone, int node)
{
struct kmem_cache *cache;
struct skb_shared_info *shinfo;
struct sk_buff *skb;
u8 *data;


///这里通过fclone的值来判断是要从fclone cache还是说从head cache中取。
cache = fclone ? skbuff_fclone_cache : skbuff_head_cache;


///首先是分配skb,也就是包头。
skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node);
if (!skb)
goto out;
///首先将size对齐,这里是按一级缓存的大小来对齐。
size = SKB_DATA_ALIGN(size);
///然后是数据区的大小,大小为size+ sizeof(struct skb_shared_info的大小。
data = kmalloc_node_track_caller(size + sizeof(struct skb_shared_info),
gfp_mask, node);
if (!data)
goto nodata;


///初始化相关域。
memset(skb, 0, offsetof(struct sk_buff, tail));
///这里truesize可以看到就是我们分配的整个skb+data的大小
skb->truesize = size + sizeof(struct sk_buff);
///users加一。
atomic_set(&skb->users, 1);
///一开始head和data是一样大的。
skb->head = data;
skb->data = data;
///设置tail指针
skb_reset_tail_pointer(skb);
///一开始tail也就是和data是相同的。
skb->end = skb->tail + size;
kmemcheck_annotate_bitfield(skb, flags1);
kmemcheck_annotate_bitfield(skb, flags2);
#ifdef NET_SKBUFF_DATA_USES_OFFSET
skb->mac_header = ~0U;
#endif


///初始化shinfo,这个我就不介绍了,前面的blog分析切片时,这个结构很详细的分析过了。
shinfo = skb_shinfo(skb);
atomic_set(&shinfo->dataref, 1);
shinfo->nr_frags  = 0;
shinfo->gso_size = 0;
shinfo->gso_segs = 0;
shinfo->gso_type = 0;
shinfo->ip6_frag_id = 0;
shinfo->tx_flags.flags = 0;
skb_frag_list_init(skb);
memset(&shinfo->hwtstamps, 0, sizeof(shinfo->hwtstamps));


///fclone为1,说明多分配了一块内存,因此需要设置对应的fclone域。
if (fclone) {
///可以看到多分配的内存刚好在当前的skb的下方。
struct sk_buff *child = skb + 1;
atomic_t *fclone_ref = (atomic_t *) (child + 1);


kmemcheck_annotate_bitfield(child, flags1);
kmemcheck_annotate_bitfield(child, flags2);
///设置标记。这里要注意,当前的skb和多分配的skb设置的fclone是不同的。
skb->fclone = SKB_FCLONE_ORIG;
atomic_set(fclone_ref, 1);


child->fclone = SKB_FCLONE_UNAVAILABLE;
}
out:
return skb;
nodata:
kmem_cache_free(cache, skb);
skb = NULL;
goto out;
}
下图就是alloc_skb之后的skb的指针的状态。这里忽略了fclone。 

 


然后我们来看skb_clone函数,clone的意思就是只复制skb而不复制data域。 

这里它会先判断将要被clone的skb的fclone段,以便与决定是否重新分配一块内存来保存skb。 

然后调用__skb_clone来初始化相关的域。 

Java代码  收藏代码
  1. struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t gfp_mask)  
  2. {  
  3.     struct sk_buff *n;  
  4.   
  5. ///n为skb紧跟着那块内存,这里如果skb是通过skb_fclone分配的,那么n就是一个skb。  
  6.     n = skb + 1;  
  7. ///skb和n的fclone都要符合要求,可以看到这里的值就是我们在__alloc_skb中设置的值。  
  8.     if (skb->fclone == SKB_FCLONE_ORIG &&  
  9.         n->fclone == SKB_FCLONE_UNAVAILABLE) {  
  10. ///到这里,就说明我们不需要alloc一个skb,直接取n就可以了,并且设置fclone的标记。并修改引用计数。  
  11.         atomic_t *fclone_ref = (atomic_t *) (n + 1);  
  12.         n->fclone = SKB_FCLONE_CLONE;  
  13.         atomic_inc(fclone_ref);  
  14.     } else {  
  15.   
  16. ///这里就需要从cache中取得一块内存。  
  17.         n = kmem_cache_alloc(skbuff_head_cache, gfp_mask);  
  18.         if (!n)  
  19.             return NULL;  
  20.   
  21.         kmemcheck_annotate_bitfield(n, flags1);  
  22.         kmemcheck_annotate_bitfield(n, flags2);  
  23. ///设置新的skb的fclone域。这里我们新建的skb,没有被fclone的都是这个标记。  
  24.         n->fclone = SKB_FCLONE_UNAVAILABLE;  
  25.     }  
  26.   
  27.     return __skb_clone(n, skb);  
  28. }  


这里__skb_clone就不介绍了,函数就是将要被clone的skb的域赋值给clone的skb。 

下图就是skb_clone之后的两个skb的结构图: 


当一个skb被clone之后,这个skb的数据区是不能被修改的,这就意为着,我们存取数据不需要任何锁。可是有时我们需要修改数据区,这个时候会有两个选择,一个是我们只修改linear段,也就是head和end之间的段,一种是我们还要修改切片数据,也就是skb_shared_info. 

这样就有两个函数供我们选择,第一个是pskb_copy,第二个是skb_copy. 

我们先来看pskb_copy,函数先alloc一个新的skb,然后调用skb_copy_from_linear_data来复制线性区的数据。 

Java代码  收藏代码
  1. struct sk_buff *pskb_copy(struct sk_buff *skb, gfp_t gfp_mask)  
  2. {  
  3.     /* 
  4.      *  Allocate the copy buffer 
  5.      */  
  6.     struct sk_buff *n;  
  7. #ifdef NET_SKBUFF_DATA_USES_OFFSET  
  8.     n = alloc_skb(skb->end, gfp_mask);  
  9. #else  
  10.     n = alloc_skb(skb->end - skb->head, gfp_mask);  
  11. #endif  
  12.     if (!n)  
  13.         goto out;  
  14.   
  15.     /* Set the data pointer */  
  16.     skb_reserve(n, skb->data - skb->head);  
  17.     /* Set the tail pointer and length */  
  18.     skb_put(n, skb_headlen(skb));  
  19. ///复制线性数据段。  
  20.     skb_copy_from_linear_data(skb, n->data, n->len);  
  21. ///更新相关域  
  22.     n->truesize += skb->data_len;  
  23.     n->data_len  = skb->data_len;  
  24.     n->len        = skb->len;  
  25.   
  26. ///下面只是复制切片数据的指针  
  27. if (skb_shinfo(skb)->nr_frags) {  
  28.         int i;  
  29.   
  30.         for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {  
  31.             skb_shinfo(n)->frags[i] = skb_shinfo(skb)->frags[i];  
  32.             get_page(skb_shinfo(n)->frags[i].page);  
  33.         }  
  34.         skb_shinfo(n)->nr_frags = i;  
  35.     }  
  36.   
  37. ...............................  
  38.     copy_skb_header(n, skb);  
  39. out:  
  40.     return n;  
  41. }  


然后是skb_copy,它是复制skb的所有数据段,包括切片数据: 

Java代码  收藏代码
  1. struct sk_buff *skb_copy(const struct sk_buff *skb, gfp_t gfp_mask)  
  2. {  
  3.     int headerlen = skb->data - skb->head;  
  4.     /* 
  5.      *  Allocate the copy buffer 
  6.      */  
  7. //先alloc一个新的skb  
  8.     struct sk_buff *n;  
  9. #ifdef NET_SKBUFF_DATA_USES_OFFSET  
  10.     n = alloc_skb(skb->end + skb->data_len, gfp_mask);  
  11. #else  
  12.     n = alloc_skb(skb->end - skb->head + skb->data_len, gfp_mask);  
  13. #endif  
  14.     if (!n)  
  15.         return NULL;  
  16.   
  17.     /* Set the data pointer */  
  18.     skb_reserve(n, headerlen);  
  19.     /* Set the tail pointer and length */  
  20.     skb_put(n, skb->len);  
  21. ///然后复制所有的数据。  
  22.     if (skb_copy_bits(skb, -headerlen, n->head, headerlen + skb->len))  
  23.         BUG();  
  24.   
  25.     copy_skb_header(n, skb);  
  26.     return n;  
  27. }  
下面这张图就表示了psb_copy和skb_copy调用后的内存模型,其中a是pskb_copy,b是skb_copy: 


 


最后来看skb的释放: 
这里主要是判断一个引用标记位users,将它减一,如果大于0则直接返回,否则释放skb。 

Java代码  收藏代码
  1. void kfree_skb(struct sk_buff *skb)  
  2. {  
  3.     if (unlikely(!skb))  
  4.         return;  
  5.     if (likely(atomic_read(&skb->users) == 1))  
  6.         smp_rmb();  
  7. ///减一,然后判断。  
  8.     else if (likely(!atomic_dec_and_test(&skb->users)))  
  9.         return;  
  10.     trace_kfree_skb(skb, __builtin_return_address(0));  
  11.     __kfree_skb(skb);  
  12. }  


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 译注: 这里实际是指”下半段”,与硬中断的”上半段”呼应,见<Linux内核设计与实现> 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:

  • 取代缺省的qdisc: tc qdisc add dev eth0 root pfifo limit 100
  • 查看统计信息: tc -s -d qdisc show dev eth0
  • 重新设置缺省qdisc: tc qdisc del dev eth0 root

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_input.c         –  处理到达packet
  • ip_output.c      –  处理发送packet
  • ip_forward.c    –  处理由本机路由的packet

还有一些模块负责处理 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_input.c           -  最大的一个文件,它处理网络中到达的packet
  • tcp_output.c        -  处理将发往网络的packet
  • tcp.c                         -  TCP代码
  • tcp_ipv4.c             -  IPv4具体的TCP代码
  • tcp_timer.c           – 定时器
  • tcp.h                         – 定义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_checksum_complete_user()计算校验码(checksum).如果检验码有误,packet被丢弃.
  • tcp_paws_discard()负责PAWS(Protection Against Wrapped Sequence Numbers).
  • STEP 1 – 检查packet序列号(sequence). 如果序列号不再接收端口中,接收模块(receiver)将 通过tcp_send_dupack()发回一个DUPACK.tcp_send_dupack()通过tcp_dsack_set()设置一个SACK,然后调用tcp_send_ack()进行发送.
  • STEP 2 – 检查RST位(th->rst),如果该位被置位,调用tcp_reset().
  • STEP 3 – 检查安全性(security)和优先级(precedence, RFC建议,但内核未实现)
  • STEP 4 – 检查SYN位,如果该位被置位,调用tcp_reset()…
  • 通过tcp_replace_ts_recent()估算RTT(RTTM).
  • STEP 5 -检查ACK位,如果该位被置位,调用tcp_ack().
  • STEP 6 -检查URG位,如果该位被置位,调用tcp_urg().
  • STEP 7 -通过tcp_data_queue()处理packet携带的数据.
  • 通过tcp_data_snd_check()检查是否有数据要发回.该函数将调用TCP发送模块( output sector)的tcp_write_xmit().
  • Finally, 通过tcp_ack_snd_check()检查是否有ACK要发回.ACK的发回有两种情况: 通过tcp_send_ack()直接发回; 或者通过tcp_send_delayed_ack()延时发回(译注: 这是TCP的延时确认(Delayed ACK)机制,目的为减少分组,可通过/proc/sys/net/ipv4/tcp_delack_min设置),被延时的ACK存储在tcp->ack.pending.

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.

拥塞控制状态有:

  • “Open” – 正常状态,无可疑事件,属于快速路径(fast path).
  • “Disorder” -当发送方检测到DACK(重复确认)或者SACK(selective ACK带选择的ACK, 见http://simohayha.iteye.com/blog/578744)时会进入该状态,它可以看成是Open状态,之所以会将”DisOrder”从”Open”中分离出来,主要是为了将某些处理从快速路径(fast path)移到慢速路径(slow one). (译注: 在这个状态,拥塞窗口不会被调整,每一次新的输入数据包都会触发一个新的端的传输)
  • “CWR”(拥塞窗口减小) – 某些拥塞通知事件(Congestion Notification event)将减小CWND(译注: 即congestion window拥塞窗口). 这些事件包括ECN(Explicit Congestion Notification显式拥塞通知,见http://en.wikipedia.org/wiki/Explicit_Congestion_Notification), ICMP远端抑制(ICMP source quench, 见http://en.wikipedia.org/wiki/ICMP_Source_Quench)和本机设备的拥塞.(译注:当接收到一个拥塞通知时,发送方并不立刻减小拥塞窗口,而是每隔一个新收到的ACK减小一段知道拥塞窗口大小减半为止,发送方在减小拥塞窗口的过程中不会有明显的重传. CWR状态可以被Recovery或Loss状态中断.)
  • “Recovery” – 该状态下CWND被减小,正在快速重传(fast-retransmitting). (译注: 默认情况下,进入Recovery状态的条件是三个连续的重复ACK.在Recovery状态期间,拥塞窗口的大小每隔一个新到的确认减少一个段,这和CWR状态类似.窗口减小直到刚进入Recovery状态时窗口大小的一半为止.拥塞窗口在恢复状态期间不增大,发送方重传那些被标记为丢失的段,或者根据包守恒原则在新数据上标记前向传输.发送方保持Recovery状态直到所有进入Recovery状态时正在发送的数据段都成功地被确认,之后该发送方恢复Open状态.重传超时有可能中断Recovery状态.)
  • “Loss” – 当RTO到期(RTO timeout)或者SACK拒绝(SACK reneging, 译注: 表示接收到的ACK的确认已经被先前的SACK确认过),发送方进入Loss状态.CWDN减小而后增大. (译注: Loss状态下所有正在发送的数据段标记为loss,拥塞窗口重置为1,发送方然后按慢启动算法增大拥塞窗口.Loss和Recovery状态的一个主要区别是: 在Loss状态,拥塞窗口在发送方重置为一个段后增大,而Recovery状态下拥塞窗口只能被减小. Loss状态不能被其他的状态中断,因此,发送方只能在所有Loss开始时正在传输的数据都成功得到确认ack后,才能回到Open状态,例如, 快速重传(fast retransmitting)不能在Loss状态期间被触发)

这些状态保存于tp->ca_state,对应值分别是TCP_CA_Open, TCP_CA_Disorder, TCP_CA_Cwr, TCP_CA_Recover 和 TCP_CA_Loss.

当收到ACK时状态不为Open,或者收到了奇怪的ACK(如SACK, DUPACK ECN ECE),内核会调用tcp_fastretrans_alert(). (译注: 此函数非常重要,它负责拥塞控制的处理,包括处理显式拥塞通知,判断SACK是否虚假,拥塞时记录的SND,NXT被确认时进行撤销,以及当前状态的处理等.详细见http://research.csc.ncsu.edu/netsrv/sites/default/files/hystart_techreport_2008.pdf)

fast path

在TCP处理过程中,receiver比较简单,因此它经常进入快速路径.

SACKs

Linux TCP协议栈完整实现了SACKS(带选择的ACK).SACK信息存储于tp->sack_ok field.

quickacks

某些情况下,receiver进入quickack模式,也就是说,延时确认机制(delayed ACK)被禁用.在quickack模式下,连接开始时,tc_rcv_sysent_state_process()将调用tcp_enter_quick_ack_mode().

Timeouts

定时器对TCP的正确工作起到了至关重要的作用.比如说,借助定时器,TCP可以判断packet是否在网络中丢失.

当发送一个packet时,其重传定时器(retransmit timer)会被设置. tcp_push_pending_frames()通过tcp_check_probe_timer()调度一个软中断(软中断有内核中非网络协议栈相关的代码处理).

定时器的超时事件会产生一个软中断,软中断通过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相关文件:

  • net/ipv4/udp.c

当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发往更低层.