Lichen Liu

与其感慨路难行,不如马上出发

  • 主页
所有文章 友链 关于我

Lichen Liu

与其感慨路难行,不如马上出发

  • 主页

sk_buff 简介

2020-10-26

sk_buff 是Linux 网络中的重要数据结构,每个包的发送和接收都要处理这个结构体。

这个结构体比较长,只讨论部分字段。开头是一个联合,它要么在一个链表里,要么在一个rb tree(netem/tcp)里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct sk_buff {
union {
struct {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;

union {
struct net_device *dev;
/* Some protocols might use this space to store information,
* while device pointer would be NULL.
* UDP receive path is one user.
*/
unsigned long dev_scratch;
};
};
struct rb_node rbnode; /* used in netem, ip4 defrag, and tcp stack */
struct list_head list;
};

接下来是一个sock 字段,显然是和这个skb 关联的socket,当这个包是socket 发出或接收时,这里指向对应的socket,而如果是转发包,这里是null.

1
struct sock *sk;

随后是一个时间戳字段,用于记录包发送或接收到的时间。net_enable_timestamp() 和net_disable_timestamp() 函数可用于启用或禁用时间戳。在用户态,可以通过socket 选项SIOCGSTAMP 管理。

1
ktime_t tstamp;

之后有一组长度相关的字段,其中len 是数据包的长度,data_len 是不存在于线性buffer,而是使用page buffer 的数据的大小,两者之差则是位于线性buffer 的数据大小,skb_headlen() 函数用于计算这个值。后续会讨论线性buffer 和page buffer.

1
2
3
4
unsigned int		len,
data_len;
__u16 mac_len,
hdr_len;

跳过中间的部分,直接看到协议头相关的字段,这些头部字段保存的是基于skb->head 的偏移量,有一组skb_set/reset/_*_header() 的方法去设置他们,使用时,skb_*_header() 方法会将skb->head + offset 的结果返回。

1
2
3
4
__be16			protocol;
__u16 transport_header;
__u16 network_header;
__u16 mac_header;

最后是skb 的尾部,这些字段必须位于sk_buff 结构的结尾,而且data 是可变长的部分,也就是前面提到的线性buffer,这里可以存放一部分包的数据。

1
2
3
4
5
6

/* These elements must be at the end, see alloc_skb() for details. */
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head,
*data;

这部分线性buffer 由以上四个指针控制,分割成三个部分,这四个指针都指向线性buffer 中的位置:

  • head 到data 之间,称为headroom.
  • data 到tail 之间,存放包的数据。
  • tail 到end 之间,称为tailroom.

对于刚刚通过alloc_skb() 方法申请出来的skb,head,tail,data 三个指针都指向同一位置,而tail 和end 之间有一段根据alloc_skb(len, flag) 方法的参数申请出来的空间。

为了给协议头预留空间,可以使用skb_reserve(skb, head_len)方法,该方法会根据参数将data 指针后移,扩展headroom.

可以通过skb_put(skb, data_len) 方法移动tail 指针,扩展用户数据空间。该方法同时会增加skb->len.

这些空间都是从tailroom “挤”出来的,因此需要保证tailroom 有足够的空间。另外要注意skb_put() 只能在没有page 的数据的情况下调用。
为了添加协议头的内容,需要调用skb_push()方法,这个方法和skb_put()类似,但它是从headroom 挤出空间,data 指针会往前移动,它同样会增加skb->len.

添加一个四层头:

再添加一个三层头:

以上是对于没有page buffer,只有线性buffer 时的操作,对于比较大的包,还需要用到线性buffer 以外的部分。对于部分驱动来讲,有一个copybreak 的字段,当包的大小大于copybreak 时,只将起始一部分数据放入skb->data(协议头等),而剩余部分会存放于page buffer. 后续添加数据时应该不再调用skb_put()方法,否则数据顺序是有问题的。

skb_is_nonlinear() 方法可以帮助判断是否存在page buffer,对于有page buffer 的skb 来讲,之前已经提到skb->data_len 用于指示这部分数据的大小,而之前刚刚提到的通过skb_put() 和skb_push() 方法添加进去的数据的大小就是skb->len-skb->data_len,也就是skb_headlen()。

对于page 的数据,使用skb_frag_t 的数据结构存放。在新版本的内核中,skb_frag_t 是 typedef bio_vec skb_frag_t.

1
2
3
4
5
struct bio_vec {
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
};

该结构是一个page,len,offset 的三元组,指示了数据在哪个page 的哪个offset 处,长度为多少。

skb_shared_info 用于记录frag 相关的信息,nr_frags 指示了在frags[] 中有多少个frags.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct skb_shared_info {
__u8 __unused;
__u8 meta_len;
__u8 nr_frags;
__u8 tx_flags;
unsigned short gso_size;
/* Warning: this field is not always filled in (UFO)! */
unsigned short gso_segs;
struct sk_buff *frag_list;
struct skb_shared_hwtstamps hwtstamps;
unsigned int gso_type;
u32 tskey;

/*
* Warning : all fields before dataref are cleared in __alloc_skb()
*/
atomic_t dataref;

/* Intermediate layers must ensure that destructor_arg
* remains valid until skb destructor */
void * destructor_arg;

/* must be last field, see pskb_expand_head() */
skb_frag_t frags[MAX_SKB_FRAGS];
};

使用skb_shinfo() 可以获取skb_shared_info 的指针,而这个指针正是skb->end 指向的位置(或计算偏移量后的位置),也就是tailroom 的后面。

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

在接收数据时,驱动一般调用skb_add_rx_frag() 方法添加page buffer 数据,而该方法实际上调用了skb_fill_page_desc(),后者是会将page,offset,len (即skb_frag_t) 和新的frag 数量更新到skb_shared_info.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void skb_add_rx_frag(struct sk_buff *skb, int i, struct page *page, int off,
int size, unsigned int truesize)
{
skb_fill_page_desc(skb, i, page, off, size);
skb->len += size;
skb->data_len += size;
skb->truesize += truesize;
}
static inline void skb_fill_page_desc(struct sk_buff *skb, int i,
struct page *page, int off, int size)
{
__skb_fill_page_desc(skb, i, page, off, size);
skb_shinfo(skb)->nr_frags = i + 1;
}

static inline void __skb_fill_page_desc(struct sk_buff *skb, int i,
struct page *page, int off, int size)
{
skb_frag_t *frag = &skb_shinfo(skb)->frags[i];

/*
* Propagate page pfmemalloc to the skb if we can. The problem is
* that not all callers have unique ownership of the page but rely
* on page_is_pfmemalloc doing the right thing(tm).
*/
frag->bv_page = page;
frag->bv_offset = off;
skb_frag_size_set(frag, size);

page = compound_head(page);
if (page_is_pfmemalloc(page))
skb->pfmemalloc = true;
}

可以使用skb_header_pointer() 来获取指定位置指定大小的数据,该方法有两种返回值,如果offset + len 位于skb->data 中,则返回skb->data 对应偏移量的指针。否则将这部分数据拷贝到本地buffer 中,并返回这个buffer 的指针,当然如果offset+len 是不合理值该方法会返回null.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static inline void * __must_check
__skb_header_pointer(const struct sk_buff *skb, int offset,
int len, void *data, int hlen, void *buffer)
{
if (hlen - offset >= len)
return data + offset;

if (!skb ||
skb_copy_bits(skb, offset, buffer, len) < 0)
return NULL;

return buffer;
}

static inline void * __must_check
skb_header_pointer(const struct sk_buff *skb, int offset, int len, void *buffer)
{
return __skb_header_pointer(skb, offset, len, skb->data,
skb_headlen(skb), buffer);
}

skb_copy_bits(const struct sk_buff *skb, int offset, void *to, int len) 可用于直接把offset 处len 长度的数据从sk_buff 中拷贝到to 中,它会检查线性buffer 的大小和offset 是否重合,从而正确从线性buffer 和page buffer 中读取到连续的数据。

参考链接

  • How SKBs work(skb)
  • How SKBs work(skb_data)
  • skbuff.h
  • skbuff.c
  • What’s the correct way to process all the payload of a sk_buff packet in Linux
  • Linux
  • sk_buff
  • kernel

扫一扫,分享到微信

微信分享二维码
manjaro kde 升级后中文字体变成方块
使用ct_state 实现出口放行防火墙
© 2023 Lichen Liu
Hexo Theme Yilia by Litten
  • 所有文章
  • 友链
  • 关于我

tag:

  • Linux
  • golang
  • xml
  • omitempty
  • test
  • art
  • AWS
  • Kernel
  • bonding
  • sk_buff
  • kernel
  • libvirt
  • libvirt-go
  • numa
  • ping
  • arp
  • 输入法
  • ovs
  • font
  • manjaro
  • 坑
  • 北京
  • 旅游

    缺失模块。
    1、请确保node版本大于6.2
    2、在博客根目录(注意不是yilia根目录)执行以下命令:
    npm i hexo-generator-json-content --save

    3、在根目录_config.yml里添加配置:

      jsonContent:
        meta: false
        pages: false
        posts:
          title: true
          date: true
          path: true
          text: false
          raw: false
          content: false
          slug: false
          updated: false
          comments: false
          link: false
          permalink: false
          excerpt: false
          categories: false
          tags: true
    

  • 北京周末

    2023-03-22

    #北京#旅游

  • manjaro kde 升级后中文字体变成方块

    2021-02-18

    #font#manjaro#坑

  • sk_buff 简介

    2020-10-26

    #Linux#sk_buff#kernel

  • 使用ct_state 实现出口放行防火墙

    2020-05-15

    #ovs

  • /dev 下的null, zero, random等文件是怎么来的

    2020-03-07

    #Linux#Kernel

  • NUMA 内存策略 preferred 的效果

    2020-02-20

    #Linux#numa

  • 在linux kernel 源码中搜索syscall 的实现

    2020-02-20

    #Linux

  • 一次ping 遇到Invalid argument 报错的问题

    2020-02-11

    #Linux#ping#arp

  • Linux 网络bond mode 4 的xmit_hash_policy layer3+4 到底是如何hash 的

    2020-01-10

    #Linux#bonding

  • golang xml 如何忽略空结构体

    2020-01-09

    #golang#xml#omitempty

  • libvirt-go 的一个坑

    2020-01-07

    #golang#libvirt#libvirt-go

  • 在AWS 的Amazon Linux 上下载源码

    2019-12-23

    #Linux#AWS#Kernel

  • 中文测试

    2019-12-23

    #test#输入法

  • Hello world

    2019-12-18

    #test#art

  • 肥叉烧
  • 安全大手子
  • 物理学博士
  • 一个愣头青
普通上班族,会一点Linux, 想学SDN