上一课讲了文件怎么存进磁盘。但还有个问题悬着:磁盘、键盘、网卡这些"外设",到底是怎么跟 CPU 对话的? 是你敲一下键盘,CPU 立刻知道;还是 CPU 一直盯着键盘问"按了吗按了吗"?这一课,我们走进 I/O 与设备,看清 CPU 和外设之间的三种对话方式,以及"等 I/O"这件事为什么是高性能系统的关键。

从"烧水"说起

你想烧一壶水。有三种做法:

  • 盯着烧:一直站在水壶边,盯着看"开了没、开了没"——人一直占着,啥也干不了;
  • 等它叫:去干别的事,水开了水壶会,听到叫声再来倒水;
  • 请人帮忙:干脆请个人帮你烧水、倒好水,倒完再喊你

这三种做法,恰好对应 CPU 和外设对话的三种方式。我们先从它讲起。


20.1 三种 I/O 方式

方式怎么做特点
轮询(polling)CPU 不停查"好了吗"简单但浪费 CPU
中断(interrupt)设备好了主动打断 CPUCPU 不用空等,效率高
DMA专用硬件直接搬数据,搬完再中断大量数据不占 CPU,快
一句话理解:轮询是"你盯着水壶烧开",中断是"水开了壶会叫",DMA 是"请人帮你倒水,倒完再喊你"。
  • 轮询:CPU 反复检查设备状态寄存器,问"好了吗?"。简单,但 CPU 被死死占住,白白浪费算力;
  • 中断:设备准备好了,主动发信号打断 CPU(还记得第 3 课的中断吗?)。CPU 平时干别的,被"叫"了再响应。效率高
  • DMA(直接内存访问):对于大块数据(比如读一个几 MB 的文件),让 CPU 逐字节搬太浪费了。DMA 是专用硬件,直接在主存和设备之间搬数据,搬完了才发一个中断通知 CPU。
一句话记:键盘输入走中断,磁盘大块读写走 DMA。 轮询只在极少数简单场景用。

20.2 设备驱动与设备文件

CPU 知道怎么跟"键盘"这种具体硬件对话吗?不知道。中间有两层抽象

一句话理解:驱动是"知道怎么跟某块硬件对话"的内核代码;对上提供统一接口(open/read/write),对下操作硬件寄存器。设备文件(/dev/sda 等)让"操作设备"像"操作普通文件"。
  • 设备驱动:内核里"最懂某块硬件"的那段代码。它对上提供统一的 open/read/write 接口,对下去操作硬件的寄存器;
  • 设备文件:把设备抽象成一个文件,放在 /dev 下。比如 /dev/sda 是第一块硬盘,/dev/null 是"黑洞设备"。

好处是什么?统一。程序员操作任何设备,都用同一套 open/read/write,不用关心底层是哪块硬件。

例子:echo "test" > /dev/null 就是把数据写进"黑洞设备"(数据直接丢弃),走的正是统一的 write 接口。"一切皆文件",是 Unix 的核心哲学,这里体现得淋漓尽致。

20.3 阻塞 I/O vs 非阻塞 I/O

现在聊"等 I/O"这件事。你调用 read 读数据,数据没到,怎么办?两种选择:

一句话理解:阻塞是"死等一个电话",非阻塞是"每隔一会看一眼有没有来电"。
  • 阻塞 I/O(blocking)read 时没数据就一直等,进程睡着(挂起)。简单,但一次只能等一个——如果同时要等 100 个连接,一个个等会等到天荒地老;
  • 非阻塞 I/O(non-blocking)read 没数据立刻返回(返回个"还没好"),进程去干别的。但代价是要自己反复试——你得不停地回来问"好了没"。
两者各有代价:阻塞省心但低效,非阻塞高效但费劲。那有没有"既不用死等、也不用自己反复试"的办法?有,下一节。

20.4 select / epoll:同时等很多个

真实的高性能服务器(比如 Nginx、Redis),要同时处理成千上万个连接。上面的两种方式都不够用,于是有了"多路复用":

一句话理解:一个进程要同时监听很多连接/文件,select/poll/epoll 让内核帮你"盯着一堆 fd,谁就绪了告诉我",不用一个个死等。

select

  • 把一堆 fd(文件描述符)交给内核,问"这些里哪些有数据了?";
  • 内核返回"哪些就绪",你再处理那些就绪的;
  • 缺点:每次调用都要重新传一遍 fd 集合、且用线性扫描、还有数量上限

epoll

  • 注册一次,之后内核用回调通知你"这个 fd 有数据了";
  • 只返回真正就绪的 fd,不用每次全量扫描;
  • 适合海量连接,是 Nginx、Redis 高性能的秘密武器
一句话:select 是"每次都得问一遍所有人",epoll 是"登记一次,有事了通知你"。 连接越多,epoll 优势越明显。

20.5 异步 I/O 的直觉

最后上升到"同步 vs 异步"的视角:

一句话理解:同步是你"发起操作 + 等结果";异步是"发起操作,结果好了再通知你",中间你能干别的。epoll 就是"事件驱动"异步模型的代表。
  • 同步:发起 I/O 后,自己等结果(阻塞式就是典型同步);
  • 异步:发起 I/O 后立刻返回,去干别的,结果好了再被通知
epoll 就是"事件驱动"异步模型的核心:程序不再"盯着等",而是"注册一堆事件,谁发生了处理谁"。这也是现代高性能网络编程的主流范式(第 22 课网络会再次相遇)。

动手实验:C 语言用 select 监听标准输入

// lesson20.c —— 用 select 监听标准输入(非阻塞等待)
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>

int main(void) {
    fd_set rfds;
    char buf[128];

    while (1) {
        FD_ZERO(&rfds);
        FD_SET(STDIN_FILENO, &rfds);   // 监听标准输入

        // 最多等 5 秒
        struct timeval tv = {5, 0};
        int ready = select(STDIN_FILENO + 1, &rfds, NULL, NULL, &tv);

        if (ready < 0) { perror("select"); return 1; }
        if (ready == 0) {
            printf("5 秒内没输入,继续等……\n");
            continue;
        }
        if (FD_ISSET(STDIN_FILENO, &rfds)) {
            int n = read(STDIN_FILENO, buf, sizeof(buf) - 1);
            if (n <= 0) break;              // Ctrl+D 退出
            buf[n] = '\0';
            printf("你输入了:%s", buf);
        }
    }
    printf("再见\n");
    return 0;
}

编译运行:

gcc lesson20.c -o lesson20 && ./lesson20

体会一下:没输入时,程序不被 read 阻塞,而是每 5 秒醒来一次打印"继续等"——这就是"非阻塞等待"。当你在键盘上敲字回车,它才真正去 read 并处理。

关键点:select 让你"同时等标准输入 + 一个超时定时器",而不是傻等。这就是"多路复用"的最小 demo。

深入点:两个进阶话题

① select vs epoll 的本质差异

  • select:每次调用都要把 fd 集合从用户态拷贝进内核,内核用线性扫描检查谁就绪,返回后还要再扫一遍。连接一多,这个"全量拷贝 + 全量扫描"就是性能瓶颈;
  • epoll:内部用红黑树管理所有注册的 fd,用就绪链表存"有事件的 fd"。注册一次,事件驱动,只返回就绪的——海量连接时快得多
一句话:select 是 O(n) 的每次全量问询,epoll 是 O(1) 的就绪通知。

② io_uring

Linux 新一代异步 I/O 接口,当前高性能存储的热点

通过共享环形队列(SQ 提交队列 / CQ 完成队列),把"提交 I/O 请求"和"拿结果"都做得极高效,减少系统调用和内存拷贝。数据库、存储引擎纷纷跟进。

小结与思考题

这一课,我们看清了外设和 CPU 的对话:

  • 三种 I/O 方式:轮询(浪费 CPU)、中断(设备主动喊)、DMA(专用硬件搬数据);
  • 驱动 + 设备文件:一切皆文件,统一 open/read/write;
  • 阻塞 vs 非阻塞:死等 vs 反复试;
  • select / epoll:内核帮盯一堆 fd,epoll 是海量连接的高性能方案;
  • 异步 I/O:发起操作、结果好了再通知。

留三个问题:

  1. 轮询、中断、DMA 三者的区别?
  2. 阻塞 I/O 和非阻塞 I/O 各自的代价?
  3. epoll 相比 select 强在哪?
到这里,第五阶段(存储与 I/O,19~21 课) 已经走了两课。文件系统和 I/O 的机制都清楚了,但还剩一个"体检报告"没看:系统到底快不快、瓶颈在哪? 磁盘满了、内存满了、CPU 飙到 100%……这些日常运维问题,背后对应哪些命令和指标?下一课,我们做一次系统性能观测,把前面学的概念,落到 topiostat 这些实战工具上。

标签: 计算机基础, 操作系统, Linux

添加新评论