织梦CMS - 轻松建站从此开始!

罗索

Linux TCP/IP 协议栈的关键数据结构Socket Buffer(sk_buff )

落鹤生 发布于 2011-02-28 13:41 点击:次 
因为太喜欢这篇文章,所以有保存在自己blog里的冲动,同时也对文章代码的相关部分加上了颜色,给阅读时黑压压的一片带来一些亮色,也减少了阅读时的单调情愫.
TAG:

sk_buff结构可能是linux网络代码中最重要的数据结构,它表示接收或发送数据包的包头信息。它在<include/linux/skbuff.h>中定义,并包含很多成员变量供网络代码中的各子系统使用。

这个结构在linux内核的发展过程中改动过很多次,或者是增加新的选项,或者是重新组织已存在的成员变量以使得成员变量的布局更加清晰。它的成员变量可以大致分为以下几类:

  • Layout 布局

  • General 通用

  • Feature-specific功能相关

  • Management functions管理函数

这个结构被不同的网络层(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 {
 ... ... ...
        #ifdef CONFIG_NET_SCHED
             _ _u32     tc_index;
        #ifdef CONFIG_NET_CLS_ACT
             _ _u32     tc_verd;
             _ _u32     tc_classid;
        #endif
#endif
}

它表明,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.4内核中,你需要查看Documentation/Configure.help文件。

2. Layout Fields

有些sk_buff成员变量的作用是方便查找或者是连接数据结构本身。内核可以把sk_buff组织成一个双向链表。当然,这个链表的结构要比常见的双向链表的结构复杂一点。

就像任何一个双向链表一样,sk_buff中有两个指针next和prev,其中,next指向下一个节点,而 
prev指向上一个节点。但是,这个链表还有另一个需求:每个sk_buff结构都必须能够很快找到链表头节点。为了满足这个需求,在第一个节点前面会插入另一个结构sk_buff_head,这是一个辅助节点,它的定义如下:

  1. struct sk_buff_head { 
  2.    /* These two members must be first. */ 
  3.     struct sk_buff     * next; 
  4.     struct sk_buff     * prev; 
  5.     _ _u32         qlen; 
  6.     spinlock_t     lock; 
  7. }; 

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
这 是一个指向拥有这个sk_buff的sock结构的指针。这个指针在网络包由本机发出或者由本机进程接收时有效,因为插口相关的信息被L4(TCP或 UDP)或者用户空间程序使用。如果sk_buff只在转发中使用(这意味着,源地址和目的地址都不是本机地址),这个指针是NULL。

unsigned int len
这是缓冲区中数据部分的长度。它包括主缓冲区中的数据长度(data指针指向它)和分片中的数据长度。它的值在缓冲区从一个层向另一个层传递时改变,因为往上层传递,旧的头部就没有用了,而往下层传递,需要添加本层的头部。len同样包含了协议头的长度。

unsigned int data_len
和len不同,data_len只计算分片中数据的长度。

unsigned int mac_len
这是mac头的长度。

atomic_t users
这 是一个引用计数,用于计算有多少实体引用了这个sk_buff缓冲区。它的主要用途是防止释放sk_buff后,还有其他实体引用这个sk_buff。因 此,每个引用这个缓冲区的实体都必须在适当的时候增加或减小这个变量。这个计数器只保护sk_buff结构本身,而缓冲区的数据部分由类似的计数器 (dataref)来保护.
有时可以用atomic_inc和atomic_dec函数来直接增加或减小users,但是,通常还是使用函数skb_get和kfree_skb来操作这个变量。

unsigned int truesize
这是缓冲区的总长度,包括sk_buff结构和数据部分。如果申请一个len字节的缓冲区,alloc_skb函数会把它初始化成len+sizeof(sk_buff)。

  1. struct sk_buff *alloc_skb(unsigned int size,int gfp_mask) 
  2. ... ... ... 
  3. skb->truesize = size + sizeof(struct sk_buff); 
  4. ... ... ... 

当skb->len变化时,这个变量也会变化。 

  1. unsigned char *head 
  2. unsigned char *end 
  3. unsigned char *data 
  4. unsigned char *tail 

它 们表示缓冲区和数据部分的边界。在每一层申请缓冲区时,它会分配比协议头或协议数据大的空间。head和end指向缓冲区的头部和尾部,而data和 tail指向实际数据的头部和尾部,参见图2。每一层会在head和data之间填充协议头,或者在tail和end之间添加新的协议数据。图2中右边数 据部分会在尾部包含一个附加的头部。

Figure 2. head/end versus data/tail pointers

 

void (*destructor)(...)
这 个函数指针可以初始化成一个在缓冲区释放时完成某些动作的函数。如果缓冲区不属于一个socket,这个函数指针通常是不会被赋值的。如果缓冲区属于一个 socket, 这个函数指针会被赋值为sock_rfree或sock_wfree(分别由skb_set_owner_r或skb_set_owner_w函数初始 化)。这两个sock_xxx函数用于更新socket的队列中的内存容量。

3. General Fields

本节描述sk_buff的主要成员变量,这些成员变量与特定的内核功能无关:

struct timeval stamp
这个变量只对接收到的包有意义。它代表包接收时的时间戳,或者有时代表包准备发出时的时间戳。它在netif_rx里面由函数net_timestamp设置,而netif_rx是设备驱动收到一个包后调用的函数。

struct net_device *dev
这 个变量的类型是net_device,net_device它代表一个网络设备。dev的作用与这个包是准备发出的包还是刚接收的包有关。当收到一个包 时,设备驱动会把sk_buff的dev指针指向收到这个包的设备的数据结构,就像下面的vortex_rx里的一段代码所做的一样,这个函数属于 3c59x系列以太网卡驱动,用于接收一个帧。(drivers/net/3c59x.c):

  1. static int vortex_rx(struct net_device *dev) 
  2. ... ... 
  3. skb->dev = dev; 
  4. ... ... 
  5. skb->protocol = eth_type_trans(skb, dev); 
  6. netif_rx(skb); /* Pass the packet to the higher layer */ 
  7. ... ... ... 

当一个包被发送时,这个变量代表将要发送这个包的设备。在发送网络包时设置 这个值的代码要比接收网络包时设置这个值的代码复杂。有些网络功能可以把多个网络设备组成一个虚拟的网络设备(也就是说,这些设备没有和物理设备直接关 联),并由一个虚拟网络设备驱动管理。当虚拟设备被使用时,dev指针指向虚拟设备的net_device结构。而虚拟设备驱动会在一组设备中选择一个设 备并把dev指针修改为这个设备的net_device结构。因此,在某些情况下, 指向传输设备的指针会在包处理过程中被改变。

 

struct net_device *input_dev
这是收到包的网络设备的指针。如果包是本地生成的,这个值为NULL。对以太网设备来说,这个值由eth_type_trans初始化,它主要被流量控制代码使用。

 

struct net_device *real_dev
这个变量只对虚拟设备有意义,它代表与虚拟设备关联的真实设备。例如,Bonding和VLAN设备都使用它来指向收到包的真实设备。

union {...} h
union {...} nh
union {...} mac

这些是指向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 three

 

struct dst_entry dst
这个变量在路由子系统中使用。

char cb[40]
这 是一个“control buffer”,或者说是一个私有信息的存储空间,由每一层自己维护并使用。它在分配sk_buff结构时分配(它目前的大小是40字节,已经足够为每一 层存储必要的私有信息了)。在每一层中,访问这个变量的代码通常用宏实现以增强代码的可读性。例如,TCP用这个变量存储tcp_skb_cb结构,这个 结构在include/net/tcp.h中定义:

  1. struct tcp_skb_cb { 
  2. ... ... 
  3. _ _u32 seq;         /* Starting sequence number */ 
  4. _ _u32 end_seq;     /* SEQ + FIN + SYN + datalen*/ 
  5. _ _u32 when;        /* used to compute rtt's     */ 
  6. _ _u8  flags;       /* TCP header flags.         */ 
  7. ... ... 
  8. }; 

下面这个宏被TCP代码用来访问cb变量。在这个宏里面,有一个简单的类型转换:

#define TCP_SKB_CB(_ _skb)     ((struct tcp_skb_cb *)&((_ _skb)->cb[0]))

下面的例子是TCP子系统在收到一个分段时填充相关数据结构的代码:

  1. int tcp_v4_rcv(struct sk_buff *skb) 
  2. ... ... 
  3. th = skb->h.th; 
  4. TCP_SKB_CB(skb)->seq = ntohl(th->seq); 
  5. TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn
  6.  + th->fin + skb->len - th->doff * 4); 
  7. TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq); 
  8. TCP_SKB_CB(skb)->when = 0; 
  9. TCP_SKB_CB(skb)->flags = skb->nh.iph->tos; 
  10. TCP_SKB_CB(skb)->sacked = 0; 
  11. ... ... 

如果想要了解cb中的参数是如何被取出的,可以查看net/ipv4/tcp_output.c中的tcp_transmit_skb函数。这个函数被TCP用于向IP层发送一个分段。

unsigned int csum
unsigned char ip_summed
表示校验和以及相关状态标记。

unsigned char cloned
一个布尔标记,当被设置时,表示这个结构是另一个sk_buff的克隆。在“克隆和拷贝缓冲区”一节中有描述。

unsigned char pkt_type
这个变量表示帧的类型,分类是由L2的目的地址来决定的。可能的取值都在include/linux/if_packet.h中定义。对以太网设备来说,这个变量由eth_type_trans函数初始化。 
类型的可能取值如下:

PACKET_HOST
包的目的地址与收到它的网络设备的L2地址相等。换句话说,这个包是发给本机的。

.PACKET_MULTICAST
包的目的地址是一个多播地址,而这个多播地址是收到这个包的网络设备所注册的多播地址。

PACKET_BROADCAST
包的目的地址是一个广播地址,而这个广播地址也是收到这个包的网络设备的广播地址。

PACKET_OTHERHOST
包的目的地址与收到它的网络设备的地址完全不同(不管是单播,多播还是广播),因此,如果本机的转发功能没有启用,这个包会被丢弃。

PACKET_OUTGOING
这个包将被发出。用到这个标记的功能包括Decnet协议,或者是为每个网络tap都复制一份发出包的函数。

PACKET_LOOPBACK
这个包发向loopback设备。由于有这个标记,在处理loopback设备时,内核可以跳过一些真实设备才需要的操作。

PACKET_FASTROUTE
这个包由快速路由代码查找路由。快速路由功能在2.6内核中已经去掉了。

 

_ _u32 priority
这 个变量描述发送或转发包的QoS类别。如果包是本地生成的,socket层会设置priority变量。如果包是将要被转发的, rt_tos2priority函数会根据ip头中的Tos域来计算赋给这个变量的值。这个变量的值与DSCP(DiffServ CodePoint)没有任何关系。

unsigned short protocol
这 个变量是高层协议从二层设备的角度所看到的协议。典型的协议包括IP,IPV6和ARP。完整的列表在include/linux/if_ether.h 中。由于每个协议都有自己的协议处理函数来处理接收到的包,因此,这个域被设备驱动用于通知上层调用哪个协议处理函数。每个网络驱动都调用 netif_rx来通知上层网络协议的协议处理函数,因此protocol变量必须在这些协议处理函数调用之前初始化。

unsigned short security
这是包的安全级别。这个变量最初由IPSec子系统使用,但现在已经作废了。

4. Feature-Specific Fields

linux内核是模块化的,你可以选择包含或者删除某些功能。因此,sk_buff结构里面的一些成员变量只有在内核选择支持某些功能时才有效,比如防火墙(netfilter)或者qos:

unsigned long nfmark
_ _u32 nfcache
_ _u32 nfctinfo
struct nf_conntrack *nfct
unsigned int nfdebug
struct nf_bridge_info *nf_bridge
这 些变量被netfilter使用(防火墙代码),内核编译选项是“Device Drivers->Networking support-> Networking options-> Network packet filtering”和两个子选项“Network packet filtering debugging”和“Bridged IP/ARP packets filtering”

union {...} private
这 个联合结构被高性能并行接口(HIPPI)使用。相应的内核编译选项是“Device->Drivers ->Networking support ->Network device support ->HIPPI driver support”

_ _u32 tc_index
_ _u32 tc_verd
_ _u32 tc_classid
这 些变量被流量控制代码使用,内核编译选项是“Device Drivers ->Networking->support ->Networking options ->QoS and/or fair queueing”和它的子选项“Packetclassifier API”

(lovecn)

本站文章除注明转载外,均为本站原创或编译欢迎任何形式的转载,但请务必注明出处,尊重他人劳动,同学习共成长。转载请注明:文章转载自:罗索实验室 [http://www.rosoo.net/a/201102/10979.html]
本文出处:CSDN博客 作者:lovecn
顶一下
(1)
100%
踩一下
(0)
0%
------分隔线----------------------------
发表评论
请自觉遵守互联网相关的政策法规,严禁发布色情、暴力、反动的言论。
评价:
表情:
用户名: 验证码:点击我更换图片
栏目列表
将本文分享到微信
织梦二维码生成器
推荐内容