I/O Multiplexing -- Linux I/O 多路复用

本文深入解析了Linux系统中的I/O模型,包括阻塞I/O、非阻塞I/O、I/O多路复用、信号驱动I/O和异步I/O。详细阐述了每种模型的工作原理、优缺点及应用场景,帮助读者理解不同I/O模型如何影响程序性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文章还会涉及到同步 I/O,异步 I/O,阻塞 I/O 和非阻塞 I/O
首先我们需要理解以下概念:

Linux用户态和内核态

在现在操作系统中,CPU通常会在两种不同的模式下工作:

内核态

此模式下,程序代码能够完全,无限制地访问底层硬件,能够执行任意的 CPU 指令和访问任意的内存地址。内核模式通常留给最底层的,受信任的系统函数来使用。程序在内核模式下崩溃是灾难性的,这甚至可以使整台 PC 宕机。

用户态

在用户模式下,程序代码不能够直接访问硬件和内存。执行在用户态的代码必须委托系统函数去访问硬件和内存。因为有这种隔离机制的保护,程序在用户态下崩溃通常是可恢复的。PC 中大多数程序也是在用户态下执行。

进程切换

指操作系统进程调度切换,从某个进程到另外的进程。切换过程需要保存当前进程的所有状态,包括寄存器状态,关联的内核状态,虚拟内存的配置等,具体会经历以下几个步骤:

  • 保存处理器上下文,包括程序计数器和其他寄存器
  • 更新进程控制块 (PCB)
  • 移动进程的 PCB 到合适的队列,例如就绪队列,事件阻塞队列
  • 选择其他进程,并更新他的 PCB
  • 更新内存数据结构
  • 恢复 PCB 上下文

进程阻塞

一个阻塞的进程通常是在等待某个事件,例如信号的释放或者消息的到达。在多任务的系统中,阻塞的进程会通过系统调用去通知调度器自己处于 wait 的状态,以便能够被移除出时序队列。进程如果在 wait 状态下还霸占 CPU 继续执行,这被称为 busy-waiting (空等?)。显然这是不合理的,因为他浪费了 CPU 时钟周期,这原本可以被其他进程使用。所以当一个进程进入了阻塞状态,不应继续占用 CPU 资源。

缓冲式 I/O

当我们写数据(到文件系统),I/O 系统会累积数据到一个中间缓冲区,当缓冲区积累到足够数据时(或者调用flush())才会把数据发送到文件系统,这样减少了文件系统的访问次数。因为对文件系统(磁盘)的访问通常来说开销很大(对比内存间的拷贝),缓冲式 I/O 能够有效提高性能,尤其是那种多次的小数据量写操作。若是大数据量的写操作,非缓冲式 I/O 会更好,因为缓冲式 I/O 并不会显著减少(对文件系统)系统调用,却引入的额外的内存拷贝工作,这些数据拷贝操作带来了更高的 CPU 和内存开销。

文件描述符 (FD)

在 Unix 及其衍生的操作系统中,文件描述符 (FD) 是一个抽象的指示符 (原文:indicator, handle,多数文章翻译成句柄),用来访问文件或其他 I/O 资源,例如管道,socket等。FD 是 POSIX 编程接口的一部分,是个非负索引值,许多底层的程序都会使用到 FD

I/O 模型

当一个读操作发生,会经历以下两个阶段:

  • 数据准备阶段 —— 例如等待网络数据到达,当数据包到达时,他们会拷贝到内核缓冲区中
  • 数据转移阶段 —— 把数据从内核拷贝到用户进程

因为这两个阶段的存在,Linux 提供了以下5种 I/O 模型

Blocking I/O Model —— 阻塞式 I/O

阻塞式 I/O 是最常见的 I/O 模式,默认地,所有的 socket 都是阻塞式的
figure_6.1.png
这里我们用 UDP 协议和 recvfrom 系统调用来举例。上图中,进程调用了 recvfrom,系统函数在有数据报到达并已经拷贝到应用程序缓冲区时,或者有错误发生时才会返回(最常见的错误是被信号中断)。我们认为进程在 recvfrom 从调用到返回的整个阶段都被阻塞了。当 recvfrom 成功返回,应用程序才会去处理数据报。

Nonblocking I/O Model —— 非阻塞式 I/O

如果一个(数据准备阶段) I/O 调用没有完成,内核会立即返回一个错误标记,而不是阻塞这个进程
figure_6.2.png
第一次调用 recvfrom 时并没有数据到达,于是内核立即返回了错误标记 EWOULDBLOCK
第四次调用 recvfrom 时数据报已经到达,拷贝到应用程序缓冲区后,recvfrom 成功返回,之后程序会处理这些数据。
像这样,程序在一个非阻塞的 FD 上循环调用 recvfrom 被称为轮询。这通常会浪费 CPU 时钟周期,但这种模型也会偶尔使用到,例如一个系统只专注于某个功能的时候。

I/O Multiplexing Model —— I/O 多路复用

在 Linux I/O 多路复用模型,我们会阻塞在 select, poll, epoll 这些系统函数中,而不是阻塞在真正的 I/O 调用上。
figure_6.3.png
上图中,我们阻塞在 select() 函数上,等待 socket 数据可读。select() 返回则表示 socket 数据可读,之后我们才调用 recvfrom 拷贝数据到应用程序缓冲区

  • 缺点:这里我们使用了两次系统调用 (select 和 recvfrom),而阻塞式 I/O 只使用了一次 recvfrom
  • 优势:我们可以监听多个 FD 是否就绪

I/O 多路复用模型与阻塞式 I/O 模式非常相似。阻塞式 I/O 使用多线程(每个线程负责一个 FD)且每个线程都可以很自由地调用(阻塞式)系统函数 recvfrom,而非使用 select 负责监听多个 FD。

Signal-Driven I/O Model —— 信号驱动 I/O

告诉内核当某个 FD 就绪时,释放 SIGIO 信号来通知应用程序
figure_6.4.png
我们首先让 socket 使用信号驱动 I/O 模式,并使用 sigaction 系统函数注册一个信号处理器 (signal handler),该系统函数立即返回,这是非阻塞的。

当数据可读,SIGIO 信号释放出来被进程接收到,我们可以进行如下操作之一

  • 在 signal handler 中调用 recvfrom 读取数据,随后通知主循环数据已经准备好了
  • signal handler 通知主循环去读取数据

该模型的优势在于,等待数据到达的阶段不会阻塞,主循环可以继续执行其他任务并等待 signal handler 的通知(数据可读或可处理)

Asynchronous I/O Model —— 异步 I/O

异步 I/O 模型告诉内核执行 I/O 操作,等到整个 I/O 操作(包括数据准备阶段和数据转移阶段)完成后再通知我们。该模式跟信号驱动 I/O 非常相似,主要的区别是:信号驱动 I/O 中,内核通知进程 I/O 操作可以开始(仍需把数据从内核拷贝到进程),而异步 I/O 中内核通知我们 I/O 操作已经完成(数据已经在进程缓冲区中)
figure_6.5.png
我们调用了系统函数 aio_read,向内核传递了以下信息:

  • FD, 缓冲区指针,缓冲区大小
  • 文件偏移量
  • I/O 执行完毕的通知方式

aio_read 会立即返回,进程在等待 I/O 操作完成的整个阶段都不会被阻塞。

I/O 模型的比较

figure_6.6.png

前面4种模型的主要区别在第一个阶段(数据准备阶段),第二阶段(数据转移阶段)是一样的:进程都阻塞在数据转移阶段(从内核拷贝到应用程序缓冲区)。异步 I/O 内核负责这两个阶段,不需要应用程序干预。

同步 I/O 和异步 I/O

POSIX 定义如下

  • 同步 I/O 会导致请求 I/O 操作的进程阻塞,直到 I/O 操作完成
  • 异步 I/O 不会导致请求 I/O 操作的进程阻塞

根据这些定义,前面4种 I/O 模型 (blocking, nonblocking, I/O multiplexing, and signal-driven I/O) 都是同步 I/O,因为实际的 I/O 操作都会阻塞进程(举例:信号驱动 I/O 在等待数据时非阻塞,但在数据转移时阻塞了,是同步 I/O),只有异步 I/O 模型符合定义。

select, poll, epoll

施工中

<think>首先,用户的问题是:“I/O多路复用”,翻译成中文是“I/O多路复用”。我需要用中文回答问题。 我的回答必须包括: 1. 对问题的解释。 2. 在回答末尾,提出3-5个相关问题。 3. 相关问题必须以“§§相关问题§§”开头,且第一行完整输出这个短语。 4. 禁止换行分散,意思是相关问题部分要连贯,不要有多余的换行。 问题主题是I/O多路复用(I/O multiplexing)。我需要解释它是什么。 关键点: - I/O多路复用是一种技术,允许一个进程监视多个文件描述符(如套接字)以检查它们是否准备好进行I/O操作。 - 常见的实现包括select、poll、epoll(在Linux中)。 - 目的是高效处理多个I/O操作,避免阻塞或使用多线程的开销。 我的回答结构: - 先解释I/O多路复用的概念。 - 然后,简要描述其工作原理或常见用法。 - 最后,输出相关问题。 相关问题必须基于用户的问题和我的回答。用户的问题是“I/O多路复用”,所以我应该提出相关的后续问题,比如: 1. 常见的I/O多路复用模型有哪些? 2. 如何在不同操作系统上实现I/O多路复用? 3. I/O多路复用与多线程或多进程模型的区别是什么? 4. 在实际应用中,I/O多路复用的优缺点是什么? 5. 如何使用select、poll或epoll编写代码? 确保相关问题数量在3-5个。 最后,输出格式: - 回答内容。 - 然后,直接输出“§§相关问题§§”。 - 接着,列出问题,用数字或点号表示,如“1. ... 2. ...”。 从示例中,相关问题是用数字列表,如: 1. ··· 2. ··· 3. ··· 所以,我应该用类似格式。 现在,构建回答: - 开头:解释I/O多路复用- 中间:可能添加一个简单代码示例(但不是必须的,用户没有要求代码,但示例中有代码,所以我可以考虑添加一个简单的代码片段)。 - 结尾:相关问题。 用户的问题只有“I/O多路复用”,没有指定语言,但示例中有Python代码。所以,我可以添加一个简单的代码示例来展示select或epoll的使用。 为了符合规则,回答必须用中文。 大纲回答: 1. 定义I/O多路复用。 2. 描述其目的和优势。 3. 简要说明常见模型。 4. 添加一个代码示例(可选)。 5. 然后,相关问题。 代码示例:使用Python的select模块。 例如: ```python import select import socket # 创建一个服务器套接字 server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.bind(('localhost', 8080)) server_socket.listen(5) # 输入列表,包含要监视的套接字 inputs = [server_socket] while True: # 使用select监视可读的套接字 readable, _, _ = select.select(inputs, [], []) for s in readable: if s is server_socket: # 新连接 client_socket, addr = server_socket.accept() inputs.append(client_socket) else: # 处理客户端数据 data = s.recv(1024) if data: # 处理数据 pass else: # 连接关闭 inputs.remove(s) s.close() ``` 然后,相关问题。 相关问题想法: 1. select、poll和epoll有什么区别? 2. 在哪些场景下应该使用I/O多路复用? 3. I/O多路复用如何处理高并发? 4. 如何避免I/O多路复用中的性能瓶颈? 5. 在Python中,除了select,还有哪些模块支持I/O多路复用? 确保3-5个。 最终输出: - 回答内容。 - “§§相关问题§§” - 1. ... 2. ... 3. ...</think>I/O多路复用(I/O Multiplexing)是一种高效处理多个输入/输出操作的网络编程技术,允许单个进程同时监视多个文件描述符(如套接字),并在其中任何一个准备好进行读写操作时通知进程。核心目的是避免阻塞式I/O或多线程的开销,提高系统并发性能。常见实现模型包括select、poll和epollLinux特有),它们通过系统调用实现非阻塞事件循环。例如,使用select模型可以同时监听多个套接字,当数据可读或可写时触发处理,减少资源消耗。基本工作流程如下: - 初始化:创建并注册要监视的文件描述符列表。 - 事件循环:调用select/poll/epoll等待事件发生(如数据到达)。 - 事件处理:遍历就绪的描述符并执行相应I/O操作。 - 重复循环:持续监听新事件。 在实际应用中,I/O多路复用常用于高并发服务器(如Web服务器或数据库),以处理数千个连接。优势包括减少线程切换开销、简化代码结构;劣势是编程复杂度较高,需处理事件循环细节。以下是一个简单Python代码示例,使用select实现TCP服务器: ```python import select import socket # 创建服务器套接字 server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.bind(('localhost', 8080)) server_socket.listen(5) inputs = [server_socket] # 监视列表 while True: readable, _, _ = select.select(inputs, [], []) # 等待可读事件 for s in readable: if s is server_socket: # 新连接 client_socket, addr = s.accept() inputs.append(client_socket) else: # 客户端数据可读 data = s.recv(1024) if data: print(f"收到数据: {data.decode()}") else: # 连接关闭 inputs.remove(s) s.close() ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值