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 { ... ... ... #ifdef CONFIG_NET_SCHED _ _u32 tc_index; #ifdef CONFIG_NET_CLS_ACT _ _u32 tc_verd; _ _u32 tc_classid; #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. 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
当skb->len变化时,这个变量也会变化。
它 们表示缓冲区和数据部分的边界。在每一层申请缓冲区时,它会分配比协议头或协议数据大的空间。head和end指向缓冲区的头部和尾部,而data和 tail指向实际数据的头部和尾部,参见图2。每一层会在head和data之间填充协议头,或者在tail和end之间添加新的协议数据。图2中右边数 据部分会在尾部包含一个附加的头部。 Figure 2. head/end versus data/tail pointers
void (*destructor)(...) 3. General Fields本节描述sk_buff的主要成员变量,这些成员变量与特定的内核功能无关: struct timeval stamp 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 three
struct dst_entry dst char cb[40]
下面这个宏被TCP代码用来访问cb变量。在这个宏里面,有一个简单的类型转换: #define TCP_SKB_CB(_ _skb) ((struct tcp_skb_cb *)&((_ _skb)->cb[0])) 下面的例子是TCP子系统在收到一个分段时填充相关数据结构的代码:
如果想要了解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_BROADCAST 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 (lovecn) |