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

发布于:2025-09-06 ⋅ 阅读:(12) ⋅ 点赞:(0)

多路 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)。

读:

#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

写:

#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 中有数据到来时进行处理

写:

#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 状态

读:

#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. 函数原型
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 集合)
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 多路复用技术。

读端:重点处理

#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;
}

写端:

#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);

参数

  • epfdepoll_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:用户空间数组,内核会将所有就绪的事件写入该数组(输出参数);
  • maxeventsevents 数组的最大长度(必须 ≥ 1,且不能超过 epoll_create 时的 size 早期限制);
  • timeout:超时时间(毫秒):
    • -1:永久阻塞,直到有事件就绪;
    • 0:立即返回,无论是否有事件就绪;
    • 正数:等待 timeout 毫秒后返回(若期间有事件就绪则提前返回)

返回值

  • 成功:返回就绪事件的数量(>0);
  • 超时:返回 0timeout 非 -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)和标准输入(终端)的功能

读端:

#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;
}

写端:

#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 数量多(如万级 / 十万级)、高性能场景(如服务器)


网站公告

今日签到

点亮在社区的每一天
去签到