因为太喜欢这篇文章,所以有保存在自己blog里的冲动,同时也对文章代码的相关部分加上了颜色,给阅读时黑压压的一片带来一些亮色,也减少了阅读时的单调情愫.
sk_buff结构可能是linux网络代码中最重要的数据结构,它表示接收或发送数据包的包头信息。它在<include/linux/skbuff.h>中定义,并包含很多成员变量供网络代码中的各子系统使用。 这个结构在linux内核的发展过程中改动过很多次,或者是增加新的选项,或者是重新组织已存在的成员变量以使得成员变量的布局更加清晰。它的成员变量可以大致分为以下几类:
这个结构被不同的网络层(MAC或者其他二层链路协议,三层的IP,四层的TCP或UDP等)使用,并且其中的成员变量在结构从一层向另一层传递时改变。L4向L3传递前会添加一个L4的头部,同样,L3向L2传递前,会添加一个L3的头部。添加头部比在不同层之间拷贝数据的效率更高。由于在缓冲区的头部添加数据意味着要修改指向缓冲区的指针,这是个复杂的操作,所以内核提供了一个函数skb_reserve(在后面的章节中描述)来完成这个功能。协议栈中的每一层在往下一层传递缓冲区前,第一件事就是调用skb_reserve在缓冲区的头部给协议头预留一定的空间。 skb_reserve同样被设备驱动使用来对齐接收到包的包头。如果缓冲区向上层协议传递,旧的协议层的头部信息就没什么用了。例如,L2的头部只有在网络驱动处理L2的协议时有用,L3是不会关心它的信息的。但是,内核并没有把L2的头部从缓冲区中删除,而是把有效荷载的指针指向L3的头部,这样做,可以节省CPU时间。 1. 网络参数和内核数据结构就像你在浏览TCP/IP规范或者配置内核时所看到的一样,网络代码提供了很多有用的功能,但是这些功能并不是必须的, 比如说,防火墙,多播,还有其他一些功能。大部分的功能都需要在内核数据结构中添加自己的成员变量。因此,sk_buff里面包含了很多像#ifdef这样的预编译指令。例如,在sk_buff结构的最后,你可以找到: struct sk_buff { 它表明,tc_index只有在编译时定义了CONFIG_NET_SCHED符号才有效。这个符号可以通过选择特定的编译选项来定义(例如:"Device Drivers Networking supportNetworking options QoS and/or fair queueing")。这些编译选项可以由管理员通过make config来选择,或者通过一些自动安装工具来选择。 前面的例子有两个嵌套的选项:CONFIG_NET_CLS_ACT(包分类器)只有在选择支持“QoS and/or fair queueing”时才能生效。 顺便提一下,QoS选项不能被编译成内核模块。原因就是,内核编译之后,由某个选项所控制的数据结构是不能动态变化的。一般来说,如果某个选项会修改内核数据结构(比如说,在sk_buff里面增加一个项tc_index),那么,包含这个选项的组件就不能被编译成内核模块。 你可能经常需要查找是哪个make config编译选项或者变种定义了某个#ifdef标记,以便理解内核中包含的某段代码。在2.6内核中,最快的,查找它们之间关联关系的方法, 就是查找分布在内核源代码树中的kconfig文件中是否定义了相应的符号(每个目录都有一个这样的文件)。在 2. Layout Fields 有些sk_buff成员变量的作用是方便查找或者是连接数据结构本身。内核可以把sk_buff组织成一个双向链表。当然,这个链表的结构要比常见的双向链表的结构复杂一点。 就像任何一个双向链表一样,sk_buff中有两个指针next和prev,其中,next指向下一个节点,而
qlen代表链表元素的个数。lock用于防止对链表的并发访问。 sk_buff和sk_buff_head的前两个元素是一样的:next和prev指针。这使得它们可以放到同一个链表中,尽管sk_buff_head要比sk_buff小得多。另外,相同的函数可以同样应用于sk_buff和sk_buff_head。 为了使这个数据结构更灵活,每个sk_buff结构都包含一个指向sk_buff_head的指针。这个指针的名字是list。图1会帮助你理解它们之间的关系。 Figure 1. List of sk_buff elements其他有趣的成员变量如下: struct sock *sk unsigned int len unsigned int data_len unsigned int mac_len atomic_t users unsigned int truesize struct sk_buff *alloc_skb(unsigned int size,int gfp_mask)当skb->len变化时,这个变量也会变化。 unsigned char *head Figure 2. head/end versus data/tail pointersvoid (*destructor)(...) 3. General Fields本节描述sk_buff的主要成员变量,这些成员变量与特定的内核功能无关: struct timeval stamp struct net_device *dev static int vortex_rx(struct net_device *dev) 当一个包被发送时,这个变量代表将要发送这个包的设备。在发送网络包时设置这个值的代码要比接收网络包时设置这个值的代码复杂。有些网络功能可以把多个网络设备组成一个虚拟的网络设备(也就是说,这些设备没有和物理设备直接关联),并由一个虚拟网络设备驱动管理。当虚拟设备被使用时,dev指针指向虚拟设备的net_device结构。而虚拟设备驱动会在一组设备中选择一个设备并把dev指针修改为这个设备的net_device结构。因此,在某些情况下, 指向传输设备的指针会在包处理过程中被改变。 struct net_device *input_dev struct net_device *real_dev union {...} h 这些是指向TCP/IP各层协议头的指针:h指向L4,nh指向L3,mac指向L2。每个指针的类型都是一个联合,包含多个数据结构,每一个数据结构都表示内核在这一层可以解析的协议。例如,h是一个包含内核所能解析的L4协议的数据结构的联合。每一个联合都有一个raw变量用于初始化,后续的访问都是通过协议相关的变量进行的。 当接收一个包时,处理n层协议头的函数从n-1层收到一个缓冲区,它的skb->data指向层n协议的头。处理 n层协议的函数把本层的指针(例如,L3对应的是skb->nh指针)初始化为skb->data,因为这个指针的值会在处理下一层协议时改 变(skb->data将被初始化成缓冲区里的其他地址)。在处理n层协议的函数结束时,在把包传递给n+1层的处理函数前,它会把skb- >data指针指向n层协议头的末尾,这正好是n+1层协议的协议头(参见图3)。 发送包的过程与此相反,但是由于要为每一层添加新的协议头,这个过程要比接收包的过程复杂。 Figure 3. Header's pointer initializations while moving from layer two to layer threestruct dst_entry dst char cb[40] struct tcp_skb_cb { 下面这个宏被TCP代码用来访问cb变量。在这个宏里面,有一个简单的类型转换: #define TCP_SKB_CB(_ _skb) ((struct tcp_skb_cb *)&((_ _skb)->cb[0])) 下面的例子是TCP子系统在收到一个分段时填充相关数据结构的代码: int tcp_v4_rcv(struct sk_buff *skb) 如果想要了解cb中的参数是如何被取出的,可以查看net/ipv4/tcp_output.c中的tcp_transmit_skb函数。这个函数被TCP用于向IP层发送一个分段。 unsigned int csum unsigned char cloned unsigned char pkt_type PACKET_HOST .PACKET_MULTICAST 包的目的地址是一个广播地址,而这个广播地址也是收到这个包的网络设备的广播地址。 PACKET_OTHERHOST PACKET_OUTGOING PACKET_LOOPBACK PACKET_FASTROUTE
_ _u32 priority unsigned short protocol unsigned short security 4. Feature-Specific Fieldslinux内核是模块化的,你可以选择包含或者删除某些功能。因此,sk_buff结构里面的一些成员变量只有在内核选择支持某些功能时才有效,比如防火墙(netfilter)或者qos: unsigned long nfmark union {...} private _ _u32 tc_index struct sec_path *sp 5. Management Functions有很多函数,通常都比较短小而且简单,内核用这些函数操作sk_buff的成员变量或者sk_buff 如果你看过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_reserve5.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); 在调用kmalloc前,size参数通过SKB_DATA_ALIGN宏强制对齐。在函数返回前,它会初始化结构中的一些变量,最后的结构如图5所示。在图5右边所示的内存块的底部,你能看到对齐操作所带来的填充区域。 Figure 5. alloc_skb functiondev_alloc_skb也是一个缓冲区分配函数,它主要被设备驱动使用,通常用在中断上下文中。这是一个 alloc_skb函数的包装函数,它会在请求分配的大小上增加16字节的空间以优化缓冲区的读写效率,它的分配要求使用原子操作 (GFP_ATOMIC),这是因为它是在中断处理函数中被调用的。 static inline struct sk_buff *dev_alloc_skb(unsigned int length) 如果没有体系架构相关的实现,缺省使用__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地情况下才释放内存(没有人引用这个结构)。 否则,它只是简单地减小 图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)的结果。这个函数通常在分配缓冲区之后就调用,此时的 如果你查看某个以太网设备驱动的收包函数(例如,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
当缓冲区在协议栈中向下层传递时,每一层都把skb->data指针向下移动,然后拷贝自己的头部,同时更新skb->len。这些操作都使用图4中所展示的函数完成。 skb_reserve函数并没有把数据移出或移入缓冲区,它只是简单地更新了缓冲区的两个指针,这两个指针在图4(d)中有描述。 static inline void skb_reserve(struct sk_buff *skb, unsigned int len) skb_push在缓冲区的开头加入一块数据,而skb_put在缓冲区的末尾加入一块数据。与skb_reserve 2.1.5.4. The skb_shared_info structure and the skb_shinfo function 如图5所示,在缓冲区数据的末尾,有一个数据结构skb_shared_info,它保存了数据块的附加信息。这个数据结构紧跟在end指针所指的地址之后(end指针指示数据的末尾)。下面是这个结构的定义: struct skb_shared_info { 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 functionskb_cloned函数可以用来测试skb的克隆状态。 图9展示了一个分片缓冲区的例子,这个缓冲区的一些数据保存在分片结构数组frags中。 skb_share_check用于检查引用计数skb->users,如果users变量表明skb是被共享的, 则克隆一个新的sk_buff。 如果一个缓冲区被克隆了,这个缓冲区的内容就不能被修改。这就意味着,访问数据的函数没有必要加锁。因此,当一个函数不仅要修改sk_buff,而且要修改缓冲区内容时, 就需要同时复制缓冲区。在这种情况下,程序员有两个选择。如果他知道所修改的数据在skb->start和skb->end 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 skb_queue_head, skb_queue_tail skb_dequeue, skb_dequeue_tail skb_queue_purge skb_queue_walk 这些函数都是原子操作,它们必须先获取sk_buff_head中的自旋锁,然后才能访问队列中的元素。否则,它们有可能被其他异步的添加或删除操作打断,比如在定时器中调用的函数,这将导致链表出现错误而使得系统崩溃。 因此,每个函数的实现都采用下面这种形式: static inline function_name ( parameter_list ) 这些函数先获取锁,然后调用一个以两个下划线开头的同名函数(这个函数做具体的操作,而且不需要锁),然后释放锁。 |