共计 1733 个字符,预计需要花费 5 分钟才能阅读完成。
socket 在创建的时候默认是阻塞的。我们可以通过 socket 系统调用的第二个参数传递 SOCK_NONBLOCK 标志,或者通过 fcntl 系统调用的 F_SETFL 命令,将其设置为非阻塞的。阻塞和非阻塞的概念能应用与所有文件描述符,不仅仅是 socket, 我们称阻塞的文件描述符为阻塞 I /O, 非阻塞的文件描述符为非阻塞 I /O.
针对阻塞 I / O 执行的系统调用可能因为无法立即完成而被操作系统挂起,直到等待的事件发生为止。比如,客户端通过 connect 向服务器发起连接时,connect 将首先发送同步报文段给服务器,然后等待服务器返回确认报文段,如果服务器的确认报文段没有立即到达客户端,则 connect 调用将被挂起,直到客户端收到确认报文段并唤醒 connect 调用,socket 的基础 API 中,可能被阻塞的系统调用包括 acept send rev 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 信号的信号处理函数将被触发,我们也就可以在该信号处理函数中对目标文件描述 II 符执行非阻塞 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 模型 读写操作和阻塞阶段
阻塞 I /O 程序阻塞与读写操作
I/ O 复用 程序阻塞于 I / O 复用系统调用,但可同时监听多个 I / O 事件,对 I / O 本身读写操作是非阻塞的
SIGIO 信号 信号触发读写就绪事件,用户程序执行读写操作,程序没有阻塞阶段
异步 I /O 内核执行读写并触发读写完成事件,程序没有阻塞阶段
同时, 在并发模型中也有同步 / 异步的方式,但是和这里的概念不同。
在 I / O 模型中,同步和异步区分的是内核向应用程序通知的是何种 I / O 事件(是就绪事件还是完成事件),以及该由谁来完成 I / O 读写(是应用程序还是内核),在并发模型中,同步指的是程序完全按照代码序列的顺序执行,异步值得是程序执行需要由系统事件来驱动,常见的系统事件包括中断,信号等。