多路 I/O复用(Multiplexed I/O):
**1.定义:**系统提供的I/O事件通知机制
2.应用: 是一种 I/O 编程模型,用于在单线程中同时处理多个(阻塞) I/O 操作,避免因等待某个 I/O 操作完成而阻塞整个程序的执行,及时处理。
3.I/O模型:①阻塞I/O(默认),闲等待()没有就等待
②非阻塞(fcntl,NONBLOCK)I/O,忙等待(不停询问)。cpu使用率高
③信号驱动I/O(不要求)SIGIO:使用少
④并发I/O:进程线程(开销大,浪费内存)
⑤多路I/O:select,epoll,poll
一,基于 有名管道(FIFO) 的简单 IO 交互程序,核心功能是同时监听 "有名管道" 和 "终端输入",并分别打印接收到的数据。下面从 功能逻辑、关键技术、代码细节 三方面逐步解析
(1)核心功能总览
程序的本质是一个 "双源数据监听器":
- 先创建一个名为
myfifo
的命名管道(用于进程间通信); - 以 "只读 + 非阻塞" 模式打开管道,同时监听终端输入(标准输入
stdin
); - 进入死循环,不断尝试:
- 从管道读取数据(非阻塞,没数据也不卡),读到就打印;
- 从终端读取用户输入(默认阻塞),读到就打印;
- 实现 "管道数据" 和 "终端输入" 的并行处理(本质是轮询非阻塞 IO)。
读:
cs
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
//创建有名管道
int ret=mkfifo("myfifo",0666);
{
if(EEXIST==errno)//管道已存在不报错(正常情况)
{
}else//其他错误,报错退出
{
perror("mkfifo errro\n");
return 1;
}
}
//打开管道并设置"非阻塞模式"
//一只读模式打开管道
int fd=open("myfifo",O_RDONLY);
if(-1==fd)
{
perror("open error\n");
return 1;
}
//将管道设置为"非阻塞IO"模式
int flag=fcntl(fd,F_GETFL,0);//获取当前fd的状态(阻塞,只读)
fcntl(fd,F_SETFL,flag|O_NONBLOCK);// F_SETFL:设置标志,在原有的基础上添加 O_NONBLOCK(非阻塞)
//fileno():将流指针转换成整型
flag=fcntl(fileno(stdin),F_GETFL,0);//获取到了输入端的状态值
//死循环监听数据
while(1)
{
char buf[512]={0};
if(read(fd,buf,sizeof(buf)>0))//将fd中的数据读到buf里面
{
printf("fifo:%s\n",buf);
}
//清空缓冲区buf,准备接收终端输入
bzero(buf,sizeof(buf));
// ② 从终端读取用户输入(默认阻塞)
if (fgets(buf, sizeof(buf), stdin))
{
// fgets返回非NULL:读到了输入
printf("terminal:%s", buf); // 打印终端输入
fflush(stdout); // 强制刷新 stdout(避免输出缓存)
}
}
close(fd);
//system("pause");
return 0;
}
-
fcntl
作用:修改文件描述符的属性(File Control):F_GETFL
:获取当前fd
的状态(比如是否是阻塞、只读 / 写等);flag | O_NONBLOCK
:在原有标志基础上,添加 "非阻塞" 标志(O_NONBLOCK
)------ 设置后,read(fd, ...)
若没数据,不会阻塞等待,而是立即返回-1
并设置errno=EAGAIN
。
写:
cs
#include <errno.h> // 提供错误码定义,用于错误处理
#include <fcntl.h> // 提供文件控制操作函数(如open)的声明
#include <stdio.h> // 标准输入输出函数
#include <stdlib.h> // 标准库函数(如exit)
#include <string.h> // 字符串处理函数(如strlen)
#include <sys/stat.h> // 提供文件状态相关定义(如mkfifo所需的权限位)
#include <sys/types.h> // 提供基本系统数据类型定义
#include <unistd.h> // 提供POSIX操作系统API(如write、sleep、close)
int main(int argc, char *argv[])
{
// 创建名为"myfifo"的命名管道,权限为0666(所有用户可读写)
int ret = mkfifo("myfifo", 0666);
// 检查mkfifo调用是否失败
if (-1 == ret)
{
// 如果错误码是EEXIST,表示FIFO已存在,属于正常情况,不做处理
if (EEXIST == errno)
{
// 空语句块:已存在则无需重新创建,继续执行后续代码
}
// 其他错误情况(如权限不足),打印错误信息并退出
else
{
perror("mkfifo error\n"); // 打印具体错误原因
return 1; // 非0返回值表示程序异常退出
}
}
// 以只写模式(O_WRONLY)打开FIFO,获取文件描述符
int fd = open("myfifo", O_WRONLY);
// 检查open调用是否失败
if (-1 == fd)
{
perror("open error\n"); // 打印打开失败的原因
return 1; // 异常退出
}
// 无限循环:持续向FIFO写入数据
while (1)
{
// 定义缓冲区并初始化要发送的字符串
char buf[512] = "hello,this is fifo tested...\n";
// 向FIFO写入数据:参数为文件描述符、缓冲区、数据长度(包含结束符)
write(fd, buf, strlen(buf) + 1);//strlen(buf) + 1确保包含字符串结束符\0
// 暂停3秒,控制发送频率,使程序每 3 秒写入一次数据
sleep(3);
}
// 关闭文件描述符(注:由于上面是无限循环,此句实际永远不会执行)
close(fd);
return 0; // 程序正常退出(实际不会执行到此处)
}
二,信号驱动I/O:
使用命名管道(FIFO)进行异步读取的程序,它通过信号机制实现当 FIFO 中有数据到来时进行处理
写:
cs
#include <errno.h> // 错误处理相关定义
#include <fcntl.h> // 文件控制操作(fcntl等)
#include <signal.h> // 信号处理相关函数和定义
#include <stdio.h> // 标准输入输出
#include <stdlib.h> // 标准库函数
#include <string.h> // 字符串处理函数
#include <sys/stat.h> // 文件状态相关定义
#include <sys/types.h> // 基本系统数据类型
#include <unistd.h> // POSIX系统API
int fd; // 全局文件描述符,供信号处理函数使用
// 信号处理函数:当接收到SIGIO信号时被调用
void myhandle(int num)
{
char buf[512] = {0};
// 从FIFO读取数据
read(fd, buf, sizeof(buf));
// 打印从FIFO接收到的数据
printf("fifo :%s\n", buf);
}
int main(int argc, char *argv[])
{
// 注册SIGIO信号的处理函数为myhandle
signal(SIGIO, myhandle);
// 创建命名管道"myfifo",权限0666
int ret = mkfifo("myfifo", 0666);
if (-1 == ret)
{
// 如果管道已存在,不做处理
if (EEXIST == errno)
{
}
// 其他错误则打印信息并退出
else
{
perror("mkfifo error\n");
return 1;
}
}
// 以只读模式打开FIFO
fd = open("myfifo", O_RDONLY);
if (-1 == fd)
{
perror("open error\n");
return 1;
}
// 获取文件描述符当前的标志
int flag = fcntl(fd, F_GETFL);
// 设置文件描述符为异步模式(O_ASYNC)
fcntl(fd, F_SETFL, flag | O_ASYNC);
// 设置异步I/O的所有者为当前进程,当有数据时内核会向该进程发送SIGIO信号
fcntl(fd, F_SETOWN, getpid());
// 主循环:持续从终端读取输入并打印
while (1)
{
char buf[512] = {0};
bzero(buf, sizeof(buf)); // 清空缓冲区
// 从标准输入读取数据
fgets(buf, sizeof(buf), stdin);
// 打印终端输入的数据
printf("terminal:%s", buf);
fflush(stdout); // 刷新输出缓冲区
}
// 关闭文件描述符(实际不会执行,因为上面是无限循环)
close(fd);
// remove("myfifo"); // 注释掉了删除FIFO的操作
return 0;
}
程序核心功能说明:
-
异步 I/O 处理机制:
- 使用
fcntl
设置 FIFO 为异步模式(O_ASYNC
) - 当 FIFO 中有数据到达时,内核会自动向进程发送
SIGIO
信号 - 注册了
SIGIO
信号的处理函数myhandle
,在信号到来时读取并处理数据
- 使用
-
程序工作流程:
- 创建并打开 FIFO(只读模式)
- 配置 FIFO 为异步通知模式,并设置当前进程为接收通知的进程
- 主循环负责从终端读取用户输入并打印
- 当有数据写入 FIFO 时,触发
SIGIO
信号,调用myhandle
读取并打印 FIFO 中的数据
-
特点:
- 实现了非阻塞式的 FIFO 读取,主程序可以同时处理其他任务(这里是终端输入)
- 信号驱动的 I/O 模型提高了效率,不需要主动轮询 FIFO 状态
读:
cs
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int ret = mkfifo("myfifo", 0666);
if (-1 == ret)
{
if (EEXIST == errno)
{
}
else
{
perror("mkfifo error\n");
return 1;
}
}
int fd = open("myfifo", O_WRONLY);
if (-1 == fd)
{
perror("open error\n");
return 1;
}
while (1)
{
char buf[512] = "hello,this is fifo tested...\n";
write(fd, buf, strlen(buf) + 1);
sleep(3);
}
close(fd);
return 0;
}
三,I/O多路复用:select,ep
(1) select函数:
通过监听一组文件描述符(
fd_set
),来判断其中是否有描述符就绪(可读、可写或异常)。程序会阻塞在select
调用上,直到有描述符就绪或者超时。核心功能:帮你同时盯着多个 "消息入口"(文件描述符:比如套接字、键盘输入),对应的信息放入对应的集合里,等某个 / 某些入口有消息了,再通知你处理 ------ 避免你自己挨个蹲守,让程序更高效、不卡顿。
select
是最早的多路 I/O 实现方式,在 POSIX 标准中定义,具有较好的跨平台性(在 Linux、Windows 等系统中都有支持)。
**1.使用步骤:**①创建集合
②加入fd;
③select轮询
④找到对应的fd r/w
2. 函数原型
bash
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
返回值:就绪的文件描述符数量;-1 表示出错;0 表示超时
3. 核心参数
nfds
:需监听的最大文件描述符 + 1(因 FD 从 0 开始编号)。readfds
:监听 "可读事件" 的 FD 集合(输入参数,内核修改后返回就绪 FD)。writefds
:监听 "可写事件" 的 FD 集合(同上)。exceptfds
:监听 "异常事件" 的 FD 集合(同上)。timeout
:超时时间(NULL
表示永久阻塞;struct timeval{0,0}
表示非阻塞;其他值表示等待指定时间)。
select
阻塞等待,程序暂停,不占用 CPU
4. 辅助宏(操作 FD 集合)
bash
FD_ZERO(fd_set *set); // 清空 FD 集合
FD_SET(int fd, fd_set *set); // 将 FD 添加到集合
FD_CLR(int fd, fd_set *set); // 从集合中移除 FD
FD_ISSET(int fd, fd_set *set); // 检查 FD 是否在就绪集合中(返回非 0 表示就绪)
基本原理 :
select
函数通过监听一组文件描述符(fd_set
),来判断其中是否有描述符就绪(可读、可写或异常)。程序会阻塞在 select
调用上,直到有描述符就绪或者超时。
5. 特点与局限
- 优点:跨平台性好,接口简单,适合监听少量 FD(如 < 1000)。
- 缺点 :
- FD 数量限制(默认最大 1024,由
FD_SETSIZE
定义)。 - 每次调用需将 FD 集合从用户空间拷贝到内核空间,效率低。
- 需遍历所有 FD 才能判断就绪状态(时间复杂度 O (n))。
- FD 数量限制(默认最大 1024,由
使用
select
多路复用机制同时监听管道(fifo)和标准输入(终端)的程序,功能与之前的epoll
版本类似,但使用了不同的 I/O 多路复用技术。
读端:重点处理
cs#include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> /* 包含select相关头文件 */ /* 根据POSIX.1-2001, POSIX.1-2008标准 */ #include <sys/select.h> /* 根据早期标准 */ #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char *argv[]) { // 创建命名管道"myfifo",权限为0666(所有用户可读写) int ret = mkfifo("myfifo", 0666); if (-1 == ret) // 创建失败 { if (EEXIST == errno) // 错误为"文件已存在",属于正常情况,不处理 { } else // 其他错误(如权限不足),输出错误信息并退出 { perror("mkfifo error\n"); return 1; } } // 以只读方式打开管道文件 int fd = open("myfifo", O_RDONLY); if (-1 == fd) // 打开失败 { perror("open error\n"); return 1; } // 定义select所需的文件描述符集合 // rd_set:用于select调用的临时集合(会被select修改) // tmp_set:保存初始集合(用于每次循环恢复rd_set) fd_set rd_set, tmp_set; // 初始化集合,清空所有位 FD_ZERO(&rd_set); FD_ZERO(&tmp_set); // 向集合中添加需要监听的文件描述符(对应集合类型) FD_SET(0, &tmp_set); // 添加标准输入(终端,文件描述符为0) FD_SET(fd, &tmp_set); // 添加管道文件描述符 // 事件循环:持续监听输入事件 while (1) { // 每次循环前,从备份集合恢复rd_set // 因为select会修改集合,只保留就绪的文件描述符 rd_set = tmp_set; // 调用select等待事件发生 // 参数1:最大文件描述符值+1(fd是管道描述符,比0大) // 参数2:监听读事件的集合 // 参数3、4:NULL,表示不监听写事件和异常事件 // 参数5:NULL,表示无限期等待,直到有事件发生 select(fd + 1, &rd_set, NULL, NULL, NULL); // 缓冲区,用于存储读取的数据 char buf[512] = {0}; // FD_ISSET函数:检查管道是否有数据可读(文件描述符在就绪集合中) if (FD_ISSET(fd, &rd_set))//如果fd文件准备就绪,就执行他 { // 从管道读取数据 read(fd, buf, sizeof(buf)); // 打印管道中的数据 printf("fifo :%s\n", buf); } // 检查终端(标准输入)是否有输入 if (FD_ISSET(0, &rd_set))//如果终端有输入,就执行他 { // 清空缓冲区 bzero(buf, sizeof(buf)); // 从终端读取一行输入 fgets(buf, sizeof(buf), stdin); // 打印终端输入的数据 printf("terminal:%s", buf); // 刷新标准输出缓冲区,确保内容立即显示 fflush(stdout); } } // 关闭管道文件描述符(实际运行中循环不会退出,此句很少执行) close(fd); // 注释:删除管道文件(此处注释掉,保留文件供后续测试) // remove("myfifo"); return 0; }
写端:
cs
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int ret = mkfifo("myfifo", 0666);
if (-1 == ret)
{
if (EEXIST == errno)
{
}
else
{
perror("mkfifo error\n");
return 1;
}
}
int fd = open("myfifo", O_WRONLY);
if (-1 == fd)
{
perror("open error\n");
return 1;
}
while (1)
{
char buf[512] = "hello,this is fifo tested...\n";
write(fd, buf, strlen(buf) + 1);
sleep(3);
}
close(fd);
return 0;
}
(2)
epoll
函数(Linux 特有):精准告诉你 "哪些设备有数据要处理了",让程序不用瞎等、不用瞎查,直接处理有动静的设备就行。(加强的select)
epoll
是 Linux 专为高并发设计的多路 I/O 复用机制,性能远超 select
/poll
,支持海量 FD 且时间复杂度为 O (1)。
(1)epoll 工作流程总结
- 创建实例 :通过
epoll_create
创建 epoll 实例(epfd
),默认创建两个集合(二叉树); - 注册事件 :通过
epoll_ctl
向epfd
中添加需要监听的 FD 及事件(如EPOLLIN
)结构体; - 等待就绪 :通过
epoll_wait
阻塞等待,内核自动将就绪事件写入events
数组; - 处理事件 :遍历
events
数组,根据就绪的 FD 和事件类型(如可读)进行处理; - 循环监听:重复步骤 3~4,持续处理新的就绪事件。
(2)核心函数详解
1. epoll_create
:创建 epoll 实例
功能 :在内核中创建一个 epoll 实例(事件表),用于管理后续需要监听的文件描述符(FD)和事件。
原型:
#include <sys/epoll.h>
int epoll_create(int size);
参数:
size
:早期版本用于指定监听的 FD 数量上限,现在已废弃(内核不再使用此值),传入任意正整数即可(通常传1
)。
返回值:
-
成功:返回一个 epoll 实例的文件描述符(
epfd
),后续操作通过该描述符进行; -
失败:返回
-1
,并设置errno
(如ENFILE
表示系统文件描述符耗尽)。int epfd = epoll_create(1); // 创建 epoll 实例
if (epfd == -1) {
perror("epoll_create failed");
exit(EXIT_FAILURE);
}
2. epoll_ctl
:管理 epoll 实例中的事件
功能 :向 epoll 实例中添加、修改或删除需要监听的文件描述符及其事件(如 "可读""可写")。
原型:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:
-
epfd
:epoll_create
返回的 epoll 实例描述符; -
op
:操作类型,可选值:EPOLL_CTL_ADD
:向 epoll 实例添加一个新的 FD 及事件;EPOLL_CTL_MOD
:修改已添加的 FD 的监听事件;EPOLL_CTL_DEL
:从 epoll 实例中删除一个 FD(此时event
可设为NULL
);
-
fd
:需要监听的文件描述符(如 socket、管道 FD 等); -
event
:指向struct epoll_event
的指针,用于描述监听的事件及用户数据:struct epoll_event { uint32_t events; // 监听的事件类型(如 EPOLLIN 表示可读) epoll_data_t data; // 用户数据(通常存储 FD 或自定义指针) }; // 用户数据联合体(可存储多种类型) typedef union epoll_data { void *ptr; // 自定义指针(如指向业务数据结构) int fd; // 最常用:存储当前监听的 FD uint32_t u32; uint64_t u64; } epoll_data_t;
常见事件类型(events
字段):
EPOLLIN
:FD 可读(如 socket 有数据、管道有输入);EPOLLOUT
:FD 可写(如 socket 发送缓冲区空闲);EPOLLERR
:FD 发生错误(无需手动设置,内核自动触发);EPOLLET
:边缘触发模式(ET,高效模式,需配合非阻塞 IO);EPOLLONESHOT
:只监听一次事件,事件触发后需重新添加才会再次监听。
返回值:
-
成功:返回
0
; -
失败:返回
-1
,并设置errno
(如EEXIST
表示添加已存在的 FD)。struct epoll_event ev;
// 监听 FD=5 的"可读事件",并设为边缘触发(ET)
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = 5; // 存储 FD 到用户数据中// 向 epoll 实例添加该 FD 及事件
if (epoll_ctl(epfd, EPOLL_CTL_ADD, 5, &ev) == -1) {
perror("epoll_ctl add failed");
exit(EXIT_FAILURE);
}
3. epoll_wait
:等待并获取就绪事件
功能 :阻塞等待 epoll 实例中监听的 FD 发生就绪事件(如可读、可写),返回就绪的事件列表。
原型:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数:
epfd
:epoll 实例描述符;events
:用户空间数组,内核会将所有就绪的事件写入该数组(输出参数);maxevents
:events
数组的最大长度(必须 ≥ 1,且不能超过epoll_create
时的size
早期限制);timeout
:超时时间(毫秒):-1
:永久阻塞,直到有事件就绪;0
:立即返回,无论是否有事件就绪;- 正数:等待
timeout
毫秒后返回(若期间有事件就绪则提前返回)
返回值:
-
成功:返回就绪事件的数量(
>0
); -
超时:返回
0
(timeout
非-1
时); -
失败:返回
-1
,并设置errno
(如EINTR
表示被信号中断)。struct epoll_event events[10]; // 最多存储 10 个就绪事件
int nfds;// 永久阻塞等待事件(-1)
nfds = epoll_wait(epfd, events, 10, -1);
if (nfds == -1) {
perror("epoll_wait failed");
exit(EXIT_FAILURE);
}// 遍历所有就绪事件并处理
for (int i = 0; i < nfds; i++) {
if (events[i].events & EPOLLIN) {
// FD 可读,处理数据(events[i].data.fd 为就绪的 FD)
handle_read(events[i].data.fd);
}
}
2. 事件类型与触发模式
- 常见事件 :
EPOLLIN
(可读)、EPOLLOUT
(可写)、EPOLLERR
(错误)。 - 触发模式 :
- 水平触发(LT,默认) :只要 FD 就绪,每次
epoll_wait
都会返回。 - 边缘触发(ET):仅在 FD 从 "未就绪" 变为 "就绪" 时触发一次(需配合非阻塞 IO 使用)。
- 水平触发(LT,默认) :只要 FD 就绪,每次
3. 特点
- 优点 :
- 无 FD 数量限制(支持上万甚至百万级 FD)。
- FD 仅在添加时拷贝到内核,后续无需重复拷贝。
- 内核直接返回就绪 FD 列表,无需遍历(O (1) 复杂度)。
- 缺点:仅支持 Linux 系统,不跨平台。
poll
多路复用机制,实现了同时监听管道(fifo)和标准输入(终端)的功能
读端:
cs
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h> // epoll相关函数头文件
#include <sys/stat.h> // 文件状态相关函数
#include <sys/types.h> // 基本数据类型定义
#include <unistd.h> // POSIX系统调用
/**
* 向epoll实例添加需要监听的文件描述符
* @param epfd epoll实例的文件描述符
* @param fd 要添加的文件描述符
* @return 0表示成功,1表示失败
*/
int add_fd(int epfd, int fd)
{
struct epoll_event ev; // epoll事件结构体,用于描述监听的事件类型和关联数据
ev.events = EPOLLIN; // 监听读事件(当有数据可读时触发)
ev.data.fd = fd; // 将文件描述符与事件绑定,方便后续识别
// 调用epoll_ctl添加文件描述符到epoll实例
// 参数:epoll实例、操作类型(添加)、目标文件描述符、事件结构体
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
if (-1 == ret)
{
perror("add fd"); // 添加失败时输出错误信息
return 1;
}
return 0; // 成功添加
}
int main(int argc, char *argv[])
{
// 创建命名管道"myfifo",权限为0666(所有用户可读写)
int ret = mkfifo("myfifo", 0666);
if (-1 == ret) // 创建失败
{
if (EEXIST == errno) // 错误码为EEXIST,表示管道已存在,属于正常情况
{
// 管道已存在,无需处理,继续执行
}
else // 其他错误(如权限不足)
{
perror("mkfifo error\n"); // 输出错误信息
return 1; // 异常退出
}
}
// 以只读方式打开管道文件
int fd = open("myfifo", O_RDONLY);
if (-1 == fd) // 打开失败
{
perror("open error\n"); // 输出错误信息
return 1; // 异常退出
}
// 定义epoll事件数组,用于存储epoll_wait返回的就绪事件
// 大小为2,因为我们最多监听2个文件描述符(终端和管道)
struct epoll_event rev[2];
// 1. 创建epoll实例
// 参数2表示期望处理的文件描述符数量(仅供内核参考,非严格限制)
int epfd = epoll_create(2);
if (-1 == epfd) // 创建失败
{
perror("epoll_create"); // 输出错误信息
return 1; // 异常退出
}
// 2. 向epoll实例添加需要监听的文件描述符
add_fd(epfd, 0); // 添加标准输入(终端,文件描述符固定为0)
add_fd(epfd, fd); // 添加管道文件描述符
// 事件循环:持续监听并处理事件
while (1)
{
char buf[512] = {0}; // 数据缓冲区,用于存储读取到的数据
// 等待事件发生,epoll_wait会阻塞直到有事件触发
// 参数:epoll实例、存储就绪事件的数组、数组大小、超时时间(-1表示无限等待)
int ep_ret = epoll_wait(epfd, rev, 2, -1);
// 遍历所有就绪的事件
for (int i = 0; i < ep_ret; i++)
{
// 判断就绪的是管道文件描述符
if (rev[i].data.fd == fd)
{
read(fd, buf, sizeof(buf)); // 从管道读取数据
printf("fifo :%s\n", buf); // 打印管道中的数据
}
// 判断就绪的是标准输入(终端)
else if (0 == rev[i].data.fd)
{
bzero(buf, sizeof(buf)); // 清空缓冲区
fgets(buf, sizeof(buf), stdin); // 从终端读取输入
printf("terminal:%s", buf); // 打印终端输入的数据
fflush(stdout); // 刷新标准输出,确保内容立即显示
}
}
}
// 关闭管道文件描述符(实际运行中因无限循环,此句很少执行)
close(fd);
// 注释:删除管道文件(此处保留注释,实际运行时不删除,方便后续测试)
// remove("myfifo");
return 0;
}
写端:
cs
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int ret = mkfifo("myfifo", 0666);
if (-1 == ret)
{
if (EEXIST == errno)
{
}
else
{
perror("mkfifo error\n");
return 1;
}
}
int fd = open("myfifo", O_WRONLY);
if (-1 == fd)
{
perror("open error\n");
return 1;
}
while (1)
{
char buf[512] = "hello,this is fifo tested...\n";
write(fd, buf, strlen(buf) + 1);
sleep(3);
}
close(fd);
return 0;
}
对比维度 | select | epoll |
---|---|---|
底层机制 | 轮询(遍历所有监听 fd) | 主动上报(有设备的中断触发) |
文件描述符限制 | 有上限(默认 1024,由 FD_SETSIZE 定义),修改需重新编译内核 |
无上限(仅受系统内存和进程 fd 限制) |
性能随 fd 增长趋势 | 性能急剧下降(O (n),n 为监听 fd 数量) | 性能稳定(O (1),与监听 fd 数量无关) |
用户态 / 内核态交互 | 每次调用需拷贝整个 fd_set 到内核(高开销) | 仅注册 / 修改时拷贝 fd 到内核,后续无拷贝 |
就绪 fd 通知方式 | 仅告知 "有就绪",需用户二次遍历检查 | 直接返回就绪 fd 列表,无需二次检查 |
支持的事件类型 | 仅支持水平触发(LT) | 支持水平触发(LT)和边缘触发(ET) |
接口复杂度 | 简单(3 个函数:select /FD_SET /FD_ISSET ) |
稍复杂(3 个函数:epoll_create /epoll_ctl /epoll_wait ) |
可移植性 | 高(POSIX 标准,支持 Linux/Windows/macOS) | 低(仅 Linux 特有,非 POSIX 标准) |
适用场景 | 监听 fd 数量少(如 < 1000)、简单场景 | 监听 fd 数量多(如万级 / 十万级)、高性能场景(如服务器) |