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
),I/O 就绪时内核发送信号通知进程,进程在信号处理函数中完成数据读写。
SIGIO
// 注册 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
去读取 / 写入数据)
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;
}
应用优势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 通过统一监控所有外设的就绪状态,确保在任意外设就绪时都能及时处理,且不会因某个外设无响应而阻塞其他设备。例如,使用
可同时监控 UART 和传感器,哪个就绪就先处理哪个。
select
应用优势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
poll
epoll
这里看代码部分的select内容。
select、poll、epoll 区分:
1. 性能瓶颈差异
select:
性能随监控的文件描述符数量增加而线性下降。原因是:① 每次调用需将整个 fd_set 拷贝到内核;② 内核需轮询所有描述符;③ 用户态需遍历整个集合寻找就绪描述符。在描述符数量超过 100 时性能明显下降。
poll:
解决了 select 的描述符上限问题,但性能瓶颈与 select 类似 —— 仍需全量拷贝和轮询,只是通过动态数组避免了固定大小限制。在描述符数量超过 1000 时,CPU 占用率会显著上升。
epoll:
性能几乎不受描述符数量影响。核心优化点:① 描述符仅在添加时拷贝一次到内核;② 内核通过红黑树高效管理描述符,通过回调函数记录就绪事件;③
直接返回就绪列表,无需轮询。在 10 万级描述符场景下仍能保持高效。
epoll_wait()
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)。参数:
是早期版本的最大监控数量,现代 Linux 已忽略,但需传入正数。返回:epoll 实例的文件描述符(
size
),后续操作都基于此句柄。
epfd
epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
作用:向 epoll 实例添加 / 修改 / 删除需要监控的 FD,并设置监控的事件类型。参数:
:epoll 实例句柄(
epfd
的返回值);
epoll_create
:操作类型(
op
添加、
EPOLL_CTL_ADD
修改、
EPOLL_CTL_MOD
删除);
EPOLL_CTL_DEL
:需要监控的文件描述符(如 UART 的 fd、GPIO 的 fd);
fd
:结构体,指定监控的事件(如
event
表示 “可读”,
EPOLLIN
表示 “可写”)。
EPOLLOUT
epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
作用:阻塞等待 epoll 实例中监控的 FD 发生事件,返回就绪的事件列表。参数:
:epoll 实例句柄;
epfd
:输出参数,用于存放就绪的事件;
events
:最多能处理的就绪事件数量(需≤
maxevents
数组大小);
events
:超时时间(毫秒,-1 表示永久阻塞,0 表示立即返回)。 返回:就绪的事件数量(>0),0 表示超时,-1 表示出错。
timeout
// 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);
}
}
}