一、IO 多路复用 --- 概念
定义:在单线程/单进程中,同时监测多个文件描述符(fd)是否可以执行 IO 操作的能力,也称为 IO 事件(读/写)的通知机制。
核心价值:
- 应用程序通常需要处理来自多条事件流的事件(键盘/鼠标输入、网络连接等)
- Web 服务器(如 Nginx)需要同时处理来自 N 个客户端的请求
- 用单个执行体检测多个阻塞设备,避免为每个连接创建进程/线程的开销
二、五种 IO 模型
|-------------------|-------------------------------------------------------------|
| IO 模型 | 说明 |
| ① 阻塞 IO | 默认模式。调用 read/recv 后线程挂起,直到数据就绪才返回。 |
| ② 非阻塞 IO | 使用 fcntl 设置 O_NONBLOCK。数据未就绪时立即返回 EAGAIN,需要轮询(忙等待),CPU 消耗高。 |
| ③ 信号驱动 IO | 使用 SIGIO 信号通知。数据就绪时系统发信号给进程,用得相对少,了解即可。 |
| ④ 并行模型 | 多进程 / 多线程各自独立处理 IO,编程简单但资源消耗大。 |
| ⑤ IO 多路复用 | select / poll / epoll。单线程监控多个 fd,高并发服务器首选。 |
三、select
3.1 使用步骤

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ① 定义描述符集合(fd_set),并清零: FD_ZERO(&rd_set) ② 向集合中添加需要监测的 fd: FD_SET(fd, &rd_set) ③ 调用 select,阻塞等待通知(轮询) ④ select 返回后,遍历 fd 集合,用 FD_ISSET 找到就绪的 fd ⑤ 对就绪 fd 执行 read / recv ⑥ 清除标志位,重新添加 fd,循环执行步骤 ③ |
3.2 函数原型
|----------------------------------------------------------------------------------------------------------|
| int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); |
|---------------|----------------------------------|
| 参数 | 说明 |
| nfds | 描述符上限值,通常为集合中最大 fd + 1,可直接写 1024 |
| readfds | 只读描述符集(最常用) |
| writefds | 只写描述符集 |
| exceptfds | 异常描述符集 |
| timeout | 超时设置;NULL = 永久阻塞;0 = 非阻塞 |
| 返回值 | > 0 就绪 fd 数量 | 0 超时 | -1 出错 |
3.3 辅助宏函数
|-------------------------|--------------------|
| 函数 | 作用 |
| FD_ZERO(&set) | 清空集合中所有描述符 |
| FD_SET(fd, &set) | 将 fd 添加到集合 |
| FD_CLR(fd, &set) | 从集合中删除 fd |
| FD_ISSET(fd, &set) | 判断 fd 是否在集合中(就绪检测) |
3.4 注意事
|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ⚠️ select 执行后会修改 readfds 集合,循环调用前必须重新设置(清除标志位) ⚠️ 超时版本:每次调用 select 前都要重新设置 timeout 结构体 ⚠️ 单个集合最多监测 1024 个描述符(FD_SETSIZE 限制) ⚠️ nfds 传入进程中最大 fd 的整数值,也可直接写 1024 |
四、epoll
4.1 使用步骤

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ① epoll_create(size) --- 创建 epoll 实例(红黑树),得到 epfd ② epoll_ctl(EPOLL_CTL_ADD, fd, event) --- 向集合添加需要监测的 fd ③ epoll_wait(epfd, events, maxevents, timeout) --- 阻塞等待,主动上报(中断机制) ④ 遍历返回的 events 数组,对就绪 fd 执行读取操作 |
4.2 相关函数
epoll_create --- 创建集合
|-----------------------------|
| int epoll_create(int size); |
|----------|----------------------------------|
| size | 集合可存储 fd 的最大数量 |
| 返回值 | > 0 epfd(表示该红黑树的文件描述符) | -1 出错 |
epoll_ctl --- 管理集合
|----------------------------------------------------------------------|
| int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); |
|-------------------|--------------------------------------------------------|
| epfd | epoll_create 返回的集合描述符 |
| op | EPOLL_CTL_ADD 添加 / EPOLL_CTL_DEL 删除 / EPOLL_CTL_MOD 修改 |
| fd | 需要操作的文件描述符 |
| event.events | EPOLLIN(可读) / EPOLLOUT(可写) |
| event.data.fd | 用户自定义数据,查找 fd 时使用 |
| 返回值 | 0 成功 | -1 出错 |
epoll_wait --- 等待 IO 事件
|------------------------------------------------------------------------------------|
| int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); |
|---------------|--------------------------------|
| epfd | 被检测的集合 |
| events | 输出集合:就绪 fd 由系统写入此数组 |
| maxevents | 一次最多复制到 events 的 fd 数量 |
| timeout | -1 永久阻塞 | 0 非阻塞 | 5000 等待 5s |
| 返回值 | > 0 就绪 fd 数量 | 0 超时 | -1 出错 |
4.3 epoll 优势
|----------------------------------------------------------------------------------------------------------------------------------------|
| ✅ 不受 1024 个 fd 限制(无 FD_SETSIZE 限制) ✅ 采用主动上报(中断)机制,效率不随 fd 数量增多而下降 ✅ 使用共享内存,避免集合在用户层和内核层多次复制 ✅ epoll_wait 返回后,就绪 fd 已汇总在 events 数组,查找方便 |
五、select vs epoll 对比
|-----------------|---------------------|-----------------|
| 对比项 | select | epoll |
| fd 数量限制 | 最多 1024(FD_SETSIZE) | 无限制 |
| 通知机制 | 轮询(遍历所有 fd) | 主动上报(中断) |
| 内存拷贝 | 每次调用都需用户↔内核拷贝 | 使用共享内存,减少拷贝 |
| 就绪 fd 定位 | 需要遍历整个集合 | 直接从 events 数组获取 |
| 跨平台性 | 跨平台(POSIX 标准) | 仅限 Linux |
| 适用场景 | fd 数量少,连接数不高 | 高并发服务器,大量连接 |
六、服务器并发模型
6.1 进程版本(多进程)
|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| 原理:accept 后 fork() 创建子进程处理客户端 关键点 1:僵尸进程回收 --- 用 signal(SIGCHLD, handle) + wait(NULL) 关键点 2:父进程关闭 conn,子进程关闭 listfd(文件描述符隔离) 缺点:进程创建开销大(0~3G 用户空间),并发量有限 |
6.2 线程版本(多线程)
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 原理:accept 后 pthread_create 创建线程处理客户端 关键点 1:栈区回收 --- 使用 pthread_detach(pthread_self()) 设置分离属性 关键点 2:conn 参数传递 --- 工作线程中必须将 conn 本地保存后,再通知主线程 方案:用信号量 sem_post 通知主线程已完成复制,再继续 accept 关键点 3:int conn = *(int*)arg; // 从指针参数中复制 conn 到线程本地 |
6.3 IO 多路复用版本(epoll + 循环服务器)
|----------------------------------------------------------------------------------------------------------------------------------------------|
| 原理:单线程用 epoll 监控 listfd 和所有 connfd - listfd 就绪 → accept 新连接 → 将 connfd 加入 epoll - connfd 就绪 → recv 数据 → 处理 → send 响应 适用:高并发 Web Server、循环服务器 |
七、速查卡片
select 使用模板
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| fd_set rd_set, tmp_set; FD_ZERO(&rd_set); FD_SET(fd1, &rd_set); FD_SET(fd2, &rd_set); while (1) { tmp_set = rd_set; // 每次循环重置(select 会修改集合) int n = select(maxfd+1, &tmp_set, NULL, NULL, NULL); if (n < 0) { perror("select"); break; } for (int i = 0; i <= maxfd; i++) { if (FD_ISSET(i, &tmp_set)) { // i 号 fd 有数据可读 read(i, buf, sizeof(buf)); } } } |
epoll 使用模板
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| int epfd = epoll_create(1024); struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = listfd; epoll_ctl(epfd, EPOLL_CTL_ADD, listfd, &ev); struct epoll_event revs[64]; while (1) { int n = epoll_wait(epfd, revs, 64, -1); for (int i = 0; i < n; i++) { int fd = revs[i].data.fd; if (fd == listfd) { int conn = accept(listfd, NULL, NULL); ev.data.fd = conn; epoll_ctl(epfd, EPOLL_CTL_ADD, conn, &ev); } else { int ret = recv(fd, buf, sizeof(buf), 0); if (ret <= 0) { epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); close(fd); } else { send(fd, buf, ret, 0); } } } } |