您的位置:首页 > 理论基础 > 数据结构算法

Linux TCP/IP 协议栈的关键数据结构Socket

2011-04-01 09:17 483 查看
struct sec_path *sp
这个变量被IPSec协议用于跟踪传输的信息。

5. Management Functions

有很多函数,通常都比较短小而且简单,内核用这些函数操作sk_buff的成员变量或者sk_buff
链表。图4会帮助我们理解其中几个重要的函数。我们首先来看分配和释放缓冲区的函数,然后是一些通过移动指针在缓冲区的头部或尾部预留空间的函数。
如果你看过include/linux/skbuff.h和net /core/skbuff.c中的函数,你会发现,基本上每个函数都有两个版本,名字分别是do_something和__do_something。通 常第一种函数是一个包装函数,它会在第二种函数的基础 上增加合法性检查或者锁。一般来说,类似__do_something的函数不能被直接调用(除非满足特定的条件,比如说锁)。那些违反这条规则而直接引 用这些函数的不良代码会最终被更正。
Figure 4. Before and after: (a)skb_put, (b)skb_push, (c)skb_pull, and (d)skb_reserve



5.1. Allocating memory: alloc_skb and dev_alloc_skb
alloc_skb是net/core/skbuff.c里面定义的,用于 分配缓冲区的函数。我们已经知道,数据缓冲区和缓冲区的描述结构(sk_buff结构)是两种不同的实体,这就意味着,在分配一个缓冲区时,需要分配两块 内存(一个是缓冲区,一个是缓冲区的描述结构 sk_buff)。
alloc_skb调用函数kmem_cache_alloc从缓存中获取一个sk_buff结构,并调用kmalloc分配缓冲区(如果有缓存的话,它同样从缓存中获取内存)。简化后的代码如下:
skb = kmem_cache_alloc(skbuff_head_cache, gfp_mask & ~_ _GFP_DMA);
... ...
size = SKB_DATA_ALIGN(size);
data = kmalloc(size + sizeof(struct skb_shared_info), gfp_mask);

在调用kmalloc前,size参数通过SKB_DATA_ALIGN宏强制对齐。在函数返回前,它会初始化结构中的一些变量,最后的结构如图5所示。在图5右边所示的内存块的底部,你能看到对齐操作所带来的填充区域。
Figure 5. alloc_skb function



dev_alloc_skb也是一个缓冲区分配函数,它主要被设备驱动使 用,通常用在中断上下文中。这是一个 alloc_skb函数的包装函数,它会在请求分配的大小上增加16字节的空间以优化缓冲区的读写效率,它的分配要求使用原子操作 (GFP_ATOMIC),这是因为它是在中断处理函数中被调用的。
static inline struct sk_buff *dev_alloc_skb(unsigned int length)
{
return _ _dev_alloc_skb(length, GFP_ATOMIC);
}
static inline struct sk_buff *_ _dev_alloc_skb(unsigned int length, int gfp_mask)
{
struct sk_buff *skb = alloc_skb(length + 16, gfp_mask);
if (likely(skb))
skb_reserve(skb, 16);
return skb;
}

如果没有体系架构相关的实现,缺省使用__dev_alloc_skb的实现。
5.2. Freeing memory: kfree_skb and dev_kfree_skb
这两个函数释放缓冲区,并把它返回给缓冲池(缓存)。kfree_skb可 以直接调用,也可以通过包装函数 dev_kfree_skb调用。后面这个函数一般被设备驱动使用,与之功能相反的函数是dev_alloc_skb。dev_kfree_skb仅是一 个简单的宏,它什么都不做,只简单地调用kfree_skb。这些函数只有在skb->users为1地情况下才释放内存(没有人引用这个结构)。 否则,它只是简单地减小
skb->users。如果缓冲区有三个引用者,那么只有第三次调用dev_kfree_skb或kfree_skb时才释放内存。
图6中的流程图显示了释放一个缓冲区所需要的步骤。当sk_buff释放后,dst_release同样会被调用以减小相关dst_entry数据结构的引用计数。
如果destructor被初始化过,相应的函数会在此时被调用.
在图5中,我们看到,一个简单的场景是:一个sk_buff结构与另一个内 存块相关,这个内存块里存储的是真正的数据。 当然,内存块底部的skb_shared_info数据结构可以包含指向其他分片的指针(参见图5)。如果存在分片,kfree_skb同样会释放这些分 片所占用的内存。最后,kfree_skb 把sk_buff结构返回给skbuff_head_cache缓存。
5.3. Data reservation and alignment: skb_reserve, skb_put, skb_push, and skb_pull
skb_reserve可以在缓冲区的头部预留一定的空间,它通常被用来在 缓冲区中插入协议头或者在某个边界上对齐。这 个函数改变data和tail指针,而data和tail指针分别指向负载的开头和结尾,图4(d)展示了调用skb_reserve(skb,n)的结 果。这个函数通常在分配缓冲区之后就调用,此时的
data和tail指针还是指向同一个地方。
如果你查看某个以太网设备驱动的收包函数(例如,drivers/net/3c59x.c中的vortex_rx), 你就会发现它在分配缓冲区之后,在向缓冲区中填充数据之前,会调用下面的函数:
skb_reserve(skb, 2);     /* Align IP on 16 byte boundaries */

Figure 6. kfree_skb function



由于以太网帧的头部长度是14个八位组,这个函数把缓冲区的头部指针向后移动了2个字节。这样,紧跟在以太网头部之后的IP头部在缓冲区中存储时就可以在16字节的边界上对齐。如图7所示。
Figure 7. (a) before skb_reserve, (b) after skb_reserve, and (c) after copying the frame on the buffer



图8展示了一个在发送过程中使用skb_reserve的例子。
Figure 8. Buffer that is filled in while traversing the stack from the TCP layer down to the link layer



当TCP发送数据时,它根据一些条件分配一个缓冲区(比如,TCP的最大分段长度(mss),是否支持散读散写I/O等

TCP在缓冲区的头部预留足够的空间(用skb_reserve)用 于填充各层的头部(如TCP,IP,链路层等)。MAX_TCP_HEADER参数是各层头部长度的总和,它考虑了最坏的情况:由于tcp层不知道将要用 哪个接口发送包,它为每一层预留了最大的头部长度。它甚至考虑了出现多个IP头的可能性(如果内核编译支持IP over IP, 我们就会遇到多个IP头的情况)。

把TCP的负载拷贝到缓冲区。需要注意的是:图8只是一个例子。TCP的负载可能会被组织成其他形式。例如它可以存储到分片中。

TCP层添加自己的头部。

TCP层把缓冲区传递给IP层,IP层同样添加自己的头部。

IP层把缓冲区传递给邻居层,邻居层添加链路层头部。

当缓冲区在协议栈中向下层传递时,每一层都把skb->data指针向下移动,然后拷贝自己的头部,同时更新skb->len。这些操作都使用图4中所展示的函数完成。
skb_reserve函数并没有把数据移出或移入缓冲区,它只是简单地更新了缓冲区的两个指针,这两个指针在图4(d)中有描述。
static inline void skb_reserve(struct sk_buff *skb, unsigned int len)
{
skb->data+=len;
skb->tail+=len;
}

skb_push在缓冲区的开头加入一块数据,而skb_put在缓冲区的末尾加入一块数据。与skb_reserve
类似,这些函数都不会真的往缓冲区中添加数据,相反,它们只是移动缓冲区的头指针和尾指针。数据由其他函数拷贝到缓冲区中。skb_pull通过把head指针往前移来在缓冲区的头部删除一块数据。图4展示了这些函数是如何工作的。
2.1.5.4. The skb_shared_info structure and the skb_shinfo function
如图5所示,在缓冲区数据的末尾,有一个数据结构skb_shared_info,它保存了数据块的附加信息。这个数据结构紧跟在end指针所指的地址之后(end指针指示数据的末尾)。下面是这个结构的定义:
struct skb_shared_info {
atomic_t         dataref;
unsigned int     nr_frags;
unsigned short   tso_size;
unsigned short   tso_seqs;
struct sk_buff   *frag_list;
skb_frag_t       frags[MAX_SKB_FRAGS];
};

dataref表示数据块的“用户”数,这个值在下一节(克隆和拷贝缓冲 区)中有描述。nf_frags, frag_list和frags用于存储IP分片。skb_is_nonlinear函数用于测试一个缓冲区是否是分片的,而skb_linearize 可以把分片组合成一个单一的缓冲区。组合分片涉及到数据拷贝,它将严重影响系统性能。
有些网卡硬件可以完成一些传统上由CPU完成的任务。最常见的例子就是计算 L3和L4校验和。有些网卡甚至可以维护L4 协议的状态机。在下面的例子中,我们主要讨论TCP段卸载TCP segmentation offload,这些网卡实现了TCP层的一部分功能。tso_size和tso_seqs就在这种情况下使用。
需要注意的是:sk_buff中没有指向skb_shared_info结构的指针。如果要访问这个结构, 就需要使用skb_info宏,这个宏简单地返回end指针:
#define skb_shinfo(SKB)     ((struct skb_shared_info *)((SKB)->end))

下面的语句展示了如何使用这个宏来增加结构中的某个成员变量的值:
skb_shinfo(skb)->dataref++;

2.1.5.5. Cloning and copying buffers
如果一个缓冲区需要被不同的用户独立地操作,而这些用户可能会修改sk_buff中某些变量的值(比如h和nh值),内核没有必要为每个用户复制一份完整的sk_buff以及相应的缓冲区。相反,为提高性能,内核克隆一个缓冲区。克隆过程只复制sk_buff结构, 同时修改缓冲区的引用计数以避免共享的数据被提前释放。克隆缓冲区使用skb_clone函数。
一个使用包克隆的场景是:一个接收包的过程需要把这个包传递给多个接收者,例如包处理函数或者一个或多个网络模块。
被克隆的sk_buff不会放在任何链表中,同时也不会有到socket的 引用。原始的和克隆的sk_buff中的 skb->cloned值都被置为1。克隆包的skb->users值被置为1,这样,在释放时,可以先释放sk_buff结构。同时,缓冲 区的引用计数(dataref)增加1(因为有多个sk_buff结构指向它)。图9展示了克隆缓冲区的例子。
Figure 9. skb_clone function



skb_cloned函数可以用来测试skb的克隆状态。

图9展示了一个分片缓冲区的例子,这个缓冲区的一些数据保存在分片结构数组frags中。
skb_share_check用于检查引用计数skb->users,如果users变量表明skb是被共享的, 则克隆一个新的sk_buff。
如果一个缓冲区被克隆了,这个缓冲区的内容就不能被修改。这就意味着,访问 数据的函数没有必要加锁。因此,当一个函数不仅要修改sk_buff,而且要修改缓冲区内容时, 就需要同时复制缓冲区。在这种情况下,程序员有两个选择。如果他知道所修改的数据在skb->start和skb->end
之间,他可以使用pskb_copy来复制这部分数据。如果他同时需要修改分片中的数据,他就必须使用skb_copy。图10展示了pskb_copy和skb_copy的结果。skb_shared_info结构也可以包含一个
sk_buff的链表(链表名称是frag_list)。这个链表在pskb_copy和skb_copy中的处理方式与frags数组的处理方式相同(图10忽略了frag_list)。
Figure 10. (a) pskb_copy function and (b) skb_copy function



在决定克隆或复制一个缓冲区时,子系统的程序员不能预测其他内核组件(其他 子系统)是否需要使用缓冲区里的原始数据。内核是模块化的,它的状态变化是不可预测的,因此,每个子系统都不知道其他子系统是如何操作缓冲区的。因此,内 核程序员需要记录它们对缓冲区的修改,并且在修改缓冲区前,复制一个新的缓冲区,以避免其他子系统在使用缓冲区的原始数据时出现错误。
2.1.5.6. List management functions
这些函数管理sk_buff的链表(也被称作队列)。在<include/linux/skbuff.h>和<net/core/skbuff.c>中有函数完整列表。以下是一些经常使用的函数:
skb_queue_head_init
初始化sk_buff_head结构,创建一个空队列。

skb_queue_head, skb_queue_tail
把一个缓冲区加入队列的头部或尾部。

skb_dequeue, skb_dequeue_tail
从队列的头部或尾部取下一个缓冲区。第一个函数的名字应该是skb_dequeue_head,以保持和其他函数的名字风格一致。

skb_queue_purge
清空一个队列。

skb_queue_walk
Runs a loop on each element of a queue in turn.

这些函数都是原子操作,它们必须先获取sk_buff_head中的自旋锁,然后才能访问队列中的元素。否则,它们有可能被其他异步的添加或删除操作打断,比如在定时器中调用的函数,这将导致链表出现错误而使得系统崩溃。
因此,每个函数的实现都采用下面这种形式:
static inline function_name ( parameter_list )
{
unsigned long flags;
spin_lock_irqsave(...);
_ _ _function_name ( parameter_list )
spin_unlock_irqrestore(...);
}

这些函数先获取锁,然后调用一个以两个下划线开头的同名函数(这个函数做具体的操作,而且不需要锁),然后释放锁。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: