Linux IO 模型
在了解 IO 多路复用之前,首先回顾一下常见的 IO 模型。总览如下:

1 阻塞 IO (Blocking IO)
在 Linux 中,所有的 IO 默认都是阻塞的。一个典型的 IO 读操作流程如下:

当应用程序调用了 read() 系统调用,kernel 首先进入等待数据阶段(对于网络 IO,可读数据还没到达或者包不完整),等待足够的数据,此时用户进程会被阻塞。当有可读数据到达后,kernel 将数据拷贝到用户内存空间,read() 系统调用返回,此时用户进程解除阻塞状态。
2 非阻塞 IO(Non-blocking IO)
可通过 O_NONBLOCK 选项配置文件或 socket 为非阻塞模式。非阻塞 IO 的请求示例:

当应用程序调用 read() 系统调用时没有数据可读,用户进程不会阻塞,而是立刻返回 error,应用程序可以通过返回值和 errno 决定下一步的操作。
应用程序尝尝考虑的情况有:
- 返回值大于 0,表示有数据可读且读操作已经完成,返回值为读到的字节数。
- 返回值等于 0,表示连接已经断开(socket)。
- 返回值为 -1,且 errno 为 EAGAIN 或 EWOULDBLOCK(两者等价),数据暂时没有准备好,用户可以决定稍后重试。
- 返回值为 -1,且 errno 不为 EAGAIN 和 EWOULDBLOCK,表示读操作遇到严重错误,重读也不会成功。
这里的非阻塞指的是在等待数据阶段不会阻塞,但内核空间拷贝数据的时候仍会阻塞。
3 异步 IO(Asynchronous IO)
以上两种 IO 均属于同步 IO 模型,即使非阻塞 IO 和接下来要讨论的 IO 多路复用也都不是真正的异步 IO。

异步 IO 与非阻塞 IO 不同之处在于其在内核空间也不会发生阻塞,是真正意义上的非阻塞。当可读数据被拷贝到用户空间后,内核会给用户进程发送一个信号(signal),通知系统调用完成。
Linux 上的异步 IO 用在磁盘 IO 读写操作,不用在网络。Windows 上的完成端口(IOCP, I/O Completion Ports)是完整的异步 IO。
既然非阻塞 IO 和异步 IO 对用户来说都是非阻塞操作,那么异步 IO 的意义在哪里呢?
- 首先异步 IO 在编程方式上往往是信号驱动,有的提供回调接口,用户程序可以自定义读写回调函数,而且不用操心内核什么准备好数据。
- 系统调用在内核态也不阻塞,系统 CPU 利用率更高。
4 IO 多路复用 (IO Multiplexing)
对于阻塞 IO,如果使用单线程,进程就无法在多个文件描述符上阻塞,为一个文件描述符提供服务的同时,就无法为其他描述符提供服务。但是文件描述符往往是关联的,如管道的两端、高并发服务中的 sockets 等。如果对其中一个文件描述符的操作一直没有返回,进程将一直阻塞。
非阻塞 IO 是上述问题的一个解决方案,应用发送的 IO 请求不阻塞,而是返回特定的错误信息。但是改方案仍然面临效率不高的问题,主要原因有:
- 应用程序需要连续随机地发送 IO 请求用来判断当前描述符是否可以操作,开发人员和维护者会可能为此恼火,毕竟谁都想从琐事中解放双手去做更有意义的事;
- 相比睡眠,不断的重试 IO 请求,更加浪费 CPU 资源。
IO 多路复用可以解决上述问题,它支持应用同时在多个文件描述符上阻塞。当没有文件描述符 IO 可以操作时,应用程序处于睡眠状态,其中一个或多个 IO 数据就绪后,应用程序被唤醒并且知道哪些文件描述符可以操作。
Linux 提供了三种 IO 多路复用方案:select、poll、epoll。
4.1 select()
4.1.1 接口
select 是一种同步 IO 多路复用。函数签名和相关宏定义如下:
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
4.1.2 参数
在没有 IO 就绪时,调用 select() 将阻塞进程。select() 监听一个文件描述符集合的三种 IO 事件:
readfds:监视是否有数据可读。writefds:监视是否可以进行无阻塞写。exceptfds:监视是否有异常发生,或者有带外数据(out-of-band)到达。
注意:指定的监听集合可以为NULL,此时,select()不监听任何事件。
select() 的第一个参数是集合中文件描述符的最大值加1,这样 select() 就知道集合中文件描述符的范围,避免处理事件时的不必要的循环。
参数 timeout 是指向 timeval 结构体的指针,精度为微秒,其定义如下:
#include <sys/time.h>
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
// 0: 立即返回
// -1: 永远阻塞
// >0: 超时返回
注意! select() 调用返回后,timout 参数会被修改,所以每次调用 select() 前必须重新初始化。
4.1.3 返回值
select() 成功返回后,返回值为 IO 就绪的文件描述符的个数,出错时返回 -1,此时 errono 值可能为:
EBADF:集合中存在非法文件描述符。EINTR:等待时捕获了一个信号,被迫中断,可以重新发起调用。EINVAL:无效的 timeout 参数。ENOMEM:没有足够的内存完成调用。
4.1.4 文件描述符操作宏
Linux 定义了三个符合 POSIX 接口规范的宏用来操作文件描述符。
FD_ZERO(&fd_set):从指定的集合中删除所有的文件描述符。FD_SET(fd, &fd_set):向指定的集合中添加一个文件描述符。FD_ISSET(fd, &fd_set):检查一个文件描述符是否在给定的集合中。FD_CLR(fd, &fd_set):从指定的集合中删除一个文件描述符。
上面持续提到文件描述符集合是静态的,其大小在运行期是固定的且有上限。该大小由 FD_SETSIZE 决定, 其定义如下,fd_set 使用位掩码来表示文件描述符,每一位的 index 可以表示一个文件描述符的值,fd_set 默认大小是 1024。这种规模受限且位掩码的遍历低效的设计是 select() 的主要缺点。
typedef long int __fd_mask;
#define __NFDBITS (8 *

本文详细介绍了Linux的IO模型,重点讲解了IO多路复用,包括select、poll和epoll。select在文件描述符数量有限的情况下存在效率问题,poll改进了这一问题但仍有遍历集合的开销。epoll通过添加和删除文件描述符的操作,解决了性能问题,支持边缘触发和水平触发,适用于高性能服务器。
&spm=1001.2101.3001.5002&articleId=122226495&d=1&t=3&u=b5e7e81170e647dd9cd9cd968d7cb376)
994

被折叠的 条评论
为什么被折叠?



