当前位置: 首页 > news >正文

linux IO介绍

1. Deepsek 版本答案

好的!我们来用「教科书式」讲解方式,把 select/pollepoll 讲清楚。它们都是 Linux 中 I/O 多路复用的核心机制,用于高效地同时监控多个文件描述符(如网络套接字)的状态变化(如可读、可写、异常)

设想一个场景:你是一个服务员(一个进程),需要同时服务多张桌子(多个文件描述符 fd,比如网络连接)。每张桌子可能点菜(有数据可读)、要求加水(可写)、或者出了什么问题(异常)。你需要一种高效的方式知道哪张桌子需要你,而不是傻傻地站在一张桌子旁边等。

传统的阻塞 I/O(效率低下):

  • 就像你一次只服务一张桌子。服务员走到 A 桌问:“需要什么?”如果没有需求,服务员就一直傻等在 A 桌旁边(阻塞),完全忽略了 B、C、D 等桌可能的需求。这是最简单但效率最低的方式。

I/O 多路复用(高效监控):

  • 目标是让一个服务员能高效地知晓多张桌子的状态变化。select, poll, epoll 就是解决这个问题的不同方法。核心思想是:服务员(进程)一次性地告诉内核(操作系统):“帮我盯着这堆桌子(fd 集合),看看它们哪些变成了我需要关心的状态(比如可以读、可以写),如果有的桌子状态变了,或者我等了 X 秒还没有任何桌子变状态,就通知我一下。”

第一代:select

核心原理

  1. 用户准备: 服务员(进程)创建三个 fd 集合(readfds, writefds, errorfds),设置好哪些 fd 要关心可读,哪些要关心可写,哪些要关心异常。同时设定一个超时时间
  2. 系统调用: 调用 select(max_fd + 1, &readfds, &writefds, &errorfds, timeout);
    • max_fd + 1: 传入选中的最大 fd 值 + 1(为了效率)。
    • &readfds / &writefds / &errorfds: 传入关心的 fd 集合指针
    • timeout: 等待超时时间(NULL 表示无限等待,0 表示立即返回)。
  3. 内核工作:
    • 把传入的三个 fd 集合复制到内核空间。
    • 遍历用户指定的整个 fd 集合(0 到 max_fd),挨个检查每个 fd 是否在传入的集合中,如果在,就检查其是否就绪(可读/可写/异常)。
    • 如果没有 fd 就绪,内核将进程挂起(阻塞),直到有 fd 就绪或者超时。
  4. 返回结果:
    • 当有 fd 就绪或超时,select 返回。
    • 修改后的三个 fd 集合(仅包含真正就绪的 fd)复制回用户空间(原来传入的集合会被覆盖)。
    • 返回就绪的 fd 的总数量。
  5. 用户处理: 服务员(进程)拿到修改后的 fd 集合,遍历(0 到 max_fd)所有它关心的 fd,检查哪些 fd 在返回的集合中被置位(就绪),然后对就绪的 fd 进行相应操作(比如读取数据)。

优点

  • 跨平台(POSIX 标准)。
  • 简单,基本所有系统都支持。

缺点(主要问题)

  1. fd 数量限制: 单个进程默认能监控的 fd 数量有限(由 FD_SETSIZE 决定,通常是 1024),编译时修改虽可能,但有风险。
  2. 线性遍历开销:
    • 用户->内核:每次调用都要传递整个巨大的 fd 集合(包含 0-max_fd),内核需要复制。
    • 内核遍历:内核需要线性扫描 0 到 max_fd 所有 fd 是否在集合中,然后检查其状态,不论其是否被关心!在监控 fd 少但 max_fd 大时(如 max_fd=10000 但只监控 10 个 fd),效率极低。
    • 内核->用户:内核修改集合后又要复制回用户空间。
    • 用户遍历:用户进程收到结果后,也要再次线性扫描(0 到 max_fd)所有关心的 fd 才能找到真正就绪的几个。最坏情况遍历次数多(两次遍历,每次都可能上千次)。
  3. 状态丢失: select 调用返回后,原始关心的集合已被内核修改覆盖。如果需要再次监控,用户进程必须重新设置所有关心的集合。

第二代:poll

核心原理(解决 select 部分痛点)

  1. 用户准备: 服务员(进程)创建一个 struct pollfd 类型的数组 fds[]。每个结构体代表一个要监控的 fd 及其关心的事件:
    struct pollfd {int   fd;         /* 文件描述符 */short events;     /* 监控哪些事件(POLLIN, POLLOUT 等) */short revents;    /* 返回哪些事件发生了(内核填充) */
    };
    
    初始化 fds,设置好每个 fd 和对应的 events(关心的事件)。
  2. 系统调用: 调用 poll(fds, nfds, timeout);
    • fds: pollfd 结构数组的指针。
    • nfds: fds 数组的大小(实际关心的 fd 数量)。
    • timeout: 超时时间(毫秒)。
  3. 内核工作:
    • 复制 fds 数组到内核空间。
    • 遍历 fds 数组(长度为 nfds),检查其中每个 fd 对应的状态是否满足其 events 要求。
    • 如果没有 fd 就绪,挂起进程(阻塞),直到有就绪或超时。
  4. 返回结果:
    • 内核将就绪的事件信息(POLLIN, POLLOUT 等)填回每个 pollfdrevents 字段。
    • 将修改后的 fds 数组复制回用户空间。
    • 返回就绪的 fd 数量(即 revents 非 0 的 pollfd 个数)。
  5. 用户处理: 服务员(进程)拿到返回的 fds 数组,遍历整个数组(长度为 nfds),检查每个 pollfdrevents 字段,如果非零,说明对应的 fd 有事件发生,进行相应处理。

相对于 select 的改进

  1. 无 fd 数量限制(重要): 监控的 fd 数量仅受系统资源和用户空间定义数组大小的限制(通过 nfds 指定)。解决了 FD_SETSIZE 的限制。
  2. 更清晰的事件分离: 使用 events 指定关心事件,revents 返回实际发生事件,互不干扰,避免了状态丢失问题。用户调用后无需重新设置事件。
  3. 更高效的传递(理论上): 只需传递 nfds 个结构体(只包含关心的 fd),而不是一个巨大的位图范围(0-max_fd)。内核遍历数量是用户关心的数量(nfds),而非最大 fd 值。用户遍历也只需要遍历关心的数量(nfds)。

缺点(依然存在的问题)

  1. 性能瓶颈(大数量监控):
    • 用户->内核->用户:每次调用仍然需要复制整个 pollfd 结构体数组(包含所有关心的 fd),如果数组很大(成千上万),开销显著。
    • 内核线性遍历:内核仍然需要线性遍历整个传入的数组(nfds 个元素)来检查 fd 的状态。当监控的 fd 数量非常多(万级别)时,遍历开销成为瓶颈(时间复杂度 O(n))。
  2. 水平触发(LT - Level Triggered):select 一样,poll 也是水平触发模式(稍后详解)。
  3. 仍需主动遍历: 用户进程仍需遍历整个返回的数组(nfds 个元素)来找出哪些 fd 确实就绪了。虽然遍历次数只等于关心的数量 nfds,但当 nfds 很大且只有少数 fd 就绪时,效率仍然不够完美。

第三代:epoll (Linux 特有)

为了解决 select/poll 在处理海量并发连接时的性能瓶颈(万级甚至十万级),Linux 2.6+ 引入了 epoll

核心思想与重大改进

  1. 状态维护在内核:

    • 创建一个 epoll 实例 (epoll_create),它在内核中维护了一个状态表(通常实现为红黑树 + 就绪链表)。
    • 用户通过 epoll_ctl 单独地 向这个状态表 添加/修改/删除 要监控的 fd 及其关心的事件类型(EPOLLIN, EPOLLOUT 等)。这步只需在你首次关注需要改变监控某个 fd 时才调用(开销很小)。
    • 关键:内核已经长期保存了用户关心的所有 fd 及其事件配置。不需要每次等待时重新告诉内核!
  2. 高效的就绪通知:

    • 用户调用 epoll_wait()等待事件的发生
    • 内核感知就绪: 当被监控的 fd 上有事件发生时(例如数据到达),内核的回调机制会立即将它放到 epoll 实例内部的就绪链表中。内核知道哪些 fd 是用户关心的,所以能精准地处理。
    • 等待与返回: epoll_wait() 检查它的就绪链表:
      • 如果链表非空:立即返回就绪的 fd 信息。
      • 如果链表为空:将进程挂起(阻塞),直到链表中有新项(有事件就绪)或超时。
    • 返回就绪项: epoll_wait() 返回时,会填充一个用户提供的 epoll_event 结构数组(调用者分配),把当前就绪链表中的元素(通常是数组大小的数量)拷贝到这个数组中。拷贝完成后,它会清空内核中的就绪链表(除非使用 EPOLLONESHOT)。
    • 返回给用户的数组中,只包含那些真正发生了事件的 fd 及其事件类型,没有任何无关信息。

关键函数

  1. epoll_create() / epoll_create1():创建一个 epoll 实例,返回一个 epoll 文件描述符(epfd)。
  2. epoll_ctl(epfd, op, fd, event)
    • epfdepoll_create 返回的文件描述符。
    • op:操作类型 EPOLL_CTL_ADD(新增监控), EPOLL_CTL_MOD(修改监控), EPOLL_CTL_DEL(删除监控)。
    • fd:要操作的 fd(网络套接字等)。
    • event:指向 struct epoll_event 的指针,包含要监控的事件(eventsEPOLLIN)和用户关联数据(data,常用来保存 fd 或其他上下文)。
  3. epoll_wait(epfd, events, maxevents, timeout)
    • epfd:epoll 实例 fd。
    • events:用户分配好的 epoll_event 结构体数组(用于接收就绪事件)。
    • maxevents:数组大小(一次最多接收的事件数)。
    • timeout:超时时间(毫秒)。
    • 返回值: 实际发生事件的 fd 数量(即填充到 events 数组中的有效项数,0 表示超时,-1 表示错误)。

工作流程 (教科书式总结)

  1. 注册阶段 (Setup - 一次或零星调用 epoll_ctl):
    • 创建 epoll 实例 (epoll_create)。
    • 对于每个需要监控的 fd,调用 epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event) 将它注册到内核的监控状态表中,明确告诉内核“我对这个 fd 的 XX 事件感兴趣”
  2. 等待阶段 (Main Loop - 频繁调用 epoll_wait):
    • 在一个循环中,调用 epoll_wait(epfd, events, MAX_EVENTS, timeout)
    • 内核直接检查内部就绪链表:
      • 空 (无事件): 当前无事件就绪,epoll_wait 挂起进程,直到有事件到达或被新添加(由内核机制自动唤醒)或者超时。
      • 非空 (有事件): 内核将就绪链表中的一部分 (最多 maxevents 项) epoll_event 结构 复制events 数组中,清空内核的就绪链表(移除了已复制的项),然后返回就绪事件数量(N)。
    • 返回的 events 数组中只包含了 N 个就绪 fd 的信息 (对应在 epoll_ctl 注册时指定的 fdevent->data + 实际发生的具体事件 event->events)。
    • 进程直接 for (i = 0; i < N; i++) 循环处理这 N 个就绪事件(events[i])。O(1) 时间获取就绪 fd,O(N) 时间处理(N 是本次返回的实际就绪数量,通常很小)。
  3. 动态维护 (Ongoing - 可随时调用 epoll_ctl):
    • 进程可以在需要时随时调用 epoll_ctl 添加新的 fd、修改已监控 fd 的事件、或者删除不再需要的 fd。

优点 (相比 select/poll)

  1. O(1) 等待返回: epoll_wait 返回的是当前就绪的事件列表(一个数组),数量是就绪的 fd 数(N),用户只需遍历 N 个元素(而不是所有关心的 fd)。这在海量连接但只有少量活跃时性能提升巨大!复杂度 O(1) vs O(n)。
  2. 避免无效遍历: 内核利用回调机制精确知道哪个 fd 就绪,直接放进就绪列表,不需要像 select/poll 那样线性扫描所有 fd。
  3. 零拷贝的潜力 (对于大数组): 用户调用 epoll_wait 时传入一个已分配好的数组接收就绪事件。如果事件很多,epoll_wait 可以一次性拷贝一批就绪事件,比 select/poll 每次传递整个集合高效得多(特别是在就绪率低时)。
  4. 无 fd 数量限制 (系统资源限制除外): 可监控的 fd 数量只受系统内存限制。
  5. 支持高效边缘触发 (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 可读(除非又有新数据到来)。
      • 可写事件同理(从不可写变为可写)。
    • 行为: 更像一个通知机制:告诉你“情况变了!可能有活干了!”。
    • 编程:
      • 必须一次性处理完! 在 ET 模式下,当你收到事件通知后,必须循环读取 (read)循环写入 (write),直到该 fd 暂时无法继续读取/写入(错误 EAGAINEWOULDBLOCK)!否则你会丢失掉剩余数据的处理机会(因为内核只会通知你一次变化)。
      • 使用非阻塞文件描述符: 为了避免因为循环读写时卡在 read/write 上导致整个进程挂起,使用 ET 模式必须将对应的 fd 设置为 非阻塞 (non-blocking)
    • 优点: 性能理论上可以更高,避免了内核对同一个就绪状态的多次通知(当状态维持但无变化时)。在极端高性能场景可能有价值。
    • 警告: 编程容易出错。如果接收缓冲区有 100KB 数据,你只读了 50KB 就退出了处理函数(或者因为其他事件被打断了),那么剩下的 50KB 你再也得不到 ET 的“可读”通知了(除非有新数据到来),这被称为“饿死”。所以必须读完所有数据直到 EAGAIN

总结 (哪种好?什么时候用?)

特性selectpollepoll
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 高的场景海量高并发连接;性能要求极高
平台POSIXPOSIX (Linux 完善)Linux 特有
  • 少量连接 (<1024) / 跨平台:selectpoll 都行,可能 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 + 1
  • readfds:你关心哪些 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) {// 有数据来了!}
}

🔍 第四部分:三者对比总结

特性selectpollepoll
最大 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 代码,展示如何用 pollepoll 实现一个“多客户端聊天服务器”。

是否继续?你更想看哪个接口的实战?



非常好!你给出的这章《高级 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() 没数据,它马上返回一个错误(通常是 -1errno=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:你现在有数据了吗?
🔔 如果有,就处理;没有就继续下一轮。

这就是 selectpoll 的工作原理。

(这一部分你在前面其实已经很好地看懂了 👍)


📍四、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文档总结

可以告诉我,我给你逐个准备 👇

http://www.dtcms.com/a/318025.html

相关文章:

  • Android系统性能分析利器:深入解析Tracing框架
  • Dify+Nginx反向代理:80端口冲突的优雅解决方案
  • ICCV 2025 | 视频生成迈入“多段一致”新时代!TokensGen用“压缩Token”玩转长视频生成
  • Mysql如何迁移数据库数据
  • mysql数据库基础操作
  • 每日任务day0806:小小勇者成长记之收获日
  • 在 Visual Studio Code 中免费使用 Gemini 2.5 Pro API
  • 滴滴招java开发
  • 利用DeepSeek改写并增强测试Duckdb和sqlite的不同插入方法性能
  • 虚幻GAS底层原理解剖四 (TAG)
  • Boosting 知识点整理:调参技巧、可解释性工具与实战案例
  • [Oracle] NVL()函数
  • 【概念学习】深度学习有何不同
  • 220降5V,30mA电流,墙壁开关和调光器应用场景WD5201
  • 【秋招笔试】2025.08.02-OPPO秋招第二套-第一题
  • Win10还未停更,对标iPad的教育版Win11也宣布停更了
  • Python爬虫 urllib 模块详细教程:零基础小白的入门指南
  • Pytest项目_day05(requests加入headers)
  • 项目中MySQL遇到的索引失效的问题
  • Conditional Modeling Based Automatic Video Summarization
  • Ubuntu20.04 离线安装 FFmpeg 静态编译包
  • 深度学习G5周:Pix2Pix理论与实战
  • Transformer模型及深度学习技术应用
  • 什么是 Kafka 中的消息?它由哪些部分组成
  • 高频面试点:深入理解 TCP 三次握手与四次挥手
  • mysql优化策略
  • qt qml实现电话簿 通讯录
  • [FBCTF2019]RCEService
  • apache-tomcat-11.0.9安装及环境变量配置
  • 认识MCP