我们假设一个场景,将本地文件上传到网络上,伪代码如下:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
注意,别看代码操作就读和写两个,实际在操作系统中涉及到4次以上的数据复制以及上下文切换,如图
注意:该图分成上下两部分,上面是上下文切换,下面是对应的数据在内存中的操作
过程是这样的:
步骤一:由user context切换到 kernel context,从磁盘读取文件数据,DMA引擎复制改数据到kernel buffer。这里涉及到一次上下文切换和数据复制。
步骤二:由kernel context 切换到user context,数据从kernel buffer 复制到user buffer,这里也涉及到一次上下文切换和cpu数据复制。
步骤三:由user context切换到kernel context,数据从user buffer 经cpu复制到socket buffer,这里涉及到一次上下文切换和数据复制。
步骤四:到达这一步的时候,操作系统会返回操作结果,什么意思呢,就是说你的数据没有传输完成就已经返回了,返回结果只是代表数据已经开始传输,并不意味着传输过程已经结束,所以这个步骤是独立和异步的,该步骤也会切换上下文,由kernel context 到user context,并且由DMA引擎从socket buffer复制数据到protocol buffer。
从上面步骤可以看出,整个过程涉及到了多次上下文切换和数据复制,那么,有没有办法避免在user和kernel buffer之间复制数据呢,答案是有的。
伪代码如下:
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);
流程图
从流程图可知,使用mmap上下文切换次数没变,但是共享了kernel buffer,直接从kernel buffer复制数据到 socket buffer,减少了kernel buffer和user buffer的交互。注意使用mmap的时候是有一个陷进的,当一个线程使用mmap进行读写的时候,如果另外一个线程删除了文件,会报错。可以通过file leasing方法进行解决,具体解决过程,这里不进行讲解。
综上,该流程还是涉及到了一次cpu copy。这里我们要弄清一个概念,所谓的零拷贝,指的是kernel buffer和user buffer之间的拷贝次数,所以这里并没有达到零拷贝的效果,继续往下看。
伪代码如下:
sendfile(socket, file, len);
流程如图
该过程不仅减少了两次上下文切换,而且也只进行了一次cpu copy,且没有陷进。
说了这么多,还是没有实现零拷贝,也就是cpu copy的次数为零。
那么有办法实现吗,答案是肯定的。依赖于网络的聚集操作接口,意思就是不需要连续的内存空间。
如图,socket buffer中存放的是数据的地址描述,而不是数据,聚集操作会根据地址描述找到对应的数据,然后进行复制。