简介
说明
本文介绍Linux的五种IO模型。
概述
IO (Input/Output,输入/输出)即数据的读取(接收)或写入(发送)操作。IO有内存IO、网络IO和磁盘IO三种,通常我们说的IO指的是后两者。
可以将IO理解为两步:
- 等待IO事件就绪
- 数据就绪后进行真正意义上的IO(真正的数据搬迁)
所以,IO的过程先是等,然后才是输入输出。进而,我们可以得到评价IO是否高效的标准:在IO过程中“等”的比重越小的性能越好,越大的性能越低。
5种IO模型
本文以钓鱼为例进行解释。
1、阻塞IO
一直看着鱼竿,自己等,自己钓,而且等的过程中不做其他的事。
这就是阻塞IO:阻塞式等待IO事件,在等的过程中不能做其他事。
这个模型也是最容易理解的,程序调用和我们基本的程序编写是一致的:
fd = connect() write(fd) read(fd) close(fd)
程序的read必须在write之后执行,当write阻塞住了,read就不能执行下去
2、非阻塞IO
自己等,自己钓。但他并不是一直盯着鱼竿,而是时刻检测钓鱼事件是否就绪,没有就做其他的事(比如:玩手机)。
这就是非阻塞IO:非阻塞等待,不断检测IO事件是否就绪。没有就绪就可以做其他事。
从图中可以看出来,这是一个轮询的过程。
每次用户询问内核是否有数据报准备好(文件描述符缓冲区是否就绪),当数据报准备好的时候,就进行拷贝数据报的操作。当数据报没有准备好的时候,也不阻塞程序,内核直接返回未准备就绪的信号,等待用户程序的下一次轮询。
3、IO复用/多路转接IO
也是自己等,自己钓。但可以同时等待多个鱼上钩事件。这样钓到鱼的概率就很大,等的时间短,效率明显比其他人高。
这就是多路转接IO:linux用select/poll函数实现IO复用模型,这两个函数也会使进程阻塞,但是和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。知道有数据可读或可写时,才真正调用IO操作函数。
IO复用模型是多了一个select函数,select函数有一个参数是文件描述符集合,意思就是对这些的文件描述符进行循环监听,当某个文件描述符就绪的时候,就对这个文件描述符进行处理。
这种IO模型是属于阻塞的IO。但是由于它可以对多个文件描述符进行阻塞监听,所以它的效率比阻塞IO模型高效。
4、信号驱动IO
将钓鱼事件是否就绪的信息转移到铃铛上,不用自己检测事件就绪,只需要将铃铛作为信号通知方式。
这就是信号驱动IO:linux用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO时间就绪,进程收到SIGIO信号。然后处理IO事件。
信号驱动IO模型是应用进程告诉内核:当你的数据报准备好的时候,给我发送一个信号哈,并且调用我的信号处理函数来获取数据报。这个模型是由信号进行驱动。
5、异步IO
没有经过等的过程,只需要发起事件(让其他人帮忙钓鱼),然后享受结果。
这就是异步IO:linux中,可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。
异步IO使用的不再是read和write的系统接口了,应用工程序调用aio_XXXX系列的内核接口。
当应用程序调用aio_read的时候,内核一方面去取数据报内容返回,另外一方面将程序控制权还给应用进程,应用进程继续处理其他事务。这样应用进程就是一种非阻塞的状态。
当内核的数据报就绪的时候,是由内核将数据报拷贝到应用进程中,返回给aio_read中定义好的函数处理程序。很少有linux系统支持,windows的IOCP则是此模型
完全异步的I/O复用机制,因为纵观上面其它四种模型,至少都会在由kernel copy data to appliction时阻塞。而该模型是当copy完成后才通知application,可见是纯异步的。好像只有windows的完成端口是这个模型,效率也很出色。
区别与联系
前4种模型都有等和IO两个阶段,并将数据从内核拷贝到调用者的缓冲区,自己等,自己进行数据搬迁。所以统称为同步IO。 与第5种异步IO相区分。
注:这里同步/异步的概念与进程&线程中的概念不同,不同的背景下应该有不同的理解。
下面是以上五种模型的比较:
可以看出,越往后,阻塞越少,理论上效率也是最优。
阻塞程度:阻塞IO > 非阻塞IO>多路复用IO>信号驱动IO>异步IO,效率是由低到高的。
5种模型的比较比较清晰了,剩下的就是把select,epoll,iocp,kqueue按号入座那就OK了。
- I/O multiplexing (select and poll and kqueue)
- signal driven I/O (SIGIO)
- asynchronous I/O (the POSIX aio_functions)
select和iocp分别对应第3种与第5种模型,那么epoll与kqueue呢?其实也于select属于同一种模型,只是更高级一些,可以看作有了第4种模型的某些特性,如callback机制。
为什么epoll,kqueue比select高级?
因为他们无轮询,因为他们用callback取代了。想想看,当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
windows or unix (IOCP or kqueue/epoll)?
诚然,Windows的IOCP非常出色,目前很少有支持asynchronous I/O的系统,但是由于其系统本身的局限性,大型服务器还是在UNIX下。而且正如上面所述,kqueue/epoll 与 IOCP相比,就是多了一层从内核copy数据到应用层的阻塞,从而不能算作asynchronous I/O类。但是,这层小小的阻塞无足轻重,kqueue与epoll已经做得很优秀了。
I/O多路复用的实现
select, poll, epoll 都是I/O多路复用的具体的实现,之所以有这三个存在,是因为他们出现是有先后顺序的。
select 被实现以后,很快就暴露出了很多问题
select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的。
select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但是并不会告诉你是那个sock上有数据,于是你只能自己一个一个的找,10几个sock可能还好,要是几万的sock每次都找一遍,这个无谓的开销就颇有海天盛筵的豪气了。
select 只能监视1024个链接,linux 定义在头文件中的,参见FD_SETSIZE。
select 不是线程安全的,如果你把一个sock加入到select, 然后突然另外一个线程发现,尼玛,这个sock不用,要收回。对不起,这个select 不支持的,如果你丧心病狂的竟然关掉这个sock, select的标准行为是不可预测的。
14年以后实现了poll, 修复了select的很多问题
poll 去掉了1024个链接的限制,可以任意多个链接。
poll 从设计上来说,不再修改传入数组,不过这个要看你的平台了。
其实拖14年那么久也不是效率问题, 而是那个时代的硬件实在太弱,一台服务器处理1千多个链接简直就是神一样的存在了,select很长段时间已经满足需求。
但poll仍然不是线程安全的, 这就意味着,不管服务器有多强悍,你也只能在一个线程里面处理一组I/O流。你当然可以那多进程来配合了,不过然后你就有了多进程的各种问题。
5年以后, 在2002, 大神 Davide Libenzi 实现了epoll。
epoll 可以说是I/O 多路复用新的实现,epoll 修复了poll 和select绝大部分问题, 比如
epoll 现在是线程安全的。
epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。
可是epoll 有个致命的缺点,只有linux支持。比如BSD上面对应的实现是kqueue。
而ngnix 的设计原则里面, 它会使用目标平台上面最高效的I/O多路复用模型咯,所以才会有这个设置。一般情况下,如果可能的话,尽量都用epoll/kqueue吧。
请先
!