0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

一文读懂I/O复用

jf_78858299 来源:阿Q正砖 作者:阿Q正砖 2023-02-15 11:17 次阅读

今天给大家聊聊I/O复用,对于大部分公司面试来说,这块肯定是必问内容,它不仅能侧面反映面试这对基础掌握的是否扎实,还能反映出求职者的知识广度。

1 从阻塞 I/O 到 I/O 多路复用

阻塞 I/O,是指进程发起调用后,会被挂起(阻塞),直到收到数据再返回。如果调用一直不返回,进程就会一直被挂起。因此,当使用阻塞 I/O 时,需要使用多线程来处理多个文件描述符。

多线程切换有一定的开销,因此引入非阻塞 I/O。非阻塞 I/O 不会将进程挂起,调用时会立即返回成功或错误,因此可以在一个线程轮询多个文件描述符是否就绪。

但是非阻塞 I/O 的缺点是:每次发起系统调用,只能检查一个文件描述符是否就绪。当文件描述符很多时,系统调用的成本很高。

因此引入了 I/O 多路复用,可以 通过一次系统调用,检查多个文件描述符的状态 。这是 I/O 多路复用的主要优点,相比于非阻塞 I/O,在文件描述符较多的场景下,避免了频繁的用户态和内核态的切换,减少了系统调用的开销。

I/O 多路复用相当于将「遍历所有文件描述符、通过非阻塞 I/O 查看其是否就绪」的过程从用户线程移到了内核中,由内核来负责轮询。

进程可以通过 select、poll、epoll 发起 I/O 多路复用的系统调用,这些系统调用都是同步阻塞的: 如果传入的多个文件描述符中,有描述符就绪,则返回就绪的描述符;否则如果所有文件描述符都未就绪,就阻塞调用进程,直到某个描述符就绪,或者阻塞时长超过设置的 timeout 后,再返回 。I/O 多路复用内部使用非阻塞 I/O 检查每个描述符的就绪状态。

如果 timeout参数设为 NULL,会无限阻塞直到某个描述符就绪;如果timeout参数设为 0,会立即返回,不阻塞。

I/O 多路复用引入了一些额外的操作和开销,性能更差。但是好处是用户可以在一个线程内同时处理多个 I/O 请求。如果不采用 I/O 多路复用,则必须通过多线程的方式,每个线程处理一个 I/O 请求。后者线程切换也是有一定的开销的。

2 为什么 I/O 多路复用内部需要使用非阻塞 I/O?

I/O 多路复用内部会遍历集合中的每个文件描述符,判断其是否就绪:

for fd in read_set if (readable(fd)) // 判断fd是否就绪 count++; FDSET(fd, &res_rset) // 将fd添加到就绪队列中 break;return count;

这里的 readable(fd) 就是一个非阻塞 I/O 调用。试想,如果这里使用阻塞 I/O,那么fd未就绪时,select会阻塞在这个文件描述符上,无法检查下个文件描述符。

注意:这里说的是 I/O 多路复用的内部实现,而不是说,使用 I/O 多路复用就必须使用非阻塞 I/O。

3 select

函数签名与参数

int select(int nfds,            fd_set *restrict readfds,            fd_set *restrict writefds,            fd_set *restrict errorfds.            struct timeval *restrict timeout);

readfds、writefds、errorfds 是三个文件描述符集合。select 会遍历每个集合的前 nfds个描述符,分别找到可以读取、可以写入、发生错误的描述符,统称为“就绪”的描述符。然后用找到的子集替换参数中的对应集合,返回所有就绪描述符的总数。

timeout 参数表示调用 select 时的阻塞时长。如果所有文件描述符都未就绪,就阻塞调用进程,直到某个描述符就绪,或者阻塞超过设置的 timeout 后,返回。如果 timeout 参数设为 NULL,会无限阻塞直到某个描述符就绪;如果 timeout 参数设为 0,会立即返回,不阻塞。

3.1 什么是文件描述符 fd

文件描述符(file descriptor)是一个非负整数,从 0 开始。进程使用文件描述符来标识一个打开的文件。

系统为每一个进程维护了一个文件描述符表,表示该进程打开文件的记录表,而 文件描述符实际上就是这张表的索引 。当进程打开(open)或者新建(create)文件时,内核会在该进程的文件列表中新增一个表项,同时返回一个文件描述符 —— 也就是新增表项的下标。

一般来说,每个进程最多可以打开 64 个文件,fd ∈ 0~63。在不同系统上,最多允许打开的文件个数不同,Linux 2.4.22 强制规定最多不能超过 1,048,576。

每个进程默认都有 3 个文件描述符:0 (stdin)、1 (stdout)、2 (stderr)。

3.2 socket 与 fd 的关系

socket 是 Unix 中的术语。socket 可以用于同一台主机的不同进程间的通信,也可以用于不同主机间的通信。一个 socket 包含地址、类型和通信协议等信息,通过 **socket() **函数创建:

int socket(int domain, int type, int protocol)

返回的就是这个 socket 对应的文件描述符 fd操作系统将 socket 映射到进程的一个文件描述符上,进程就可以通过读写这个文件描述符来和远程主机通信。

可以这样理解:socket 是进程间通信规则的高层抽象,而 fd 提供的是底层的具体实现。socket 与 fd 是一一对应的。通过 socket 通信,实际上就是通过文件描述符 fd 读写文件。这也符合 Unix“一切皆文件”的哲学。

3.3 fd_set 文件描述符集合

参数中的 **fd_set **类型表示文件描述符的集合。

由于文件描述符 fd 是一个从 0 开始的无符号整数,所以可以使用 fd_set二进制每一位来表示一个文件描述符。某一位为 1,表示对应的文件描述符已就绪。比如比如设 fd_set 长度为 1 字节,则一个 fd_set 变量最大可以表示 8 个文件描述符。当 **select **返回 **fd_set = 00010011 **时,表示文件描述符 **125 **已经就绪。

3.4 select 使用示例

下图的代码说明:

(1)先声明一个 fd_set 类型的变量 readFDs

(2)调用 FD_ZERO,将 readFDs 所有位 置 0

(3)调用 FD_SET,将 readFDs 感兴趣的位置 1,表示要监听这几个文件描述符

(4)将 readFDs 传给 select,调用 select

(5)select会将 readFDs 中就绪的位置 1,未就绪的位置 0,返回就绪的文件描述符的数量

(6)当 select 返回后,调用 FD_ISSET 检测给定位是否为 1,表示对应文件描述符是否就绪

比如进程想监听 1、2、5 这三个文件描述符,就将 readFDs 设置为 00010011,然后调用 select

如果 fd=1fd=2 就绪,而 fd=5 未就绪,select 会将 readFDs 设置为 00000011 并返回 2。

如果每个文件描述符都未就绪,select 会阻塞 timeout 时长,再返回。这期间,如果 readFDs 监听的某个文件描述符上发生可读事件,则 select 会将对应位置 1,并立即返回。

图片

**3.5 **select 的缺点

  1. 性能开销大
    1. 调用 select 时会陷入内核,这时需要将参数中的 fd_set 从用户空间拷贝到内核空间
    2. 内核需要遍历传递进来的所有 fd_set 的每一位,不管它们是否就绪
  2. 同时能够监听的文件描述符数量太少。受限于 sizeof(fd_set) 的大小,在编译内核时就确定了且无法更改。一般是 1024,不同的操作系统不相同。

4 poll

poll 和 select 几乎没有区别。poll 在用户态通过数组方式传递文件描述符,在内核会转为链表方式 存储 ,没有最大数量的限制 。

poll 的函数签名如下:

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

其中 fds 是一个 pollfd 结构体类型的数组,调用 poll() 时必须通过 nfds 指出数组 fds 的大小,即文件描述符的数量。

从性能开销上看,poll 和 select 的差别不大。

5 epoll

epoll 是对 select 和 poll 的改进,避免了“性能开销大”和“文件描述符数量少”两个缺点。

简而言之,epoll 有以下几个特点:

  • 使用红黑树存储文件描述符集合
  • 使用队列存储就绪的文件描述符
  • 每个文件描述符只需在添加时传入一次;通过事件更改文件描述符状态

select、poll 模型都只使用一个函数,而 epoll 模型使用三个函数:epoll_createepoll_ctlepoll_wait

5.1 epoll_create

int epoll_create(int size);

epoll_create 会创建一个 epoll 实例,同时返回一个引用该实例的文件描述符。

返回的文件描述符仅仅指向对应的 epoll 实例,并不表示真实的磁盘文件节点。其他 APIepoll_ctlepoll_wait 会使用这个文件描述符来操作相应的 epoll 实例。

当创建好 epoll 句柄后,它会占用一个 fd 值,在 linux 下查看 /proc/进程id/fd/,就能够看到这个 fd。所以在使用完 epoll 后,必须调用 close(epfd) 关闭对应的文件描述符,否则可能导致 fd 被耗尽。当指向同一个 epoll 实例的所有文件描述符都被关闭后,操作系统会销毁这个 epoll 实例。

epoll 实例内部存储:

  • 监听列表:所有要监听的文件描述符,使用红黑树
  • 就绪列表:所有就绪的文件描述符,使用链表

5.2 epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll_ctl 会监听文件描述符 fd 上发生的 event 事件。

参数说明:

  • epfdepoll_create 返回的文件描述符,指向一个 epoll 实例
  • fd 表示要监听的目标文件描述符
  • event 表示要监听的事件(可读、可写、发送错误…)
  • op 表示要对 fd 执行的操作,有以下几种:
    • EPOLL_CTL_ADD:为 fd 添加一个监听事件 event
    • EPOLL_CTL_MOD:Change the event event associated with the target file descriptor fd(event 是一个结构体变量,这相当于变量 event 本身没变,但是更改了其内部字段的值)
    • EPOLL_CTL_DEL:删除 fd 的所有监听事件,这种情况下 event 参数没用

返回值 0 或 -1,表示上述操作成功与否。

epoll_ctl 会将文件描述符 fd 添加到 epoll 实例的监听列表里,同时为 fd 设置一个回调函数,并监听事件 event。当 fd 上发生相应事件时,会调用回调函数,将 fd 添加到 epoll 实例的就绪队列上。

5.3 epoll_wait

int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);

这是 epoll 模型的主要函数,功能相当于 select

参数说明:

  • epfdepoll_create 返回的文件描述符,指向一个 epoll 实例
  • events 是一个数组,保存就绪状态的文件描述符,其空间由调用者负责申请
  • maxevents 指定 events 的大小
  • timeout 类似于 select 中的 timeout。如果没有文件描述符就绪,即就绪队列为空,则 epoll_wait 会阻塞 timeout 毫秒。如果 timeout 设为 -1,则 epoll_wait 会一直阻塞,直到有文件描述符就绪;如果 timeout 设为 0,则 epoll_wait 会立即返回

返回值表示 events 中存储的就绪描述符个数,最大不超过 maxevents

5.4 epoll 的优点

一开始说,epoll 是对 select 和 poll 的改进,避免了“性能开销大”和“文件描述符数量少”两个缺点。

对于“文件描述符数量少”,select 使用整型数组存储文件描述符集合,而 epoll 使用红黑树存储,数量较大。

对于“性能开销大”,epoll_ctl 中为每个文件描述符指定了回调函数,并在就绪时将其加入到就绪列表,因此 epoll 不需要像 select 那样遍历检测每个文件描述符,只需要判断就绪列表是否为空即可。这样,在没有描述符就绪时,epoll 能更早地让出系统资源。

相当于时间复杂度从 O(n) 降为 O(1)

此外,每次调用 select 时都需要向内核拷贝所有要监听的描述符集合,而 epoll 对于每个描述符,只需要在 epoll_ctl 传递一次,之后 epoll_wait 不需要再次传递。这也大大提高了效率。

5.5 水平触发、边缘触发

select 只支持水平触发,epoll 支持水平触发和边缘触发。

水平触发 (LT,Level Trigger):当文件描述符就绪时,会触发通知,如果用户程序没有一次性把数据读/写完,下次还会发出可读/可写信号进行通知。

边缘触发 (ET,Edge Trigger):仅当描述符从未就绪变为就绪时,通知一次,之后不会再通知。

区别:边缘触发效率更高, 减少了事件被重复触发的次数 ,函数不会返回大量用户程序可能不需要的文件描述符。

水平触发、边缘触发的名称来源:数字电路当中的电位水平,高低电平切换瞬间的触发动作叫边缘触发,而处于高电平的触发动作叫做水平触发。

5.6 为什么边缘触发必须使用非阻塞 I/O?

关于这个问题的解答,强烈建议阅读这篇文章。下面是一些关键摘要:

  • 每次通过 read 系统调用读取数据时,最多只能读取缓冲区大小的字节数;如果某个文件描述符一次性收到的数据超过了缓冲区的大小,那么需要对其 read 多次才能全部读取完毕
  • select 可以使用阻塞 I/O 。通过 select 获取到所有可读的文件描述符后,遍历每个文件描述符,read 一次数据(见上文 select 示例)
    • 这些文件描述符都是可读的,因此即使 read 是阻塞 I/O,也一定可以读到数据,不会一直阻塞下去
    • select 采用水平触发模式,因此如果第一次 read 没有读取完全部数据,那么下次调用 select 时依然会返回这个文件描述符,可以再次 read
    • select 也可以使用非阻塞 I/O 。当遍历某个可读文件描述符时,使用 for 循环调用 read 多次 ,直到读取完所有数据为止(返回 EWOULDBLOCK)。这样做会多一次 read 调用,但可以减少调用 select 的次数
  • epoll 的边缘触发模式下,只会在文件描述符的可读/可写状态发生切换时,才会收到操作系统的通知
    • 因此,如果使用 epoll边缘触发模式 ,在收到通知时,**必须使用非阻塞 I/O,并且必须循环调用 ** readwrite 多次,直到返回 EWOULDBLOCK 为止 ,然后再调用 epoll_wait 等待操作系统的下一次通知
    • 如果没有一次性读/写完所有数据,那么在操作系统看来这个文件描述符的状态没有发生改变,将不会再发起通知,调用 epoll_wait 会使得该文件描述符一直等待下去,服务端也会一直等待客户端的响应,业务流程无法走完
    • 这样做的好处是每次调用 epoll_wait 都是有效的——保证数据全部读写完毕了,等待下次通知。在水平触发模式下,如果调用 epoll_wait 时数据没有读/写完毕,会直接返回,再次通知。因此边缘触发能显著减少事件被触发的次数
    • 为什么 epoll边缘触发模式不能使用阻塞 I/O ?很显然,边缘触发模式需要循环读/写一个文件描述符的所有数据。如果使用阻塞 I/O,那么一定会在最后一次调用(没有数据可读/写)时阻塞,导致无法正常结束

6 三者对比

  • select:调用开销大(需要复制集合);集合大小有限制;需要遍历整个集合找到就绪的描述符
  • poll:poll 采用数组的方式存储文件描述符,没有最大存储数量的限制,其他方面和 select 没有区别
  • epoll:调用开销小(不需要复制);集合大小无限制;采用回调机制,不需要遍历整个集合

selectpoll 都是在用户态维护文件描述符集合,因此每次需要将完整集合传给内核;epoll 由操作系统在内核中维护文件描述符集合,因此只需要在创建的时候传入文件描述符。

此外 select 只支持水平触发,epoll 支持边缘触发。

7 适用场景

当连接数较多并且有很多的不活跃连接时,epoll 的效率比其它两者高很多。当连接数较少并且都十分活跃的情况下,由于 epoll 需要很多回调,因此性能可能低于其它两者。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 编程
    +关注

    关注

    88

    文章

    3556

    浏览量

    93513
  • i/o
    i/o
    +关注

    关注

    0

    文章

    33

    浏览量

    4561
收藏 人收藏

    评论

    相关推荐

    读懂i/o端口地址译码

    I/O端口是接口电路中能被CPU直接访问的寄存器。访问端口就是访问接口电路中的寄存器。个接口电路(外设)通常拥有不止个端口,如命令口、状态口、数据口等。端口地址编码形式有统
    的头像 发表于 11-16 09:40 1.4w次阅读
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>读懂</b><b class='flag-5'>i</b>/<b class='flag-5'>o</b>端口地址译码

    读懂接口模块的组合应用有哪些?

    读懂接口模块的组合应用有哪些?
    发表于 05-17 07:15

    读懂如何去优化AC耦合电容?

    读懂如何去优化AC耦合电容?
    发表于 06-08 07:04

    读懂什么是NEC协议

    读懂什么是NEC协议?
    发表于 10-15 09:22

    读懂中断方式和轮询操作有什么区别吗

    读懂中断方式和轮询操作有什么区别吗?
    发表于 12-10 06:00

    使用引脚作为普通的I/O定要进行引脚的功能复用

    ##学习笔记.相关表格1.PB3,PB4,PA13,PA14,PA15引脚可根据上表复用成普通IO口。在mcu复位的时候这几个引脚被作为jtag的功能。当我们要使用这些引脚作为普通的I/O
    发表于 03-01 07:03

    数字I/O介绍

    数字I/O脚有专用和复用。数字I/O脚的功能通过9个16位控制寄存器来控制。控制寄存器分为两类:(1)I
    发表于 09-16 12:20 19次下载

    Java I/O 的相关方法分析

    asynchronous I/O。 Java 是种跨平台语言,为了支持异步 I/O,诞生了 NIO,Java1.4 引入的 NIO1.0
    发表于 09-27 13:18 0次下载
    Java <b class='flag-5'>I</b>/<b class='flag-5'>O</b> 的相关方法分析

    Linux I/O多路复用

    /O,非阻塞I/O,I/O多路复用,信号驱动I/
    发表于 04-02 14:31 300次阅读

    Linux中如何使用信号驱动式I/O

    大图 I/O 复用 (select、poll、epoll): 通过 I/O 复用函数向内核注册
    的头像 发表于 03-12 14:47 2367次阅读
    Linux中如何使用信号驱动式<b class='flag-5'>I</b>/<b class='flag-5'>O</b>?

    关于STM32通用和复用I/O

    关于STM32通用和复用I/O,概述​ STM32F10x系列具有丰富的端口可供使用包括26、37、51、80、112个多功能双向5V兼容的快速
    发表于 12-03 09:51 9次下载
    关于STM32通用和<b class='flag-5'>复用</b><b class='flag-5'>I</b>/<b class='flag-5'>O</b>口

    读懂MCU的特点、功能及如何编写

    读懂MCU的特点、功能及如何编写
    发表于 12-05 09:51 24次下载
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>读懂</b>MCU的特点、功能及如何编写

    关于STM32的 I/O 复用功能

    今天给大家分享两点内容: 是,为什么我们要先开启STM32外设时钟;二是,关于STM32的 I/O 复用功能及什么时候开启AFIO时钟。
    的头像 发表于 10-20 14:19 3496次阅读

    读懂,什么是BLE?

    读懂,什么是BLE?
    的头像 发表于 11-27 17:11 2062次阅读
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>读懂</b>,什么是BLE?

    读懂车规级AEC-Q认证

    读懂车规级AEC-Q认证
    的头像 发表于 12-04 16:45 846次阅读