0Pricing
C Academy · 课时

阻塞与非阻塞 I/O

理解事件循环为何重要。

阻塞与非阻塞 I/O 是 CoddyKit 上的免费 C Academy 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C Academy 课程共包含 4 节课。

阻塞意味着什么

阻塞系统调用会挂起调用线程,直到操作可以继续。当您在没有数据的套接字上调用recv()时,内核会让线程休眠,直到字节到达。

这种方式很容易理解:一个连接对应一个线程,代码按直线执行。但当您需要同时服务数千个客户端时,代价就会显现出来。

ssize_t n = recv(fd, buf, sizeof buf, 0);
/* thread sleeps here until data or error */
if (n > 0) handle(buf, n);

扩展性问题

在阻塞式 I/O 中,一个卡住的客户端会阻塞整个线程。经典的解决办法是每个连接使用一个线程(或进程)。

这种方式在一定规模内有效,但 10,000 个线程意味着 10,000 个栈、频繁的上下文切换以及调度器开销。这就是著名的 C10k 问题,它推动服务器转向事件驱动设计。

非阻塞模式

非阻塞套接字永远不会休眠。如果调用无法立即完成,它会立刻返回-1,并将errno设置为EAGAIN或EWOULDBLOCK。

现在,您的代码需要负责稍后重试。这样,单个线程就能处理许多套接字,而不会卡在其中任何一个上。

ssize_t n = recv(fd, buf, sizeof buf, 0);
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
    /* no data right now, try again later */
}

使用 fcntl 设置 O_NONBLOCK

通过fcntl()添加O_NONBLOCK标志,可以将描述符切换为非阻塞模式。请始终先读取当前标志,再使用 OR 运算加入该位,以免覆盖其他设置。

同一个辅助函数也用于监听套接字、已接受的客户端套接字和管道。

int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) return -1;
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

处理部分读取

非阻塞 I/O 会使部分操作成为常态。recv()返回的字节数可能少于请求数量,而send()也可能只接受缓冲区的一部分。

您必须跟踪已经发送或接收了多少,并从那里继续。绝不要假设一次调用就能移动全部字节。

size_t sent = 0;
while (sent < len) {
    ssize_t w = send(fd, buf + sent, len - sent, 0);
    if (w < 0) { if (errno == EAGAIN) break; else return -1; }
    sent += w;
}

忙等待是错误的做法

使用非阻塞套接字的朴素方式,是不断遍历所有套接字并持续重试。这种忙等待即使什么也没发生,也会消耗 100% 的 CPU。

我们真正想做的是询问内核:“告诉我哪些描述符已就绪,并让我休眠到那时。”这正是就绪通知所提供的功能。

就绪通知

I/O 多路复用允许一个线程同时等待许多描述符,并且只在至少一个描述符就绪时唤醒。内核会替您完成监视。

经典接口是select()和poll()。它们确实可用,但每次调用都会重新扫描所有描述符,在大规模场景下开销很高。

fd_set rfds;
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
select(fd + 1, &rfds, NULL, NULL, NULL);

为什么 select 和 poll 无法扩展

select()和poll()都是 O(n):每次调用都会将完整的描述符集合传入内核,由内核扫描全部描述符,然后您还要再次扫描全部描述符来找出已就绪的描述符。

select()还会受FD_SETSIZE限制,通常上限为 1024。对于数千个连接,这种开销会占据主导地位。

引入 epoll

epoll是 Linux 提供的可扩展解决方案。您只需注册一次对某个描述符的关注,内核就会维护一个跟踪就绪状态的内部数据结构。

每次等待只返回实际就绪的描述符,因此开销取决于活动连接数,而不是总连接数。这样,每个就绪事件的开销大致为 O(1)。

int epfd = epoll_create1(0);
/* register fds once, then wait for ready events */

非阻塞加 epoll

epoll 与非阻塞套接字是理想搭档。epoll 告诉您某个描述符已经就绪;非阻塞调用则让您可以排空它,而不会休眠。

您应该始终在交给 epoll 的套接字上设置O_NONBLOCK。否则,一次虚假唤醒或部分读取就可能阻塞单线程的事件循环。

set_nonblocking(conn_fd);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = conn_fd };
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);

思维模型

可以把服务器想象成一个循环:阻塞在epoll_wait()上,获取一小组已就绪的描述符,对每个描述符执行非阻塞操作,然后重复。

线程空闲时休眠,只有在确实有工作时才唤醒。这样,一个线程就能高效地服务数万个连接。

快速检查

检验您对非阻塞套接字的理解。

回顾

阻塞式 I/O 很简单,但会为每个连接占用一个线程,在大规模场景下无法胜任。非阻塞式 I/O 会立即返回EAGAIN,而不是进入休眠。

在紧循环中轮询套接字会浪费 CPU,因此我们使用就绪通知。select/poll是 O(n);epoll可以扩展到数千甚至更多连接。接下来,我们将设置 epoll。

常见问题解答

「阻塞与非阻塞 I/O」课时是免费的吗?

是的 — 「阻塞与非阻塞 I/O」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C Academy 课程的其余内容,请升级到 CoddyKit PRO。 C Academy 课程共包含 4 节课。

「阻塞与非阻塞 I/O」这节课中我会学到什么?

理解事件循环为何重要。 你通过在浏览器中直接运行的动手代码来练习 C Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 C Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 C Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「阻塞与非阻塞 I/O」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 C Academy 课中编写并运行代码吗?

能。每节 C Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 阻塞与非阻塞 I/O
  2. 设置 epoll
  3. 事件循环
  4. 简单的回显服务器
← 返回 C Academy