多路复用IO

       

        Linux IO模型是指Linux操作系统中用于实现输入输出的一种机制。

        Linux IO模型主要分为五种:阻塞IO、非阻塞IO、IO复用、信号驱动IO和异步IO。

1. 阻塞IO是最常见的IO模型,当用户进程发起一个IO请求后,内核会一直等待,直到IO操作完成并返回结果。在此期间,用户进程会被阻塞,无法进行其他操作。

核心函数:标准的 
read
/
write

2. 非阻塞IO是在阻塞IO的基础上进行改进的一种IO模型。当用户进程发起一个IO请求后,内核会立即返回一个错误码,表示IO操作还未完成。用户进程可以继续进行其他操作,随后再通过轮询的方式来查询IO操作是否完成。

核心函数
fcntl
(设置 
O_NONBLOCK
 标志)+ 
read
/
write



fcntl(设置 O_NONBLOCK 标志)+ read/write
 
// 设置非阻塞
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
 
// 非阻塞读(立即返回)
char buf[1024];
ssize_t n = read(fd, buf, sizeof(buf));
if (n == -1 && errno == EAGAIN) {
    // 数据未就绪,可稍后再试
}

3. IO复用是指通过select、poll、epoll等系统调用来监听多个文件描述符的IO事件。当某个文件描述符就绪时,内核会通知用户进程进行IO操作。相比于阻塞IO和非阻塞IO,IO复用可以同时监听多个文件描述符,提高了IO效率。

4. 信号驱动IO是指用户进程通过signal或sigaction系统调用来注册一个信号处理函数,当IO操作完成时,内核会向用户进程发送一个SIGIO信号,用户进程在信号处理函数中进行IO操作。相比于阻塞IO和非阻塞IO,信号驱动IO可以避免用户进程被阻塞,提高了IO效率。

        举例:如果同时写 10 个 SD 卡(信号驱动IO 与 非阻塞IO的区别)

如果你的嵌入式设备需要同时控制 10 个 SD 卡写入数据(比如工业数据记录仪):

非阻塞 IO:CPU 要每隔 1ms 检查 10 个 SD 卡的状态,每次检查 10 次,40ms 内要做 400 次无用检查,CPU 大部分时间都在 “问设备”,根本没时间干别的;信号驱动 IO:CPU 只需要给 10 个 SD 卡各注册一个中断,然后专注处理业务逻辑,哪个 SD 卡写完了,就响应哪个的中断 (不用一直看着)——10 个设备的 IO 完成,也只需要处理 10 次中断,CPU 占用极低。

核心函数
fcntl
(设置 
O_ASYNC
 标志)+ 信号处理函数(
signal
/
sigaction
)。通过 
fcntl
 注册信号(通常是 
SIGIO
),I/O 就绪时内核发送信号通知进程,进程在信号处理函数中完成数据读写。



// 注册 SIGIO 信号处理函数
signal(SIGIO, handle_sigio);
 
// 设置文件描述符异步模式,并指定接收信号的进程
fcntl(fd, F_SETOWN, getpid());  // 信号发给当前进程
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_ASYNC);  // 启用信号驱动
 
// 信号处理函数中读取数据
void handle_sigio(int signo) {
    char buf[1024];
    read(fd, buf, sizeof(buf));  // I/O 已就绪,读取数据
}

5. 异步IO是指当用户进程发起一个IO请求后,内核会立即返回,表示IO操作已经开始。当IO操作完成后,内核会通知用户进程,用户进程在此时才进行IO操作。相比于其他IO模型,异步IO可以避免用户进程被阻塞,提高了IO效率。

        简单来说:同步是 “等结果再干活”,异步是 “先干活,结果好了再处理”—— 这就是异步 I/O 最生动的逻辑。

        异步IO 和 信号驱动IO逻辑是相似的,最大区别是:

1. 信号驱动 I/O:“点单后拿号,店员喊号时,你需要自己去柜台确认咖啡是否真的做好”(主动调用 
read/write
 去读取 / 写入数据
2. 异步 I/O:“点单时直接说‘做好了放我桌上’,店员会把咖啡送过来,你直接喝”

核心函数  POSIX 异步 I/O(
aio_read
/
aio_write



struct aiocb aio;  // 全局变量,让信号处理函数可访问
 
void handle_aio(int signo) {
    if (signo == SIGIO) {  // 确认是目标信号
        printf("异步读完成,数据:%s
", (char*)aio.aio_buf);
    }
}
 
int main() {
    int fd;
    char buf[1024];
    // ... 打开文件/设备获取fd(省略) ...
 
    // 1. 绑定SIGIO信号与handle_aio函数
    signal(SIGIO, handle_aio);
 
    // 2. 初始化异步I/O参数
    memset(&aio, 0, sizeof(aio));
    aio.aio_fildes = fd;
    aio.aio_buf = buf;
    aio.aio_nbytes = sizeof(buf);
    aio.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
    aio.aio_sigevent.sigev_signo = SIGIO;  // 完成后发送SIGIO
 
    // 3. 发起异步读(立即返回,不阻塞)
    aio_read(&aio);
 
    // ... 主程序可执行其他任务(省略) ...
    return 0;
}

多路复用IO


应用优势1:

        嵌入式系统几乎都需要同时管理多个外设,例如:

一个智能家居网关需要同时处理 WiFi 模块、红外传感器、RS485 串口和以太网;一个工业控制器需要监控多个 ADC 通道、GPIO 输入、SPI 设备和 CAN 总线。

        若采用传统的多线程 / 多进程模型处理这些外设,会带来不可接受的资源开销。而多路复用 I/O 通过单线程 / 单进程即可管理多个外设,避免了线程切换和同步开销,仅在事件发生时才进行处理,完美适配嵌入式系统的资源约束。


应用优势2:

        传统的阻塞式 I/O(如
read
/
write
)会导致程序在等待一个外设时阻塞其他外设的处理,例如:



// 错误示例:阻塞式I/O导致外设间相互阻塞
read(uart_fd, buf, 100);  // 若UART无数据,会一直阻塞
read(sensor_fd, &data, 1);  // 传感器数据到达也无法及时处理

        多路复用 I/O 通过统一监控所有外设的就绪状态,确保在任意外设就绪时都能及时处理,且不会因某个外设无响应而阻塞其他设备。例如,使用
select
可同时监控 UART 和传感器,哪个就绪就先处理哪个。


应用优势3:

        以前嵌入式设备处理多个外设(比如同时监控传感器、串口、按键),常用的笨办法是 “轮询”—— 就像你每隔 10 秒就解锁手机,挨个看微信、短信、电话有没有新消息。

        而多路复用 I/O(select/poll/epoll)的作用,就像给手机装了 “消息通知中心”—— 不用你频繁解锁查看,有消息了通知中心会主动提醒你,没消息时手机就黑屏休眠。

最后再用 “电池物联网节点” 的例子总结:

比如一个装电池的土壤湿度传感器,要同时监控 “湿度数据” 和 “上位机指令(比如让它改采样频率)”。

用 select 设置 1 秒超时:CPU 每 1 秒醒一次,先看有没有湿度数据或指令 —— 有就处理,没有就继续睡;结果:CPU 一天里 90% 的时间都在睡觉,只有 10% 的时间在处理数据,电池能用半年;要是用轮询:CPU 每秒得问 100 次 “有数据吗”“有指令吗”,99% 的时间在做无用功,电池可能 10 天就没电了。

        简单说:多路复用 I/O 让嵌入式设备 “该干活时马上干(实时),没事干时赶紧睡(省电)”,完美解决了 “又要快、又要省” 的矛盾。


注意注意:


  select
/
poll
/
epoll
 的定位是 “I/O 事件监控工具”,解决的是 “如何高效等待多个 I/O 就绪” 的问题;而 “事件处理的并行性” 是 “程序执行模型” 的问题(单线程 / 多线程),需要额外的线程 / 进程机制来配合实现。

这里看代码部分的select内容。


select、poll、epoll 区分:


1. 性能瓶颈差异

select
性能随监控的文件描述符数量增加而线性下降。原因是:① 每次调用需将整个 fd_set 拷贝到内核;② 内核需轮询所有描述符;③ 用户态需遍历整个集合寻找就绪描述符。在描述符数量超过 100 时性能明显下降。

poll
解决了 select 的描述符上限问题,但性能瓶颈与 select 类似 —— 仍需全量拷贝和轮询,只是通过动态数组避免了固定大小限制。在描述符数量超过 1000 时,CPU 占用率会显著上升

epoll
性能几乎不受描述符数量影响。核心优化点:① 描述符仅在添加时拷贝一次到内核;② 内核通过红黑树高效管理描述符,通过回调函数记录就绪事件;③ 
epoll_wait()
直接返回就绪列表,无需轮询。在 10 万级描述符场景下仍能保持高效。


2. 触发模式差异

水平触发(LT,Level Triggered)
select 和 poll 仅支持此模式。只要描述符处于就绪状态(如缓冲区有数据),每次调用都会触发通知。优点是编程简单,缺点是可能产生冗余通知。

边缘触发(ET,Edge Triggered)
epoll 同时支持上述两种触发。边缘触发仅在描述符状态从非就绪变为就绪时触发一次通知(如缓冲区从空变为有数据)。优点是减少冗余通知,效率更高;缺点是编程复杂(需一次性读完所有数据,否则可能遗漏事件)。

举例:水平触发就像家里的水龙头:

你打开水龙头,水开始流(对应外设 “有数据了”);水平触发会一直喊你 “快来接水!”,直到你把水接完(把缓冲区数据读完);哪怕你中途停下来(读了一部分数据,剩下的没读),它也会继续喊,直到水彻底流光。

对应到代码里
比如串口收到 100 字节数据,水平触发会:

第一次通知 CPU “有数据”;如果 CPU 只读了 50 字节,剩下 50 字节还在缓冲区,水平触发会再次通知;直到 100 字节全读完,才停止通知。

select 和 poll只支持水平触发,就像个 “唠叨的提醒器”,确保你不会漏掉数据,但可能频繁喊你。

边缘触发水龙头的例子:

你打开水龙头的瞬间(水从 “没流” 变成 “流”,状态发生变化),边缘触发会喊你一次 “开始流水了!”之后不管水有没有流光,哪怕你一直没接,它也不会再喊;只有当你关掉水龙头再打开(状态再次变化),它才会再喊一次。

对应到代码里
同样串口收到 100 字节数据,边缘触发会:

只在数据刚到达时通知一次 CPU;哪怕 CPU 只读了 50 字节,剩下的 50 字节还在缓冲区,也不会再通知;只有新的数据又来了(比如再发 30 字节,缓冲区从 50→80),才会再通知一次。

epoll支持边缘触发,就像个 “高冷的提醒器”,只在状态变化时说一次,逼着你一次把事做完

关键区别总结表

场景 水平触发(LT) 边缘触发(ET)
通知时机 只要有数据没处理完,就一直通知 只有数据从 “无” 到 “有”(状态变化)时通知
数据处理要求 可以分多次读(读一半也行) 必须一次读完所有数据(否则会漏掉)
编程难度 简单(不用担心漏数据) 复杂(必须处理干净,还要设非阻塞 IO)
效率 较低(可能多次通知) 较高(通知次数少)
支持的机制 select、poll、epoll 都支持 只有 epoll 支持

嵌入式场景中怎么选?

如果你是新手,或者设备数据量小(比如每秒几次),用水平触发(简单不容易出错);如果你做高并发场景(比如每秒几千条数据),用边缘触发(效率高,少占 CPU);比如工业网关处理大量传感器数据时,边缘触发能减少 70% 的通知次数,明显降低功耗。


3. 资源占用差异

select
内存占用低(fd_set 在栈上分配),但因固定大小限制,无法扩展。适合 KB 级 RAM 的低端嵌入式设备。

poll
内存占用随描述符数量动态增长(堆上分配数组),无固定限制,但拷贝开销随数量增加而增大。适合中等规模场景(数百个描述符)。

epoll
内核需维护红黑树和就绪链表,初始化内存占用略高但长期运行中因零拷贝特性,资源效率远高于前两者。适合大规模场景(数千至数万描述符)。


4. 应用场景对比

场景特征 首选机制 典型应用案例
监控描述符数量少(<100) select 低端传感器节点(如基于 Cortex-M4 的温湿度采集器,同时监控 UART、I2C 传感器)
描述符数量中等(100~1000) poll 工业网关(如 16 路 RS485 串口 + 32 个 I2C 传感器,需突破 1024 限制且可能移植到 QNX)
高并发(>1000) epoll 边缘计算网关(如 2000+MQTT 设备连接 + 128 个人体传感器,需低延迟和高吞吐量)

编程举例:

1. select 适用场景:小型嵌入式设备的简单多外设监控

      1.1 select函数原型



#include <sys/select.h>
#include <sys/time.h>
 
int select(int nfds, 
           fd_set *readfds, 
           fd_set *writefds, 
           fd_set *exceptfds, 
           struct timeval *timeout);
 
1.nfds:需要监控的最大文件描述符值 + 1(因为文件描述符从 0 开始编号)。
2.readfds:需要监控 “可读(有数据写入)” 事件的文件描述符集合(传入 NULL 表示不监控)。
3.writefds:需要监控 “可写” 事件的文件描述符集合(传入 NULL 表示不监控)。
4.exceptfds:需要监控 “异常” 事件的文件描述符集合(传入 NULL 表示不监控),较少使用。
5.timeout:超时时间设置:

        1.2 相关的宏操作



fd_set 是一个位图结构,用于表示文件描述符集合。系统提供了以下宏操作该集合:
 
FD_ZERO(fd_set *)	清空集合(初始化)
FD_SET(int fd, fd_set *)	将 fd 加入集合
FD_CLR(int fd, fd_set *)	将 fd 从集合中移除
FD_ISSET(int fd, fd_set *)	检查 fd 是否在集合中(是否就绪)
 
使用流程
1. 初始化文件描述符集合:用 FD_ZERO 清空,用 FD_SET 添加需要监控的 fd。
2. 调用 select:传入集合、最大 fd 值和超时时间。
3. 检查就绪的 fd:用 FD_ISSET 遍历集合,处理就绪的 I/O 事件。
4. 循环复用:由于 select 返回后集合会被修改(只保留就绪的 fd),需重新初始化集合后再次调用。

场景描述:例如一个低端物联网传感器节点,需要同时监控:

1 个 UART 接口(接收上位机指令)2 个 GPIO(检测按键输入)1 个 SPI 接口(与传感器通信)



#include <sys/select.h>
#include <unistd.h>
#include <fcntl.h>
 
// 处理UART指令
void handle_uart(int fd) {
    char buf[64];
    int len = read(fd, buf, sizeof(buf)-1);
    if (len > 0) {
        buf[len] = '';
        printf("收到上位机指令: %s
", buf);
        // 解析指令(如调整采样频率)
        parse_uart_command(buf);
    }
}
 
// 处理SPI接口信息
//.......
 
int main() {
    int uart_fd = open("/dev/ttyS0", O_RDWR | O_NONBLOCK);  // UART设备
    int gpio1_fd = open("/sys/class/gpio/gpio1/value", O_RDONLY | O_NONBLOCK);  // GPIO1
    int gpio2_fd = open("/sys/class/gpio/gpio2/value", O_RDONLY | O_NONBLOCK);  // GPIO2
    int spi_fd = open("/dev/spidev0.0", O_RDWR | O_NONBLOCK);  // SPI设备
 
    fd_set readfds;
 
    //获取所有需要监控的文件描述符中的最大值
    int max_fd = max(uart_fd, max(gpio1_fd, max(gpio2_fd, spi_fd)));
    
    while (1) {
 
        //I/O 多路复用(select 机制)的初始化操作
        FD_ZERO(&readfds);           //相当于重置集合,准备后续添加需要监控的文件描述符。
        FD_SET(uart_fd, &readfds);    //添加....
        FD_SET(gpio1_fd, &readfds);
        FD_SET(gpio2_fd, &readfds);
        FD_SET(spi_fd, &readfds);
 
        // 超时设置为500ms
        struct timeval timeout = {0, 500000};
        
        // 等待事件发生
        int ret = select(max_fd + 1, &readfds, NULL, NULL, &timeout);
        if (ret < 0) {
            perror("select error");
            break;
        } else if (ret == 0) {
            // 超时处理(如定期发送心跳)
            continue;
        }
 
        // 检查哪个设备有事件
        if (FD_ISSET(uart_fd, &readfds)) {
            // 处理UART数据接收
            handle_uart_data(uart_fd);
        }
        if (FD_ISSET(gpio1_fd, &readfds)) {
            // 处理按键1输入
            handle_gpio1_input(gpio1_fd);
        }
        // 其他设备事件处理...
    }
    
    close(uart_fd);
    close(gpio1_fd);
    close(gpio2_fd);
    close(spi_fd);
    return 0;
}

注意:        

        虽然 
select(poll、epoll)
 能 “同时监控多个设备”,但后续处理设备事件的代码(如 
handle_uart_data
)本质仍是串行执行的,一旦某个处理函数耗时较长,就会阻塞其他设备的事件响应。



        // 检查哪个设备有事件
        if (FD_ISSET(uart_fd, &readfds)) {
            // 处理UART数据接收
            handle_uart_data(uart_fd);
        }
        if (FD_ISSET(gpio1_fd, &readfds)) {
            // 处理按键1输入
            handle_gpio1_input(gpio1_fd);
        }
        // 其他设备事件处理...

优化方案一:可给设备设置非阻塞模式,并在处理时增加超时控制,避免单次 
read
/
write
 阻塞过久。



//给文件描述符设置非阻塞属性:
// 为设备设置非阻塞模式
void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK); // 添加非阻塞标志
}
 
// 初始化时调用
set_nonblocking(uart_fd);
set_nonblocking(gpio1_fd);
 
 
//在处理函数中控制单次操作时长:
void handle_uart_data(int fd) {
    unsigned char buf[1024];
    // 非阻塞read:没有数据时立即返回-1,不会阻塞
    int len = read(fd, buf, sizeof(buf));
    if (len > 0) {
        // 处理数据(即使耗时,也限制单次处理的最大长度)
        process_partial_data(buf, min(len, 32)); // 每次最多处理32字节
    }
}

优化方案二:使用线程池。如果设备较多,为每个设备创建独立线程会浪费资源,可改用线程池(固定数量的工作线程共享处理所有任务)。



// 线程池初始化(如创建4个工作线程)
thread_pool_t *pool = thread_pool_create(4);
 
// 主线程分发任务
if (FD_ISSET(uart_fd, &readfds)) {
    // 将UART处理函数作为任务提交到线程池
    thread_pool_add_task(pool, handle_uart_data, uart_fd);
}
 
if (FD_ISSET(gpio1_fd, &readfds)) {
    // GPIO1任务同理
    thread_pool_add_task(pool, handle_gpio1_input, gpio1_fd);
}

关于线程池:关于线程池-CSDN博客


2. poll

1. 函数原型



int poll(struct pollfd *fds, nfds_t nfds, int timeout);
 
## 参数:
1. fds:监听事件结构体数组。这个结构体包含三个属性:1.1、fd: 监听文件描述符。
                                  1.2、events:监听事件集合,用于注册监听事件。
                                  1.3、revents:返回事件集合,用于存储返回事件,后续判断哪个事件发生
2. nfds:监听事件结构体数组长度。
3. timeout:等于-1:一直阻塞。等于0:立即返回。大于0:等待超时时间,单位毫秒。
 

2. 判断监听事件是否发生



 for (int i = 0; i < dev_count; i++) {
        if (fds[i].pfd.revents & POLLIN) {
           switch (fds[i].type) {
                    case DEV_TYPE_UART:
                        handle_modbus_uart(&fds[i]);
                        break;
                    case DEV_TYPE_I2C_SENSOR:
                        handle_i2c_sensors(&fds[i]);
                        break;
                    case DEV_TYPE_ETHERNET:
                        handle_ethernet(&fds[i]);
                        break;
                }
            }
 
其中主要为:
if (fds[i].pfd.revents & POLLIN) 
 
 fds:自定义的设备数组,每个元素代表一个被监控的设备(如 UART、传感器等)。
 fds[i]:数组中的第 i 个设备。
 fds[i].pfd:设备结构体中包含的 struct pollfd 成员(用于 poll 函数监控)。
1. fds[i].pfd.revents:poll 函数返回后,内核填充的 “实际发生的事件”(输出参数)。
 
2. POLLIN:宏定义的事件标志(值为 0x0001),表示 “有数据可读”。

举例:

场景描述:一个基于 ARM Cortex-A7 的工业数据采集网关,需同时管理:

16 个 RS485 串口(连接 Modbus RTU 设备:变频器、PLC 等,波特率参差不齐,1200~115200)4 个 I2C 总线(每条总线挂接 8 个温湿度传感器 SHT30,共 32 个,支持地址复用)1 个以太网接口(接收远程监控中心的查询指令,转发采集到的数据)



#include <poll.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
 
#define MAX_DEVICES 49  // 16串口 + 32 I2C传感器 + 1以太网
#define ETHERNET_DEV "/dev/eth0"
 
// 设备类型枚举
typedef enum {
    DEV_TYPE_UART,
    DEV_TYPE_I2C_SENSOR,
    DEV_TYPE_ETHERNET
} DeviceType;
 
// 扩展pollfd结构,增加设备类型信息
typedef struct {
    struct pollfd pfd;
    DeviceType type;
    int index;  // 设备编号(如串口0~15,I2C传感器0~31)
} ExtendedPollFd;
 
// 处理Modbus串口数据
void handle_modbus_uart(ExtendedPollFd *dev) {
}
 
// 处理I2C传感器数据
void handle_i2c_sensors(ExtendedPollFd *dev) {
}
 
// 处理以太网指令
void handle_ethernet(ExtendedPollFd *dev) {
}
 
int main() {
    ExtendedPollFd fds[MAX_DEVICES];
    int dev_count = 0;
 
    // 初始化16个RS485串口
    for (int i = 0; i < 16; i++) {
        char path[32];
        snprintf(path, sizeof(path), "/dev/ttyUSB%d", i);
        fds[dev_count].pfd.fd = open(path, O_RDWR | O_NONBLOCK);
        fds[dev_count].pfd.events = POLLIN | POLLPRI;  // 监控可读和紧急数据
        fds[dev_count].type = DEV_TYPE_UART;
        fds[dev_count].index = i;
        dev_count++;
    }
 
    // 初始化32个I2C传感器(4条总线,每条8个)
    for (int i = 0; i < 32; i++) {
        char path[32];
        snprintf(path, sizeof(path), "/dev/i2c-%d", i / 8);  // 0~7对应i2c-0,以此类推
        fds[dev_count].pfd.fd = open(path, O_RDONLY | O_NONBLOCK);
        fds[dev_count].pfd.events = POLLIN;
        fds[dev_count].type = DEV_TYPE_I2C_SENSOR;
        fds[dev_count].index = i;
        dev_count++;
    }
 
    // 初始化以太网接口
 
 
    while (1) {
        // 等待事件,超时200ms(兼顾响应速度和CPU占用)
        int ret = poll((struct pollfd*)fds, dev_count, 200);
        if (ret < 0) {
            perror("poll错误");
            break;
        } else if (ret == 0) {
            // 超时处理:批量上传缓存数据到云端
            upload_cached_data();
            continue;
        }
 
        // 遍历所有设备,处理就绪事件
        for (int i = 0; i < dev_count; i++) {
            if (fds[i].pfd.revents & POLLIN) {
                switch (fds[i].type) {
                    case DEV_TYPE_UART:
                        handle_modbus_uart(&fds[i]);
                        break;
                    case DEV_TYPE_I2C_SENSOR:
                        handle_i2c_sensors(&fds[i]);
                        break;
                    case DEV_TYPE_ETHERNET:
                        handle_ethernet(&fds[i]);
                        break;
                }
            }
            // 处理错误事件(如设备断开)
            if (fds[i].pfd.revents & (POLLERR | POLLHUP)) {
                printf("设备%d错误,尝试重连...
", fds[i].index);
                reconnect_device(&fds[i]);
            }
        }
    }
 
    // 关闭所有设备
    for (int i = 0; i < dev_count; i++) {
        close(fds[i].pfd.fd);
    }
    return 0;
}

3. epoll

epoll 核心功能函数

epoll 的使用主要依赖 3 个核心函数,按执行顺序排列:


epoll_create(int size)

作用:创建一个 epoll 实例(句柄),用于管理需要监控的文件描述符(FD)。参数:
size
是早期版本的最大监控数量,现代 Linux 已忽略,但需传入正数。返回:epoll 实例的文件描述符(
epfd
),后续操作都基于此句柄。


epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

作用:向 epoll 实例添加 / 修改 / 删除需要监控的 FD,并设置监控的事件类型。参数:

epfd
:epoll 实例句柄(
epoll_create
的返回值);
op
:操作类型(
EPOLL_CTL_ADD
添加、
EPOLL_CTL_MOD
修改、
EPOLL_CTL_DEL
删除);
fd
:需要监控的文件描述符(如 UART 的 fd、GPIO 的 fd);
event
:结构体,指定监控的事件(如
EPOLLIN
表示 “可读”,
EPOLLOUT
表示 “可写”)。


epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

作用:阻塞等待 epoll 实例中监控的 FD 发生事件,返回就绪的事件列表。参数:

epfd
:epoll 实例句柄;
events
:输出参数,用于存放就绪的事件;

maxevents
:最多能处理的就绪事件数量(需≤
events
数组大小);
timeout
:超时时间(毫秒,-1 表示永久阻塞,0 表示立即返回)。 返回:就绪的事件数量(>0),0 表示超时,-1 表示出错。



    // 4. 循环等待事件并处理
    struct epoll_event events[MAX_EVENTS]; // 存放就绪事件的数组
    while (1) {
        // 阻塞等待事件(超时时间-1表示永久阻塞)
        int num_events = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (num_events == -1) {
            perror("epoll_wait failed");
            break;
        }
 
        // 遍历所有就绪事件并处理
        for (int i = 0; i < num_events; i++) {
            int fd = events[i].data.fd; // 从事件中获取触发的FD
 
            // 根据FD判断是哪个设备,并调用对应处理函数
            if (fd == uart_fd) {
                handle_uart(uart_fd);
            } else if (fd == gpio_fd) {
                handle_gpio(gpio_fd);
            } else if (fd == socket_fd) {
                handle_socket(socket_fd);
            }
        }
    }

© 版权声明

相关文章

暂无评论

none
暂无评论...