sk_buff 是Linux 网络中的重要数据结构,每个包的发送和接收都要处理这个结构体。
这个结构体比较长,只讨论部分字段。开头是一个联合,它要么在一个链表里,要么在一个rb tree(netem/tcp)里。
1 | struct sk_buff { |
接下来是一个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 | unsigned int len, |
跳过中间的部分,直接看到协议头相关的字段,这些头部字段保存的是基于skb->head 的偏移量,有一组skb_set/reset/_*_header() 的方法去设置他们,使用时,skb_*_header() 方法会将skb->head + offset 的结果返回。
1 | __be16 protocol; |
最后是skb 的尾部,这些字段必须位于sk_buff 结构的结尾,而且data 是可变长的部分,也就是前面提到的线性buffer,这里可以存放一部分包的数据。
1 |
|
这部分线性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 | struct bio_vec { |
该结构是一个page,len,offset 的三元组,指示了数据在哪个page 的哪个offset 处,长度为多少。
skb_shared_info 用于记录frag 相关的信息,nr_frags 指示了在frags[] 中有多少个frags.
1 | struct skb_shared_info { |
使用skb_shinfo() 可以获取skb_shared_info 的指针,而这个指针正是skb->end 指向的位置(或计算偏移量后的位置),也就是tailroom 的后面。
1 |
在接收数据时,驱动一般调用skb_add_rx_frag() 方法添加page buffer 数据,而该方法实际上调用了skb_fill_page_desc(),后者是会将page,offset,len (即skb_frag_t) 和新的frag 数量更新到skb_shared_info.
1 | void skb_add_rx_frag(struct sk_buff *skb, int i, struct page *page, int off, |
可以使用skb_header_pointer() 来获取指定位置指定大小的数据,该方法有两种返回值,如果offset + len 位于skb->data 中,则返回skb->data 对应偏移量的指针。否则将这部分数据拷贝到本地buffer 中,并返回这个buffer 的指针,当然如果offset+len 是不合理值该方法会返回null.
1 | static inline void * __must_check |
skb_copy_bits(const struct sk_buff *skb, int offset, void *to, int len)
可用于直接把offset 处len 长度的数据从sk_buff 中拷贝到to 中,它会检查线性buffer 的大小和offset 是否重合,从而正确从线性buffer 和page buffer 中读取到连续的数据。
参考链接