多路IO学习笔记
·
目录
五、代码实现:TCP 服务器(select + epoll)
一、多路 IO(IO 多路复用)
1. 定义
一种 IO 处理机制,单个进程 / 线程同时监听多个文件描述符(socket、fd),内核监控 IO 事件,当某个描述符就绪(可读 / 可写)时,立即通知程序处理,避免阻塞等待。
2. 作用
- 单线程 / 进程实现高并发 IO 处理,无需为每个连接创建进程 / 线程
- 减少资源消耗,提升系统并发处理能力
- 解决传统 IO 阻塞导致的效率低下问题
3. 与进程 / 线程并发对比
- 多进程 / 线程:每个连接独立进程 / 线程,资源占用高、切换开销大、并发量有限
- 多路 IO 复用:单线程管理所有连接,资源占用低、无切换开销、支持高并发
二、IO 模型分类
- 阻塞 IO:进程发起 IO 后一直阻塞,直到 IO 完成(最简单,效率低)
- 非阻塞 IO:进程轮询检查 IO 状态,不阻塞但 CPU 占用高
- 信号驱动 IO:IO 就绪后内核发送信号通知进程,异步性弱
- 并行模型:多进程 / 线程处理 IO,并发能力差
- IO 多路复用:重点,单线程监听多个 IO,select/poll/epoll 是实现方式
三、IO 多路复用核心:select & epoll
(1)select 详解
工作步骤
- 定义并初始化文件描述符集合(fd_set),添加需要监听的 fd
- 循环中重置监听集合(select 会修改集合,每次循环必须重新赋值)
- 调用 select (),阻塞等待内核监控 IO 事件,筛选就绪 fd
- 遍历所有 fd,检查是否就绪
- 处理就绪的 IO 事件(读 / 写 / 接受连接)



特性
- 支持跨平台,兼容性最好
- 有最大文件描述符限制(默认 1024)
- 每次都需要遍历全部 fd,效率随 fd 数量增加急剧下降
- 用户态与内核态数据拷贝频繁
代码示例:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
//创建有名管道
int ret = mkfifo("myfifo", 0666);
if (ret == -1)
{
if (EEXIST == errno) //管道已经存在的错误
{
//程序继续
}
else
{
perror("mkfifo fail");
return 1;
}
}
//打开有名管道
int fd = open("myfifo", O_RDONLY);
if (fd == -1)
{
perror("open myfifo");
return 1;
}
//创建集合(存放fd)、标志位集合
fd_set rd_set, tmp_set;
//添加fd到标志位集合
FD_ZERO(&rd_set); //集合清空
FD_ZERO(&tmp_set);
FD_SET(0, &tmp_set); //把标准输入放入集合
FD_SET(fd, &tmp_set); //把fd放入集合
//读管道
while (1)
{
char buf[100] = {0};
//每次循环先清除标志位
rd_set = tmp_set;
//等待读事件
select(fd + 1, &rd_set, NULL, NULL, NULL);
//判断哪个事件触发了
int i = 0;
for (i = 0; i < fd + 1; i++) // i表示文件描述符
{
//触发事件fd
if (FD_ISSET(i, &rd_set) && i == fd)
{
read(fd, buf, sizeof(buf));
printf("fifo : %s\n", buf);
}
//触发事件0
if (FD_ISSET(i, &rd_set) && i == 0)
{
bzero(buf, sizeof(buf));
fgets(buf, sizeof(buf), stdin);
printf("terminal:%s\n", buf);
fflush(stdout);
}
}
}
//关闭管道
close(fd);
return 0;
}
(2)epoll 详解
工作步骤
epoll_create()创建 epoll 实例epoll_ctl()向内核添加 / 删除 / 修改需要监听的 fdepoll_wait()阻塞等待 IO 事件,仅返回就绪 fd- 遍历就绪 fd 列表,直接处理事件
- 循环持续监听



特性
- Linux 专属,无最大连接数限制
- 内核事件驱动,只返回就绪 fd,无需全量遍历
- 用户态与内核态共享内存,减少数据拷贝
- 高并发场景性能远超 select/poll
代码示例:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/epoll.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int add_fd(int epfd, int fd)
{
struct epoll_event ev = {0};
//监听事件类型:可读事件
ev.events = EPOLLIN;
//把要监听的文件描述符存入
ev.data.fd = fd;
//将事件fd存入epfd
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
if (ret == -1)
{
perror("epoll_ctl");
return ret;
}
return 0;
}
int main(int argc, char **argv)
{
//创建有名管道
int ret = mkfifo("myfifo", 0666);
if (ret == -1)
{
if (EEXIST == errno) //管道已经存在的错误
{
//程序继续
}
else
{
perror("mkfifo fail");
return 1;
}
}
//打开有名管道
int fd = open("myfifo", O_RDONLY);
if (fd == -1)
{
perror("open myfifo");
return 1;
}
//创建集合-二叉树
struct epoll_event rev[2]; //用来放准备好的文件
int epfd = epoll_create(2);
if (epfd == -1)
{
perror("epoll_create");
return -1;
}
//添加fd到epfd集合中
add_fd(epfd, 0);
add_fd(epfd, fd);
//读管道
while (1)
{
char buf[100] = {0};
//把监听到的文件描述符存入rev数组中
int ep_ret = epoll_wait(epfd, rev, 2, -1);
int i = 0;
for (i = 0; i < ep_ret; i++)
{
if (rev[i].data.fd == fd) // fifo可读
{
read(fd, buf, sizeof(buf));
//打印
printf("fifo : %s\n", buf);
}
if (rev[i].data.fd == 0) //标准输入 可读
{
bzero(buf, sizeof(buf));
fgets(buf, sizeof(buf), stdin);
printf("terminal:%s\n", buf);
fflush(stdout);
}
}
}
//关闭管道
close(fd);
return 0;
}
四、select vs epoll 对比
| 特性 | select | epoll |
|---|---|---|
| 连接限制 | 有(1024) | 无,受系统文件数限制 |
| 遍历方式 | 全量遍历所有 fd | 仅遍历就绪 fd |
| 效率 | 低,连接越多越慢 | 高,O (1) 复杂度 |
| 数据拷贝 | 每次都拷贝 | 共享内存,零拷贝 |
| 平台支持 | 全平台 | Linux 专用 |
| 使用场景 | 少量连接、跨平台 | 高并发服务器(百万连接) |
总结:select 兼容性强、性能差;epoll 性能极致、Linux 专属,是高并发首选。
五、代码实现:TCP 服务器(select + epoll)
1. select 版 TCP 服务器(多客户端)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/select.h>
#define PORT 8888
#define MAX_FD 1024
int main() {
int lfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = INADDR_ANY;
bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
listen(lfd, 128);
fd_set read_fds, tmp_fds;
FD_ZERO(&read_fds);
FD_SET(lfd, &read_fds);
int maxfd = lfd;
while(1) {
tmp_fds = read_fds;
int nready = select(maxfd+1, &tmp_fds, NULL, NULL, NULL);
if(FD_ISSET(lfd, &tmp_fds)) {
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
FD_SET(cfd, &read_fds);
if(cfd > maxfd) maxfd = cfd;
if(--nready == 0) continue;
}
for(int i = lfd+1; i <= maxfd; i++) {
if(FD_ISSET(i, &tmp_fds)) {
char buf[1024] = {0};
int ret = read(i, buf, sizeof(buf));
if(ret <= 0) {
close(i);
FD_CLR(i, &read_fds);
} else {
printf("client %d: %s\n", i, buf);
write(i, buf, ret);
}
if(--nready == 0) break;
}
}
}
close(lfd);
return 0;
}
2. epoll 版 TCP 服务器(多客户端,高性能)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#define PORT 8888
#define MAX_EVENTS 1024
int main() {
int lfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = INADDR_ANY;
bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
listen(lfd, 128);
int epfd = epoll_create(1);
struct epoll_event ev, events[MAX_EVENTS];
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
while(1) {
int nready = epoll_wait(epfd, events, MAX_EVENTS, -1);
for(int i = 0; i < nready; i++) {
int fd = events[i].data.fd;
if(fd == lfd) {
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
ev.events = EPOLLIN;
ev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
} else {
char buf[1024] = {0};
int ret = read(fd, buf, sizeof(buf));
if(ret <= 0) {
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
} else {
printf("client %d: %s\n", fd, buf);
write(fd, buf, ret);
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
笔记核心总结
- IO 多路复用:单线程管理多 IO,高并发核心技术
- select:全平台、有限连接、全量遍历、性能低
- epoll:Linux 专属、无连接限制、仅返回就绪、高性能
- 工作流程:监听 → 等待事件 → 处理就绪 IO → 循环
- 应用场景:高并发 TCP 服务器、网关、聊天室等
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)