前面有讲到过,socket在创建的时候是默认阻塞的。我们可以给socket系统调用的第2个参数传递SOCK_NONBLOCK标志,或者通过fcntl系统调用的F_SETFL命令,将其设置为非阻塞的。阻塞和非阻塞的概念能应用于所有文件描述符,而不仅仅是socket。我们称阻塞的文件描述符为阻塞I/O,称非阻塞的文件描述符为非阻塞I/O。
针对阻塞I/O执行的系统调用可能因为无法立即完成而被操作系统挂起,直到等待的事件发生为止。比如,客户端通过connect将首先发送同步报文段给服务器,然后等待服务器确认返回确认报文。如果服务器的确认报文段没有立即到达客户端,则connect调用将被挂起,直到客户端收到确认报文段并唤醒connect调用。socket的基础API中,可能被阻塞的系统调用包括accept、send、recv和connect。
针对非阻塞I/O执行的系统调用则总是立即返回,不管事件是否已经发生。如果事件没有立即发生,这些系统调用就返回-1,和出错的情况一样。此时我们必须根据errno来区分这两种情况。对accept、send和recv而言,事件未发生时errno通常被设置成EAGAIN(意为“再来一次”)或者EWOULDBLOCK(意为“期望阻塞”);对connect而言,errno则被置成EINPROGRESS(意为“在处理中”)。
只有在事件已将发生的情况下操作非阻塞I/O(读、写等),才能提高程序的效率。因此,非阻塞I/O通常要和其他I/O机制一起使用,比如I/O复用和SIGIO信号。
I/O复用是最常用的I/O通知机制。即应用程序通过I/O复用函数向内核注册一组事件,内核通过I/O复用函数把其中就绪的事件通知给应用程序。Linux上常用的I/O复用函数是select、poll和epoll_wait。注意:I/O复用函数本身是阻塞的,它们能提高程序效率的原因在于它们具有同时监听多个I/O事件的能力。
SIGIO信号也可以用来报告I/O事件。我们可以为一个目标文件描述符指定宿主进程,那么被指定的宿主进程将捕获到SIGIO信号。这样,当目标文件描述符上有事件发生时,SIGIO信号的信号处理函数将被触发,我们也可以在该信号处理函数中对目标文件描述符执行非阻塞I/O操作了。
从理论上说,阻塞I/O、I/O附庸和信号驱动I/O都是同步I/O模型。因为这三种I/O模型中,I/O的读写操作,都是在I/O事件发生之后,由应用程序来完成的。而POSIX规范所定义的异步I/O模型则不同。对异步I/O而言,用户可以直接对I/O执行读写操作,这些操作告诉内核用户读写缓冲区的位置,以及I/O操作完成之后内核通知应用程序的方式。异步I/O的读写操作总是立即返回,而不论I/O是否阻塞,真正的读写操作已经由内核接管。也就是说,同步I/O模型要求用户代码自行执行I/O操作(将数据从内核缓冲区读入用户缓冲区,或将数据从用户缓冲区写入内核缓冲区),而异步I/O机制则由内核来执行I/O操作(数据在内核缓冲区和用户缓冲区之间的得移动是由内核在“后台”完成的)。即:同步I/O向应用程序通知的是I/O就绪事件,而异步I/O向应用程序通知的是I/O完成事件。Linux环境下,aio.h头文件中定义的函数提供了对异步I/O的支持。
总结几种I/O模型的差异列于下表: