水平触发和边缘触发是 epoll
提供的两种事件通知模式,它们在处理文件描述符的 I/O 事件时有不同的行为:
水平触发
- 默认模式:这是
epoll
的默认工作模式,与传统的select
和poll
类似。 - 行为:只要文件描述符上有数据可读、可写或发生错误,
epoll_wait
就会返回该文件描述符。即使你没有处理这些事件,它们也会在后续的epoll_wait
调用中继续返回。 - 优点:编程相对简单,因为你可以逐步处理事件,不必担心错过任何通知。
- 缺点:如果处理不当,可能导致低效,因为每次调用
epoll_wait
都可能返回大量已经通知过的文件描述符。
边缘触发
- 非默认模式:需要在调用
epoll_ctl
时显式指定。 - 行为:只有在文件描述符的状态发生变化时(例如,从不可读变为可读)才会通知。换句话说,事件只会在状态变化的瞬间被触发。
- 优点:减少了不必要的通知次数,适合高性能应用,因为它减少了系统调用的次数。
- 缺点:编程复杂度较高,因为你需要确保在每次事件触发时尽可能多地处理数据(例如,循环读取直到没有数据可读),否则可能会错过后续的数据到达。
使用场景
- 水平触发适合于简单的应用程序或者不需要极致性能优化的场景,因为它的编程模型相对简单。
- 边缘触发适合于需要高性能和低延迟的应用程序,比如高并发的网络服务器,但要求开发人员更加小心地处理 I/O 操作,以避免错过事件。
水平触发示例
#include <iostream>
#include <sys/epoll.h>
#include <unistd.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <cstring>
#define MAX_EVENTS 10 // epoll 实例中最大事件数
#define PORT 8080 // 服务器监听端口
int main() {
int server_fd, new_socket, epoll_fd;
struct sockaddr_in address;
int addrlen = sizeof(address);
struct epoll_event ev, events[MAX_EVENTS];
// 创建服务器 socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置地址和端口
address.sin_family = AF_INET; // 使用 IPv4
address.sin_addr.s_addr = INADDR_ANY; // 监听所有接口
address.sin_port = htons(PORT); // 设置端口
// 绑定 socket
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, 3) < 0) {
perror("listen");
close(server_fd);
exit(EXIT_FAILURE);
}
// 创建 epoll 实例
epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
close(server_fd);
exit(EXIT_FAILURE);
}
// 添加服务器 socket 到 epoll 实例中
ev.events = EPOLLIN; // 水平触发为默认模式
ev.data.fd = server_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) == -1) {
perror("epoll_ctl: server_fd");
close(server_fd);
exit(EXIT_FAILURE);
}
// 事件循环
while (true) {
// 等待事件发生
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
break;
}
// 处理每个事件
for (int n = 0; n < nfds; ++n) {
if (events[n].data.fd == server_fd) {
// 接受新的连接
new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen);
if (new_socket == -1) {
perror("accept");
continue;
}
// 将新连接添加到 epoll 实例中
ev.events = EPOLLIN;
ev.data.fd = new_socket;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &ev) == -1) {
perror("epoll_ctl: new_socket");
close(new_socket);
continue;
}
} else {
// 处理客户端数据
char buffer[1024];
int bytes_read = read(events[n].data.fd, buffer, sizeof(buffer));
if (bytes_read <= 0) {
// 如果读取失败或连接关闭,关闭文件描述符
close(events[n].data.fd);
} else {
buffer[bytes_read] = '\0'; // 确保字符串以 null 结尾
std::cout << "Received: " << buffer << std::endl;
}
}
}
}
close(server_fd);
return 0;
}
边缘触发示例
要使用边缘触发模式,你需要在设置事件时添加 EPOLLET
标志,并确保在处理事件时读取所有数据。以下是如何修改上述代码以使用边缘触发模式的示例:
// 在添加 socket 到 epoll 实例时,使用 EPOLLET
ev.events = EPOLLIN | EPOLLET; // 使用边缘触发模式
在处理客户端数据时,需要确保读取所有数据:
while (true) {
char buffer[1024];
int bytes_read = read(events[n].data.fd, buffer, sizeof(buffer));
if (bytes_read <= 0) {
if (bytes_read == -1 && errno == EAGAIN) {
// 所有数据已被读取
break;
}
// 关闭连接
close(events[n].data.fd);
break;
}
buffer[bytes_read] = '\0';
std::cout << "Received: " << buffer << std::endl;
}
注意
- 非阻塞模式:在边缘触发模式下,确保 socket 是非阻塞的,以避免在读取或写入时阻塞。你可以使用
fcntl
函数将 socket 设置为非阻塞。 - 数据读取:在边缘触发模式下,必须在每次事件触发时完全处理所有 I/O 操作,以确保不会遗漏任何数据。