简介
epoll
是 Linux 系统中的一种高效的 I/O 事件通知机制,用于高效地处理大量并发网络连接。它优于传统的 select
或 poll
方法,特别适用于高并发场景。
epoll 的工作方式
最小案例伪代码
python
import select
import socket
# 创建一个 epoll 对象
epoll = select.epoll()
# 假设 fds 是一个文件描述符列表
fds = [socket1, socket2, ...] # socket1, socket2 等是已创建的套接字
# 将文件描述符注册到 epoll 实例上
for fd in fds:
epoll.register(fd, select.EPOLLIN | select.EPOLLOUT)
# 等待事件发生
events = epoll.poll(timeout=10) # 阻塞等待直到事件发生或超时
# 处理发生的事件
for fileno, event in events:
if event & select.EPOLLIN:
# 文件描述符可读
data = fileno.recv(1024)
print("Received data:", data)
elif event & select.EPOLLOUT:
# 文件描述符可写
fileno.send(b"Some data")
# 完成后,注销文件描述符并关闭 epoll 实例
for fd in fds:
epoll.unregister(fd)
epoll.close()
- 注册文件描述符 :
epoll.register
将多个文件描述符(如网络套接字)注册到epoll
实例。 - 等待事件 :调用
epoll_wait
函数等待这些文件描述符上的事件。 - 事件通知 :当注册的文件描述符发生事件时,
epoll
机制会唤醒epoll_wait
调用。 - 处理事件:程序处理每个文件描述符上的事件。
epoll 的高性能关键
简易背诵版本
- 边缘触发(ET, Edge Triggered)和水平触发(LT, Level Triggered)模式:支持两种事件通知模式。
- 有效事件列表:只返回有事件发生的文件描述符。
- 内核和用户空间之间的高效数据交换:减少数据拷贝,提高效率。
- 只通知一次机制 :在每次
epoll_wait
调用之间,文件描述符就绪后只会通知一次。
详细版本
- 边缘触发(ET, Edge Triggered)和水平触发(LT, Level Triggered)模式 :
epoll
支持两种事件通知模式。在边缘触发模式下,epoll
仅在文件描述符状态发生变化时通知一次,这意味着如果你没有处理完所有数据,你需要记住继续检查这个文件描述符。在水平触发模式下,只要文件描述符处于可读写状态,epoll
就会不断通知。默认情况下,epoll
使用的是水平触发模式,但是边缘触发模式更适用于高性能服务器,因为它减少了不必要的事件通知。 - 有效事件列表 :当文件描述符的状态改变(比如变得可读或可写),
epoll
会将这个文件描述符添加到一个内部的"就绪"列表中。当你调用epoll_wait
时,它仅返回这个列表中的文件描述符,而不是你监视的所有文件描述符。这意味着epoll_wait
返回的每个文件描述符都是有事件发生的,不需要遍历整个监视集合来找出哪些文件描述符是活跃的。 - 内核和用户空间之间的高效数据交换 :
epoll
使用一种有效的机制来减少内核和用户空间之间的数据拷贝。当你向epoll
实例注册文件描述符时,epoll
只存储指向这些文件描述符的指针,而不是整个文件描述符。这减少了数据的移动,提高了效率。 - 只通知一次 :与
select
和poll
不同,epoll
在文件描述符就绪后只会通知一次,除非再次有新的活动发生。这意味着在每次epoll_wait
调用之间,你不会收到关于同一个文件描述符的多余通知。
epoll 的应用场景
- 网络编程:如 HTTP 服务器、数据库服务器等。
- 高性能服务器:处理大量并发客户端连接。
- 实时通信系统:如实时消息传递、游戏服务器等。
- 异步 I/O 处理:支持异步 I/O。
epoll 在 Android 中的应用
在 Android 的 EventHub.cpp
中,epoll
用于高效地处理来自各种设备的输入事件,如触摸、按键等。
epoll 的限制
- 适用性:主要用于处理可能会阻塞的文件描述符,如网络套接字或管道。
- 普通文件的监控:不适用于监控普通文件的内容变化。
epoll 用于 Socket 编程的示例
epoll_demo.cpp
cpp
#include <sys/epoll.h>
#include <netinet/in.h>
#include <unistd.h>
#include <iostream>
#include <cstring>
const int MAX_EVENTS = 10;
const int PORT = 8080;
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
// 创建套接字文件描述符
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == 0) {
std::cerr << "socket failed" << std::endl;
return -1;
}
// 绑定套接字到端口 8080
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
std::cerr << "bind failed" << std::endl;
return -1;
}
if (listen(server_fd, 3) < 0) {
std::cerr << "listen failed" << std::endl;
return -1;
}
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
std::cerr << "Failed to create epoll file descriptor" << std::endl;
return 1;
}
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = server_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event)) {
std::cerr << "Failed to add file descriptor to epoll" << std::endl;
return -1;
}
struct epoll_event events[MAX_EVENTS];
while (true) {
int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < num_events; i++) {
if (events[i].data.fd == server_fd) {
new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
if (new_socket < 0) {
std::cerr << "accept failed" << std::endl;
continue;
}
struct epoll_event new_event;
new_event.events = EPOLLIN;
new_event.data.fd = new_socket;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &new_event)) {
std::cerr << "Failed to add new socket to epoll" << std::endl;
continue;
}
} else {
// 处理接收到的数据
char buffer[1024] = {0};
ssize_t count = read(events[i].data.fd, buffer, sizeof(buffer));
if (count == -1) {
std::cerr << "read error" << std::endl;
continue;
} else if (count == 0) {
// 客户端关闭连接
close(events[i].data.fd);
continue;
}
std::cout << "Received data: " << buffer << std::endl;
}
}
}
close(server_fd);
return 0;
}
注:这个程序创建了一个简单的 TCP 服务器,监听端口 8080,并使用 epoll
来处理接入的连接和读取数据。
运行步骤
- g++ epoll_demo.cpp -o epoll_demo
- ./epoll_demo
测试脚本
test.py
python
import socket
import time
HOST = '127.0.0.1' # 服务器地址
PORT = 8080 # 服务器监听的端口
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
while True:
s.sendall(b'Hello, world')
time.sleep(5)
测试脚本运行
python3 test.py