嵌入式解谜日志—多路I/O复用

多路 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)核心功能总览

程序的本质是一个 "双源数据监听器":

  1. 先创建一个名为 myfifo 的命名管道(用于进程间通信);
  2. 以 "只读 + 非阻塞" 模式打开管道,同时监听终端输入(标准输入 stdin);
  3. 进入死循环,不断尝试:
    • 从管道读取数据(非阻塞,没数据也不卡),读到就打印;
    • 从终端读取用户输入(默认阻塞),读到就打印;
  4. 实现 "管道数据" 和 "终端输入" 的并行处理(本质是轮询非阻塞 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;
}

程序核心功能说明:

  1. 异步 I/O 处理机制:

    • 使用fcntl设置 FIFO 为异步模式(O_ASYNC)
    • 当 FIFO 中有数据到达时,内核会自动向进程发送SIGIO信号
    • 注册了SIGIO信号的处理函数myhandle,在信号到来时读取并处理数据
  2. 程序工作流程:

    • 创建并打开 FIFO(只读模式)
    • 配置 FIFO 为异步通知模式,并设置当前进程为接收通知的进程
    • 主循环负责从终端读取用户输入并打印
    • 当有数据写入 FIFO 时,触发SIGIO信号,调用myhandle读取并打印 FIFO 中的数据
  3. 特点:

    • 实现了非阻塞式的 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))。

使用 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 工作流程总结

  1. 创建实例 :通过 epoll_create 创建 epoll 实例(epfd),默认创建两个集合(二叉树);
  2. 注册事件 :通过 epoll_ctl 向 epfd 中添加需要监听的 FD 及事件(如 EPOLLIN)结构体;
  3. 等待就绪 :通过 epoll_wait 阻塞等待,内核自动将就绪事件写入 events 数组;
  4. 处理事件 :遍历 events 数组,根据就绪的 FD 和事件类型(如可读)进行处理;
  5. 循环监听:重复步骤 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 使用)。
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 数量多(如万级 / 十万级)、高性能场景(如服务器)
相关推荐
破茧蝴蝶9 小时前
Python分布式爬虫完整教程
开发语言·python
大哥您好9 小时前
QEMU 11.1.2 多架构编译指南(x86_64 / aarch64 / riscv64)
linux·服务器
Autumn_ing9 小时前
Design.md生成UI怎么验收?举例运营后台的颜色、状态和数据核对
前端·ui·ai·设计系统·paico·ai生成ui·design.md
狗凯之家源码网9 小时前
全新 PHP 轻量客服聊天室源码深度评测
开发语言·php·聊天室系统
远离UE49 小时前
UE5 Substrate Thin Film 源码拆析
开发语言·javascript·ue5
keyipatience10 小时前
JavaScript学习(1)书写位置,变量、数据类型、运算符
开发语言·前端·javascript·css·学习
道满科技10 小时前
Function Calling 参数总出错?只修参数就行
linux·windows·python
朱涛的自习室10 小时前
我给 Gemini 4 和 DeepSeek 搞了套「末位淘汰」,AI 干活会更猛吗?(Day 3)
android·前端·后端
Gauss松鼠会10 小时前
高斯数据库(openGauss)的物理存储结构
运维·服务器·数据库·sql·opengauss·经验总结
Android系统攻城狮10 小时前
Linux Gstreamer深度解析之gst_audio_ring_buffer_prepare_read调用流程与实战(七十四)
linux·运维·服务器·gstreamer音视频·音视频进阶