目录
1. 前言
正文概述UNIX的五种IO操作方式,阻塞IO、非阻塞IO、IO多路复用、信号驱动IO、异步IO。
关于IO多路复用,讲述了三种方案与差异:select、poll、epoll。
2. 套接字与文件描述符
套接字可以理解为:基于标准的UNIX文件描述符,与其他的程序通讯的一个方法。
在UNIX 系统中,任何对I/O的操作,都是通过读写一个文件描述符来实现的。
一个文件描述符只是一个简单的整形数值,代表一个被打开的文件(这里的文件是广义的文件,并不只代表磁盘文件,还以代表一个网络连接,一个先进先出队列,一个终端显示屏幕,以及其他的一切)。在UNIX 系统中任何东西都是一个文件!!所以如果想通过Internet和另外一个程序通讯的话,必然是使用一个文件来描述符实现。
3. I/O操作方式
在Linux/UNIX下,有下面这五种I/O操作方式:
- 阻塞IO
- 非阻塞IO
- I/O多路复用
- 信号驱动IO
- 异步IO
一般来说,程序进行输入操作有两步:
- 等待有数据可读
- 将数据从系统内核拷贝到程序的数据区
对于一个对套接字的输入操作,第一步是等待数据从网络上传到本地(当数据包到达的时候,数据将会从网络层拷贝到内核的缓存中),
第二步是从内核中把数据拷贝到程序的数据区中。
4. 阻塞IO
图示
一个进程调用recvfrom ,然后由系统内核处理,当有数据报到达本地系统时内核不告知进程,而是系统内核将数据拷贝到进程的缓存中,然后返回,由用户进程继续处理。
用户进程自调用recvfrom直到收到返回的这段时间是阻塞的。只有当recvfrom返回,用户进程才可以执行后续操作。
在不使用线程的情况下,使用阻塞模式,导致用户进程只能在一个文件描述符进行阻塞,无法满足用户进程在多个文件描述符同时阻塞的要求。
5. 非阻塞IO
当我们将一个套接字设置为非阻塞模式,相当于告诉了系统内核:“当我请求的I/O操作不能够马上完成,你不要让我的进程进行休眠等待,而请马上返回一个错误给我。”
图示
recvfrom函数的前三次调用,因为系统没有接收到网络数据,所以内核马上返回一个EWOULDBLOCK的错误。第四次我们调用recvfrom函数,一个数据报已经到达了,内核将它拷贝到应用程序的缓冲区中,然后recvfrom正常返回,我们就可以对接收到的数据进行处理了。
使用非阻塞模式,满足用户进程在多个文件描述符同时阻塞的要求,但需要循环不停的测试是否文件描述符有数据可读(称做polling),这是一个极浪费CPU资源的操作。
6. I/O多路复用
IO多路复用满足用户进程在多个文件描述符同时阻塞的要求,并在其中某个可以读写时收到通知。
使用IO多路复用时的设计规则:
- 调用IO多路复用:当任何文件描述符就绪后告知我;
- 在一个或多个文件描述符就绪前,处于睡眠状态;
- 被唤醒,被告知有哪些文件描述符就绪;
- 在不阻塞的情况下,对就绪的文件描述符进行IO处理;
- 返回第一步
Linux提供了三种多路复用方案:select、poll、epoll。
图示(以select方案为例)
select函数是阻塞的,其等待套接字进入读就绪状态。当套接字可以读取数据的时候,select函数返回。接下来就可以调用recvfrom函数,将数据拷贝到我们的程序缓冲区中。
多路复用的高级之处在于,它能同时等待多个文件描述符,而这些文件描述符中的任意一个进入读就绪状态,select()函数就可以返回。
IO多路技术一般在下面这些情况中被使用:
当一个客户端需要同时处理多个文件描述符的输入输出操作的时候(一般来说是标准的输入输出和网络套接字)。 |
当程序需要同时进行多个套接字的操作的时候。 |
如果一个 TCP 服务器程序同时处理正在侦听网络连接的套接字和已经连接好的套接字。 |
如果一个服务器程序同时使用 TCP 和UDP 协议。 |
如果一个服务器同时使用多种服务并且每种服务可能使用不同的协议(比如inetd就是这样的)。 |
I/O 多路服用技术并不只局限与网络程序应用上。几乎所有的程序都可以找到应用I/O多路复用的地方。 |
6.1. Select方案
select()系统函数定义
监测的文件描述符分为三类,分别等待不同的事件。
监测readfds集合中的文件描述符,确认是否有一个可完成不阻塞读数据;
监测writefds集合中的文件描述符,确认是否有一个可完成不阻塞写数据;
监测exceptfds集合中的文件描述符,确认是否有一个出现异常。如果集合为null,则select不监测该事件。
成功返回时,每个集合仅包括对应IO事件就绪的文件描述符。
例如readfds开始包含文件描述符7和9,返回时包含7,说明文件描述符7的可不阻塞的读,而9为阻塞。
上文说过,文件描述符是非负的整数。第一个参数n为:三个文件描述符集合中最大值+1。即select()调用者需要计算n。
timeout参数对应的数据结构:
如果timeout不为null,则select()在sec秒usec微秒后返回,而不管文件描述符是否就绪。
文件描述符数量的上限和文件描述符的最大值是存在限制的,由FD_SETSIZE设置。
在Linux中,该值为1024。
因此select()可处理的文件描述符集合中元素数量上限为1024。调用select()前,都需要重新设置所有参数。
6.2. poll方案
poll()系统调用是SystemV的IO多路复用方案。它解决了一些select的不足,但出于习惯和移植性考虑,select仍经常被使用。
Poll使用一个由nfds个poolfd结构组成的数组,poolfd:
fd表示要监视文件描述符;
events表示监视文件描述符事件的一组位掩码,由调用者设置;
revents表示发生在该文件描述符上事件的位掩码,内核返回时由内核设置;
所以events中的事件可能在revents中。
与select()函数对比,参数不一样,且不需要另外请求报告异常。
每次调用poll()前,无需再次构建参数的结构体,可重复使用。而调用select()前需要重新设置参数。
6.3. poll和seelct对比
尽管两者完成同样的工作,但poll是优于select的。
poll的优点 | poll的不足 |
poll无需计算需监视的文件描述符集合中的最大值+1并设置参数; | Poll由于某些Unix系统不支持,所以select的移植性更好。 |
poll在应对文件描述符较大时更有效率。例如select监视值为900的文件描述符,内核需要遍历文件描述符集合的每个比特,直到第900个。 | Select提供了微秒级的超时方案。 |
poll可以创建合适大小的文件描述符集合。Select的文件描述符集合是静态大小的,所以当集合小时,限制了可监视文件描述符的最大值,当集合大时,又影响效率。尤其是在不确定集合的组成是否稀疏时,对于大掩码的操作效率不高。 | |
poll分离监视的事件集合和就绪的事件集合,数组无需改变可重复使用。Select每次返回时,文件描述符集合被重建,因此每次调用select前,必须重新初始化结合。 |
两者的局限:每次调用必须提供所有需要监视的文件描述符,内核必须遍历所有监视的文件描述符。当集合变得很大时(成百上千),遍历成了明显的瓶颈。
6.4. epoll方案
2.6内核引入event poll,实现复杂,但是解决了select和poll的性能问题,以及引入新特性。
创建epoll实例并返回文件描述符
返回的文件描述符需要close()关闭。Size为内核需要监视的文件描述符的近似数量,不是最大值。
控制epoll
关联epoll实例和epfd,op指定对fd执行的操作,epoll_event中的events指定在文件描述符fd上监听的事件。
epoll_event中的data数据由用户使用,data.fd为监听的文件描述符,data会返回给用户,这样用户就知道哪个文件描述符触发事件。
等待epoll事件
epoll_wait等待给定epoll实例关联的文件描述符上的事件。
events是epoll_event结构体的数组,且最多有max个事件。返回事件数量。events中每个元素代表一个文件描述符及其就绪的事件集合。
7. 信号驱动IO模式
让内核在文件描述符就绪的时候使用SIGIO信号来通知我们,将这种模式称为信号驱动I/O 模式。
使用这种模式,我们首先需要允许套接字使用信号驱动I/O ,还要安装一个SIGIO的处理函数。在这种模式下,系统调用将会立即返回,然后我们的程序可以继续做其他的事情。当数据就绪的时候,系统会向我们的进程发送一个SIGIO信号。这样我们就可以在SIGIO
信号的处理函数中进行I/O操作(或是我们在函数中通知主函数有数据可读)。
对于信号驱动I/O模式,它的先进之处在于它在等待数据的时候不会阻塞,程序可以做自己的事情。当有数据到达的时候,系统内核会向程序发送一个SIGIO信号进行通知,这样程序就可以获得更大的灵活性,不必为等待数据进行额外的编码。
8. 异步I/O模式
运行在异步I/O模式下时,如果想进行I/O操作,只需要告诉内核我们要进行I/O操作,然后内核会马上返回。具体的I/O和数据的拷贝全部由内核来完成,我们的程序可以继续向下执行。当内核完成所有的I/O操作和数据拷贝后,内核将通知我们的程序。
异步I/O和信号驱动I/O的区别是:
- 信号驱动I/O模式下,当操作可以被操作的时候(如可读取),内核发送SIGIO消息,通知给我们的应用程序。
- 异步I/O模式下,内核在所有的操作都已经被内核操作结束之后才会通知我们的应用程序。
如下图,当我们进行一个IO操作的时候,我们传递给内核我们的文件描述符,我们的缓存区指针和缓存区的大小,一个偏移量offset,以及在内核结束所有操作后和我们联系的方法。这种调用也是立即返回的,我们的程序不需要阻塞住来等待数据的就绪。可以要求系统内核在所有的操作结束后(包括从网络上读取信息,然后拷贝到我们提供给内核的缓存区中)给我们发一个消息。
9. 几种I/O模式的比较
参考
Linux网络编程第六章套接字 6.10 I/O模式
Linux系统编程第二章文件IO 2.10I/O多路复用
Linux系统编程第四章高级文件IO 4.2Event Poll接口