linux内核sk_buff的结构分析

来源:互联网 发布:opencv3.0 sift算法 编辑:程序博客网 时间:2024/05/16 17:26

 

原文地址:http://simohayha.iteye.com/blog/556168

 

我看的内核版本是2.6.32.

在内核中sk_buff表示一个网络数据包,它是一个双向链表,而链表头就是sk_buff_head,在老的内核里面sk_buff会有一个list域直接指向sk_buff_head也就是链表头,现在在2.6.32里面这个域已经被删除了。

而sk_buff的内存布局可以分作3个段,第一个就是sk_buff自身,第二个是linear-data buff,第三个是paged-data buff(也就是skb_shared_info)。



ok.我们先来看sk_buff_head的结构。它也就是所有sk_buff的头。

Java代码 复制代码 收藏代码
  1. struct sk_buff_head { 
  2.     /* These two members must be first. */ 
  3.     struct sk_buff  *next; 
  4.     struct sk_buff  *prev; 
  5.  
  6.     __u32       qlen; 
  7.     spinlock_t  lock; 
  8. }; 


这里可以看到前两个域是和sk_buff一致的,而且内核的注释是必须放到最前面。这里的原因是:

这使得两个不同的结构可以放到同一个链表中,尽管sk_buff_head要比sk_buff小巧的多。另外,相同的函数可以同样应用于sk_buff和sk_buff_head。

然后qlen域表示了当前的sk_buff链上包含多少个skb。

lock域是自旋锁。

然后我们来看sk_buff,下面就是skb的结构:

我这里注释了一些简单的域,复杂的域下面会单独解释。
Java代码 复制代码 收藏代码
  1. struct sk_buff { 
  2.     /* These two members must be first. */ 
  3.     struct sk_buff      *next; 
  4.     struct sk_buff      *prev; 
  5.  
  6. //表示从属于那个socket,主要是被4层用到。 
  7.     struct sock     *sk; 
  8. //表示这个skb被接收的时间。 
  9.     ktime_t         tstamp; 
  10. //这个表示一个网络设备,当skb为输出时它表示skb将要输出的设备,当接收时,它表示输入设备。要注意,这个设备有可能会是虚拟设备(在3层以上看来) 
  11.     struct net_device   *dev; 
  12. ///这里其实应该是dst_entry类型,不知道为什么内核要改为ul。这个域主要用于路由子系统。这个数据结构保存了一些路由相关信息 
  13.     unsigned long       _skb_dst; 
  14. #ifdef CONFIG_XFRM 
  15.     struct  sec_path    *sp; 
  16. #endif 
  17. ///这个域很重要,我们下面会详细说明。这里只需要知道这个域是保存每层的控制信息的就够了。 
  18.     char            cb[48]; 
  19. ///这个长度表示当前的skb中的数据的长度,这个长度即包括buf中的数据也包括切片的数据,也就是保存在skb_shared_info中的数据。这个值是会随着从一层到另一层而改变的。下面我们会对比这几个长度的。 
  20.     unsigned int        len, 
  21. ///这个长度只表示切片数据的长度,也就是skb_shared_info中的长度。 
  22.                 data_len; 
  23. ///这个长度表示mac头的长度(2层的头的长度) 
  24.     __u16           mac_len, 
  25. ///这个主要用于clone的时候,它表示clone的skb的头的长度。 
  26.                 hdr_len; 
  27.  
  28. ///接下来是校验相关的域。 
  29.     union { 
  30.         __wsum      csum; 
  31.         struct { 
  32.             __u16   csum_start; 
  33.             __u16   csum_offset; 
  34.         }; 
  35.     }; 
  36. ///优先级,主要用于QOS。 
  37.     __u32           priority; 
  38.     kmemcheck_bitfield_begin(flags1); 
  39. ///接下来是一些标志位。 
  40. //首先是是否可以本地切片的标志。 
  41.     __u8            local_df:1
  42. ///为1说明头可能被clone。 
  43.                 cloned:1
  44. ///这个表示校验相关的一个标记,表示硬件驱动是否为我们已经进行了校验(前面的blog有介绍) 
  45.                 ip_summed:2
  46. ///这个域如果为1,则说明这个skb的头域指针已经分配完毕,因此这个时候计算头的长度只需要head和data的差就可以了。 
  47.                 nohdr:1
  48. ///这个域不太理解什么意思。 
  49.                 nfctinfo:3
  50.  
  51. ///pkt_type主要是表示数据包的类型,比如多播,单播,回环等等。 
  52.     __u8            pkt_type:3
  53. ///这个域是一个clone标记。主要是在fast clone中被设置,我们后面讲到fast clone时会详细介绍这个域。 
  54.                 fclone:2
  55. ///ipvs拥有的域。 
  56.                 ipvs_property:1
  57. ///这个域应该是udp使用的一个域。表示只是查看数据。 
  58.                 peeked:1
  59. ///netfilter使用的域。是一个trace 标记 
  60.                 nf_trace:1
  61. ///这个表示L3层的协议。比如IP,IPV6等等。 
  62.     __be16          protocol:16
  63.     kmemcheck_bitfield_end(flags1); 
  64. ///skb的析构函数,一般都是设置为sock_rfree或者sock_wfree. 
  65.     void            (*destructor)(struct sk_buff *skb); 
  66.  
  67. ///netfilter相关的域。 
  68. #if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE) 
  69.     struct nf_conntrack *nfct; 
  70.     struct sk_buff      *nfct_reasm; 
  71. #endif 
  72. #ifdef CONFIG_BRIDGE_NETFILTER 
  73.     struct nf_bridge_info   *nf_bridge; 
  74. #endif 
  75.  
  76. ///接收设备的index。 
  77.     int         iif; 
  78.  
  79. ///流量控制的相关域。 
  80. #ifdef CONFIG_NET_SCHED 
  81.     __u16           tc_index;   /* traffic control index */ 
  82. #ifdef CONFIG_NET_CLS_ACT 
  83.     __u16           tc_verd;    /* traffic control verdict */ 
  84. #endif 
  85. #endif 
  86.  
  87.     kmemcheck_bitfield_begin(flags2); 
  88. ///多队列设备的映射,也就是说映射到那个队列。 
  89.     __u16           queue_mapping:16
  90. #ifdef CONFIG_IPV6_NDISC_NODETYPE 
  91.     __u8            ndisc_nodetype:2
  92. #endif 
  93.     kmemcheck_bitfield_end(flags2); 
  94.  
  95.     /* 0/14 bit hole */ 
  96.  
  97. #ifdef CONFIG_NET_DMA 
  98.     dma_cookie_t        dma_cookie; 
  99. #endif 
  100. #ifdef CONFIG_NETWORK_SECMARK 
  101.     __u32           secmark; 
  102. #endif 
  103. ///skb的标记。 
  104.     __u32           mark; 
  105.  
  106. ///vlan的控制tag。 
  107.     __u16           vlan_tci; 
  108.  
  109. ///传输层的头 
  110.     sk_buff_data_t      transport_header; 
  111. ///网络层的头 
  112.     sk_buff_data_t      network_header; 
  113. ///链路层的头。 
  114.     sk_buff_data_t      mac_header; 
  115. ///接下来就是几个操作skb数据的指针。下面会详细介绍。 
  116.     sk_buff_data_t      tail; 
  117.     sk_buff_data_t      end; 
  118.     unsigned char       *head, 
  119.                 *data; 
  120. ///这个表示整个skb的大小,包括skb本身,以及数据。 
  121.     unsigned int        truesize; 
  122. ///skb的引用计数 
  123.     atomic_t        users; 
  124. }; 


我们来看前面没有解释的那些域。

先来看cb域,他保存了每层所独自需要的内部数据。我们来看tcp的例子。

我们知道tcp层的控制信息保存在tcp_skb_cb中,因此来看内核提供的宏来存取这个数据结构:

Java代码 复制代码 收藏代码
  1. #define TCP_SKB_CB(__skb)  ((struct tcp_skb_cb *)&((__skb)->cb[0])) 


在ip层的话,我们可能会用cb来存取切片好的帧。

Java代码 复制代码 收藏代码
  1. #define FRAG_CB(skb)    ((struct ipfrag_skb_cb *)((skb)->cb)) 


到这里你可能会问如果我们想要在到达下一层后,还想保存当前层的私有信息怎么办。这个时候我们就可以使用skb的clone了。也就是之只复制sk_buff结构。

然后我们来看几个比较比较重要的域 len,data,tail,head,end。

这几个域都很简单,下面这张图表示了buffer从tcp层到链路层的过程中len,head,data,tail以及end的变化,通过这个图我们可以非常清晰的了解到这几个域的区别。




可以很清楚的看到head指针为分配的buffer的起始位置,end为结束位置,而data为当前数据的起始位置,tail为当前数据的结束位置。len就是数据区的长度。

然后来看transport_header,network_header以及mac_header的变化,这几个指针都是随着数据包到达不同的层次才会有对应的值,我们来看下面的图,这个图表示了当从2层到达3层对应的指针的变化。




这里可以看到data指针会由于数据包到了三层,而跳过2层的头。这里我们就可以得到data起始真正指的是本层的头以及数据的起始位置。

然后我们来看skb的几个重要操作函数。

首先是skb_put,skb_push,skb_pull以及skb_reserve这几个最长用的操作data指针的函数。

这里可以看到内核skb_XXX都还有一个__skb_XXX函数,这是因为前一个只是将后一个函数进行了一个包装,加了一些校验。

先来看__skb_put函数。
可以看到它只是将tail指针移动len个位置,然后len也相应的增加len个大小。

Java代码 复制代码 收藏代码
  1. static inline unsigned char *__skb_put(struct sk_buff *skb, unsignedint len) 
  2.     unsigned char *tmp = skb_tail_pointer(skb); 
  3.     SKB_LINEAR_ASSERT(skb); 
  4. ///改变相应的域。 
  5.     skb->tail += len; 
  6.     skb->len  += len; 
  7.     return tmp; 


然后是__skb_push,它是将data指针向上移动len个位置,对应的len肯定也是增加len大小。

Java代码 复制代码 收藏代码
  1. static inline unsigned char *__skb_push(struct sk_buff *skb, unsignedint len) 
  2.     skb->data -= len; 
  3.     skb->len  += len; 
  4.     return skb->data; 


剩下的两个就不贴代码了,都是很简单的函数,__skb_pull是将data指针向下移动len个位置,然后len减小len大小。__skb_reserve是将整个数据区,也就是data以及tail指针一起向下移动len大小。这个函数一般是用来对齐地址用的。

看下面的图,描述了4个函数的操作:




接着是skb的alloc函数。

在内核中分配一个skb是在__alloc_skb中实现的,接下来我们就来看这个函数的具体实现。

这个函数起始可以看作三部分,第一部分是从cache中分配内存,第二部分是初始化分配的skb的相关域。第三部分是处理fclone。

还有一个要注意的就是这里__alloc_skb是被三个函数包装后才能直接使用的,我们只看前两个,一个是skb_alloc_skb,一个是alloc_skb_fclone函数,这两个函数传递进来的第三个参数,也就是fclone前一个是0,后一个是1.

那么这个函数是什么意思呢,它和alloc_skb有什么区别的。

这个函数可以叫做Fast SKB cloning函数,这个函数存在的主要原因是,以前我们每次skb_clone一个skb的时候,都是要调用kmem_cache_alloc从cache中alloc一块新的内存。而现在当我们拥有了fast clone之后,通过调用alloc_skb_fclone函数来分配一块大于sizeof(struct sk_buff)的内存,也就是在这次请求的skb的下方多申请了一些内存,然后返回的时候设置返回的skb的fclone标记为SKB_FCLONE_ORIG,而多申请的那块内存的sk_buff的fclone为SKB_FCLONE_UNAVAILABLE,这样当我们调用skb_clone克隆这个skb的时候看到fclone的标记就可以直接将skb的指针+1,而不需要从cache中取了。这样的话节省了一次内存存取,提高了clone的效率,不过调用flcone 一般都是我们确定接下来这个skb会被clone很多次。

更详细的fclone的介绍可以看这里:

http://lwn.net/Articles/140552/

这样我们先来看_alloc_skb,然后紧接着看skb_clone,这样就能更好的理解这些。

这里fclone的多分配的内存部分,没太弄懂从那里多分配的,自己对内核的内存子系统还是不太熟悉。觉得应该是skbuff_fclone_cache中会自动多分配些内存。

Java代码 复制代码 收藏代码
  1. struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask, 
  2.                 int fclone, int node) 
  3.     struct kmem_cache *cache; 
  4.     struct skb_shared_info *shinfo; 
  5.     struct sk_buff *skb; 
  6.     u8 *data; 
  7.  
  8. ///这里通过fclone的值来判断是要从fclone cache还是说从head cache中取。 
  9.     cache = fclone ? skbuff_fclone_cache : skbuff_head_cache; 
  10.  
  11. ///首先是分配skb,也就是包头。 
  12.     skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node); 
  13.     if (!skb) 
  14.         goto out; 
  15. ///首先将size对齐,这里是按一级缓存的大小来对齐。 
  16.     size = SKB_DATA_ALIGN(size); 
  17. ///然后是数据区的大小,大小为size+ sizeof(struct skb_shared_info的大小。 
  18.     data = kmalloc_node_track_caller(size + sizeof(struct skb_shared_info), 
  19.             gfp_mask, node); 
  20.     if (!data) 
  21.         goto nodata; 
  22.  
  23. ///初始化相关域。 
  24.     memset(skb, 0, offsetof(struct sk_buff, tail)); 
  25. ///这里truesize可以看到就是我们分配的整个skb+data的大小 
  26.     skb->truesize = size + sizeof(struct sk_buff); 
  27. ///users加一。 
  28.     atomic_set(&skb->users, 1); 
  29. ///一开始head和data是一样大的。 
  30.     skb->head = data; 
  31.     skb->data = data; 
  32. ///设置tail指针 
  33.     skb_reset_tail_pointer(skb); 
  34. ///一开始tail也就是和data是相同的。 
  35.     skb->end = skb->tail + size; 
  36.     kmemcheck_annotate_bitfield(skb, flags1); 
  37.     kmemcheck_annotate_bitfield(skb, flags2); 
  38. #ifdef NET_SKBUFF_DATA_USES_OFFSET 
  39.     skb->mac_header = ~0U; 
  40. #endif 
  41.  
  42. ///初始化shinfo,这个我就不介绍了,前面的blog分析切片时,这个结构很详细的分析过了。 
  43.     shinfo = skb_shinfo(skb); 
  44.     atomic_set(&shinfo->dataref, 1); 
  45.     shinfo->nr_frags  = 0
  46.     shinfo->gso_size = 0
  47.     shinfo->gso_segs = 0
  48.     shinfo->gso_type = 0
  49.     shinfo->ip6_frag_id = 0
  50.     shinfo->tx_flags.flags = 0
  51.     skb_frag_list_init(skb); 
  52.     memset(&shinfo->hwtstamps, 0, sizeof(shinfo->hwtstamps)); 
  53.  
  54. ///fclone为1,说明多分配了一块内存,因此需要设置对应的fclone域。 
  55.     if (fclone) { 
  56. ///可以看到多分配的内存刚好在当前的skb的下方。 
  57.         struct sk_buff *child = skb + 1
  58.         atomic_t *fclone_ref = (atomic_t *) (child + 1); 
  59.  
  60.         kmemcheck_annotate_bitfield(child, flags1); 
  61.         kmemcheck_annotate_bitfield(child, flags2); 
  62. ///设置标记。这里要注意,当前的skb和多分配的skb设置的fclone是不同的。 
  63.         skb->fclone = SKB_FCLONE_ORIG; 
  64.         atomic_set(fclone_ref, 1); 
  65.  
  66.         child->fclone = SKB_FCLONE_UNAVAILABLE; 
  67.     } 
  68. out: 
  69.     return skb; 
  70. nodata: 
  71.     kmem_cache_free(cache, skb); 
  72.     skb = NULL; 
  73.     goto out; 


下图就是alloc_skb之后的skb的指针的状态。这里忽略了fclone。




然后我们来看skb_clone函数,clone的意思就是只复制skb而不复制data域。

这里它会先判断将要被clone的skb的fclone段,以便与决定是否重新分配一块内存来保存skb。

然后调用__skb_clone来初始化相关的域。

Java代码 复制代码 收藏代码
  1. struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t gfp_mask) 
  2.     struct sk_buff *n; 
  3.  
  4. ///n为skb紧跟着那块内存,这里如果skb是通过skb_fclone分配的,那么n就是一个skb。 
  5.     n = skb + 1
  6. ///skb和n的fclone都要符合要求,可以看到这里的值就是我们在__alloc_skb中设置的值。 
  7.     if (skb->fclone == SKB_FCLONE_ORIG && 
  8.         n->fclone == SKB_FCLONE_UNAVAILABLE) { 
  9. ///到这里,就说明我们不需要alloc一个skb,直接取n就可以了,并且设置fclone的标记。并修改引用计数。 
  10.         atomic_t *fclone_ref = (atomic_t *) (n + 1); 
  11.         n->fclone = SKB_FCLONE_CLONE; 
  12.         atomic_inc(fclone_ref); 
  13.     } else
  14.  
  15. ///这里就需要从cache中取得一块内存。 
  16.         n = kmem_cache_alloc(skbuff_head_cache, gfp_mask); 
  17.         if (!n) 
  18.             return NULL; 
  19.  
  20.         kmemcheck_annotate_bitfield(n, flags1); 
  21.         kmemcheck_annotate_bitfield(n, flags2); 
  22. ///设置新的skb的fclone域。这里我们新建的skb,没有被fclone的都是这个标记。 
  23.         n->fclone = SKB_FCLONE_UNAVAILABLE; 
  24.     } 
  25.  
  26.     return __skb_clone(n, skb); 


这里__skb_clone就不介绍了,函数就是将要被clone的skb的域赋值给clone的skb。

下图就是skb_clone之后的两个skb的结构图:



当一个skb被clone之后,这个skb的数据区是不能被修改的,这就意为着,我们存取数据不需要任何锁。可是有时我们需要修改数据区,这个时候会有两个选择,一个是我们只修改linear段,也就是head和end之间的段,一种是我们还要修改切片数据,也就是skb_shared_info.

这样就有两个函数供我们选择,第一个是pskb_copy,第二个是skb_copy.

我们先来看pskb_copy,函数先alloc一个新的skb,然后调用skb_copy_from_linear_data来复制线性区的数据。

Java代码 复制代码 收藏代码
  1. struct sk_buff *pskb_copy(struct sk_buff *skb, gfp_t gfp_mask) 
  2.     /*
  3.      *  Allocate the copy buffer
  4.      */ 
  5.     struct sk_buff *n; 
  6. #ifdef NET_SKBUFF_DATA_USES_OFFSET 
  7.     n = alloc_skb(skb->end, gfp_mask); 
  8. #else 
  9.     n = alloc_skb(skb->end - skb->head, gfp_mask); 
  10. #endif 
  11.     if (!n) 
  12.         goto out; 
  13.  
  14.     /* Set the data pointer */ 
  15.     skb_reserve(n, skb->data - skb->head); 
  16.     /* Set the tail pointer and length */ 
  17.     skb_put(n, skb_headlen(skb)); 
  18. ///复制线性数据段。 
  19.     skb_copy_from_linear_data(skb, n->data, n->len); 
  20. ///更新相关域 
  21.     n->truesize += skb->data_len; 
  22.     n->data_len  = skb->data_len; 
  23.     n->len        = skb->len; 
  24.  
  25. ///下面只是复制切片数据的指针 
  26. if (skb_shinfo(skb)->nr_frags) { 
  27.         int i; 
  28.  
  29.         for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) { 
  30.             skb_shinfo(n)->frags[i] = skb_shinfo(skb)->frags[i]; 
  31.             get_page(skb_shinfo(n)->frags[i].page); 
  32.         } 
  33.         skb_shinfo(n)->nr_frags = i; 
  34.     } 
  35.  
  36. ............................... 
  37.     copy_skb_header(n, skb); 
  38. out: 
  39.     return n; 


然后是skb_copy,它是复制skb的所有数据段,包括切片数据:

Java代码 复制代码 收藏代码
  1. struct sk_buff *skb_copy(const struct sk_buff *skb, gfp_t gfp_mask) 
  2.     int headerlen = skb->data - skb->head; 
  3.     /*
  4.      *  Allocate the copy buffer
  5.      */ 
  6. //先alloc一个新的skb 
  7.     struct sk_buff *n; 
  8. #ifdef NET_SKBUFF_DATA_USES_OFFSET 
  9.     n = alloc_skb(skb->end + skb->data_len, gfp_mask); 
  10. #else 
  11.     n = alloc_skb(skb->end - skb->head + skb->data_len, gfp_mask); 
  12. #endif 
  13.     if (!n) 
  14.         return NULL; 
  15.  
  16.     /* Set the data pointer */ 
  17.     skb_reserve(n, headerlen); 
  18.     /* Set the tail pointer and length */ 
  19.     skb_put(n, skb->len); 
  20. ///然后复制所有的数据。 
  21.     if (skb_copy_bits(skb, -headerlen, n->head, headerlen + skb->len)) 
  22.         BUG(); 
  23.  
  24.     copy_skb_header(n, skb); 
  25.     return n; 


下面这张图就表示了psb_copy和skb_copy调用后的内存模型,其中a是pskb_copy,b是skb_copy:





最后来看skb的释放:
这里主要是判断一个引用标记位users,将它减一,如果大于0则直接返回,否则释放skb。

Java代码 复制代码 收藏代码
  1. void kfree_skb(struct sk_buff *skb) 
  2.     if (unlikely(!skb)) 
  3.         return
  4.     if (likely(atomic_read(&skb->users) ==1)) 
  5.         smp_rmb(); 
  6. ///减一,然后判断。 
  7.     else if (likely(!atomic_dec_and_test(&skb->users))) 
  8.         return
  9.     trace_kfree_skb(skb, __builtin_return_address(0)); 
  10.     __kfree_skb(skb); 
  11. }
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 cf的fps低怎么办win7 游戏倒闭冲的钱怎么办 一闭眼就做噩梦怎么办 吃鸡游戏上瘾了怎么办 使命召唤7很卡怎么办 w10升级系统卡死怎么办 答题卡写错位置怎么办 高考答错区域该怎么办 荒野行动画面中间有条横怎么办 荒野行动pc闪退怎么办 幽灵行动荒野子弹没了怎么办 看门狗2枪没子弹怎么办 爱奇艺不小心删除了本地视频怎么办 80岁老太太就爱闹肚子怎么办? 皇牌空战5弹药不够怎么办 辐射4玩着头晕怎么办 官司打赢了法院不给钱怎么办 电脑玩dnf太卡怎么办 soul被禁止私聊怎么办 刺激战场空投挂树上怎么办 由于经济原因心态不好怎么办 公司经济不好不裁员怎么办 家里经济不好没有钱怎么办 银行柜员找不到工作怎么办 在球队中打替补怎么办 大学生毕业后找不到工作怎么办 30岁不敢换工作怎么办 投完简历没回复怎么办 工业废气一年总量超标怎么办 安监局行政处罚没能力交怎么办 被社会淘汰的人怎么办 宝宝吐奶的时候怎么办 网友要我发红包怎么办 电脑久了很慢怎么办 影驰显卡花屏怎么办 反恐精英全球攻势加载地图慢怎么办 老滚5视角锁死了怎么办 苹果描述文件没有了怎么办 苹果6s发热严重怎么办 苹果6s发烫严重怎么办 手机型号不适配全军出击怎么办?