linux IO介绍
1. Deepsek 版本答案
好的!我们来用「教科书式」讲解方式,把 select
/poll
和 epoll
讲清楚。它们都是 Linux 中 I/O 多路复用的核心机制,用于高效地同时监控多个文件描述符(如网络套接字)的状态变化(如可读、可写、异常)。
设想一个场景:你是一个服务员(一个进程),需要同时服务多张桌子(多个文件描述符 fd,比如网络连接)。每张桌子可能点菜(有数据可读)、要求加水(可写)、或者出了什么问题(异常)。你需要一种高效的方式知道哪张桌子需要你,而不是傻傻地站在一张桌子旁边等。
传统的阻塞 I/O(效率低下):
- 就像你一次只服务一张桌子。服务员走到 A 桌问:“需要什么?”如果没有需求,服务员就一直傻等在 A 桌旁边(阻塞),完全忽略了 B、C、D 等桌可能的需求。这是最简单但效率最低的方式。
I/O 多路复用(高效监控):
- 目标是让一个服务员能高效地知晓多张桌子的状态变化。
select
,poll
,epoll
就是解决这个问题的不同方法。核心思想是:服务员(进程)一次性地告诉内核(操作系统):“帮我盯着这堆桌子(fd 集合),看看它们哪些变成了我需要关心的状态(比如可以读、可以写),如果有的桌子状态变了,或者我等了 X 秒还没有任何桌子变状态,就通知我一下。”
第一代:select
核心原理
- 用户准备: 服务员(进程)创建三个 fd 集合(
readfds
,writefds
,errorfds
),设置好哪些 fd 要关心可读,哪些要关心可写,哪些要关心异常。同时设定一个超时时间。 - 系统调用: 调用
select(max_fd + 1, &readfds, &writefds, &errorfds, timeout);
max_fd + 1
: 传入选中的最大 fd 值 + 1(为了效率)。&readfds
/&writefds
/&errorfds
: 传入关心的 fd 集合指针。timeout
: 等待超时时间(NULL 表示无限等待,0 表示立即返回)。
- 内核工作:
- 把传入的三个 fd 集合复制到内核空间。
- 遍历用户指定的整个 fd 集合(0 到
max_fd
),挨个检查每个 fd 是否在传入的集合中,如果在,就检查其是否就绪(可读/可写/异常)。 - 如果没有 fd 就绪,内核将进程挂起(阻塞),直到有 fd 就绪或者超时。
- 返回结果:
- 当有 fd 就绪或超时,
select
返回。 - 将修改后的三个 fd 集合(仅包含真正就绪的 fd)复制回用户空间(原来传入的集合会被覆盖)。
- 返回就绪的 fd 的总数量。
- 当有 fd 就绪或超时,
- 用户处理: 服务员(进程)拿到修改后的 fd 集合,遍历(0 到
max_fd
)所有它关心的 fd,检查哪些 fd 在返回的集合中被置位(就绪),然后对就绪的 fd 进行相应操作(比如读取数据)。
优点
- 跨平台(POSIX 标准)。
- 简单,基本所有系统都支持。
缺点(主要问题)
- fd 数量限制: 单个进程默认能监控的 fd 数量有限(由
FD_SETSIZE
决定,通常是 1024),编译时修改虽可能,但有风险。 - 线性遍历开销:
- 用户->内核:每次调用都要传递整个巨大的 fd 集合(包含 0-
max_fd
),内核需要复制。 - 内核遍历:内核需要线性扫描 0 到
max_fd
所有 fd 是否在集合中,然后检查其状态,不论其是否被关心!在监控 fd 少但max_fd
大时(如max_fd=10000
但只监控 10 个 fd),效率极低。 - 内核->用户:内核修改集合后又要复制回用户空间。
- 用户遍历:用户进程收到结果后,也要再次线性扫描(0 到
max_fd
)所有关心的 fd 才能找到真正就绪的几个。最坏情况遍历次数多(两次遍历,每次都可能上千次)。
- 用户->内核:每次调用都要传递整个巨大的 fd 集合(包含 0-
- 状态丢失:
select
调用返回后,原始关心的集合已被内核修改覆盖。如果需要再次监控,用户进程必须重新设置所有关心的集合。
第二代:poll
核心原理(解决 select
部分痛点)
- 用户准备: 服务员(进程)创建一个
struct pollfd
类型的数组fds[]
。每个结构体代表一个要监控的 fd 及其关心的事件:
初始化struct pollfd {int fd; /* 文件描述符 */short events; /* 监控哪些事件(POLLIN, POLLOUT 等) */short revents; /* 返回哪些事件发生了(内核填充) */ };
fds
,设置好每个fd
和对应的events
(关心的事件)。 - 系统调用: 调用
poll(fds, nfds, timeout);
fds
:pollfd
结构数组的指针。nfds
:fds
数组的大小(实际关心的 fd 数量)。timeout
: 超时时间(毫秒)。
- 内核工作:
- 复制
fds
数组到内核空间。 - 遍历
fds
数组(长度为nfds
),检查其中每个fd
对应的状态是否满足其events
要求。 - 如果没有 fd 就绪,挂起进程(阻塞),直到有就绪或超时。
- 复制
- 返回结果:
- 内核将就绪的事件信息(
POLLIN
,POLLOUT
等)填回每个pollfd
的revents
字段。 - 将修改后的
fds
数组复制回用户空间。 - 返回就绪的 fd 数量(即
revents
非 0 的pollfd
个数)。
- 内核将就绪的事件信息(
- 用户处理: 服务员(进程)拿到返回的
fds
数组,遍历整个数组(长度为nfds
),检查每个pollfd
的revents
字段,如果非零,说明对应的 fd 有事件发生,进行相应处理。
相对于 select
的改进
- 无 fd 数量限制(重要): 监控的 fd 数量仅受系统资源和用户空间定义数组大小的限制(通过
nfds
指定)。解决了FD_SETSIZE
的限制。 - 更清晰的事件分离: 使用
events
指定关心事件,revents
返回实际发生事件,互不干扰,避免了状态丢失问题。用户调用后无需重新设置事件。 - 更高效的传递(理论上): 只需传递
nfds
个结构体(只包含关心的 fd),而不是一个巨大的位图范围(0-max_fd
)。内核遍历数量是用户关心的数量(nfds
),而非最大 fd 值。用户遍历也只需要遍历关心的数量(nfds
)。
缺点(依然存在的问题)
- 性能瓶颈(大数量监控):
- 用户->内核->用户:每次调用仍然需要复制整个
pollfd
结构体数组(包含所有关心的 fd),如果数组很大(成千上万),开销显著。 - 内核线性遍历:内核仍然需要线性遍历整个传入的数组(
nfds
个元素)来检查 fd 的状态。当监控的 fd 数量非常多(万级别)时,遍历开销成为瓶颈(时间复杂度 O(n))。
- 用户->内核->用户:每次调用仍然需要复制整个
- 水平触发(LT - Level Triggered): 和
select
一样,poll
也是水平触发模式(稍后详解)。 - 仍需主动遍历: 用户进程仍需遍历整个返回的数组(
nfds
个元素)来找出哪些 fd 确实就绪了。虽然遍历次数只等于关心的数量nfds
,但当nfds
很大且只有少数 fd 就绪时,效率仍然不够完美。
第三代:epoll
(Linux 特有)
为了解决 select
/poll
在处理海量并发连接时的性能瓶颈(万级甚至十万级),Linux 2.6+ 引入了 epoll
。
核心思想与重大改进
-
状态维护在内核:
- 创建一个
epoll
实例 (epoll_create
),它在内核中维护了一个状态表(通常实现为红黑树 + 就绪链表)。 - 用户通过
epoll_ctl
单独地 向这个状态表 添加/修改/删除 要监控的 fd 及其关心的事件类型(EPOLLIN
,EPOLLOUT
等)。这步只需在你首次关注或需要改变监控某个 fd 时才调用(开销很小)。 - 关键:内核已经长期保存了用户关心的所有 fd 及其事件配置。不需要每次等待时重新告诉内核!
- 创建一个
-
高效的就绪通知:
- 用户调用
epoll_wait()
来等待事件的发生。 - 内核感知就绪: 当被监控的 fd 上有事件发生时(例如数据到达),内核的回调机制会立即将它放到
epoll
实例内部的就绪链表中。内核知道哪些 fd 是用户关心的,所以能精准地处理。 - 等待与返回:
epoll_wait()
检查它的就绪链表:- 如果链表非空:立即返回就绪的 fd 信息。
- 如果链表为空:将进程挂起(阻塞),直到链表中有新项(有事件就绪)或超时。
- 返回就绪项:
epoll_wait()
返回时,会填充一个用户提供的epoll_event
结构数组(调用者分配),把当前就绪链表中的元素(通常是数组大小的数量)拷贝到这个数组中。拷贝完成后,它会清空内核中的就绪链表(除非使用EPOLLONESHOT
)。 - 返回给用户的数组中,只包含那些真正发生了事件的 fd 及其事件类型,没有任何无关信息。
- 用户调用
关键函数
epoll_create()
/epoll_create1()
:创建一个 epoll 实例,返回一个 epoll 文件描述符(epfd
)。epoll_ctl(epfd, op, fd, event)
:epfd
:epoll_create
返回的文件描述符。op
:操作类型EPOLL_CTL_ADD
(新增监控),EPOLL_CTL_MOD
(修改监控),EPOLL_CTL_DEL
(删除监控)。fd
:要操作的 fd(网络套接字等)。event
:指向struct epoll_event
的指针,包含要监控的事件(events
如EPOLLIN
)和用户关联数据(data
,常用来保存 fd 或其他上下文)。
epoll_wait(epfd, events, maxevents, timeout)
:epfd
:epoll 实例 fd。events
:用户分配好的epoll_event
结构体数组(用于接收就绪事件)。maxevents
:数组大小(一次最多接收的事件数)。timeout
:超时时间(毫秒)。- 返回值: 实际发生事件的 fd 数量(即填充到
events
数组中的有效项数,0 表示超时,-1 表示错误)。
工作流程 (教科书式总结)
- 注册阶段 (Setup - 一次或零星调用
epoll_ctl
):- 创建
epoll
实例 (epoll_create
)。 - 对于每个需要监控的 fd,调用
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event)
将它注册到内核的监控状态表中,明确告诉内核“我对这个 fd 的 XX 事件感兴趣”。
- 创建
- 等待阶段 (Main Loop - 频繁调用
epoll_wait
):- 在一个循环中,调用
epoll_wait(epfd, events, MAX_EVENTS, timeout)
。 - 内核直接检查内部就绪链表:
- 空 (无事件): 当前无事件就绪,
epoll_wait
挂起进程,直到有事件到达或被新添加(由内核机制自动唤醒)或者超时。 - 非空 (有事件): 内核将就绪链表中的一部分 (最多
maxevents
项)epoll_event
结构 复制到events
数组中,清空内核的就绪链表(移除了已复制的项),然后返回就绪事件数量(N)。
- 空 (无事件): 当前无事件就绪,
- 返回的
events
数组中只包含了 N 个就绪 fd 的信息 (对应在epoll_ctl
注册时指定的fd
和event->data
+ 实际发生的具体事件event->events
)。 - 进程直接
for (i = 0; i < N; i++)
循环处理这 N 个就绪事件(events[i]
)。O(1) 时间获取就绪 fd,O(N) 时间处理(N 是本次返回的实际就绪数量,通常很小)。
- 在一个循环中,调用
- 动态维护 (Ongoing - 可随时调用
epoll_ctl
):- 进程可以在需要时随时调用
epoll_ctl
添加新的 fd、修改已监控 fd 的事件、或者删除不再需要的 fd。
- 进程可以在需要时随时调用
优点 (相比 select/poll)
- O(1) 等待返回:
epoll_wait
返回的是当前就绪的事件列表(一个数组),数量是就绪的 fd 数(N),用户只需遍历 N 个元素(而不是所有关心的 fd)。这在海量连接但只有少量活跃时性能提升巨大!复杂度 O(1) vs O(n)。 - 避免无效遍历: 内核利用回调机制精确知道哪个 fd 就绪,直接放进就绪列表,不需要像
select/poll
那样线性扫描所有 fd。 - 零拷贝的潜力 (对于大数组): 用户调用
epoll_wait
时传入一个已分配好的数组接收就绪事件。如果事件很多,epoll_wait
可以一次性拷贝一批就绪事件,比select/poll
每次传递整个集合高效得多(特别是在就绪率低时)。 - 无 fd 数量限制 (系统资源限制除外): 可监控的 fd 数量只受系统内存限制。
- 支持高效边缘触发 (ET): 除了默认的水平触发 (LT),
epoll
支持高性能的边缘触发 (Edge-Triggered) 模式 (EPOLLET
标志)。
触发模式:水平触发 (LT) vs 边缘触发 (ET)
- 水平触发 (Level-Triggered - LT) (select/poll/epoll 默认):
- 条件: 只要文件描述符处于就绪状态(例如套接字接收缓冲区中有数据可读),
epoll_wait
(或select/poll
) 就会一直报告这个 fd 是可读的(直到你读完所有数据)。 - 行为: 你可以选择处理部分数据后退出循环,下次调用
epoll_wait
时,如果缓冲区还有数据,它仍然会再次报告该 fd 可读。类似水杯里有水,服务员来了随时能倒。 - 编程: 编程相对简单,因为不会轻易错过事件。即使你只处理了一部分数据就返回了,下次还能接着处理。
- 条件: 只要文件描述符处于就绪状态(例如套接字接收缓冲区中有数据可读),
- 边缘触发 (Edge-Triggered - ET) (
EPOLLET
标志):- 条件: 只会在 fd 的状态发生变化时才报告一次。比如:
- 对于可读事件:fd 从 “无数据可读” 变成 “有数据可读”(或新数据到达导致低水位标记被触发) 时,报告一次。之后即使缓冲区还有大量数据没读完,
epoll_wait
也不会再次报告该 fd 可读(除非又有新数据到来)。 - 可写事件同理(从不可写变为可写)。
- 对于可读事件:fd 从 “无数据可读” 变成 “有数据可读”(或新数据到达导致低水位标记被触发) 时,报告一次。之后即使缓冲区还有大量数据没读完,
- 行为: 更像一个通知机制:告诉你“情况变了!可能有活干了!”。
- 编程:
- 必须一次性处理完! 在 ET 模式下,当你收到事件通知后,必须循环读取 (read) 或 循环写入 (write),直到该 fd 暂时无法继续读取/写入(错误
EAGAIN
或EWOULDBLOCK
)!否则你会丢失掉剩余数据的处理机会(因为内核只会通知你一次变化)。 - 使用非阻塞文件描述符: 为了避免因为循环读写时卡在
read
/write
上导致整个进程挂起,使用 ET 模式必须将对应的 fd 设置为 非阻塞 (non-blocking)。
- 必须一次性处理完! 在 ET 模式下,当你收到事件通知后,必须循环读取 (read) 或 循环写入 (write),直到该 fd 暂时无法继续读取/写入(错误
- 优点: 性能理论上可以更高,避免了内核对同一个就绪状态的多次通知(当状态维持但无变化时)。在极端高性能场景可能有价值。
- 警告: 编程容易出错。如果接收缓冲区有 100KB 数据,你只读了 50KB 就退出了处理函数(或者因为其他事件被打断了),那么剩下的 50KB 你再也得不到 ET 的“可读”通知了(除非有新数据到来),这被称为“饿死”。所以必须读完所有数据直到
EAGAIN
。
- 条件: 只会在 fd 的状态发生变化时才报告一次。比如:
总结 (哪种好?什么时候用?)
特性 | select | poll | epoll |
---|---|---|---|
fd 数量限制 | 有 (默认 1024) | 无 (系统资源限制) | 无 (系统资源限制) |
内核数据结构 | fd 集合(位图) | pollfd 结构数组 | 内核状态表 (红黑树+链表) |
监控操作成本 | 每次调用复制全部位图 | 每次调用复制整个结构数组 | epoll_ctl 注册/修改/删除代价低 (维护树);epoll_wait 无监控成本 |
内核遍历成本 | 线性扫描所有位 (0 - max_fd) O(n) | 线性扫描所有 pollfd 项 O(n) | 回调触发,仅维护就绪项 O(1) (O(k) k=就绪数) |
返回值遍历成本 | 线性扫描所有位 (0 - max_fd) O(n) | 线性扫描所有 pollfd 项 O(n) | 只遍历就绪项 O(k) (k=本次返回的就绪数) |
事件信息保存 | 无,用户每次调用需重置所有关心的集合 | 是 (revents 独立) | 是 (长期维护在状态表中) |
水平触发 (LT) | 是 | 是 | 是 (默认) |
边缘触发 (ET) | 否 | 否 | 是 (EPOLLET ) |
复杂度 | O(n) | O(n) | O(1) 等待 + O(k) 处理 |
适用场景 | 少量 fd;跨平台要求 | 中量 fd;要求比 select 高的场景 | 海量高并发连接;性能要求极高 |
平台 | POSIX | POSIX (Linux 完善) | Linux 特有 |
- 少量连接 (<1024) / 跨平台: 用
select
或poll
都行,可能poll
代码更清晰。 - 中等数量连接 / 无边缘触发要求:
poll
是一个不错的通用选择。 - 海量并发连接 (>数千) / 高性能要求 (Web 服务器、消息队列、数据库等):
epoll
是目前 Linux 上最主流、最高效的解决方案!。它通过分离配置(epoll_ctl
)和等待(epoll_wait
)以及精准返回就绪项,极大地提升了效率。
记住核心:它们都是为了解决 “一个进程如何高效监控数十、数百、甚至数十万个 I/O 通道的状态变化” 这个关键问题而设计的 Linux 网络编程核心机制。理解了它们,就打下了高性能服务器开发的基础!
chatgpt 版本答案
很好,咱们今天就用 教科书式、循序渐进、通俗易懂 的方式,彻底讲清楚 Linux 下的 poll
/ select
/ epoll
到底是干啥的、怎么用的、各自的区别在哪儿。
🧠 第一部分:这些东西解决什么问题?
❓为什么要用 poll
/ select
/ epoll
?
假设你写了一个服务器,它要同时处理多个客户端连接:
- 客户端 1 可能正在发数据;
- 客户端 2 可能暂时没反应;
- 客户端 3 刚断开连接……
问题:你怎么知道哪个客户端有数据来,哪个没数据?
👉 如果你直接 read(fd)
,而这个 fd 没数据,会一直“阻塞”等待,非常浪费!
所以我们需要一种方式:
告诉我们:哪些 fd(文件描述符)现在“就绪了”(可读、可写、出错),哪些没有,就先别管它。
这就是 select
/ poll
/ epoll
的使命!
📘 第二部分:三个机制概览
名称 | 时代 | 特点 | 文件描述符数量支持 | 性能 |
---|---|---|---|---|
select | 古早(POSIX) | 用 bit 位记录 fd 状态 | 最多 1024 个 | 差 |
poll | 进化版 | 用结构体数组管理 fd | 理论无限 | 一般 |
epoll | 高性能(Linux专属) | 内核维护一个红黑树结构 | 理论无限 | 最好(尤其高并发) |
🛠️ 第三部分:select/poll/epoll 的用法详解
1️⃣ select
:古老但经典
📌 基本用法:
#include <sys/select.h>int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds
:最大的 fd + 1readfds
:你关心哪些 fd 可读writefds
:你关心哪些 fd 可写timeout
:超时时间,NULL
表示永久阻塞
🧪 示例:
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(fd, &rfds); // 关注fd是否可读struct timeval tv = {5, 0}; // 最多等5秒
int ret = select(fd + 1, &rfds, NULL, NULL, &tv);
if (ret > 0 && FD_ISSET(fd, &rfds)) {// fd有数据可读
}
❌ 缺点:
- fd个数不能超过
FD_SETSIZE
(默认1024) - 每次都要“重新构建”fd集合(效率低)
- 每次系统都要拷贝用户空间数据到内核(性能差)
2️⃣ poll
:结构体代替 bit 操作,更灵活
📌 基本用法:
#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds
:你要监听的 fd 数组timeout
:超时时间(毫秒)
🧪 示例:
struct pollfd fds[2];
fds[0].fd = fd1;
fds[0].events = POLLIN;
fds[1].fd = fd2;
fds[1].events = POLLIN;int ret = poll(fds, 2, 1000); // 最多等待1秒
if (ret > 0) {if (fds[0].revents & POLLIN) {// fd1 可读}
}
✅ 优点:
- 支持更多的 fd(没有硬限制)
- 接口简单
❌ 缺点:
- 每次仍然要传递所有 fd(O(n) 时间复杂度)
3️⃣ epoll
:为高并发设计(推荐!)
📌 工作方式:
epoll 不是一次性传递所有 fd,而是:
- ✅ 把感兴趣的 fd 提前注册到内核(epoll_create + epoll_ctl)
- ✅ 每次只查询“就绪事件”(epoll_wait)
- ✅ 只返回发生事件的 fd,效率极高
📌 接口函数
int epoll_create(int size); // 创建epoll实例
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 添加/修改/删除监听的fd
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
🧪 示例:
int epfd = epoll_create(10); // 创建epoll实例struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd;epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); // 添加fd监听struct epoll_event events[10];
int n = epoll_wait(epfd, events, 10, 1000); // 最多等1秒
for (int i = 0; i < n; i++) {if (events[i].events & EPOLLIN) {// 有数据来了!}
}
🔍 第四部分:三者对比总结
特性 | select | poll | epoll |
---|---|---|---|
最大 fd 数量限制 | 有 (1024) | 无限制 | 无限制 |
内核/用户拷贝 | 每次拷贝全部 | 每次拷贝全部 | 只拷贝就绪事件 |
使用难度 | 简单 | 简单 | 稍复杂(但值得) |
适合场景 | 少量 fd | 中等数量 fd | 高并发、大量连接 |
性能 | 慢 | 一般 | 快(事件驱动模型) |
🧠 第五部分:真实应用场景举例
- ✅ 单线程高性能服务器 → 用 epoll(如 Nginx、Redis)
- ✅ 简单网络小程序 → 用 poll 或 select 就够了
- ✅ 多路串口监听、多个传感器读取 → 用 poll 比较方便
💡 类比解释(通俗类比)
想象你在等很多人来敲你家的门:
- select:你每秒都跑一圈,看看谁在敲门;
- poll:你带了名单,一一去看谁敲了门;
- epoll:你请了门铃公司,只通知你真的有人按门铃。
epoll 是“通知机制”,而 select/poll 是“轮询机制”。
📚 补充阅读推荐
-
man 手册:
man 2 select man 2 poll man 7 epoll
-
实践建议:
- 写个 echo 服务器对比三种方法
- 用 strace 看系统调用差异
- 在 Linux 下调试程序理解底层行为
✅ 总结一句话:
poll
/select
/epoll
是用于检测多个 fd 的事件状态(可读、可写、异常),epoll
是 Linux 下高性能的推荐方式。
如果你想,我可以继续帮你写一个简单的 demo 代码,展示如何用 poll
或 epoll
实现一个“多客户端聊天服务器”。
是否继续?你更想看哪个接口的实战?
非常好!你给出的这章《高级 I/O》的学习大纲内容非常经典,它涉及了 Linux 文件 I/O 的高阶应用技巧与机制,是理解高性能编程、网络编程乃至系统设计的核心。
下面我们就完全按照你这个大纲结构,逐个深入讲解,用“教科书式 + 通俗易懂类比 + 代码示例”三合一方式,带你彻底理解《高级 I/O》的所有内容。
📘《高级 I/O》全章导学
📍一、阻塞 I/O 与非阻塞 I/O
1️⃣ 阻塞 I/O 是什么?
我们常用的 read()
/ write()
默认就是阻塞的。
它的行为是:
当你调用
read(fd, buf, size)
时,如果没有数据,你会一直卡住,直到有数据为止。
✅ 举例:
char buf[100];
read(fd, buf, 100); // 如果fd还没数据,你就在这里等着……
📌 阻塞的好处:逻辑简单
📌 缺点:可能造成卡顿或线程资源浪费
2️⃣ 非阻塞 I/O 是什么?
非阻塞模式下,如果 read()
没数据,它马上返回一个错误(通常是 -1
,errno=EAGAIN
)。
✅ 设置方式:
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
✅ 举例:
char buf[100];
int ret = read(fd, buf, 100);
if (ret == -1 && errno == EAGAIN) {// 暂时没数据,不阻塞
}
📌 优点:不会卡住线程
📌 缺点:你得自己不停地轮询,很烦
📍二、阻塞 I/O 所带来的困境
想象你有一个 TCP 服务器,监听很多客户端连接:
- 如果你只用
read()
监听,每个线程处理一个客户端,很快线程就爆了; - 如果你改成非阻塞,不停地
read()
+sleep()
,效率又低得可怜。
👎 不论阻塞还是非阻塞,对“多个 I/O 同时监听”的能力都很差!
所以就诞生了下一节的主角:
📍三、非阻塞 I/O + 多设备轮询(poll/select)
我们已经设置好了非阻塞 I/O,那么接下来:
就该用
select
/poll
/epoll
来同时检测多个 fd 是否有事件发生了。
📌 多路复用轮询核心思路:
🔁 主动询问一堆设备 fd:你现在有数据了吗?
🔔 如果有,就处理;没有就继续下一轮。
这就是 select
和 poll
的工作原理。
(这一部分你在前面其实已经很好地看懂了 👍)
📍四、I/O 多路复用详解(重点)
所谓 I/O 多路复用:
是指一个线程通过一个系统调用,监视多个文件描述符的 I/O 状态变化。
📌 实现方式:
select
:古老方式,用fd_set
poll
:结构体数组epoll
:高性能事件通知系统(Linux专属)
🧠 为什么叫“多路复用”?
因为一个线程复用来监控多个 I/O 路径(socket、管道、串口、文件等),提高效率。
👨🏫 类比解释:
就像老师一次性批改全班作业,而不是每改一个就等下一个学生自己来叫你。
📍五、异步 I/O(真正的非阻塞)
📌 真正的异步 I/O 与非阻塞的区别:
- 非阻塞:你发起
read()
,它可能立即返回 “没数据” - 异步 I/O(
aio_read()
):你发起请求,内核后台执行,完成后通知你
这才是真·异步,系统帮你读,读完告诉你。
✅ 示例(POSIX AIO):
#include <aio.h>struct aiocb cb;
memset(&cb, 0, sizeof(cb));
cb.aio_fildes = fd;
cb.aio_buf = buf;
cb.aio_nbytes = 100;
cb.aio_offset = 0;aio_read(&cb); // 发起异步读while (aio_error(&cb) == EINPROGRESS) {// do other things
}int ret = aio_return(&cb); // 获取返回值
💡 类比解释:
你点了个外卖(aio_read),不需要一直看门口(轮询),等外卖到了(内核完成),外卖员自动敲门通知你。
📍六、存储映射 I/O(mmap)
将文件映射到内存,直接像操作数组一样操作文件内容。
✅ 示例:
int fd = open("file.txt", O_RDWR);
char *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);// 直接读写内存即可
printf("%c\n", ptr[0]);
ptr[0] = 'A';
📌 优点:
- 文件读写变成了内存读写(非常快)
- 可用于多个进程共享内存
💡 类比:
把文件“插入”你的大脑(内存)中,想看就看,想改就改,不用反复读文件。
📍七、文件加锁
📌 为什么要锁?
多进程/多线程读写同一个文件时,可能出现“读写冲突”或“数据错乱”。
所以需要加锁机制保证一致性。
✅ 类型:
类型 | 作用 |
---|---|
共享锁(读锁) | 允许多个读,不允许写 |
排它锁(写锁) | 只允许一个写 |
✅ 示例(fcntl
加锁):
struct flock lock;
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0; // 整个文件fcntl(fd, F_SETLK, &lock); // 尝试加锁(非阻塞)
📌 如果别人已经加锁,这里会失败
💡 类比:
写文件就像上厕所,要排队。读锁=多人能看,写锁=一个人独占。
📌 总结图解(总览)
Linux 高级 I/O 总览──────────────────┌────────────┐│ 阻塞 I/O │ ← 默认 read/write,线程会挂起└─────┬──────┘│▼┌────────────┐│ 非阻塞 I/O │ ← 设置 O_NONBLOCK,不挂起└─────┬──────┘▼┌──────────────────────────────┐│ I/O 多路复用(select/poll) │ ← 轮询多个 fd 状态└─────┬──────────────┬────────┘│ │▼ ▼┌──────────────┐ ┌──────────────┐│ epoll │ │ 异步 I/O │ ← aio_read() 后台自动完成└──────────────┘ └──────────────┘│▼┌──────────────────────┐│ mmap 文件映射 I/O │ ← 文件变成内存,快速访问└────────┬─────────────┘▼┌─────────────────┐│ 文件加锁(fcntl)│ ← 控制并发访问,防止冲突└─────────────────┘
🧠 学完这些你能做什么?
- ✅ 写一个高并发 socket 服务器
- ✅ 实现高性能数据读取和处理
- ✅ 编写跨进程共享文件或内存
- ✅ 安全地读写同一个文件
- ✅ 理解 Linux 系统是怎么处理 I/O 的
如果你需要:
- 实战代码示例(如:select / poll / epoll / aio 服务器模型)
- 各机制的性能对比测试
- 分模块复习讲义 or PDF文档总结
可以告诉我,我给你逐个准备 👇