关于skb_header_pointer函数

本文详细解析了skb_header_pointer函数的功能及其实现原理,探讨了它如何处理非线性skb包,尤其是在Linux 2.6内核碎片重组后的场景下。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


             关于skb_header_pointer函数 2012-02-21 15:17:46

分类:

原文地址:关于skb_header_pointer函数 作者:luoyan_xy

   最近一段时间看内核代码,总是看到skb_header_pointer函数,这个函数的主要功能很简单,就是从skb字段中获取指定长度到内容到缓存中。函数原型是这个样子的:
static inline void *skb_header_pointer(const struct sk_buff *skb, int offset,
           int len, void *buffer)
   也就是从skb中skb->data开始的offset偏移处,获取len长度的内容到buff中。
  
   看起来这个并没有什么问题,不过无意中在网上搜了一下这个函数,却发现还真有一些别的意义在里面。。好吧,看一下代码,依然是2.6.30版本内核。
static inline void *skb_header_pointer(const struct sk_buff *skb, int offset,
           int len, void *buffer)
    {
       int hlen = skb_headlen(skb);
 
       if (hlen - offset >= len)
          return skb->data + offset;
 
       if ( skb_copy_bits(skb, offset, buffer, len) < 0)
          return NULL;
 
       return buffer;
    }
参数为:
   skb:数据包struct sk_buff的指针
   offset:相对数据起始头(如IP头)的偏移量
   len:数据长度
   buffer:缓冲区,大小不小于len
 
   其中skb_headlen()函数的定义为:
static inline unsigned int skb_headlen(const struct sk_buff *skb)
   {
       return skb->len - skb->data_len;
   }
   其中skb->len是数据包长度,在IPv4中就是单个完整IP包的总长,但这些数据并不一定都在当前内存页;skb->data_len表示在其他页的数据长度(包括本skb在其他页中的数据以及分片skb中的数据),因此skb->len - skb->data_len表示在当前页的数据大小。
 
   如果skb->data_len不为0,表示该IP包的数据分属不同的页,该数据包也就被成为非线性化的,函数skb_is_nonlinear()就是通过该参数判断,一般刚进行完碎片重组的skb包就属于此类。
   这样skb_header_pointer()函数就好理解了,先判断要处理的数据是否都在当前页面内,如果是,则返回可以直接对数据处理,返回所求数据指针,否则用skb_copy_bits()函数进行拷贝,下面再来看一下这个函数的实现过程,并不复杂。
 
   /* Copy some data bits from skb to kernel buffer. */
int skb_copy_bits(const struct sk_buff *skb, int offset, void *to, int len)
   {
      int i, copy;
      int start = skb_headlen(skb);
 
      if (offset > (int)skb->len - len)
         goto fault;
 
      /* Copy header. */
      /*拷贝在本页中的部分*/
      if ((copy = start - offset) > 0) {
         if (copy > len)
             copy = len;
         skb_copy_from_linear_data_offset(skb, offset, to, copy);
      if ((len -= copy) == 0)
         return 0;
      offset += copy;
      to     += copy;
     }
 
      /*拷贝本skb中其他碎片的部分*/
      for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
         int end;
 
         WARN_ON(start > offset + len);
 
         end = start + skb_shinfo(skb)->frags[i].size;
         if ((copy = end - offset) > 0) {
              u8 *vaddr;
 
              if (copy > len)
                 copy = len;
 
              vaddr = kmap_skb_frag(&skb_shinfo(skb)->frags[i]);
              memcpy(to,
                    vaddr + skb_shinfo(skb)->frags[i].page_offset+
                    offset - start, copy);
              kunmap_skb_frag(vaddr);
 
              if ((len -= copy) == 0)
                  return 0;
              offset += copy;
              to     += copy;
         }
         start = end;
     }
 
     /*拷贝其他碎片skb中的数据部分,对于skb的递归调用*/
     if (skb_shinfo(skb)->frag_list) {
         struct sk_buff *list = skb_shinfo(skb)->frag_list;
 
         for (; list; list = list->next) {
            int end;
 
            WARN_ON(start > offset + len);
 
            end = start + list->len;
            if ((copy = end - offset) > 0) {
                if (copy > len)
                    copy = len;
                if ( skb_copy_bits(list, offset - start,
                               to, copy))
                    goto fault;
                if ((len -= copy) == 0)
                    return 0;
                offset += copy;
                to     += copy;
            }
            start = end;
        }
   }
   if (!len)
       return 0;
 
fault:
   return -EFAULT;
}
 
   其中的skb_copy_from_linear_data_offset函数,就是一个线性拷贝的过程,内部是对memcpy的一个封装。
   kmap_skb_frap没有看明白,不过其意思应该是把分片数据所在的page地址映射到了一个内核可访问的虚拟地址上,通过这个虚拟地址完成数据的拷贝,最后再通过kunmap_skb_frag完成映射地址的释放。
 
 
   这些都不是重点,主要的是我在网上看到了这样一些话,便直接把它们拷贝过来吧:
 
   在2.4中是没有这一函数的,因为2.4的netfilter首先进行碎片包重组,随即进行skb的线性化检查,对非线性skb包进行线性化,因此合法skb包进入后续hook点操作时实际skb->data_len就都是0了,可以直接操作。
 
   netfilter的碎片重组函数为ip_ct_gather_frags(),在2.4中碎片重组完还进行线性化,而2.6中重组完就直接返回了,并不进行线性化操作,因此以后在使用的时候必须检查要处理的数据是否在内存页面中。
 
   由于2.6中的碎片重组操作后不进行skb数据包的线性化,因此数据可能存在于不同的内存页面中,对于不在同一页面中的情况不能直接进行数据操作,需要将数据拷贝到一个单独缓冲区后再进行处理。
 
   关于内核为什么这样做,我没找到标志答案,下面是一个网友的回复,也粘贴过来,有机会的话就慢慢了解了:
  
   非线性化很重要的一点是为了支持网卡芯片的一些功能,这些功能可以大大增加TCP的性能。

   如TSO(tcp segment offload),在sendfile中。 系统只是增加file对应的page cache 的引用数,接着将一个个页面放在SKB中发送,一次可以放接近65535 - TCP头的数据。网卡如E1000,会根据MSS大小切割报文并计算校验后发送。
<think>我们正在讨论Linux内核网络处理中的skb_reset_network_header函数的使用。这个函数用于重置套接字缓冲区(skb)中的网络层头部指针。 根据之前提供的引用[3],我们看到了alloc_skb函数的实现,它用于分配skb。而引用[4]提到了IPoIB头部结构,这涉及到网络层头部的处理。 因此,我们需要解释skb_reset_network_header的作用以及如何使用它。 在Linux内核中,sk_buff结构(简称skb)是网络数据包在内核中的表示。一个skb包含多个指针,用于标记数据包中各个层次的头部位置: - 传输层头部(如TCP/UDP) - 网络层头部(如IP) - 链路层头部(如以太网头部) skb_reset_network_header函数用于将网络层头部指针(network_header)重置为数据区的起始位置(即head指针指向的位置加上data指针的偏移量,但通常我们会在构建数据包时调整这些指针)。 函数原型(在include/linux/skbuff.h中): static inline void skb_reset_network_header(struct sk_buff *skb) 使用场景:当我们分配了一个skb并准备构建网络层数据包时,通常需要设置网络层头部的位置。例如,在分配skb后,我们可能将数据包的负载(payload)放入skb的数据区,然后设置网络层头部指针指向数据区的某个位置(即网络层头部的起始位置)。如果我们已经将网络层头部放在数据区的开头,那么就可以调用skb_reset_network_header(skb)来将network_header指向数据区的起始位置(即skb->data)。 注意:在调用skb_reset_network_header之前,通常需要确保skb->data指向网络层头部的起始位置。 步骤: 1. 分配skb(例如使用alloc_skb)。 2. 使用skb_reserve调整data指针,为各层头部预留空间(可选,但常见做法)。 3. 将网络层协议的数据(如IP头)复制到skb的数据区中,并确保skb->data指向该网络层头部的起始位置。 4. 调用skb_reset_network_header(skb)来设置network_header。 示例代码片段(假设我们正在构建一个IP数据包): ```c #include <linux/skbuff.h> #include <linux/ip.h> struct sk_buff *skb; struct iphdr *iph; // 分配一个足够大的skb,包括链路层头部、IP头部和负载 skb = alloc_skb(len + LL_HEADER_LEN + IP_HEADER_LEN, GFP_ATOMIC); if (!skb) return -ENOMEM; // 预留空间:将data指针向后移动,为链路层头部和网络层头部预留空间 skb_reserve(skb, LL_HEADER_LEN + IP_HEADER_LEN); // 现在,将data指针向前移动IP_HEADER_LEN,这样我们就可以开始填充IP头了 skb_push(skb, IP_HEADER_LEN); // 获取指向IP头部的指针 iph = (struct iphdr *)skb->data; // 填充IP头部 iph->version = 4; iph->ihl = 5; // 5个32位字,即20字节 // ... 其他字段 // 重置网络层头部指针:此时skb->data指向IP头部的起始位置,所以我们可以重置网络层头部 skb_reset_network_header(skb); // 现在我们可以使用网络层头部相关的辅助函数,例如ip_hdr(skb)来获取IP头 // 注意:在设置好网络层头部后,我们可以继续添加传输层头部和负载 // 例如,添加TCP头部和负载 skb_push(skb, TCP_HEADER_LEN); // 填充TCP头部... // 设置传输层头部 skb_reset_transport_header(skb); // 设置传输层头部指针 ``` 注意:上面的示例中,我们使用skb_push来调整data指针,以便为IP头部预留空间(在之前已经预留了链路层和IP层的基础上,因为我们之前预留了LL_HEADER_LEN+IP_HEADER_LEN,然后通过skb_push(skb, IP_HEADER_LEN)将data指针向前移动IP_HEADER_LEN,这样我们就有了IP头部的空间)。 另外,我们也可以不在一开始预留空间,而是分步进行。但通常,预留空间是常见的做法。 重要辅助函数: - skb_reset_network_header: 将network_header设置为当前data指针的位置。 - skb_set_network_header: 可以设置network_header为相对于head指针的偏移量(如果你知道确切的偏移量)。 - ip_hdr(skb): 返回指向IP头部的指针(在已经设置好network_header后使用)。 引用[3]提到了alloc_skb函数,引用[4]提到了网络层头部结构(ipoib_header),这都与我们的主题相关。 因此,skb_reset_network_header的使用是在构建或修改网络数据包时,确保网络层头部指针正确指向数据包中的网络层头部起始位置。 注意:在接收路径中,当网络设备驱动接收到一个数据包并分配了skb后,通常也会设置这些头部指针,以便上层协议栈处理。但此时,驱动可能会调用类似eth_type_trans的函数来设置协议类型,并设置头部指针。 相关问题: 1. 在Linux内核中,如何分配一个套接字缓冲区(skb)? 2. 除了skb_reset_network_header,还有哪些用于设置skb头部指针的函数? 3. 在接收数据包时,网络设备驱动是如何处理skb的头部指针的? 4. 如何访问skb中的传输层头部? 我们将在回答中引用相关的引用[3]和[4]。</think>### Linux 内核中 `skb_reset_network_header` 的使用 在 Linux 内核网络协议栈中,`skb_reset_network_header` 是处理套接字缓冲区(`struct sk_buff`,简称 **skb**)的关键函数,用于重置网络层头部指针。以下是详细说明: --- #### **功能与作用** - **核心目的**:将 `skb->network_header` 指针重置为当前 `skb->data` 的位置,表明**网络层头部起始于此**。 - **使用场景**: - 构建新的网络层数据包(如 IP 报文)时。 - 修改或重组 skb 后需重新定位网络层头部。 - 在驱动接收路径中初始化 skb 的协议头位置(参考引用[4]的 IPOIB 头部处理流程)[^4]。 --- #### **函数原型与依赖** ```c #include <linux/skbuff.h> void skb_reset_network_header(struct sk_buff *skb); ``` - **参数**:`skb` 目标套接字缓冲区。 - **关联操作**: - `skb_push(skb, len)`:扩展数据区头部空间(常用于添加协议头)。 - `skb_set_network_header(skb, offset)`:直接设置 `network_header` 的偏移量。 --- #### **典型使用流程** 以下示例展示如何构建一个 IP 数据包: ```c struct sk_buff *skb; struct iphdr *iph; // 1. 分配 skb(参考引用[3]的 alloc_skb 优化)[^3] skb = alloc_skb(len + LL_HEADER_LEN + IP_HEADER_LEN, GFP_ATOMIC); if (!skb) return -ENOMEM; // 2. 预留空间:链路层头 + IP 头 skb_reserve(skb, LL_HEADER_LEN + IP_HEADER_LEN); // 3. 填充负载数据(如 TCP/UDP 报文) skb_put(skb, payload_len); memcpy(skb->data, payload, payload_len); // 4. 添加 IP 头部空间 skb_push(skb, IP_HEADER_LEN); iph = (struct iphdr *)skb->data; // 5. 填充 IP 头字段 iph->version = 4; iph->ihl = 5; iph->tot_len = htons(skb->len); // ... 其他字段初始化 // 6. 关键步骤:重置网络层头部指针 skb_reset_network_header(skb); // 此时 network_header = data // 7. 设置协议类型(可选) skb->protocol = htons(ETH_P_IP); ``` --- #### **关键注意事项** 1. **顺序要求**: - 必须在 `skb_push` 扩展头部空间**后调用**,确保 `skb->data` 指向网络层头部起始位置。 - 若未正确重置,上层协议栈(如 IP 层)无法通过 `ip_hdr(skb)` 获取头部。 2. **性能优化**: - 与 `alloc_skb` 类似(引用[3]),此函数设计为 `inline` 以减少函数调用开销[^3]。 3. **接收路径处理**: - 在网卡驱动中,收到数据包后需根据协议类型调用 `skb_reset_network_header`,以便协议栈解析(参考引用[4]的 IPOIB 处理流程)[^4]。 --- #### **常见问题分析** - **Q**: 为何调用后 `ip_hdr(skb)` 仍返回错误地址? **A**: 检查 `skb->data` 是否指向 IP 头部起始位置。可能需要先执行 `skb_push` 或调整偏移量。 - **Q**: 能否用于传输层头部重置? **A**: 否!传输层需使用 `skb_reset_transport_header`。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值