epoll并发编程

发布时间:2023年12月30日

epoll并发编程背景知识

epoll 是 Linux 中用于处理大量文件描述符的 I/O 事件通知机制。在传统的 I/O 模型中,一般使用 select 或 poll 来进行多路复用,但随着连接数的增加,它们的性能开始下降。而 epoll 的设计旨在解决这个问题,提高大规模并发的网络应用性能。

epoll 的特点

高性能: epoll 可以有效处理大量的连接,且随着连接数增加,性能基本保持稳定。
事件驱动: epoll 是事件驱动的,只有当有事件发生时才会通知应用程序,而不是轮询所有文件描述符。
支持水平触发和边缘触发: epoll 支持两种触发模式。在水平触发模式下,只要有数据可读或可写,就会通知应用程序;而在边缘触发模式下,只在状态发生变化时通知,需要手动清除事件。

epoll 的工作原理

创建 epoll 句柄: 应用程序通过 epoll_create 创建一个 epoll 句柄。
添加文件描述符: 使用 epoll_ctl 将文件描述符添加到 epoll 句柄中,同时指定关注的事件类型(读、写、异常等)。
等待事件发生: 应用程序使用 epoll_wait 等待事件发生。当文件描述符上发生关注的事件时,epoll_wait 将返回,同时告诉应用程序是哪些文件描述符发生了事件。
处理事件: 应用程序得知有事件发生后,可以执行相应的操作,如读取数据、发送数据等。

epoll 和 select/poll 的区别

效率: epoll 的效率更高,因为它不需要轮询所有文件描述符,而是在事件发生时通知应用程序。
连接数: epoll 能够处理大规模的并发连接,而 select 和 poll 的性能随着连接数的增加而下降。
触发模式: epoll 支持水平触发和边缘触发,而 select 和 poll 只支持水平触发。
文件描述符管理: epoll 使用一组文件描述符来管理事件,而 select 和 poll 使用单一的文件描述符集合。

epoll并发服务器思路

初始化: 创建一个监听套接字,该套接字用于接受客户端的连接请求。同时,你需要创建一个 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) {
    // 处理读写事件,例如接收数据、发送数据等
    // 可以根据业务需要在这里进行具体的操作
}

git地址

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++网络编程:构建高效且灵活的网络系统》.

文章来源:https://blog.csdn.net/changbaishannefu/article/details/135305919
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。