一、epoll 的 LT 与 ET 模式(核心区别与实现)
1.核心定义与本质差异
epoll 支持两种事件触发模式,核心差异在于 "事件通知的时机",直接影响高并发场景下的性能和编程复杂度:
| 触发模式 | 核心特性 | 触发逻辑(以可读事件为例) |
|---|---|---|
| LT(水平触发) | 1. epoll 默认模式,开发简单,无数据漏读风险;2. 支持阻塞 / 非阻塞 socket;3. 适配对性能要求不高、追求开发效率的场景 | 只要 socket 接收缓冲区有未读数据,每次调用epoll_wait都会持续触发 "可读事件",直到数据全部读完;示例:缓冲区收到 10 字节,仅读 5 字节,下次epoll_wait仍会通知 "可读" |
| ET(边缘触发) | 1. 需手动设置EPOLLET标志启用;2. 仅支持非阻塞 socket,编程复杂;3. 高并发、高性能场景首选(如 Nginx 默认模式) |
仅在 socket 状态发生 "突变" 的瞬间触发一次事件;示例:缓冲区从 "空→有数据""满→未满" 时触发一次,后续即使有未读数据,也不再通知,除非有新数据写入 |
2. 关键技术细节
(1)ET 模式的核心要求(避免数据漏读)
- 必须将 socket 设为非阻塞 (防止
recv()/read()阻塞在无数据时); - 事件触发后,需循环读取数据 ,直到
recv()返回EAGAIN或EWOULDBLOCK(表示缓冲区无更多数据),确保一次性读完所有数据。
(2**)模式设置方法(epoll_event 结构体)**
cpp
struct epoll_event ev;
ev.data.fd = conn_fd;
ev.events = EPOLLIN | EPOLLET; // 启用ET模式(默认LT,仅需加EPOLLET)
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);
(3)ET 模式循环读数据示例(非阻塞 socket)
cpp
// 前提:conn_fd已设为非阻塞
void handle_et_read(int conn_fd) {
char buf[1024] = {0};
while (1) {
ssize_t len = recv(conn_fd, buf, sizeof(buf)-1, 0);
if (len > 0) {
// 处理读取到的数据
printf("ET模式读取数据:%s\n", buf);
memset(buf, 0, sizeof(buf));
} else if (len == 0) {
// 客户端关闭连接
printf("客户端断开连接\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, conn_fd, NULL);
close(conn_fd);
break;
} else {
// 区分真错误和"无数据"(EAGAIN/EWOULDBLOCK)
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 缓冲区无更多数据,退出循环
break;
} else {
// 真错误,关闭连接
perror("recv失败");
epoll_ctl(epfd, EPOLL_CTL_DEL, conn_fd, NULL);
close(conn_fd);
break;
}
}
}
}
3**. LT 与 ET 模式对比总结**
| 对比维度 | LT 模式 | ET 模式 |
|---|---|---|
| 通知频率 | 持续通知(只要有未处理事件) | 仅一次通知(状态突变瞬间) |
| socket 要求 | 支持阻塞 / 非阻塞 | 仅支持非阻塞 |
| 编程复杂度 | 低(无需循环读,框架自动提醒) | 高(需手动设非阻塞 + 循环读) |
| 性能 | 较低(无效通知多,系统调用频繁) | 高(减少通知次数,降低系统开销) |
| 数据漏读风险 | 无 | 有(未循环读则漏读,需严格按规范实现) |
| 适用场景 | 中小规模连接、快速开发(如内部工具、简单服务器) | 大规模高并发(如互联网服务器、高吞吐场景) |
二、select/poll/epoll 底层实现与性能对比
1. 底层核心差异
| 特性 | select | poll | epoll(Linux) |
|---|---|---|---|
| 内核存储结构 | 位图(fd_set) | 数组(struct pollfd) | 红黑树(存储监控 FD)+ 就绪链表(存储就绪 FD) |
| FD 拷贝方式 | 每次select()调用都需将fd_set拷贝到内核 |
每次poll()调用都需将pollfd数组拷贝到内核 |
仅epoll_ctl()添加 FD 时拷贝一次,后续无需拷贝 |
| 内核实现方式 | 轮询(遍历所有监控 FD,时间复杂度 O (n)) | 轮询(遍历所有监控 FD,时间复杂度 O (n)) | 事件驱动(注册回调函数,FD 就绪时自动加入就绪链表,时间复杂度 O (1)) |
| 就绪 FD 查找 | 需用户层遍历所有 FD(O (n)) | 需用户层遍历所有监控 FD(O (n)) | 内核直接返回就绪链表,用户层无需遍历(O (1)) |
| 触发模式 | 仅 LT 模式 | 仅 LT 模式 | 支持 LT(默认)和 ET 模式 |
2. 性能结论
- 低并发(FD<1024):三者性能差异不大,select/poll 更易跨平台;
- 高并发(FD>10000):epoll 性能碾压 select/poll,核心优势是 "无 FD 拷贝 + 事件驱动 + O (1) 就绪查找"。
三、同步 / 异步、阻塞 / 非阻塞
一、先明确两个核心维度的定义
同步 / 异步、阻塞 / 非阻塞是操作系统和编程中描述 I/O 操作、任务执行模式的核心概念 ,两者属于不同的维度(同步异步关注任务结果的通知方式 ,阻塞非阻塞关注任务执行时的等待状态),但经常结合使用(如同步阻塞、异步非阻塞)。
关键前提:同步 / 异步与阻塞 / 非阻塞并无直接关联,属于两个完全独立的维度。
1. 阻塞(Blocking)vs 非阻塞(Non-Blocking)
这是描述任务执行时,调用方是否需要等待操作完成的维度,关注的是 **"等待状态"**。
- 阻塞:调用方发起操作后,必须等待操作完全完成才能继续执行后续代码,等待期间进程 / 线程会被挂起(CPU 不分配时间片,资源释放给其他进程 / 线程)。
- 非阻塞:调用方发起操作后,无需等待操作完成,立即返回结果(成功 / 失败 / 未完成),等待期间进程 / 线程可正常执行其他任务,不会被挂起。
2. 同步(Synchronous)vs 异步(Asynchronous)
这是描述操作完成后,结果的通知方式的维度,关注的是 **"结果如何返回"**。
- 同步:调用方发起操作后,需要主动等待 / 查询操作结果(无论是否阻塞),结果的获取由调用方主动发起、主动掌控。
- 异步:调用方发起操作后,无需主动等待 / 查询结果,操作完成后由操作系统 / 框架 / 第三方通过回调、信号、消息通知等方式主动告知调用方结果,结果的传递由被调用方触发、被动接收。
核心口诀:同步 = 主动要结果,异步 = 被动收通知。
二、四个组合场景的实例解析
同步 / 异步与阻塞 / 非阻塞可组合为四种常见的 I/O 模型,通过 **"去食堂打饭"** 的生活实例(类比程序的 I/O 操作,如读取文件、网络通信、数据库查询)来通俗理解,同时搭配对应程序示例。
1. 同步阻塞(最常见的基础模式)
-
生活场景:你走到食堂窗口点饭,然后站在窗口前一直等待打饭阿姨做完饭,期间不做任何其他事情,直到拿到饭才离开窗口,继续后续行程(如回教室、去餐厅)。
-
程序类比 :C 语言的
read()/write()系统调用读取普通文件、accept()阻塞等待客户端连接、recv()阻塞等待网络数据,都是典型的同步阻塞 I/O。cpp// 同步阻塞读文件:调用read后,线程被内核挂起,直到数据读取完成才返回 #include <stdio.h> #include <unistd.h> #include <fcntl.h> int main() { int fd = open("test.txt", O_RDONLY); if (fd == -1) { perror("open失败"); return 1; } char buf[1024] = {0}; // 阻塞调用:数据未就绪时,线程挂起,CPU释放,直到读取完成或出错 ssize_t len = read(fd, buf, sizeof(buf)-1); if (len > 0) { printf("读取完成:%s\n", buf); } else { perror("read失败"); } close(fd); return 0; } -
核心特点:实现简单、开发成本低,无需处理复杂的状态查询和错误判断;但等待期间线程挂起,无法处理其他任务,并发性能差,CPU 资源利用率低,适合简单场景(如小文件读取、低并发服务)。
2. 同步非阻塞(轮询模式)
- 生活场景:你走到食堂窗口点饭,阿姨说 "饭还没好,你先别在这等",你没有站在窗口前原地等待,而是回到座位刷手机、和同学聊天(执行其他任务),每隔 1 分钟主动去窗口问一次 "我的饭好了吗?",直到阿姨告知饭已做好,拿到饭为止。
- 程序类比:将文件描述符(FD)设为非阻塞模式后,循环调用
read()/recv(),直到读取到有效数据,期间可处理其他任务。
cpp
// 同步非阻塞读文件:调用read后立即返回,数据未就绪时循环轮询
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
// 设置文件描述符为非阻塞模式
void setnonblock(int fd) {
int flags = fcntl(fd, F_GETFL);
if (flags == -1) {
perror("fcntl F_GETFL失败");
return;
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl F_SETFL失败");
}
}
int main() {
int fd = open("test.txt", O_RDONLY);
if (fd == -1) {
perror("open失败");
return 1;
}
setnonblock(fd); // 转为非阻塞模式
char buf[1024] = {0};
while (1) {
// 非阻塞调用:数据未就绪时立即返回-1,errno设为EAGAIN/EWOULDBLOCK
ssize_t len = read(fd, buf, sizeof(buf)-1);
if (len > 0) {
printf("读取完成:%s\n", buf);
break;
} else if (len == 0) {
printf("文件读取完毕\n");
break;
} else {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 数据未就绪,执行其他任务(模拟业务处理)
printf("数据未就绪,先处理其他任务...\n");
sleep(1); // 模拟轮询间隔,避免频繁查询占用CPU
} else {
perror("read失败");
break;
}
}
}
close(fd);
return 0;
}
- 核心特点:等待期间线程不挂起,可执行其他任务,CPU 资源利用率较同步阻塞更高;但存在轮询开销(频繁查询操作状态),若轮询间隔过短会占用大量 CPU 资源,间隔过长会导致结果获取延迟,适合中等并发、对延迟要求不高的场景。
3. 异步阻塞(逻辑可行,实际极少使用)
- 生活场景:你让同学帮你去食堂打饭(发起异步操作,由第三方执行),然后你坐在教室座位上一动不动地等待同学回来,期间不刷手机、不学习,什么事情都不做,直到同学把饭送到你手上,才继续后续行动。
- 程序类比:调用 Linux 异步 I/O(AIO)接口
aio_read()后,阻塞调用aio_wait()等待异步操作完成通知,期间线程挂起,无法处理其他任务。 - 核心特点:操作本身是异步的(由内核 / 第三方执行),但调用方仍阻塞等待结果,失去了异步操作 "解放调用方" 的核心优势,既没有同步阻塞的简单性,也没有异步非阻塞的高性能,实际开发中几乎不使用,仅存在于逻辑组合中。
cpp
// libevent实现异步非阻塞读文件:注册事件后无需等待,就绪后回调通知
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <event2/event.h>
// 读事件回调函数(操作完成后,由libevent主动调用,传递结果)
void read_callback(evutil_socket_t fd, short events, void *arg) {
char buf[1024] = {0};
ssize_t len = read(fd, buf, sizeof(buf)-1);
if (len > 0) {
printf("读取完成:%s\n", buf);
} else if (len == 0) {
printf("文件读取完毕\n");
} else {
perror("read失败");
}
// 退出事件循环
struct event_base *base = (struct event_base*)arg;
event_base_loopexit(base, NULL);
}
int main() {
int fd = open("test.txt", O_RDONLY);
if (fd == -1) {
perror("open失败");
return 1;
}
// 1. 创建事件反应堆(自动适配底层epoll/select/poll)
struct event_base *base = event_base_new();
if (base == NULL) {
printf("event_base创建失败\n");
close(fd);
return 1;
}
// 2. 创建非阻塞读事件(注册回调函数,持久化触发)
struct event *read_ev = event_new(base, fd, EV_READ | EV_PERSIST, read_callback, base);
if (read_ev == NULL) {
printf("event创建失败\n");
event_base_free(base);
close(fd);
return 1;
}
// 3. 添加事件到反应堆,无需阻塞等待,直接进入事件循环
event_add(read_ev, NULL);
printf("事件注册完成,进入事件循环(可处理其他任务)...\n");
event_base_dispatch(base); // 事件循环,阻塞等待就绪事件,但不阻塞单个I/O操作
// 4. 释放资源
event_free(read_ev);
event_base_free(base);
close(fd);
return 0;
}
4. 异步非阻塞(高性能 I/O 的核心模式)
- 生活场景:你给食堂阿姨留了手机号,点完饭后直接回教室专心学习(执行其他核心任务),无需等待、无需主动询问,阿姨做好饭后主动给你打电话 / 发消息通知你取饭,你收到通知后再去食堂拿饭,全程不耽误自身核心任务的执行。
- 程序类比:Linux 的
epoll+ 异步 I/O、libevent 事件驱动框架、Node.js 回调机制、Java NIO 的 Reactor 模式,都是典型的异步非阻塞 I/O。
cpp
// libevent实现异步非阻塞读文件:注册事件后无需等待,就绪后回调通知
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <event2/event.h>
// 读事件回调函数(操作完成后,由libevent主动调用,传递结果)
void read_callback(evutil_socket_t fd, short events, void *arg) {
char buf[1024] = {0};
ssize_t len = read(fd, buf, sizeof(buf)-1);
if (len > 0) {
printf("读取完成:%s\n", buf);
} else if (len == 0) {
printf("文件读取完毕\n");
} else {
perror("read失败");
}
// 退出事件循环
struct event_base *base = (struct event_base*)arg;
event_base_loopexit(base, NULL);
}
int main() {
int fd = open("test.txt", O_RDONLY);
if (fd == -1) {
perror("open失败");
return 1;
}
// 1. 创建事件反应堆(自动适配底层epoll/select/poll)
struct event_base *base = event_base_new();
if (base == NULL) {
printf("event_base创建失败\n");
close(fd);
return 1;
}
// 2. 创建非阻塞读事件(注册回调函数,持久化触发)
struct event *read_ev = event_new(base, fd, EV_READ | EV_PERSIST, read_callback, base);
if (read_ev == NULL) {
printf("event创建失败\n");
event_base_free(base);
close(fd);
return 1;
}
// 3. 添加事件到反应堆,无需阻塞等待,直接进入事件循环
event_add(read_ev, NULL);
printf("事件注册完成,进入事件循环(可处理其他任务)...\n");
event_base_dispatch(base); // 事件循环,阻塞等待就绪事件,但不阻塞单个I/O操作
// 4. 释放资源
event_free(read_ev);
event_base_free(base);
close(fd);
return 0;
}
- 核心特点:调用方发起操作后无需等待、无需主动轮询,可全力执行其他核心任务;操作完成后由内核 / 框架主动回调通知结果,无无效等待和轮询开销,CPU 资源利用率最高,是高并发、高吞吐网络编程的首选模式(如 Nginx、Redis、Node.js 的底层 I/O 模型);缺点是开发复杂度高,需要处理回调嵌套、事件调度、资源管理等问题。
三、核心区别与关键对比表
1. 同步 vs 异步(核心:结果获取方式)
| 对比项 | 同步(Synchronous) | 异步(Asynchronous) |
|---|---|---|
| 核心逻辑 | 调用方主动获取 / 查询结果 | 被调用方主动通知 / 回调传递结果 |
| 等待方式 | 主动等待 / 轮询 | 被动接收,无需主动等待 |
| 结果掌控方 | 调用方(主动掌控) | 被调用方(内核 / 框架 / 第三方,被动接收) |
| 开发复杂度 | 低(无需处理回调 / 通知) | 高(需处理回调嵌套、事件调度、异常处理) |
| 并发性能 | 低 - 中(高并发下存在轮询 / 阻塞瓶颈) | 高(无无效等待,充分利用 CPU 资源) |
| 典型场景 | 小文件读取、低并发服务、简单工具类程序 | 高并发 Web 服务器、音视频传输、大数据处理 |
2. 阻塞 vs 非阻塞(核心:等待时的线程状态)
| 对比项 | 阻塞(Blocking) | 非阻塞(Non-Blocking) |
|---|---|---|
| 核心逻辑 | 操作未完成时,线程被挂起(暂停执行) | 操作未完成时,线程持续运行(立即返回) |
| CPU 资源占用 | 等待期间不占用(线程挂起,释放 CPU) | 等待期间可占用(线程运行,执行其他任务) |
| 返回结果时机 | 仅当操作完全完成 / 出错时返回 | 发起操作后立即返回(无论操作是否完成) |
| 错误处理 | 简单(仅处理操作本身的错误) | 复杂(需区分 "真错误" 和 "操作未就绪") |
| 典型接口 | 普通read()/accept()/recv() |
非阻塞read()/recv()、epoll_wait() |
| 适用场景 | 简单场景、对并发无要求的程序 | 中等及以上并发、需要高效利用 CPU 的程序 |
3. 四种组合模型综合对比表
| 组合模型 | 核心特点 | 优点 | 缺点 | 典型适用场景 |
|---|---|---|---|---|
| 同步阻塞 | 主动等结果,等待时线程挂起 | 实现简单、开发成本低、无额外开销 | 并发性能差、CPU 利用率低、无法处理多任务 | 小文件读取、低并发服务、内部工具类程序 |
| 同步非阻塞 | 主动查结果,等待时线程可执行其他任务 | 较同步阻塞并发性能提升、CPU 利用率更高 | 存在轮询开销、延迟不可控、开发复杂度中等 | 中等并发、对延迟要求不高的后台服务 |
| 异步阻塞 | 被动收结果,等待时线程挂起 | 操作异步执行,无需主动轮询 | 失去异步核心优势、并发性能无提升、极少使用 | 几乎无实际应用场景,仅存在逻辑组合中 |
| 异步非阻塞 | 被动收结果,等待时线程可执行其他任务 | 并发性能最高、CPU 利用率最优、无无效等待 | 开发复杂度高、需处理回调 / 事件调度、调试难 | 高并发 Web 服务器、音视频传输、大数据处理 |
4. 维度交叉总结表
| 概念维度 | 核心关注点 | 阻塞(等待时线程挂起) | 非阻塞(等待时线程可执行其他任务) |
|---|---|---|---|
| 同步 | 调用方主动获取结果 | 同步阻塞(食堂窗口原地等饭) | 同步非阻塞(食堂窗口轮询问饭) |
| 异步 | 被调用方主动通知结果 | 异步阻塞(等同学带饭,原地发呆) | 异步非阻塞(等食堂阿姨打电话通知取饭) |
四、常见误区纠正
-
误区一:"同步就是阻塞,异步就是非阻塞"纠正:两者是完全独立的维度,同步可搭配非阻塞(轮询),异步也可搭配阻塞(极少用),不能直接划等号。
-
误区二:"非阻塞一定比阻塞好,异步一定比同步优"纠正:没有绝对的优劣,需结合场景选择。简单场景下(如小文件读取),同步阻塞的实现成本更低,性能差异可忽略;高并发场景下,异步非阻塞才是最优解,避免过度设计。
-
误区三 :"异步非阻塞就是多线程"纠正:异步非阻塞是事件驱动模型,可在单线程中实现高并发(如 Node.js、单线程 + epoll);多线程是另一种并发模式,通过线程切换处理多任务,两者实现逻辑不同,可结合使用(如多线程 + epoll),但并非同一概念。
-
误区四:"非阻塞 I/O 不会等待"纠正:非阻塞 I/O 只是 "不挂起线程的等待",并非 "无需等待操作完成"。同步非阻塞仍需主动轮询等待操作完成,异步非阻塞只是将等待过程交给内核 / 框架,调用方无需感知而已。
五、核心总结
- 两个核心维度:同步 / 异步关注结果如何返回(主动 / 被动),阻塞 / 非阻塞关注等待时线程状态(挂起 / 运行);
- 四种组合模型:同步阻塞(简单)、同步非阻塞(轮询)、异步阻塞(极少用)、异步非阻塞(高性能);
- 选型原则:简单场景选同步阻塞,中等并发选同步非阻塞,高并发高吞吐选异步非阻塞;
- 核心目标:理解不同模型的本质,根据业务场景和性能要求选择合适的 I/O 模型,平衡开发效率和系统性能。
四、errno 与错误处理(系统调用必备)
1. 核心概念
- errno :全局变量(声明于
<errno.h>),用于存储系统调用 / 库函数的错误原因(错误码); - 适用场景 :
open()/read()/epoll_wait()等系统调用执行失败时,会自动设置 errno,程序员通过 errno 判断具体错误类型。
2. 常见错误码与含义
| 错误码宏定义 | 含义说明 | 典型场景 |
|---|---|---|
EAGAIN/EWOULDBLOCK |
非阻塞 I/O 无数据可读写(临时错误,可重试) | ET 模式下recv()无更多数据、非阻塞accept()无新连接 |
EINTR |
系统调用被信号中断(如epoll_wait()被ctrl+c中断) |
select()/epoll_wait()阻塞时收到信号 |
EBADF |
无效的文件描述符(FD 未打开或已关闭) | 用已关闭的 FD 调用recv()/epoll_ctl() |
EINVAL |
参数无效(如epoll_create()传入负数、epoll_wait()maxevents=0) |
函数参数不符合要求 |
3. 错误处理示例
cpp
int fd = open("test.txt", O_RDONLY);
if (fd == -1) {
// 根据errno打印具体错误
if (errno == ENOENT) {
printf("错误:文件不存在\n");
} else if (errno == EACCES) {
printf("错误:权限不足\n");
} else {
perror("open失败"); // perror自动结合errno打印错误信息
}
exit(1);
}
五、libevent 库详解(I/O 复用封装神器)
1. 核心定位
libevent 是一个跨平台、高性能的事件驱动网络库,封装了 select/poll/epoll(自动适配底层系统),实现了 Reactor(反应堆)模式,让开发者无需关注底层 I/O 复用细节,专注于业务逻辑。
2. Reactor 模式(反应堆模式)
- 本质:事件多路分发器,核心是 "事件注册→事件监控→事件触发→回调处理" 的闭环;
- 核心组件 :
event_base:反应堆核心,管理事件循环、I/O 复用机制、定时器等;event:事件对象(可对应 I/O 事件、信号事件、定时器事件);- 回调函数:事件触发时执行的业务逻辑函数。
3. 事件的二进制位编码原理
-
每个事件类型对应一个唯一的二进制位(位掩码),通过 "按位或(|)" 组合多个事件;
-
示例(libevent 事件宏定义):
cpp#define EV_READ 0x01 // 二进制00000001(第0位):可读事件 #define EV_WRITE 0x02 // 二进制00000010(第1位):可写事件 #define EV_SIGNAL 0x04 // 二进制00000100(第2位):信号事件 #define EV_TIMEOUT 0x08 // 二进制00001000(第3位):定时器事件 #define EV_PERSIST 0x10 // 二进制00010000(第4位):持久化事件(触发后不自动删除) -
组合事件:
EV_READ | EV_PERSIST(0x11)表示 "持续监控可读事件"。
4. libevent 核心函数(按使用流程)
| 函数名 | 功能说明 | 示例 | |
|---|---|---|---|
event_base_new() |
创建反应堆核心对象(event_base),自动适配底层 I/O 复用(epoll/select) |
struct event_base *base = event_base_new(); |
|
event_new() |
创建事件对象(支持 I/O、信号、定时器事件) | `struct event *ev = event_new(base, fd, EV_READ | EV_PERSIST, callback, NULL);` |
evtimer_new() |
快捷创建定时器事件(简化event_new,无需手动设EV_TIMEOUT) |
struct event *timer_ev = evtimer_new(base, timeout_cb, NULL); |
|
event_add() |
将事件添加到反应堆,注册到事件循环(可设置超时时间) | struct timeval tv={5,0}; event_add(timer_ev, &tv);(5 秒定时器) |
|
event_base_dispatch() |
启动事件循环(阻塞监控事件),触发后自动调用回调函数,循环直至事件全部删除或主动退出 | event_base_dispatch(base); |
|
event_free() |
释放事件对象内存(包含event_del,自动从反应堆移除事件) |
event_free(ev); |
|
event_base_free() |
释放反应堆核心对象内存 | event_base_free(base); |
5. 回调函数规范
-
固定格式(返回值
void,参数固定 3 个):cppvoid callback(int fd, short events, void *arg) { // fd:事件对应的文件描述符(信号事件为信号值) // events:触发的事件类型(如EV_READ、EV_TIMEOUT) // arg:event_new()传入的自定义参数 }6. 完整示例(libevent 实现多事件监控)
功能:同时监控 "标准输入可读""SIGINT 信号(ctrl+c)""5 秒定时器"
cpp#include <stdio.h> #include <stdlib.h> #include <event2/event.h> #include <signal.h> // 标准输入可读事件回调 void stdin_cb(int fd, short events, void *arg) { char buf[128] = {0}; ssize_t len = read(fd, buf, sizeof(buf)-1); if (len > 0) { buf[len-1] = '\0'; printf("标准输入:%s\n", buf); if (strcmp(buf, "quit") == 0) { // 退出事件循环 event_base_loopexit((struct event_base*)arg, NULL); } } } // SIGINT信号回调(ctrl+c) void sigint_cb(int sig, short events, void *arg) { printf("收到信号SIGINT(%d),准备退出\n", sig); event_base_loopbreak((struct event_base*)arg); } // 定时器回调(5秒触发一次,持久化) void timeout_cb(int fd, short events, void *arg) { printf("定时器触发:5秒到了\n"); } int main() { // 1. 创建反应堆核心 struct event_base *base = event_base_new(); if (base == NULL) { printf("event_base创建失败\n"); return 1; } // 2. 创建标准输入事件(FD=0,可读+持久化) struct event *stdin_ev = event_new(base, 0, EV_READ | EV_PERSIST, stdin_cb, base); event_add(stdin_ev, NULL); // 无超时时间 // 3. 创建SIGINT信号事件(信号2,持久化) struct event *sig_ev = event_new(base, SIGINT, EV_SIGNAL | EV_PERSIST, sigint_cb, base); event_add(sig_ev, NULL); // 4. 创建定时器事件(5秒,持久化) struct event *timer_ev = evtimer_new(base, timeout_cb, NULL); struct timeval tv = {5, 0}; // 5秒 event_add(timer_ev, &tv); // 5. 启动事件循环(阻塞) printf("事件循环启动,按ctrl+c或输入quit退出\n"); event_base_dispatch(base); // 6. 释放资源 event_free(stdin_ev); event_free(sig_ev); event_free(timer_ev); event_base_free(base); return 0; }编译与运行(需安装 libevent 库)
cpp# 安装libevent(Ubuntu) sudo apt-get install libevent-dev # 编译(链接libevent库) gcc libevent_demo.c -o libevent_demo -levent # 运行 ./libevent_demo7. libevent 核心优势
-
跨平台:自动适配 Linux(epoll)、Windows(select)、BSD(kqueue),无需修改代码;
-
简化开发:封装底层 I/O 复用、事件循环、定时器,开发者仅需关注回调逻辑;
一句话来说就是封装了这三个i/o复用的工具,然后我们要使用i/o服用的话直接使用libenevt里面的库函数就可以了,不需要自己去思考用哪个工具合适。
六、关键补充:超时时间的统一理解
- 定义:程序等待事件的最大阻塞时长,类比 "等待闹钟"------ 闹钟响前事件触发则处理,超时无事件则结束等待;
- 适用场景 :
select()/poll()/epoll_wait()/event_add()均支持设置超时时间; - 参数格式 :
- 系统调用:
struct timeval { long tv_sec; long tv_usec; }(秒 + 微秒); - libevent:
event_add()直接传入struct timeval,evtimer_new()简化为直接传时长。
- 系统调用:
- 高性能:底层优先使用 epoll 等高效 I/O 复用机制,性能接近原生实现;
- 支持多事件类型:统一管理 I/O 事件、信号事件、定时器事件,无需单独处理。