零拷贝技术前戏
在了解零拷贝之前,我们先了解下传统IO的读写操作
我们经常会使用下面两个函数向操作系统发起IO请求
//从文件或者网络socket读数据
read(file, tmp_buf, len);
//写数据到文件或者网络socket
write(socket, tmp_buf, len);
这两个函数,会阻塞客户端线程,直到IO操作完成。读写经历的过程通过下图来理解:
从上下文切换与数据拷贝两个方面来描述上面传统IO读写过程
1、首先应用程序发起read 系统调用,此时发生了从应用程序态转化成内核态一次转换
2、操作系统接收到read系统调用后,通过DMA(Direct Memory Access, 协助cpu拷贝数据的工具) copy 将硬件磁盘上的数据拷贝到内核缓冲区,此时发生了一次数据拷贝
3、从内核缓冲区拷贝数据到用户缓冲区,发生了一次数据拷贝与上写文切换
4、此时再发起系统write调用,从用户态转化成系统态,发了一次上下文切换
4、从用户缓冲区拷贝数据到socket缓冲区,发生了一次数据拷贝,并发生了一次上下文切换
5、DMA再将socket缓冲区数据拷贝到协议层,发生了一次数据拷贝
总结:以上过程共发生了4次上下文切换,数据拷贝,其实很多拷贝与切换操作不是必须的,造成IO性能低下
那么有没有办法避免上面提到的多次数据拷贝和上下文切换呢?这个时候零拷贝技术就可以上场了。零拷贝技术通常有两种方式实现。
零拷贝杀手锏之一mmap+write
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);
流程梳理:
1、调用mmap系统调用,DMA将磁盘数据拷贝到内核缓冲区,随后,内核缓冲区与用户缓冲区共享该数据,并不会发生复制操作
2、接着写系统调用执行,将内核缓冲区数据拷贝到socket缓冲区
3、最后DM将socket缓冲区数据拷贝到协议层引擎,完成数据传输
总结:mmap方式减少了一半的上下文切换与数据拷贝开销,存在1个问题
1、共享内核数据时候,当内存映射一个文件,然后在另一个进程截断同一文件时调用write时,文件将可能被并发修改。写入系统调用将被总线错误信号SIGBUS中断,该信号的默认行为是终止进程并转储核心。有两种方式解决该问题:
第一种可以通过自定义信号处理器,处理该信号,发挥中断前写好的数据,并将失败标志设置返回成功
第二种解决方案涉及从内核中租借文件(在微软Windows中称为“机会锁定”),操作系统会实时发送信号量给你,标志有其他程序需要使用该文件,可以在需要该文件前租约该文件,使用完再销毁该租约,类似于分布式锁,先锁住,我要用,用完了别人才能用。
零拷贝杀手锏之二sendfile机制
sendfile机制不仅减少了上下文切换也减少了数据拷贝,减少网络与本地文件之间拷贝次数。
分析以上过程发现
1、应用程序发起sendfile调用,DMA引擎将文件内容复制到内核缓冲区中
2、没有数据被复制到套接字缓冲区中。相反,只有包含数据位置和长度信息的描述符才会附加到套接字缓冲区。DMA引擎将数据直接从内核缓冲区传递到协议引擎,从而消除剩余的最终副本
3、因为数据实际上仍然是从磁盘复制到内存和从内存复制到导线,所以有些人可能会认为这不是真正的零拷贝。不过,从操作系统的角度来看,这是零拷贝,因为数据不会在内核缓冲区之间重复。使用零拷贝时,除了避免拷贝之外,还可以获得其他性能优势,例如更少的上下文切换、更少的CPU数据缓存污染和无CPU校验和计算。
总结
零拷贝技术对于网络IO的优化作用非常有利,本文分析了令拷贝出现的价值,零拷贝实现的手段,在很多中间件中使用了比较多,比如netty,kafka,rocketmq,zookeeper,redis,可知零拷贝技术是非常有用的,它大大的改善了传统IO慢的问题,提升服务IO整体性能。