epoll 是 Linux 中用于处理大量文件描述符的 I/O 事件通知机制。在传统的 I/O 模型中,一般使用 select 或 poll 来进行多路复用,但随着连接数的增加,它们的性能开始下降。而 epoll 的设计旨在解决这个问题,提高大规模并发的网络应用性能。
高性能: epoll 可以有效处理大量的连接,且随着连接数增加,性能基本保持稳定。
事件驱动: epoll 是事件驱动的,只有当有事件发生时才会通知应用程序,而不是轮询所有文件描述符。
支持水平触发和边缘触发: epoll 支持两种触发模式。在水平触发模式下,只要有数据可读或可写,就会通知应用程序;而在边缘触发模式下,只在状态发生变化时通知,需要手动清除事件。
创建 epoll 句柄: 应用程序通过 epoll_create 创建一个 epoll 句柄。
添加文件描述符: 使用 epoll_ctl 将文件描述符添加到 epoll 句柄中,同时指定关注的事件类型(读、写、异常等)。
等待事件发生: 应用程序使用 epoll_wait 等待事件发生。当文件描述符上发生关注的事件时,epoll_wait 将返回,同时告诉应用程序是哪些文件描述符发生了事件。
处理事件: 应用程序得知有事件发生后,可以执行相应的操作,如读取数据、发送数据等。
效率: epoll 的效率更高,因为它不需要轮询所有文件描述符,而是在事件发生时通知应用程序。
连接数: epoll 能够处理大规模的并发连接,而 select 和 poll 的性能随着连接数的增加而下降。
触发模式: epoll 支持水平触发和边缘触发,而 select 和 poll 只支持水平触发。
文件描述符管理: epoll 使用一组文件描述符来管理事件,而 select 和 poll 使用单一的文件描述符集合。
初始化: 创建一个监听套接字,该套接字用于接受客户端的连接请求。同时,你需要创建一个 epoll 实例,用于注册和监听套接字上的事件。
监听套接字设置为非阻塞: 使用 fcntl 函数将监听套接字设置为非阻塞模式,以便能够使用非阻塞 accept。
创建线程池: 初始化一个线程池,线程池的作用是处理具体的连接请求。线程池中的每个线程都可以处理一个独立的连接。
循环处理事件: 在一个主循环中使用 epoll_wait 等待事件的发生。当有新的连接请求到达时,你可以使用线程池中的一个线程来处理该连接。
处理连接: 当新的连接到达时,线程池中的一个线程会处理该连接。这可能涉及到接收、发送数据,或执行其他相关任务。
使用线程池的好处: 线程池可以帮助你充分利用系统的多核心资源,提高并发处理能力。每个线程独立处理一个连接,避免了在单线程中处理所有连接时的性能瓶颈。
注意线程安全: 在处理连接时,需要确保线程安全性。可以使用互斥锁等机制来保护共享资源,以防止多个线程同时访问导致的问题。
这部分没有展示具体的实现代码,而是把一些业务逻辑省略掉了,只展示了epoll和线程池相关的核心代码,具体实现代码见git仓库。
初始化: 创建一个监听套接字,使用 epoll_create 创建一个 epoll 句柄,以及一个线程池。
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
// 设置 listen_sock 为非阻塞
fcntl(listen_sock, F_SETFL, fcntl(listen_sock, F_GETFL, 0) | O_NONBLOCK);
epoll_fd = epoll_create(MAX_EVENTS); // MAX_EVENTS 是事件表的大小
ThreadPool pool(NUM_THREADS); // NUM_THREADS 是线程池的大小
绑定和监听: 将监听套接字绑定到指定地址,并开始监听。
struct sockaddr_in server_addr;
// 设置 server_addr
bind(listen_sock, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(listen_sock, SOMAXCONN);
添加监听套接字到 epoll 事件表中
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 设置边缘触发模式
ev.data.fd = listen_sock;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_sock, &ev);
事件循环: 使用 epoll_wait 等待事件的发生,根据不同的事件类型执行相应的操作。
while (true) {
struct epoll_event events[MAX_EVENTS];
int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < num_events; ++i) {
if (events[i].data.fd == listen_sock) {
// 处理新连接
handle_new_connection(listen_sock);
} else {
// 处理其他事件
pool.enqueue(handle_event, events[i].data.fd);
}
}
}
线程池处理事件: 每个事件都由线程池中的线程来处理,这样可以充分利用多核 CPU 的性能。
void handle_event(int client_sock) {
// 处理读写事件,例如接收数据、发送数据等
// 可以根据业务需要在这里进行具体的操作
}
https://gitee.com/xinquanfu/epoll-chat
Stevens, W. R., Fenner, B., & Rudoff, A. M. .《UNIX网络编程 卷1: 套接字联网API》.
Chen, S. 《Linux多线程服务端编程:使用muduo C++网络库》.
游双. 《Linux高性能服务器编程》.
莫烦周. 《深入理解Linux内核》.
云天励.《Linux性能优化实战》.
Stevens, W. R. 《TCP/IP详解 卷1:协议》.
Richter, J. M. 《C++网络编程:构建高效且灵活的网络系统》.