当前位置: 代码迷 >> 综合 >> sk_buffer 详细分析 (下)
  详细解决方案

sk_buffer 详细分析 (下)

热度:18   发布时间:2024-01-09 09:56:40.0

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;