一、什么是零拷贝?
在很多性能优化方案中都有提到零拷贝,零拷贝到底是怎么回事,是真的没有数据的拷贝吗?零拷贝(Zero-copy)是一种数据传输技术,旨在减少数据在内核态和用户态之间的复制操作。其实并不是真的没有数据的拷贝。
二、内核发送数据包
2.1 网卡启动准备
linux启动的时候,在网卡能够收发数据包之前,要做很多准备工作。比如ksoftirqd内核线程的创建,注册好协议处理函数,网卡驱动初始化等。这些初始化工作完成后,就可以启动网卡了。网卡启动的时候,会创建好RingBuffer,现在的服务器上的网卡一般都是支持多队列的,每一个队列都是由一个RingBuffer表示。
2.2 数据包发送
整体流程是:用户数据被拷贝到内核态,然后经过协议栈处理后进入网卡RingBuffer,网卡驱动真正将数据发送出去,当发送完成时,网卡发起硬中断来通知CPU,最后清理RingBuffer。
2.2.1 协议栈处理
用户进程进行系统调用时,找到内核的socket对象,之后进入内核协议栈处理。
在进入协议栈inet_sendmsg函数后,内核会找到socket对象上具体的协议发送函数,对于TCP协议就是tcp_sendmsg。
//file: net/ipv4/tcp.c
int tcp_sendmsg(...){while(...){......//申请内核态内存skbskb = sk_stream_alloc_skb(...);//把skb挂到socket的发送队列上,sk就是socketskb_entail(sk,skb);......//将用户空间的数据拷贝到skb,from是用户空间的数据地址skb_add_data_nocache(sk,skb,from,copy);}
}
注意:在协议栈处理这里,完成了一次用户数据到内核socket对象发送队列的拷贝。
2.2.1.1 传输层处理
//file: net/ipv4/tcp_output.c
static bool tcp_write_xmit(...){//循环从socket发送队列获取待发送skbwhile((skb = tcp_send_head(sk))){......//传输层发送tcp_transmit_skb(sk,skb,1,gfp);}
}
//file net/ipv4/tcp_output.c
static int tcp_transmit_skb(...){//循环socket发送队列克隆出新的skbif(likely(clone_it)){skb = skb_clone(skb,gfp_mask);......}//封装TCP头th = tcp_hdr(skb);th->source = inet->inet_sport;th->dest = inet->inet_dport;......//调用网络层发送接口ip_queue_xmit(...);
}
这里需要注意的是:传输层需要从socket发送队列克隆一个新的skb,那么为什么要复制一个skb出来?这是因为skb后续在调用网络层,最后到达网卡发送完成的时候,这个skb会被释放掉。而TCP协议是支持丢失重传的,在收到对方的ACK之前,socket发送队列上的skb不能被删除,等收到ACK再真正删除。 因此,传输层从socket发送队列拷贝skb也是不能少的。
自此,传输层的工作都完成了。数据离开传输层,接下来将会进入内核网络层的处理。
2.2.1.2 网络层处理
//file: net/ipv4/ip_output.c
int ip_queue_xmit(...){......//为skb设置路由表,路由表可以查到目的网络应该通过哪个网卡,哪个网关发送出去skb_dst_set_noref(skb,&rt->dst);//设置IP头iph = ip_hdr(skb);iph->protocol = sk->sk_protocol;......//发送ip_local_out(skb);
}
如果使用iptables配置了一些规则,那么这里将检测是否命中规则,如果设置复杂的netfiler规则,将会增大CPU开销。
2.2.1.3 邻居子系统处理
邻居子系统是位于网络层和数据链路层中间的一个系统,其作用是为网络层提供一个下层的封装,让下层决定发送到哪个MAC地址。
2.2.1.4 网络设备子系统处理
QDisc(queueing discipline )位于IP层和网卡的ringbuffer之间。ringbuffer是一个简单的FIFO队列,这种设计使网卡的驱动层保持简单和快速。而QDisc实现了流量管理的高级功能,包括流量分类,优先级和流量整形(rate-shaping)。
2.2.1.5 网卡驱动处理
在驱动函数中,会将skb挂到RingBuffer上,并且将skb数据映射到网卡可以访问的DMA内存区域。最后驱动会触发真实的数据发送。
三、零拷贝到底是怎么回事?
传统的read + send系统调用:
sendfile系统调用:
在sendfile系统调用中,数据不需要拷贝到用户空间,用户进程可直接操作内核Page Cache数据,减少了拷贝的次数,所以,零拷贝并不是说完全没有数据拷贝。java的fileChannel.transferTo()底层就是sendfile系统调用。