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大小切割报文并计算校验后发送。