📌 核心结论(TL;DR):
阻塞 vs 非阻塞:阻塞是死等结果,非阻塞是立刻返回并轮询。
五种 IO 模型:阻塞 IO、非阻塞 IO、信号驱动 IO、IO 多路复用(epoll)、异步 IO。
fcntl函数:用于修改文件描述符的属性,通过设置O_NONBLOCK标志可以将阻塞 IO 变为非阻塞。非阻塞 IO 的陷阱:如果在循环中频繁调用非阻塞
read,会导致 CPU 占用率飙升(忙等待),通常需要配合epoll等机制使用。
一、 为什么说 IO 是后端开发的瓶颈?
任何 IO 操作都分为两个阶段:等待数据就绪和数据拷贝。在实际应用中,“等待数据”消耗的时间远大于“拷贝数据”。要想提高 IO 效率,核心就是减少程序在“等待”时对 CPU 的占用。
二、 五种 IO 模型的“钓鱼”比喻
阻塞 IO:死盯浮漂,鱼不上钩不动弹(默认的 read/write)。
非阻塞 IO:扔下鱼竿去干别的,过一会看一眼(轮询),极度耗费体力(CPU)。
信号驱动 IO:浮漂上挂铃铛,铃响去收杆。
IO 多路复用:同时看多根鱼竿(
select/poll/epoll),哪一个有动静收哪个,高并发服务器的标配。异步 IO:雇人钓鱼,钓好了直接送嘴边(内核把数据拷贝完才通知)。
三、 实战:把阻塞 IO 变成非阻塞
void SetNoBlock(int fd) { int fl = fcntl(fd, F_GETFL); // 获取状态 fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置非阻塞 }利用fcntl修改文件描述符属性后,read如果读不到数据就不会死等,而是立刻返回-1,并设置errno为EAGAIN。我们通过在死循环里判断这个错误码,就实现了非阻塞的读取。
四、 踩坑记录:非阻塞 IO 的忙等待
如果在循环里去掉了usleep,程序会疯狂地请求 CPU 去查有没有数据,导致 CPU 占用率瞬间跑满。为了解决这个问题,同时又能在非阻塞的情况下监听多个 socket,IO 多路复用(epoll)应运而生,它既能一次监听上万个连接,又能在没有数据时让出 CPU。
自我实战操作
cd ~/linux_study/network_test实战 1:体验默认的阻塞 IObash
vim io_block.c(输入以下代码)
#include <stdio.h> #include <unistd.h> int main() { char buf[1024]; printf("请输入内容(我会在这里一直死等):\n"); // read 默认是阻塞的,如果没有输入,主线程会一直挂起 read(0, buf, sizeof(buf)); printf("你输入了: %s\n", buf); return 0; }(编译并运行gcc io_block.c -o io_block && ./io_block)
👉现象:程序运行后,如果你不敲键盘,它就一直卡在那里。这就叫阻塞。
实战 2:用fcntl将标准输入改为非阻塞
vim io_nonblock.c(输入以下代码)
#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <string.h> // 核心函数:将文件描述符设置为非阻塞 void SetNoBlock(int fd) { int fl = fcntl(fd, F_GETFL); // 1. 获取当前文件描述符的属性 if (fl < 0) { perror("fcntl"); return; } fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 2. 在原有基础上加上非阻塞属性 } int main() { SetNoBlock(0); // 0 代表标准输入(键盘) char buf[1024]; while (1) { memset(buf, 0, sizeof(buf)); ssize_t n = read(0, buf, sizeof(buf) - 1); if (n > 0) { printf("你输入了: %s", buf); } else if (n < 0) { // 如果没有数据,read 会返回 -1,并把 errno 设置为 EAGAIN 或 EWOULDBLOCK if (errno == EAGAIN || errno == EWOULDBLOCK) { // 这里就是经典的“轮询”过程 // 为了不在终端疯狂刷屏,我们睡 1 秒 usleep(1000000); } else { perror("read"); break; } } } return 0; }(编译并运行gcc io_nonblock.c -o io_nonblock && ./io_nonblock)
👉现象:程序运行后,终端不再卡死,而是处于一个“活”的状态。当你敲下键盘并回车,它能立刻捕捉到并打印出来。这就是非阻塞 IO。
⚠️ 实战排坑:别在非阻塞 IO 里写死循环!
如果你把上面代码里的usleep删掉,程序会变成一个死循环,CPU 占用率瞬间飙升到 100%(这就是经典的“忙等待”)。这就引出了我们下一课的重点:IO 多路复用(epoll),它可以在不浪费 CPU 的情况下,等待多个文件描述符。