I/O多路复用
在没有多路复用时,如果我们调用fgets等待标准输入,就没有办法在套接字有数据的时候读出数据;如果调用read等待套接字数据返回,就没有办法在标准输入有数据的情况下,读入数据。
而I/O多路复用,就是把上述的标准输入、套接字等都看作是I/O 的一路,在任何一路I/O有事件发生的情况下,可以通知应用程序进行处理。注意:在检测到I/O事件发生之前,仍然是阻塞的,只是可以在一处阻塞点同时检测来自多个I/O的事件。
通过调用select函数,通知内核挂起进程,当一个或多个I/O事件发生后,控制权会返回给应用程序,由应用程序进行I/O事件的处理。
I/O事件的类型有很多,比如:
- 标准输入文件描述符准备好可以读。
- 监听套接字准备好,新的连接已经建立成功。
- 已连接套接字准备好可以写。
- 如果一个 I/O 事件等待超过了 10 秒,发生了超时事件。
很多情况下需要使用I/O复用技术:
- 客户端程序要同时处理多个socket(比如非阻塞connect)
- 客户端程序同时处理用户输入和网络连接(比如上述的例子)
- 服务器程序要同时处理监听socket和连接socket(I/O复用使用最多的场合)
- 服务器程序要同时处理TCP请求和UDP请求
- 服务器程序要同时监听多个端口,或者处理多种服务。
注意:I/O复用虽然能够同时监听多个文件描述符,但它本身是阻塞的。并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依次处理其中的每一个文件描述符,这使得服务器程序看起来像串行工作的。如果要实现并发,只能使用多进程或多线程等编程手段。
Linux下实现I/O多路复用的系统调用主要有select、poll和epoll。
select函数
函数原型
#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
参数
nfds:指定被监听的文件描述符的总数。它通常被设置为select监听的所有文件描述符中的最大值+1,因为文件描述符是从0开始计数的。(如果没有这个参数的话,为了找到所有被设置的文件描述符就需要遍历整个fd_set中的数组,通过这个参数可以缩小遍历的范围,提高效率)
readfds,writefds,exceptfds:分别指向可读、可写、异常等事件对应的文件描述符集合。这三个参数分别通知内核,在哪些描述符上检测数据可以读,可以写和有异常发生。通过这三个参数去传入自己感兴趣的文件描述符,select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪(将就绪的文件描述符所对应的位设置为1)。
三个文件描述符集合中的每一个都可以设置为空,这样就表示内核不需要进行相关的检测。
timeout:用来设置select函数的超时时间。如果给timeval结构体的成员都传递0,那么select将立即返回,如果给timeout传递NULL,则select将一直阻塞,直到某个文件描述符就绪。
返回值
成功时返回就绪(可读、可写和异常)文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,select将返回0。select失败时返回-1,并设置errno。
fd_set结构体
fd_set结构体中包含一个整型数组,该数组的每个元素的每一位(bit)标记一个文件描述符。比如:一个32位的整型数可以表示32个文件描述符,如果数组中含有50个元素,那么一共可以包含的文件描述符为 32*50。在数组中第一个整型数可以表示0-31文件描述符,第二个整型数可以表示32-63文件描述符,以此类推。
设置文件描述符集合
#include <sys/select.h>
FD_ZERO (fd_set* fdset); /* 清除fdset的所有位 */
FD_SET (int fd, fd_set* fdset); /* 设置fdset的位fd */
FD_CLR (int fd, fd_set* fdset); /* 清除fdset的位fd */
int FD_ISSET (int fd, fd_set* fdset); /* 测试fdset的位fd是否被设置 */
可以这样理解:FD_SET就是将fd对应的位设置成1,FD_CLR就是将fd对应的位设置成0。其中1表示需要处理,0表示不需要处理。通过FD_ISSET进行检测,判断fd对应的位是0还是1。(在select调用完成之后,内核会修改描述符集合,通过修改完的描述符集合来和应用程序交互,只有那些事件准备就绪的描述符对应位为1,其他的为0,然后通过FD_ISSET进行检测)
由于内核会修改描述符集合,所以应该设置两个fd_set,一个用来保存感兴趣的文件描述符集合A,一个(B)用来作为参数传给select,以便修改,然后在每次循环调用select之前,用之前保存的集合A,来对准备传入的集合B,进行初始化。(或者每次调用select之前,重新FD_SET)
文件描述符就绪条件
使用select时,必须清楚哪些情况下文件描述符被认为是可读、可写或者出现异常的。
socket可读:
- socket内核接收缓冲区中的字节数大于或等于其低水位标记 SO_RCVLOWAT 。此时我们可以无阻塞地读该socket,并且读操作返回的字节数大于0。
- socket通信的对方关闭连接,即对方发送了FIN,此时使用read函数执行读操作,不会被阻塞,会直接返回0。
- 监听socket上,有已经完成的连接建立,此时调用accept函数不会阻塞,直接返回已经完成的连接。
- socket上有错误待处理,此时对于此socket调用read执行读操作,不会阻塞,直接返回-1。
综上,内核通知我们socket可读时,执行读操作不会阻塞。
socket可写:
- socket内核发送缓冲区中的可用字节数大于或等于其低水位标记 SO_SNDLOWAT,此时我们可以无阻塞地写该socket,并且写操作返回地字节数大于0。
- socket的写操作被关闭。对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号。
- socket上有错误待处理,使用write函数执行写操作,不阻塞,且返回-1
- socket使用非阻塞connect连接成功或者失败(超时)之后
综上,内核通知我们socket可写时,执行写操作不会阻塞。