当应用层调用sendmsg将数据包从用户空间地址移动到内核空间地址,这个复制是ip_append_data函数的输入参数getfrag函数完成。内核还为应用层提供了sendfile接口,它优化数据包的发送复制,这个接口称为零拷贝。sendfile接口只有当网络设备支持Scatter/Gather I/O功能才能使用,这时的ip_append_page不需要复制任何数据,内核只需要将frags数组初始化指向接受数据包缓冲区位置,在必要的时候计算传输层的校验和。ip_append_page收到数据包的位置void*指针,就直接用收到的页面指针和偏移量来初始化frag数组的一个成员。
ip_append_data和ip_append_page的唯一不同在处理Scatter/Gather I/O功能,代码如下:
i = skb_shinfo(skb)->nr_frags;
if (len > size)
len = size;
//检查数据段是否可以和已有的数据段合并
if (skb_can_coalesce(skb, i, page, offset)) {
//可以合并,就加上数据段的长度
skb_shinfo(skb)->frags[i-1].size += len;
} else if (i < MAX_SKB_FRAGS) {
get_page(page);
//不能合并出事一个新的数据段
skb_fill_page_desc(skb, i, page, offset, len);
} else {
err = -EMSGSIZE;
goto error;
}
向页面加入一个新的数据段时,ip_append_page首先判断新的数据段是否可以和已有的数据段合并,这个检查由skb_can_coalesce函数完成,skb_can_coalesce检查新的数据段指针是否是页面中最后一个成员指针的结束位置,如果可以合并就更新页面中数据段的长度。
如果不能合并就调用skb_fill_page_desc初始化一个新的数据段,这时get_pae对页面的引用计数加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];
frag->page = page;
frag->page_offset = off;
frag->size = size;
//引用计数加1
skb_shinfo(skb)->nr_frags = i + 1;
}
目前只有UDP使用ip_append_page函数,TCP在tcp_sendmsg中实现了同样的逻辑对应零拷贝接口TCP使用do_tcp_sendpage。