1.   定义

SKB:           struct sk_buffer 的简写

 

 

2.   概述

Struct sk_buffer 是 linux TCP/IP stack 中,用于管理Data Buffer的结构。Sk_buffer 在数据包的发送和接收中起着重要的作用。

为了提高网络处理的性能,应尽量避免数据包的拷贝。Linux 内核开发者们在设计 sk_buffer 结构的时候,充分考虑到这一点。目前 Linux 协议栈在接收数据的时候,需要拷贝两次:数据包进入网卡驱动后拷贝一次,从内核空间递交给用户空间的应用时再拷贝一次。

Sk_buffer结构随着内核版本的升级,也一直在改进。

学习和理解 sk_buffer 结构,不仅有助于更好的理解内核代码,而且也可以从中学到一些设计技巧。

 ---------------------------------------

注:

Packet:       通过网卡收发的报文,包括链路层、网络层、传输层的协议头和携带的数据

Data Buffer:用于存储 packet 的内存空间

-------------------------------------------

3.   Sk_buffer 定义

struct sk_buff {

                struct sk_buff                     *next;

                struct sk_buff                     *prev;

                struct sock                          *sk;

                struct skb_timeval             tstamp;

                struct net_device         *dev;

                struct net_device         *input_dev;

 

                union {

                                struct tcphdr       *th;

                                struct udphdr      *uh;

                                struct icmphdr    *icmph;

                                struct igmphdr    *igmph;

                                struct iphdr          *ipiph;

                                struct ipv6hdr      *ipv6h;

                                unsigned char     *raw;

                } h;

                union {

                                struct iphdr          *iph;

                                struct ipv6hdr      *ipv6h;

                                struct arphdr       *arph;

                                unsigned char     *raw;

                } nh;

                union {

                                unsigned char     *raw;

                } mac;

 

                struct  dst_entry                 *dst;

                struct     sec_path              *sp;

                char                                       cb[40];

 

                unsigned int                         len,

                                                                data_len,

                                                                mac_len,

                                                                csum;

                __u32                                    priority;

 

                __u8                                       local_df:1,

                                                                cloned:1,

                                                                ip_summed:2,

                                                                nohdr:1,

                                                                nfctinfo:3;

                __u8                                       pkt_type:3,

                                                                fclone:2;

                __be16                                  protocol;

                void                                        (*destructor)(struct sk_buff *skb);

 

                /* These elements must be at the end, see alloc_skb() for details.  */

                unsigned int                         truesize;

                atomic_t                               users;

                unsigned char                     *head,

                                                *data,

                                                *tail,

                                                *end;

};

4.   成员变量

 

·              struct skb_timeval    tstamp;

此变量用于记录 packet 的到达时间或发送时间。由于计算时间有一定开销,因此只在必要时才使用此变量。需要记录时间时,调用net_enable_timestamp(),不需要时,调用net_disable_timestamp() 。

tstamp 主要用于包过滤,也用于实现一些特定的 socket 选项,一些 netfilter 的模块也要用到这个域。

·              struct net_device      *dev;

·              struct net_device      *input_dev;

 

这几个变量都用于跟踪与 packet 相关的 device。由于 packet 在接收的过程中,可能会经过多个 virtual driver 处理,因此需要几个变量。

接收数据包的时候, dev 和 input_dev 都指向最初的 interface,此后,如果需要被 virtual driver 处理,那么 dev 会发生变化,而 input_dev 始终不变。

 

 

(These three members help keep track of the devices assosciated with a packet. The reason we have three different device pointers is that the main 'skb->dev' member can change as we encapsulate and decapsulate via a virtual device.

So if we are receiving a packet from a device which is part of a bonding device instance, initially 'skb->dev' will be set to point the real underlying bonding slave. When the packet enters the networking (via 'netif_receive_skb()') we save 'skb->dev' away in 'skb->real_dev' and update 'skb->dev' to point to the bonding device.

Likewise, the physical device receiving a packet always records itself in 'skb->input_dev'. In this way, no matter how many layers of virtual devices end up being decapsulated, 'skb->input_dev' can always be used to find the top-level device that actually received this packet from the network. )

 

·              char                               cb[40];

此数组作为 SKB 的控制块,具体的协议可用它来做一些私有用途,例如 TCP 用这个控制块保存序列号和重传状态。

 

 

·              unsigned int               len,

·                                                   data_len,

·                                                   mac_len,

·                                                   csum;

 

‘len’ 表示此 SKB 管理的 Data Buffer 中数据的总长度;

通常,Data Buffer 只是一个简单的线性 buffer,这时候 len 就是线性 buffer 中的数据长度;

但在有 ‘paged data’ 情况下, Data Buffer 不仅包括第一个线性 buffer ,还包括多个 page buffer;这种情况下, ‘data_len’ 指的是 page buffer 中数据的长度,’len’ 指的是线性 buffer 加上 page buffer 的长度;len – data_len 就是线性 buffer 的长度。

 

‘mac_len’ 指 MAC 头的长度。目前,它只在 IPSec 解封装的时候被使用。将来可能从 SKB 结构中

去掉。

‘csum’ 保存 packet 的校验和。

(Finally, 'csum' holds the checksum of the packet. When building send packets, we copy the data in from userspace and calculate the 16-bit two's complement sum in parallel for performance. This sum is accumulated in 'skb->csum'. This helps us compute the final checksum stored in the protocol packet header checksum field. This field can end up being ignored if, for example, the device will checksum the packet for us.

On input, the 'csum' field can be used to store a checksum calculated by the device. If the device indicates 'CHECKSUM_HW' in the SKB 'ip_summed' field, this means that 'csum' is the two's complement checksum of the entire packet data area starting at 'skb->data'. This is generic enough such that both IPV4 and IPV6 checksum offloading can be supported. )

 

·              __u32                            priority;

 

“priority”用于实现 QoS,它的值可能取之于 IPv4 头中的 TOS 域。Traffic Control 模块需要根据这个域来对 packet 进行分类,以决定调度策略。

 

 

·              __u8                              local_df:1,

·                                                   cloned:1,

·                                                   ip_summed:2,

·                                                   nohdr:1,

·                                                   nfctinfo:3;

 

 

为了能迅速的引用一个 SKB 的数据,

当 clone 一个已存在的 SKB 时,会产生一个新的 SKB,但是这个 SKB 会共享已有 SKB 的数据区。

当一个 SKB 被 clone 后,原来的 SKB 和新的 SKB 结构中,”cloned” 都要被设置为1。

 

(The 'local_df' field is used by the IPV4 protocol, and when set allows us to locally fragment frames which have already been fragmented. This situation can arise, for example, with IPSEC.

The 'nohdr' field is used in the support of TCP Segmentation Offload ('TSO' for short). Most devices supporting this feature need to make some minor modifications to the TCP and IP headers of an outgoing packet to get it in the right form for the hardware to process. We do not want these modifications to be seen by packet sniffers and the like. So we use this 'nohdr' field and a special bit in the data area reference count to keep track of whether the device needs to replace the data area before making the packet header modifications.

The type of the packet (basically, who is it for), is stored in the 'pkt_type' field. It takes on one of the 'PACKET_*' values defined in the 'linux/if_packet.h' header file. For example, when an incoming ethernet frame is to a destination MAC address matching the MAC address of the ethernet device it arrived on, this field will be set to 'PACKET_HOST'. When a broadcast frame is received, it will be set to 'PACKET_BROADCAST'. And likewise when a multicast packet is received it will be set to 'PACKET_MULTICAST'.

The 'ip_summed' field describes what kind of checksumming assistence the card has provided for a receive packet. It takes on one of three values: 'CHECKSUM_NONE' if the card provided no checksum assistence, 'CHECKSUM_HW' if the two's complement checksum over the entire packet has been provides in 'skb->csum', and 'CHECKSUM_UNNECESSARY' if it is not necessary to verify the checksum of this packet. The latter usually occurs when the packet is received over the loopback device. 'CHECKSUM_UNNECESSARY' can also be used when the device only provides a 'checksum OK' indication for receive packet checksum offload. )

 

·              void                               (*destructor)(struct sk_buff *skb);

·              unsigned int               truesize;

 

一个 SKB 所消耗的内存包括 SKB 本身和 data buffer。

truesize 就是 data buffer 的空间加上 SKB 的大小。

struct sock 结构中,有两个域,用于统计用于发送的内存空间和用于接收的内存空间,它们是:

rmem_alloc

wmem_alloc

 

另外两个域则统计接收到的数据包的总大小和发送的数据包的总大小。

rcvbuf

sndbuf

 

rmem_alloc 和 rcvbuf,wmem_alloc 和sndbuf 用于不同的目的。

 

当我们收到一个数据包后,需要统计这个 socket 总共消耗的内存,这是通过skb_set_owner_r() 来做的。

 

static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
{
        skb->sk = sk;
        skb->destructor = sock_rfree;
        atomic_add(skb->truesize, &sk->sk_rmem_alloc);
}
 最后,当释放一个 SKB 后,需要调用 skb->destruction() 来减少rmem_alloc 的值。
 
同样,在发送一个 SKB 的时候,需要调用skb_set_owner_w() ,
 
static inline void skb_set_owner_w(struct sk_buff *skb, struct sock *sk)
{
        sock_hold(sk);
        skb->sk = sk;
        skb->destructor = sock_wfree;
        atomic_add(skb->truesize, &sk->sk_wmem_alloc);
}
 在释放这样的一个 SKB 的时候,需要 调用 sock_free()
 
void sock_wfree(struct sk_buff *skb)
{
        struct sock *sk = skb->sk;
 
        /* In case it might be waiting for more memory. */
        atomic_sub(skb->truesize, &sk->sk_wmem_alloc);
        if (!sock_flag(sk, SOCK_USE_WRITE_QUEUE))
               sk->sk_write_space(sk);
        sock_put(sk);
}
 

 

(Another subtle issue is worth pointing out here. For receive buffer accounting, we do not grab a reference to the socket (via 'sock_hold()'), because the socket handling code will always make sure to free up any packets in it's receive queue before allowing the socket to be destroyed. Whereas for send packets, we have to do proper accounting with 'sock_hold()' and 'sock_put()'. Send packets can be freed asynchronously at any point in time. For example, a packet could sit in a devices transmit queue for a long time under certain conditions. If, meanwhile, the socket is closed, we have to keep the socket reference around until SKBs referencing that socket are liberated. )

 

·              unsigned char                        *head,

·                                                   *data,

·                                                   *tail,

·                                                   *end;

 SKB 对 Data Buffer 的巧妙管理,就是靠这四个指针实现的。

 

下图展示了这四个指针是如何管理数据 buffer 的:

Head 指向 buffer 的开始,end 指向 buffer 结束。 Data 指向实际数据的开始,tail 指向实际数据的结束。这四个指针将整个 buffer 分成三个区:

 

Packet data:这个空间保存的是真正的数据

Head room:处于 packet data 之上的空间,是一个空闲区域

Tail room:处于 packet data 之下的空间,也是空闲区域。

 

由于 TCP/IP 协议族是一种分层的协议,传输层、网络层、链路层,都有自己的协议头,因此 TCP/IP 协议栈对于数据包的处理是比较复杂的。为了提高处理效率,避免数据移动、拷贝,sk_buffer 在对数据 buffer 管理的时候,在 packet data 之上和之下,都预留了空间。如果需要增加协议头,只需要从 head room 中拿出一块空间即可,而如果需要增加数据,则可以从 tail room 中获得空间。这样,整个内存只分配一次空间,此后 协议的处理,只需要挪动指针。

5.   Sk_buffer 对内存的管理

我们以构造一个用于发送的数据包的过程,来理解 sk_buffer 是如何管理内存的。

 

5.1.                    构造Skb_buffer

 

alloc_skb() 用于构造 skb_buffer,它需要一个参数,指定了存放 packet 的空间的大小。

构造时,不仅需要创建 skb_buffer 结构本身,还需要分配空间用于保存 packet。

 

skb = alloc_skb(len, GFP_KERNEL);

  

上图是在调用完 alloc_skb() 后的情况:

 

head, data, tail 指向 buffer 开始,end 指向 buffer 结束,整个 buffer 都被当作 tail room。

Sk_buffer 当前的数据长度是0。

 

 

5.2.                    为 protocol header 留出空间

 

通常,当构造一个用于发送的数据包时,需要留出足够的空间给协议头,包括 TCP/UDP header, IP header 和链路层头。

对 IPv4 数据包,可以从 sk->sk_prot->max_header 知道协议头的最大长度。

 

skb_reserve(skb, header_len);

 

  

图是调用 skb_reserver() 后的情况

 

 

5.3.                    将用户空间数据拷贝到 buffer 中

 

首先通过 skb_put(skb, user_data_len) ,从 tail room 中留出用于保存数据的空间

然后通过csum_and_copy_from_user() 将数据从用户空间拷贝到这个空间中。

 

5.4.                    构造UDP协议头

 

通过 skb_push() ,向 head room 中要一块空间

然后在此空间中构造 UDP 头。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

5.5.                    构造 IP 头

 

通过 skb_push() ,向 head room 中要一块空间

然后在此空间中构造 IP 头。

 

6.   Sk_buffer 的秘密

当调用 alloc_skb() 构造 SKB 和 data buffer时,需要的 buffer 大小是这样计算的:

 

data = kmalloc(size + sizeof(struct skb_shared_info), gfp_mask);

 

除了指定的 size 以外,还包括一个 struct skb_shared_info 结构的空间大小。也就是说,当调用 alloc_skb(size) 要求分配 size 大小的 buffer 的时候,同时还创建了一个 skb_shared_info 。

 

这个结构定义如下:

 

struct skb_shared_info {

            atomic_t            dataref;

            unsigned int       nr_frags;

            unsigned short   tso_size;

            unsigned short   tso_segs;

            struct sk_buff     *frag_list;

            skb_frag_t         frags[MAX_SKB_FRAGS];

};

 

 

 

我们只要把 end 从 char* 转换成skb_shared_info* ,就能访问到这个结构

Linux 提供一个宏来做这种转换:

 

#define skb_shinfo(SKB)             ((struct skb_shared_info *)((SKB)->end))

 

 

 

那么,这个隐藏的结构用意何在?

它至少有两个目的:

1、  用于管理 paged data

2、  用于管理分片

 

接下来分别研究 sk_buffer 对paged data 和分片的处理。

7.   对 paged data 的处理

 
某些情况下,希望能将保存在文件中的数据,通过 socket 直接发送出去,这样,避免了把数据先从文件拷贝到缓冲区,从而提高了效率。
Linux 采用一种 “paged data” 的技术,来提供这种支持。这种技术将文件中的数据直接被映射为多个 page。
 
Linux 用 struct skb_frag_strut 来管理这种 page:
 
typedef struct skb_frag_struct skb_frag_t;
 
struct skb_frag_struct {
        struct page *page;
        __u16 page_offset;
        __u16 size;
};
 
并在shared info 中,用数组 frags[] 来管理这些结构。
 
 

如此一来,sk_buffer 就不仅管理着一个 buffer 空间的数据了,它还可能通过 share info 结构管理一组保存在 page 中的数据。

 

在采用 “paged data” 时,data_len 成员派上了用场,它表示有多少数据在 page 中。因此,

如果 data_len 非0,这个 sk_buffer 管理的数据就是“非线性”的。

Skb->len – skb->data_len 就是非 paged 数据的长度。

 

在有 “paged data” 情况下, skb_put()就无法使用了,必须使用 pskb_put() 。。。

 
 
 
 
 
 
 
 
 
 
 

8.   对分片的处理

9.   SKB 的管理函数

 

9.1.                    Data Buffer 的基本管理函数

 

·              unsigned char *skb_put(struct sk_buff *skb, unsigned int len)

 

“推”入数据

在 buffer 的结束位置,增加数据,len是要增加的长度。

这个函数有两个限制,需要调用者自己注意,否则后果由调用者负责

1)、不能用于 “paged data” 的情况

这要求调用者自己判断是否为 “paged data” 情况

2)、增加新数据后,长度不能超过 buffer 的实际大小。

这要求调用者自己计算能增加的数据大小

 

·              unsigned char *skb_push(struct sk_buff *skb, unsigned int len)

“压”入数据

从 buffer 起始位置,增加数据,len 是要增加的长度。

实际就是将新的数据“压”入到 head room 中

 

·              unsigned char *skb_pull(struct sk_buff *skb, unsigned int len)

“拉”走数据

从 buffer 起始位置,去除数据, len 是要去除的长度。

如果 len 大于 skb->len,那么,什么也不做。

在处理接收到的 packet 过程中,通常要通过 skb_pull() 将最外层的协议头去掉;例如当网络层处理完毕后,就需要将网络层的 header 去掉,进一步交给传输层处理。

 

 

·              void skb_trim(struct sk_buff *skb, unsigned int len)

调整 buffer 的大小,len 是调整后的大小。

如果 len 比 buffer 小,则不做调整。

因此,实际是将 buffer 底部的数据去掉。

对于没有 paged data 的情况,很好处理;

但是有 paged data 情况下,则需要调用 __pskb_trim() 来进行处理。

 

 

 

9.2.                    “Paged data” 和 分片的管理函数

 

·              char *pskb_pull(struct sk_buff *skb, unsigned int len)

 

“拉“走数据

如果 len 大于线性 buffer 中的数据长度,则调用__pskb_pull_tail()  进行处理。

(Q:最后, return skb->data += len;  是否会导致 skb->data 超出了链头范围?)

 

·              int pskb_may_pull(struct sk_buff *skb, unsigned int len)

在调用 skb_pull() 去掉外层协议头之前,通常先调用此函数判断一下是否有足够的数据用于“pull”。

如果线性 buffer足够 pull,则返回1;

如果需要 pull 的数据超过 skb->len,则返回0;

最后,调用__pskb_pull_tail() 来检查 page buffer 有没有足够的数据用于 pull。

 

 

·              int pskb_trim(struct sk_buff *skb, unsigned int len)

将 Data Buffer 的数据长度调整为 len

在没有 page buffer 情况下,等同于 skb_trim();

在有 page buffer 情况下,需要调用___pskb_trim() 进一步处理。

 

·              int skb_linearize(struct sk_buff *skb, gfp_t gfp)

 

 

·              struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t gfp_mask)

‘clone’ 一个新的 SKB。新的 SKB 和原有的 SKB 结构基本一样,区别在于:

1)、它们共享同一个 Data Buffer

2)、它们的 cloned 标志都设为1

3)、新的 SKB 的 sk 设置为空

(Q:在什么情况下用到克隆技术?)

 

·              struct sk_buff *skb_copy(const struct sk_buff *skb, gfp_t gfp_mask)

 

·              struct sk_buff *pskb_copy(struct sk_buff *skb, gfp_t gfp_mask)

 

·              struct sk_buff *skb_pad(struct sk_buff *skb, int pad)

 

·              void skb_clone_fraglist(struct sk_buff *skb)

 

·              void skb_drop_fraglist(struct sk_buff *skb)

 

·              void copy_skb_header(struct sk_buff *new, const struct sk_buff *old)

 

·              pskb_expand_head(struct sk_buff *skb, int nhead, int ntail, gfp_t gfp_mask)

 

·              int skb_copy_bits(const struct sk_buff *skb, int offset, void *to, int len)

 

·              int skb_store_bits(const struct sk_buff *skb, int offset, void *from, int len)

 

·              struct sk_buff *skb_dequeue(struct sk_buff_head *list)

 

·              struct sk_buff *skb_dequeue(struct sk_buff_head *list)

 

·              void skb_queue_purge(struct sk_buff_head *list)

 

·              void skb_queue_purge(struct sk_buff_head *list)

 

·              void skb_queue_tail(struct sk_buff_head *list, struct sk_buff *newsk)

 

·              void skb_unlink(struct sk_buff *skb, struct sk_buff_head *list)

 

·              void skb_append(struct sk_buff *old, struct sk_buff *newsk, struct sk_buff_head *list)

 

·              void skb_insert(struct sk_buff *old, struct sk_buff *newsk, struct sk_buff_head *list)

 

·              int skb_add_data(struct sk_buff *skb, char __user *from, int copy)

 

·              struct sk_buff *skb_padto(struct sk_buff *skb, unsigned int len)

 

·              int skb_cow(struct sk_buff *skb, unsigned int headroom)

 

这个函数要对 SKB 的 header room 调整,调整后的 header room 大小是 headroom.

如果 headroom 长度超过当前header room 的大小,或者 SKB 被 clone 过,那么需要调整,方法是:

分配一块新的 data buffer 空间,SKB 使用新的 data buffer 空间,而原有空间的引用计数减1。在没有其它使用者的情况下,原有空间被释放。

 

 

 

·              struct sk_buff *dev_alloc_skb(unsigned int length)

 

·              void skb_orphan(struct sk_buff *skb)

 

·              void skb_reserve(struct sk_buff *skb, unsigned int len)

 

·              int skb_tailroom(const struct sk_buff *skb)

 

·              int skb_headroom(const struct sk_buff *skb)

 

·              int skb_pagelen(const struct sk_buff *skb)

 

·              int skb_headlen(const struct sk_buff *skb)

 

·              int skb_is_nonlinear(const struct sk_buff *skb)

 

·              struct sk_buff *skb_share_check(struct sk_buff *skb, gfp_t pri)

 

如果skb 只有一个引用者,直接返回 skb

否则 clone 一个 SKB,将原来的 skb->users 减1,返回新的 SKB

 

 

需要特别留意 pskb_pull() 和 pskb_may_pull() 是如何被使用的:

 

1)、在接收数据的时候,大量使用 pskb_may_pull(),其主要目的是判断 SKB 中有没有足够的数据,例如在 ip_rcv() 中:

 

if (!pskb_may_pull(skb, sizeof(struct iphdr)))

                        goto inhdr_error;

 

iph = skb->nh.iph;

 

它的目的是拿到 IP header,但取之前,先通过 pskb_may_pull() 判断一下有没有足够一个 IP header 的数据。

 

 

2)、当我们构造 IP 分组的时候,对于数据部分,通过 put向下扩展空间(如果一个sk_buffer 不够用怎么分片?);对于 传输层、网络层、链路层的头,通过 push 向上扩展空间;

 

3)、当我们解析 IP 分组的时候,通过 pull(),从头开始,向下压缩空间。

 

因此,put 和 push 主要用在发送数据包的时候;而pull 主要用在接收数据包的时候。

 

 

 

 

 

10.                     各种 header

 

union {

                        struct tcphdr      *th;

                        struct udphdr     *uh;

                        struct icmphdr   *icmph;

                        struct igmphdr   *igmph;

                        struct iphdr        *ipiph;

                        struct ipv6hdr     *ipv6h;

                        unsigned char    *raw;

            } h;

 

            union {

                        struct iphdr        *iph;

                        struct ipv6hdr     *ipv6h;

                        struct arphdr      *arph;

                        unsigned char    *raw;

            } nh;

 

            union {

                        unsigned char    *raw;

            } mac;

=======================================================================================

sk_buff结构体 (skb)

这个结构体是套接字的缓冲区,详细记录了一个数据包的组成,时间、网络设备、各层的首部及首部长度和数据的首尾指针。

下面是他的定义

[cpp] view plaincopy

  1. struct sk_buff {
  2. /* These two members must be first. */
  3. struct sk_buff      *next;
  4. struct sk_buff      *prev;
  5. ktime_t         tstamp;
  6. struct sock     *sk;
  7. struct net_device   *dev;
  8. /*
  9. * This is the control buffer. It is free to use for every
  10. * layer. Please put your private variables there. If you
  11. * want to keep them across layers you have to do a skb_clone()
  12. * first. This is owned by whoever has the skb queued ATM.
  13. */
  14. char            cb[48] __aligned(8);
  15. unsigned long       _skb_refdst;
  16. #ifdef CONFIG_XFRM
  17. struct  sec_path    *sp;
  18. #endif
  19. unsigned int        len,
  20. data_len;
  21. __u16           mac_len,
  22. hdr_len;
  23. union {
  24. __wsum      csum;
  25. struct {
  26. __u16   csum_start;
  27. __u16   csum_offset;
  28. };
  29. };
  30. __u32           priority;
  31. kmemcheck_bitfield_begin(flags1);
  32. __u8            local_df:1,
  33. cloned:1,
  34. ip_summed:2,
  35. nohdr:1,
  36. nfctinfo:3;
  37. __u8            pkt_type:3,
  38. fclone:2,
  39. ipvs_property:1,
  40. peeked:1,
  41. nf_trace:1;
  42. kmemcheck_bitfield_end(flags1);
  43. __be16          protocol;
  44. void            (*destructor)(struct sk_buff *skb);
  45. #if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
  46. struct nf_conntrack *nfct;
  47. #endif
  48. #ifdef NET_SKBUFF_NF_DEFRAG_NEEDED
  49. struct sk_buff      *nfct_reasm;
  50. #endif
  51. #ifdef CONFIG_BRIDGE_NETFILTER
  52. struct nf_bridge_info   *nf_bridge;
  53. #endif
  54. int         skb_iif;
  55. #ifdef CONFIG_NET_SCHED
  56. __u16           tc_index;   /* traffic control index */
  57. #ifdef CONFIG_NET_CLS_ACT
  58. __u16           tc_verd;    /* traffic control verdict */
  59. #endif
  60. #endif
  61. __u32           rxhash;
  62. __u16           queue_mapping;
  63. kmemcheck_bitfield_begin(flags2);
  64. #ifdef CONFIG_IPV6_NDISC_NODETYPE
  65. __u8            ndisc_nodetype:2;
  66. #endif
  67. __u8            ooo_okay:1;
  68. __u8            l4_rxhash:1;
  69. kmemcheck_bitfield_end(flags2);
  70. /* 0/13 bit hole */
  71. #ifdef CONFIG_NET_DMA
  72. dma_cookie_t        dma_cookie;
  73. #endif
  74. #ifdef CONFIG_NETWORK_SECMARK
  75. __u32           secmark;
  76. #endif
  77. union {
  78. __u32       mark;
  79. __u32       dropcount;
  80. };
  81. __u16           vlan_tci;
  82. sk_buff_data_t      transport_header;
  83. sk_buff_data_t      network_header;
  84. sk_buff_data_t      mac_header;
  85. /* These elements must be at the end, see alloc_skb() for details.  */
  86. sk_buff_data_t      tail;
  87. sk_buff_data_t      end;
  88. unsigned char       *head,
  89. *data;
  90. unsigned int        truesize;
  91. atomic_t        users;
  92. };

可以看到新版本内核中发生了很多变化,其中数据包的首部在早期版本是以union的形式定义的,例如mac_header的定义方式如下:

[cpp] view plaincopy

  1. union{
  2. struct ethhdr *ethernet;
  3. unsigned char *raw;
  4. }mac;

这里是以指针的形式给出的

[cpp] view plaincopy

  1. #ifdef NET_SKBUFF_DATA_USES_OFFSET
  2. typedef unsigned int sk_buff_data_t;
  3. #else
  4. typedef unsigned char *sk_buff_data_t;
  5. #endif

这里主要说明下后面几个后面的四个属性的含义head、data、tail、end

head是缓冲区的头指针,data是数据的起始地址,tail是数据的结束地址,end是缓冲区的结束地址。

char cb[48] __aligned(8);中的48个字节是控制字段,配合各层协议工作,为每层存储必要的控制信息。

2、sk_buff_head结构体

[cpp] view plaincopy

  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. };

这个结构体比较简单,前面两个指针是用于和sk_buff结构串成双向链表,用于管理sk_buff双链表,qlen属性表示该链表中sk_buff的数目,lock是自旋锁。

3、skb_shared_info结构体

[cpp] view plaincopy

  1. struct skb_shared_info {
  2. unsigned short  nr_frags;
  3. unsigned short  gso_size;//尺寸
  4. /* Warning: this field is not always filled in (UFO)! */
  5. unsigned short  gso_segs;//顺序
  6. unsigned short  gso_type;
  7. __be32          ip6_frag_id;
  8. __u8        tx_flags;
  9. struct sk_buff  *frag_list;//分片的sk_buff列表
  10. struct skb_shared_hwtstamps hwtstamps;//硬件时间戳
  11. /*
  12. * Warning : all fields before dataref are cleared in __alloc_skb()
  13. */
  14. atomic_t    dataref;//使用计数
  15. /* Intermediate layers must ensure that destructor_arg
  16. * remains valid until skb destructor */
  17. void *      destructor_arg;
  18. /* must be last field, see pskb_expand_head() */
  19. skb_frag_t  frags[MAX_SKB_FRAGS];
  20. };

该类型用来管理数据包分片信息,通过宏可以表示与skb的关系

[cpp] view plaincopy

  1. #define skb_shinfo(SKB) ((struct skb_shared_info *)(skb_end_pointer(SKB)))

[cpp] view plaincopy

  1. #ifdef NET_SKBUFF_DATA_USES_OFFSET
  2. static inline unsigned char *skb_end_pointer(const struct sk_buff *skb)
  3. {
  4. return skb->head + skb->end;
  5. }
  6. #else
  7. static inline unsigned char *skb_end_pointer(const struct sk_buff *skb)
  8. {
  9. return skb->end;
  10. }
  11. #endif

可以看到如果用户没有自己使用偏移量,就是skb的end属性指针,也就是该信息存储在缓冲区之后。

================================================

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,这是一个辅助节点,它的定义如下:

 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和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)。

struct sk_buff *alloc_skb(unsigned int size,int gfp_mask)
{... ... ...skb->truesize = size + sizeof(struct sk_buff);... ... ...
}

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

unsigned char *head
unsigned char *end
unsigned char *data
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):

static int vortex_rx(struct net_device *dev)
{... ... ...skb->dev = dev;... ... ...skb->protocol = eth_type_trans(skb, dev);netif_rx(skb); /* Pass the packet to the higher layer */... ... ...
} 

当一个包被发送时,这个变量代表将要发送这个包的设备。在发送网络包时设置这个值的代码要比接收网络包时设置这个值的代码复杂。有些网络功能可以把多个网络设备组成一个虚拟的网络设备(也就是说,这些设备没有和物理设备直接关联),并由一个虚拟网络设备驱动管理。当虚拟设备被使用时,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中定义:

struct tcp_skb_cb {... ... ..._ _u32         seq;         /* Starting sequence number */_ _u32         end_seq;     /* SEQ + FIN + SYN + datalen*/_ _u32         when;        /* used to compute rtt's     */_ _u8          flags;       /* TCP header flags.         */... ... ...
};

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

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

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

int tcp_v4_rcv(struct sk_buff *skb)
{... ... ...th = skb->h.th;TCP_SKB_CB(skb)->seq = ntohl(th->seq);TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin +skb->len - th->doff * 4);TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);TCP_SKB_CB(skb)->when = 0;TCP_SKB_CB(skb)->flags = skb->nh.iph->tos;TCP_SKB_CB(skb)->sacked = 0;... ... ...
}

如果想要了解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”

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

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

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

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

  4. TCP层添加自己的头部。

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

  6. 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(...);
}

这些函数先获取锁,然后调用一个以两个下划线开头的同名函数(这个函数做具体的操作,而且不需要锁),然后释放锁。

SKB buff 数据结构相关推荐

  1. linux协议栈skb操作函数

    一. SKB_BUFF的基本概念 1. 一个完整的skb buff组成 (1) struct sk_buff--用于维护socket buffer状态和描述信息 (2) header data--独立 ...

  2. 《探寻linux协议栈》之二:源于skb的一场大联欢

    写在前面 skb是什么 skb长什么样 skb中和网络协议有关的指针 skb的储存 skb如何到linux内核协议栈中 内核接收驱动skb的入口函数netif_receive_skb 总结 写在前面 ...

  3. 【转】内核通信之 Netlink 源码分析和实例分析

    目录 前言 什么是 netlink netlink 内核代码走读 netlink 内核相关文件介绍 af_netlink.c 代码走读 netlink 用户态和内核交互过程 netlink 关键数据结 ...

  4. 如何快速优化 Linux 内核 UDP 收包效率? | CSDN 博文精选

    作者 | dog250 责编 | 郭芮 出品 | CSDN 博客 现在很多人都在诟病Linux内核协议栈收包效率低,不管他们是真的懂还是一点都不懂只是听别人说的,反正就是在一味地怼Linux内核协议栈 ...

  5. Linux内核UDP收包为什么效率低?能做什么优化?

    现在很多人都在诟病Linux内核协议栈收包效率低,不管他们是真的懂还是一点都不懂只是听别人说的,反正就是在一味地怼Linux内核协议栈,他们的武器貌似只有DPDK. 但是,即便Linux内核协议栈收包 ...

  6. netlink使用简介

    一.什么是netlink Netlink套接字是用以实现用户进程与内核进程通信的一种特殊的进程间通信(IPC) ,也是网络应用程序与内核通信的最常用的接口. 在Linux 内核中,使用netlink ...

  7. skb_buff 详解(二)

    skbuffs是那些linux内核处理网络分组的缓存.网卡收到分组后,将它们放进skbuff,然后再传送给网络堆栈.网络堆栈一直 要用到skbuff. 1. 一个完整的skb buff组成 (1) s ...

  8. Linux内核UDP性能优化(超详细讲解)

    现在很多人都在诟病Linux内核协议栈收包效率低,不管他们是真的懂还是一点都不懂只是听别人说的,反正就是在一味地怼Linux内核协议栈,他们的武器貌似只有DPDK. 但是,即便Linux内核协议栈收包 ...

  9. Linux驱动-Netlink通信

    什么是Netlink通信机制? Netlink套接字是用以实现用户进程与内核进程通信的一种特殊的进程间通信(IPC) ,也是网络应用程序与内核通信的最常用的接口.Netlink 是一种特殊的 sock ...

最新文章

  1. humanparsing自然场景人体语义分割
  2. abap 常用系统变量
  3. mysql创建用户以及授权
  4. 【每周CV论文】深度学习文本检测与识别入门必读文章
  5. windows安装 Chocolatey
  6. html引用webpack插件,使用html-webpack-plugin,运行webpack,老是报错?
  7. android可以定义函数吗,Android自定义view 你所需要知道的基本函数总结
  8. opencv物品定位_使用OpenCV获取零件位置的学习笔记
  9. python基于scipy模块实现统计学中三大相关系数的计算
  10. 16家企业、11个领域……京东首开AI加速器,拜倒,拜倒!
  11. 什么是网站前端框架?目前常用的网站前端框架都有哪些?
  12. python matplotlib绘图总结
  13. Java项目前后端文件简体转换繁体
  14. 大部头出版物排版软件
  15. Xenu软件检查网站死链接
  16. 新基建时代,5万亿市值腾讯还有大发展
  17. ae制作小球轨迹运动_在AE里如何让物体沿着路径运动?
  18. HLOJ 1936 铺满方格
  19. 三国志战略版S5赛季事件战法简评
  20. 三级等保备案去哪查_民贷天下通过国家信息安全等级保护三级备案

热门文章

  1. 你创建的 Java 对象搁哪了
  2. 猴子选大王问题(Monkey King)
  3. 设置提高电脑CPU处理性能使电脑变快
  4. 老卫带你学---8bit图像转24bit图像
  5. C# 三种发送邮件方法
  6. Sublime Text3文件栏乱码、文件内容乱码及文字大小的解决方法
  7. Word中自动生成自定义标题的目录时,左侧有很多空格
  8. CCNP OSPF实验
  9. Unity3D引擎中的Timeline使用方法
  10. go自动下载所有的依赖包 go module使用