Linux IO 多路复用(select、poll、epoll)

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 多路复用方案:selectpollepoll

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):从指定的集合中删除一个文件描
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值