io - yaokun123/php-wiki GitHub Wiki

IO模型

有人说,比特天生就是用来被复制的,数据的生命意义便在于输入/输出,的确,计算机的重要工作之一便是负责各种设备的数据输入/输出,也就是I/O (In/Out)操作。 事实上IO根据设备的不同分为很多种类型,比如内存IO、网络IO、磁盘IO

对于网络IO和磁盘IO,他们的速度要慢很多,尽管使用RAID磁盘阵列可以通过并行磁盘访问来加快磁盘IO速度;
购买大量独享网络带宽以及使用高带宽网络适配器可以提高网络IO的速度。
但问题在于,这些I/O操作需要由内核系统调用来完成,同时系统调用显然需要由CPU来调度,而CPU的速度毫无疑问
是非常快的,这就使CPU不得不浪费宝贵的时间来等待慢速I/O操作。

一、PIO 与 DMA

在介绍I/O模型之前,有必要简单地说说慢速I/O设备和内存之前的数据传输方式。

我们拿磁盘来说,很早以前,磁盘和内存之间得数据传输是需要CPU控制的,也就是说如果我们读取磁盘文件到内存中,数据要经过CPU存储转发,这种方式成为PIO。显然这种方式非常不合理,需要占用大量的CPU时间来读取文件,造成文件访问时系统几乎停止响应。

后来,DMA(直接内存访问,Direct Memory Access)取代了PIO,它可以不经过CPU而直接进行磁盘和内存的数据交换。在DMA模式下,CPU只需要向DMA控制器下达命令,让DMA控制器来处理数据的传送即可,DMA控制器通过系统总线来传输数据,传送完毕再通知CPU,这样就在很大程度上降低了CPU的占有率,大大节省了系统资源,而它的传输速度与PIO的差异其实并不十分明显,因为这主要取决于慢速设备的速度。

可以肯定的是,PIO模式的计算机我们现在已经很少见到了。

二、同步阻塞IO

说到阻塞,首先得说说I/O等待。造成等待的原因非常多,比如Web服务器在等待用户的访问,这便是等待,因为他不知道谁会来访问,所以只能等。随后当某个用户通过浏览器发出请求,Web服务器与该浏览器建立TCP连接后,又要等待用户发出HTTP请求数据,用户的请求数据在网络上传输需要时间,进入服务器接受缓冲区队列以及被复制到进程地址空间都需要时间。另外,假如浏览器和Web服务器采用HTTP长链接模式,那么在超时关闭之前,服务器还要等待浏览器发送其他的请求,这也是I/O等待。

再比如,读取磁盘上某个文件的I/O操作,可能先要等待其他的磁盘访问操作,因为磁头数量是有限的,所以只能一个个排队读取,即使轮到自己,在磁盘上读取数据本身也要花费时间。值得一提的是,对于RAID磁盘的某些规格(如RAID0),通过磁盘阵列将数据分布在多个磁盘上,大大提高了磁盘访问吞吐率。

可见I/O等待是不可避免的,那么既然有了等待,就会有阻塞,但是注意,我们说的阻塞是指当前发起I/O操作的进程被阻塞,并不是CPU被阻塞,事实上没有什么能让CPU阻塞的,CPU只知拼命地计算,对于阻塞一无所知。

另外“同步”的概念在这里显得并不那么重要,只是为了和后面的异步I/O加一区分

同步阻塞I/O是指当进程调用某些涉及I/O操作的系统调用或库函数时,比如accept()、send()、recv()等,进程便暂停下来,等待I/O操作完成之后再继续运行。这是一种简单而有效的I/O模型,他可以和多进程结合起来有效的利用CPU资源,但是其代价就是多进程的大量内存开销。 为了简单说明同步阻塞I/O以及其他I/O模型的区别,我们举个有意思的例子。比如你去逛街,逛着逛着有点饿了,这时你看到商场里有小吃城,就去一个小吃店买了一碗面条,交了钱,可面条做起来得需要时间,你也不知道可以什么时候做好,没办法,只好坐在那里等,等面条做好后吃完再继续逛街。显然,这里的吃面条便是I/O操作,他要等待厨师做面条,还要等待自己把面条吃完。

1、process 要求 kernal 返回数据;
2、process 等待;
3、kernal 开始准备数据;
4、kernal 准备好数据;
5、将数据从内核空间复制到用户空间;
6、process 处理 数据。
kernal准备数据并返回数据的整个过程中,process不能做别的事,这就是所谓的阻塞

三、同步非阻塞I/O

在同步阻塞I/O中,进程实际上等待的时间可能包括两部分,一个是等待数据的就绪,另一个是等待数据的复制,对于网络I/O来说,前者的时间可能要更长一些。

与此不同的是,同步非阻塞I/O的调用不会等待数据的就绪,如果数据不可读或不可写,他会立即告诉进程。比如我们使用非阻塞recv()接受网络数据的时候,如果网卡缓冲区中没有可接受的数据,函数就及时返回,告诉进程没有数据可读了。相比于阻塞I/O,这种非阻塞I/O结合反复轮询来尝试数据是否就绪,防止进程被阻塞,最大的好处便在于可以在一个进程里可以同时处理多个I/O操作。

但正是由于需要进程执行多次的轮询来查看数据是否就绪,这花费了大量的CPU时间,使得进程处于忙碌等待状态。

回到买面条的故事中,假如你不甘心坐着等待面条做好,想去顺便逛逛街,但又担心面条做好后没有及时领取,所以你逛一会便跑回去看面条是否做好,往返了很多次,最后虽然及时地吃上了面条,但是却累的气喘吁吁。

非阻塞I/O一般只针对网络I/O有效,我们只要在socket的选项设置中使用O_NONBLOCK即可,这样对于该socket的send()或recv()便采用非阻塞方式。值得注意的是,对于磁盘I/O,非阻塞I/O并不产生效果

1、process 要求 kernal 返回数据;
2、kernal 还没准备好,通知 process 没准备好你去干点别的吧;
3、process 又问 kernal;
4、kernal 说还没准备好;
5、process 去喝了口水,又问kernal;
6、说准备好了,你来取吧;
7、开始读取数据(阻塞);
8、读取数据完成;

四、多路I/O就绪通知(有些地方也称这种IO方式为事件驱动IO(event driven IO)。)

在实际应用中,特别是Web服务器,同时处理大量的文件描述符是必不可少的,但是使用同步非阻塞I/O显然不是最佳的选择,在这种模型下,我们知道服务器如果想要同时接收多个TCP连接的数据,就必须轮流对每个socket调用接受的方法,比如recv()。不管这些socket有没有可以接受的数据,都要询问一遍,假如大部分socket没有数据可以接受,那么进程便会浪费很多CPU时间用于检查这些socket,这显然不是我们希望看到的。

多路I/O就绪通知的出现,提供了对大量文件描述符就绪检查的高性能方案它允许进程通过一种方法来同时监视所有的文件描述符并可以快速获得所有就绪的文件描述符然后只针对这些文件描述符进行数据访问

回到买面条的故事中,假如你不止买了一份面条,还在其他小吃店买了饺子、粥、馅饼等,因为一起逛街的朋友看到你的面条后也饿了。这些东西都需要时间来制作。在同步非阻塞I/O模型中,你要轮流不停地去各个小吃店询问进度,痛苦不堪。现在引入多路I/O就绪通知后,小吃城管理处给大厅安装了一块电子屏幕,以后所有小吃店的食物做好后,都会显示在屏幕上,这可真是个好消息,你只需要间隔性地看看大屏幕就可以了,也许你还可以同时逛逛附近的商店,在不远处也可以看到大屏幕。

需要注意的是,I/O就绪通知只是帮助我们快速获得就绪的文件描述符,当得知数据就绪后,就访问数据本身而言,仍然需要选择阻塞或非阻塞的访问方式,一般我们选择非阻塞方式,以防止任何意外的等待阻止整个进程,比如有时就绪通知只代表一个内核的提示,也许此时文件描述符尚未真正准备好或者已经被客户端关闭连接。

1、process想要kernal返回数据;
2、process让fd去做这个事;
3、process把fd交给select;
4、process问select可以取数据了;
5、select说不行,再等等;
6、process问select可以取数据了么;
7、select说可以;
8、process开始取数据;
9、process取数据完成。

五、异步IO

1、process想要kernal返回数据;
2、kernal开始准备数据 ,process去做别的事情了;
3、kernal把数据准备好,并送到process手中;
4、process拿到数据了,开始处理数据

六、几种实现多路I/O就绪通知方法的介绍

1.select

select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视包含多个文件描述符的数组,当select()返回后,该数组中就绪的文件 描述符便会被内核修改标识位,使得进程可以获得这些文件描述符从而进行后续的读写操作。

select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方 式提升这一限制。所以,假如使用了select的服务器已经维持了1024个连接,那么你的请求可能会被拒绝。

另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟是 的大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。

2.poll

poll在1986年诞生于System V Release 3,显然UNIX不愿意直接沿用BSD的select,而是自己重新实现了一遍,它和select在本质上没有多大差 别,但是poll没有最大文件描述符数量的限制

poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的 开销随着文件描述符数量的增加而线性增大。

另外,select()和poll()将就绪文件描述符告诉进程后,如果进程没有对其进行I/O操作,那么下次调用select()或poll()a的时候将再次报告这些 文件描述符,所以他们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。

3.SIGIO

Linux2.4提供了SIGIO,它通过实时信号(Real Time Signal)来实现select/poll的通知方法,但是他们的不同在于,select/poll告诉我们哪些文件描述符是就绪的,一直到我们读写它之前,每次select/poll都会告诉我们;而SIGIO则是告诉我们哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发(Edge Triggered)。SIGIO几乎是Linux2.4下性能最好的多路I/O就绪通知方法。

但是SIGIO也存在一些缺点,在SIGIO机制中,代表事件的信号由内核中的事件队列来维护,信号按照顺序进行通知,这可能导致当一个信号到达的时候,该事件已经过期,它所描述的文件描述符已经被关闭。另一方面,事件队列是有长度限制的无论你设置多大的上限,总有可能被事件装满,这就很容易发生事件丢失,所以这时候需要采取其他方法来弥补损失。

4./dev/poll

Sun在Solaris中提供了新的实现方法,它使用虚拟的/dev/poll设备,你可以将需要监视的文件描述符数组写入这个设备,然后通过ioctl()来等待事件通知。当ioctl()返回就绪文件描述符后,你可以从/dev/poll中读取所有的就绪的文件描述符数组,这点类似于SIGIO,节省了扫描所有文件描述符的开销。

在Linux下有很多方法可与你实现类似/dev/poll的设备,但都没有提供直接的内核支持,这些方法在服务器负载较大时性能不够稳定。

5./dev/epoll

随后,名为/dev/epoll的设备以补丁的形式出现在Linux2.4上,它提供了类似/dev/poll的功能,而且增加了内存映射(mmap)技术,在一定成都上提高了性能。但是/dev/epoll仍然只是一个补丁,Linux2.4并没有将它的实现假如内核。

6.epoll

直到Linux2.6才出现由内核直接支持的实现方法,那就是epoll,它几乎具备之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。

epoll可以同时支持水平触发和边缘触发,理论上边缘触发的性能要更高一些,但是代码实现相当复杂,因为任何意外的丢失事件都会造成请求处理错误。在默认情况下epoll采用水平触发,如果需要使用边缘触发,则需要在事件注册时增加EPOLLET选项。

另外,epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的并不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。

另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册每一个文件描述符,一旦某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

回到买面条的故事中,虽然有了电子屏幕,但是显示的内容是所有餐品的状态,包括正在制作的和已经做好的,这显然给你造成阅读上的麻烦,就好像select/poll每次返回所有监视的文件描述符一样。如果能够只显示做好的餐品,那该多好,随后小吃城管理处改进了大屏幕,实现了这一点,这就像/dev/poll一样只告知就绪的文件描述符。在显示做好的餐点时,如果只显示一次,而不管你有没有看到,这就相当于边缘触发,而如果在你领取餐点之前,每次都显示,这相当于水平触发。

但尽管这样,一旦你走的比较远,就的花事件走到小吃城去看电子屏幕,能不能让你更加轻松的获取通知呢?管理处这次采用了手机短信通知的方法,你只需要去管理处注册后,便可以在餐点就绪时及时收到短信通知,这类似于epoll的事件机制。