计算机处理的任务大致可以分为两类:CPU密集型和IO密集型。目前流行的互联网应用都是IO密集型的。传统的IO标准接口是基于数据拷贝的。在追赶P8方面。为什么IO接口是基于数据复制的?为了让广大码农更好的沉浸在自己的一亩三分地,不让他们分心去关心计算机的硬件资源分配,操作系统诞生了。操作系统本质上是一个管家,其目的是更加公平合理地为各个进程分配硬件资源。在操作系统出现之前,程序员需要面对各种各样的硬件,就像这样:在这个时期,程序员真正掌握了全局。控制全局的后果是你需要控制所有的细节,这显然不利于生产力的释放。操作系统应用程序诞生了。计算机系统变成这样:现在应用程序不需要直接与硬件交互。从IO的角度来看,操作系统变成了一个类似路由器的角色,将应用程序提交的数据分发给具体的硬件。up,或者从硬件接收数据,分发给相应的进程。数据传输是通过什么?也就是我们常说的buffer。所谓缓冲区就是一块可用的内存空间,用来临时存放数据。操作系统这个中间人带来的问题是,你需要先把东西交给操作系统,然后操作系统再交给硬件,这就不可避免地涉及到数据拷贝。这也是传统IO操作必然需要数据拷贝的原因。完整的操作系统说明可以参考博主的《深入理解操作系统》。但是,数据复制有性能损失。下面我们通过一个例子,让大家对这个问题有一个更直观的认识。一个web服务器浏览器打开一个网页需要大量的数据,包括图片、html文件、css文件、js文件等,当浏览器请求这些文件时,服务器端的工作其实很简单:服务器端只需要从磁盘读取文件。抓取文件扔到网络上发送出去。代码基本上类似于:read(fileDesc,buf,len);write(socket,buf,len);这两段代码很简单,第一行代码从文件中读取数据存入buf,然后存入buf数据通过网络发送。注意观察buf,整个过程server没有对buf中的数据做任何修改,buf中的数据在用户态一挥袖就回了内核态,没有带走任何数据云。这两行看似简单的代码到底发生了什么?答案是这样的:程序看似简单的两行代码,底层却比较复杂。看到这张图,你真的应该很欣赏这个操作系统。操作系统就像一个极其称职的管家,为你打理一切脏活累活,让你在用户状态下从容不迫地指导国家。这简单的两行代码涉及到:四次数据拷贝和四次上下文切换:read函数会涉及到用户态到内核态的切换,操作系统会向磁盘发起IO请求。数据被复制到内核的缓冲区中。请注意,此数据副本不需要CPU参与。之后,操作系统开始将这段数据从内核中拷贝到用户态的缓冲区中。这时read()函数返回,从内核态切换回用户态。这时,read(fileDesc,buf,len);这行代码返回了,把刚出炉的数据装进了buf。接下来,发送函数再次引起用户态和内核态之间的切换。这时需要将数据从用户态buf复制到网络协议子系统的buf中。具体来说,buf属于代码中使用的socket。之后send函数返回,再次从内核态返回到用户态;这时候程序员就认为数据发送成功了,但实际上数据可能还停留在内核中。之后开始第四次数据拷贝,使用DMA技术将socketbuf中的数据拷贝到网卡中,然后真正发送出去。这就是最下面看似简单的两行代码的完整过程。你觉得这个过程有什么问题吗?有发现问题的同学一定注意到了,既然在用户态都没有进行数据修改,为什么还要费这么大的力气让数据在用户态一日游呢?直接在把内核态从磁盘传到网卡上不就行了吗?恭喜,你做对了!这种优化思路就是所谓的零拷贝技术,ZeroCopy。一般来说,优化数据拷贝有三个方向:用户态不需要真正访问数据,就像上面的例子,用户态根本不需要知道buf里有什么。在这种情况下,不需要将数据从内核态复制到用户态,然后再从用户态复制回内核态。数据不需要用户态感知,数据复制完全发生在内核态。内核态并不真正需要访问数据,用户态程序可以绕过内核直接与硬件交互,从而避免了内核的参与,减少了数据拷贝的可能性。内核不需要知道数据。如果内核态和用户态要交互,优化用户态和内核态数据交互的方式。知道了解题思路,我们再来看看计算机系统中为了实现零拷贝的巧妙设计。mmap没错,就是mmap。我们在文章《mmap可以让程序员实现哪些骚操作》中有详细的解释。你能想到mmap也能实现零拷贝吗?对于本文提到的网络服务器,我们可以这样修改代码:buf=mmap(file,len);写(套接字,buf,len);你可能会想,直接把read换成mmap会有什么优化吗?如果真正了解mmap就会知道,mmap只是将文件内容映射到进程地址空间,在进程中,并没有真正的拷贝到进程地址空间,省去了一次从内核态到用户态的数据拷贝。同样,调用write时,数据直接从内核buf复制到socketbuf,而不是在read/write方法中将用户态数据复制到socketbuf。可以看到我们使用mmap保存了一份数据副本,上下文切换还是四次。mmap虽然可以省掉数据拷贝,但是维护文件和地址空间的映射关系是有代价的。除非CPU复制数据的时间超过维护映射关系的成本,否则基于mmap的程序性能可能不如传统的read/write。另外,如果映射文件被其他进程截断,你的进程在Linux下会立即收到SIGBUS信号,所以这个异常需要正确处理。除了mmap,还有其他方式可以实现零拷贝。你没看错sendfile,这个系统调用是专门用来解决linux系统下数据拷贝问题的:#include
