Linux五 ——I/O复用

IO复用

I/O (数据交换过程)

1. 背景

I/O即Input/Output,由于程序和运行时数据是在内存中驻留的,由CPU这个超快的计算核心来执行,涉及到数据交换的地方,通常是磁盘、网络等,就需要I/O接口

I/O编程中涉及到流,其是相对于内存而言的,所以,Input Stream就是数据从外面(磁盘、网络)流进内存,Output Stream就是数据从内存流到外面去。

2. 概念

I/O操作就是在运行代码的过程中,可能需要对文件读写,即将文件输入到内存和将代码结果输出到外设(网络、磁盘)的过程。

3. 分类

  • 网络I/O:通过网络进行数据的拉取和输出;
  • 磁盘I/O:主要是对磁盘进行读写工作

五种I/O类型

1. 阻塞式I/O (同步I/O)

进程或线程等待某个条件,如果条件不满足,则一直等下去。条件满足,则进行下一步操作(默认情况)

应用进程通过系统调用recvfrom接收数据,但由于内核还未准备好数据报,应用进程就会阻塞住,知道内核准备好数据报,recvfrom完成数据报复制工作,应用进程才能结束阻塞状态。

在这里插入图片描述

优点:

设备文件不可操作时,可进入休眠状态,将CPU资源让出;当设备文件可以操作的时候,就必须唤醒进程,一般在中断函数中完成唤醒工作。

缺点:

耗费时间,适合并发低,时效性要求低的情况。

2. 非阻塞式I/O(同步I/O)

应用进程与内核交互,目的未到达之前,不再一味地等待,而是直接返回;

通过轮询的方式,不停的去问内核数据有没有准备好。如果一次轮询发现数据已经准备好了,那就把数据拷贝到用户空间中。

在这里插入图片描述

前三次调用recvfrom时,没有数据可返回,内核立即返回EWOULDBOLOCK错误;

第四次调用recvfrom时,已有一个数据准备好,将其从内核复制到应用进程缓冲区,recvfrom成功返回。

轮询(polling)

应用进程持续轮询内核,以查看某个操作是否就绪

缺点:

往往消耗大量CPU时间

3. I/O复用 (select和poll) (同步I/O)

通过调用select或poll,阻塞在这两个系统调用中的某一个上,而不是阻塞在真正的I/O系统调用上。

阻塞于select调用,等待数据报套接字变为可读,当select返回套接字可读这一条件时,调用recvfrom把所读数据报复制到应用进程缓存区。

多个进程I/O注册到同一个select,当用户进程调用select,select监听所有注册好的I/O

在这里插入图片描述

  1. 若所有的被监听I/O需要的数据未准备好,则阻塞
  2. 任意一个所需数据准备好后,select调用返回
  3. 用户进程通过recvfrom进行数据拷贝

优点:

可以等待多个描述符就绪

4. 信号驱动式I/O (SIGIO)(同步I/O)

在这里插入图片描述

  1. 开启套接字信号驱动I/O功能,通过sigaction系统调用安装一个信号处理函数,该系统函数立即返回,不阻塞;

  2. 数据报准备好后,内核为该进程产生一个SIGIO信号递交给进程;

  3. 可以在信号处理函数中调用recvfrom读取数据报,通知主循环数据已准备好待处理;

  4. 可以立即通知主循环,读取数据报

优点:

等待数据报到达期间,进程不被阻塞。主循环可以继续执行,等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。

5. 异步I/O (POSIX的aio_系列函数)

用户进程告知内核启动某个操作,并由内核在整个操作中完成后通知用户进程

与信号I/O驱动的区别
  • 信号驱动I/O是由内核通知我们何时可以启动一个I/O操作
  • 异步I/O是由内核通知我们I/O操作何时完成
步骤
  1. 用户进程调用aio_read函数,给内核传递描述符、缓冲区指针、缓冲区大小和文件偏移,告诉内核整个操作完成时如何通知我们,然后就立刻去做其他事情;
  2. 当内核受到aio_read后,会立即返回,然后内核开始等待数据准备,数据准备好以后,直接把数据拷贝到用户空间,然后再通知进程本次I/O已完成

在这里插入图片描述

各种I/O模型的比较

同步I/O

导致请求进程阻塞,直到I/O操作完成;

四种同步模型的区别在于第一阶段等待数据的处理方式不同,第二阶段均为将数据从内核空间复制到用户空间缓冲区期间,进程阻塞于recvfrom调用。

异步I/O

不导致请求进程阻塞

在这里插入图片描述

I/O 复用函数

多路复用接口select/poll/epoll,内核提供给用户态的多路复用系统调用,进程可以通过一个系统调用函数从内核中获取多个事件:

(1)select—>两次遍历 + 两次拷贝

  1. 把已连接的socket放在一个文件描述符集合,调用select函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生;
  2. 通过遍历,有事件产生就把此socket标记为可读/可写,然后再整个拷贝回用户态;
  3. 用户态还需要遍历找到刚刚标记的socket。

(2)poll

动态数组,以链表形式来组织,相比于select,没有文件描述符个数这个限制,当然还会受到系统文件描述符限制。

(3)epoll(event poll)—>红黑树

  1. 在内核里使用红黑树来个弄脏进程所有待检测的文件描述字
  2. 调用epoll_ctl()函数,把需要监控的socket加入内核中的红黑树里;(红黑树的增删查时间复杂度是 O ( l o g n ) O(logn) O(logn),不需要每次操作都传入整个集合,只需要传入一个待检测的socket,减少了内核和用户空间的大量数据拷贝和内存分配)
  3. epoll使用事件驱动的机制,内核里维护了一个链表来记录就绪事件(当某个socket有事件发生时,通过回调函数,内核会将其加入到这个就绪事件列表中)
  4. 当用户调用epoll_waiit()函数时,只会返回有事件发生的文件描述符的个数,不需要像select/poll那样轮询扫描整个socket集合,大大提高了检测的效率。

(4)epoll触发机制

  1. epoll支持的时间触发模式:边缘触发ET水平触发LT

  2. 边缘触发模式ET

    当被监控的socket描述符上有可读事件发生时,服务器只会从epoll_wait中苏醒一次,即使经常没有调用read函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读完,只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。

  3. 水平触发模式LT

    当被监控的socket上有可读事件发生时,服务器不断地从epoll_wait中苏醒,直到内核缓冲区数据被read函数读完才结束,目的是告诉我们有数据,只要满足事件的条件,比如内核中有数据需要读取,就一直不断地把这个事件传递给用户。

(5)select和poll的区别

  1. select 和 poll 采⽤轮询的⽅式检查就绪事件,每次都要扫描整个⽂件描述符,复杂度O(N);
  2. epoll 采⽤回调⽅式检查就绪事件,只会返回有事件发⽣的⽂件描述符的个数,复杂度O(1);
  3. select 只⼯作在低效的LT模式, epoll 可以在 ET ⾼效模式⼯作;
  4. epoll 是 Linux 所特有,⽽ select 则应该是 POSIX 所规定,⼀般操作系统均有实现;
  5. select 单个进程可监视的fd数量有限,即能监听端⼝的⼤⼩有限, 64位是2048; epoll 没有最⼤并发连接的限制,能打开的 fd 的上限远⼤于2048(1G的内存上能监听约10万个端⼝) ;
  6. select:内核需要将消息传递到⽤户空间,都需要内核拷⻉动作; epoll通过内核和⽤户空间共享⼀块内存来实现的。
### Linux 种 I/O 模型详解 #### 阻塞 I/O (Blocking I/O) 在阻塞 I/O 模型中,当应用程序发出 I/O 请求时,进程会进入等待状态直到操作完成。在此期间,CPU 不会被分配给该进程,从而导致资源浪费。这种模型的特点是简单直观,但在高并发场景下性能较差[^1]。 #### 非阻塞 I/O (Non-blocking I/O) 非阻塞 I/O 的特点是调用不会使进程挂起。如果数据尚未准备好,则函数会立即返回一个错误码;只有当数据可用时才会成功读取或写入。为了获取数据,应用层需要不断轮询设备的状态,这种方式虽然提高了 CPU 利用率,但也带来了额外开销[^4]。 #### I/O 多路复用 (I/O Multiplexing) 利用 `select` 或者更高效的 `poll`, `epoll` 函数可以实现单个线程同时监控多个文件描述符上的事件变化情况。一旦某个 FD 就绪即可对其进行相应的操作而无需反复查询每一个连接是否可读/写。这种方法显著减少了上下文切换次数并提升了服务器端处理能力[^3]。 #### 信号驱动式 I/O (Signal-driven I/O) 此模式允许内核向用户空间发送 SIGIO 信号告知特定条件已经满足(比如套接字上有新数据到达),之后再由专门设置好的 handler 来负责实际的数据传输动作。相比前几种方法它不需要主动询问或者长时间停留于某一点上,但是编写起来较为复杂且存在一定的局限性。 #### 异步 I/O (Asynchronous I/O) 真正意义上的 AIO 是指提交了一个请求后就可以去做别的事情了,等到操作系统完成了所有的底层工作并将最终的结果反馈回来为止整个流程才算结束。这意味着在整个过程中既不会有显式的等待也不会涉及到任何手动干预的行为——一切都是自动化的。 ```python import asyncio async def main(): print('Waiting...') await asyncio.sleep(2) # Simulate an asynchronous operation. print('Done!') # Run the event loop to execute coroutine objects. asyncio.run(main()) ``` 上述代码展示了如何使用 Python 中的 `asyncio` 库来模拟异步 I/O 操作。通过定义协程并通过事件循环运行它们,可以在不阻塞主线程的情况下执行耗时任务。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

L☆★

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值