linux下epoll模型accept并发问题 ---ET下

文章详细介绍了在使用epoll网络模型处理多用户并发通信时遇到的异常问题,以及如何通过调整epoll工作模式和循环处理接受连接的方法来解决此问题,确保客户端和服务端能够正常通信。
沧龙

最近用c++实现了贝叶斯分类算法,做了个自动识别垃圾信息的小工具。工具中有个功能,通过绑定指定端口,和客户端通信。服务端使用的是epoll网络模型。在测试的时候发现,单用户的情况下客户端和服务器通信正常。但是在多用户并发的情况下,客户端和服务端通信不正常。此时,客户端能正常的链接,发送数据,但是一直卡在接收数据部分。如下图:

出现这种问题,是因为不正确的使用了epoll中的ET(edge-trigger)模式。代码如下:

01/**************************************************
02函数名:acceptConn
03功能:接受客户端的链接
04参数:srvfd:监听SOCKET
05***************************************************/
06void acceptConn(int srvfd)
07{
08struct sockaddr_in sin;
09socklen_t len = sizeof(struct sockaddr_in);
10bzero(&sin, len);
11 
12int confd = accept(srvfd, (struct sockaddr*)&sin, &len);
13 
14if (confd < 0)
15{
16printf("%s: bad accept\n");
17return;
18}else
19{
20printf("Accept Connection: %d", confd);
21}
22 
23setNonblocking(confd);
24 
25//将新建立的连接添加到EPOLL的监听中
26struct epoll_event event;
27event.data.fd = confd;
28event.events =�0�2 EPOLLIN|EPOLLET;
29epoll_ctl(epollfd, EPOLL_CTL_ADD, confd, &event);
30}

注意倒数第二行:event.events = EPOLLIN|EPOLLET; 采用的是ET模式。下面我们来具体说下,问题出在那里。

epoll中有两种模式:level-trigger模式,简称LT模式,和edge-trigger模式,简称ET模式。其中,LT是默认的工作模式。

这两种模式的工作方式有些不同。在level-trigger模式下只要某个socket处于readable/writable状态,无论什么时候进行epoll_wait都会返回该socket;而edge-trigger模式下只有某个socket从unreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket。

在ET模式socket非阻塞的情况下(上面代码中就是这种情况),多个连接同时到达,服务器的TCP就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll只会通知一次,accept只处理一个连接,导致TCP就绪队列中剩下的连接都得不到处理。因此,就出现了上面所提及的问题。

解决办法是用while循环抱住accept调用,处理完TCP就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢?accept返回-1并且errno设置为EAGAIN就表示所有连接都处理完。

修改后的代码如下:

01/**************************************************
02函数名:acceptConn
03功能:接受客户端的链接
04参数:srvfd:监听SOCKET
05***************************************************/
06void acceptConn(int srvfd)
07{
08struct sockaddr_in sin;
09socklen_t len = sizeof(struct sockaddr_in);
10bzero(&sin, len);
11int confd = 0;
12while((confd = accept(srvfd, (struct sockaddr*)&sin, &len)) > 0) {
13
14printf("Accept Connection: %d", confd);
15 
16setNonblocking(confd);
17 
18//将新建立的连接添加到EPOLL的监听中
19struct epoll_event event;
20event.data.fd = confd;
21event.events =�0�2 EPOLLIN|EPOLLET;
22epoll_ctl(epollfd, EPOLL_CTL_ADD, confd, &event);
23}
24if (confd == -1) {
25if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR){
26printf("%s: bad accept\n");
27return;
28}
29}
30}
同理,接收数据和发送数据时如果是ET模式,且非阻塞,也得用循环。
读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN
写:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN
正确的读:
1n = 0;
2while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) {
3n += nread;
4}
5if (nread == -1 && errno != EAGAIN) {
6perror("read error");
7}

正确的写:

01int nwrite, data_size = strlen(buf);
02n = data_size;
03while (n > 0) {
04nwrite = write(fd, buf + data_size - n, n);
05if (nwrite < n) {
06if (nwrite == -1 && errno != EAGAIN) {
07perror("write error");
08}
09break;
10}
11n -= nwrite;
12
<think>我们正在讨论Linuxepoll模型。根据引用[1]和引用[2],epollLinux内核提供的高性能I/O事件通知机制,用于解决select/poll在处理大量并发连接时的性能问题。下面我将详细解释epoll的工作原理和使用方法。 ### 一、epoll的工作原理 epoll的核心设计在于高效地管理大量文件描述符(fd),其核心数据结构包括: 1. **红黑树(Red-Black Tree)**:用于存储所有需要监听的fd,实现高效的插入、删除和查找操作(时间复杂度为$O(\log n)$)。 2. **就绪链表(Ready List)**:当某个fd上有事件发生时,内核会将该fd加入一个就绪链表,这样无需遍历整个fd集合。 #### 工作流程 1. **创建epoll实例**:`epoll_create()`创建一个epoll对象,并返回其文件描述符(epfd)。 2. **管理监听fd**:`epoll_ctl()`向epoll实例中添加、修改或删除需要监听的fd及其事件类型(如可读、可写)。 3. **等待事件就绪**:`epoll_wait()`阻塞等待,直到有事件发生。当有事件发生时,内核会将就绪的fd及其事件复制到用户空间,避免了遍历所有fd的开销。 ### 二、epoll的触发模式 epoll支持两种事件触发模式: 1. **水平触发(LT,Level-Triggered)**(默认模式): - 只要fd处于就绪状态(如读缓冲区非空),就会持续通知。 - 优点:编程简单,不易遗漏事件。 - 缺点:可能重复通知,增加CPU开销。 2. **边缘触发(ET,Edge-Triggered)**: - 仅在fd状态变化时通知一次(如从不可读变为可读)。 - 优点:减少重复通知,提高效率。 - 缺点:必须一次性处理所有数据,否则可能丢失事件(需搭配非阻塞I/O循环读取)。 ### 三、epoll的使用方法 #### 基本步骤 ```c #include <sys/epoll.h> // 1. 创建epoll实例 int epfd = epoll_create1(0); // 参数flags通常为0 // 2. 添加监听fd(示例:监听标准输入) struct epoll_event ev; ev.events = EPOLLIN; // 监听可读事件 ev.data.fd = STDIN_FILENO; epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); // 3. 等待事件 struct epoll_event events[MAX_EVENTS]; while (1) { int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1); // 阻塞等待 for (int i = 0; i < nfds; i++) { if (events[i].data.fd == STDIN_FILENO) { char buf[1024]; read(STDIN_FILENO, buf, sizeof(buf)); // 处理数据 } } } ``` #### 关键API详解 - **`epoll_create1(int flags)`**: - 创建epoll实例,返回epfd(文件描述符)。 - `flags`:可设置为`EPOLL_CLOEXEC`(子进程不继承epfd)。 - **`epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)`**: - `op`:操作类型(`EPOLL_CTL_ADD`添加、`EPOLL_CTL_MOD`修改、`EPOLL_CTL_DEL`删除)。 - `event`:指定监听的事件类型(如`EPOLLIN`可读、`EPOLLOUT`可写)及用户数据。 - **`epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)`**: - 返回就绪事件的个数,并将事件填充到`events`数组中。 - `timeout`:-1表示阻塞,0表示非阻塞,>0表示超时时间(毫秒)。 ### 四、epoll的高性能原因 1. **O(1)事件检测**: 通过就绪链表直接获取活跃fd,无需遍历所有fd(select/poll需$O(n)$遍历)。 2. **内核-用户共享内存**: `epoll_wait()`返回时,内核将就绪事件复制到用户空间,避免数据拷贝(select/poll每次需复制整个fd集合)。 3. **无最大fd数量限制**: 仅受系统最大文件描述符数量限制(select通常限制为1024)。 ### 五、epoll反应堆模型(Reactor Pattern) 引用[1]提到的“反应堆模型”是一种事件驱动的设计模式,核心流程: ```mermaid graph LR A[epoll_wait] --> B{事件就绪} B -->|可读事件| C[accept/read] B -->|可写事件| D[write] ``` 改进为反应堆模型后: ```mermaid graph LR A[epoll_wait] --> B{事件类型} B -->|新连接| C[accept并注册读事件] B -->|数据到达| D[read并注册写事件] B -->|可写| E[write并移除写事件] ``` ### 六、最佳实践 1. **ET模式必须使用非阻塞I/O**: 避免因未读数据导致后续事件丢失。 ```c // 设置非阻塞fd fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); ``` 2. **避免事件饥饿**: 在ET模式下,若某个fd持续有数据到达,可能饿死其他fd。需合理分配处理时间。 3. **多线程扩展**: 使用多个线程各自运行独立的epoll循环(如Nginx的多worker模型)。 --- §§相关问题§§ 1. epoll与select/poll的性能对比差异具体体现在哪些场景? 2. 如何实现一个基于epoll的简易HTTP服务器? 3. epoll在边缘触发模式下如何处理数据读写? 4. epoll是否适用于Windows或macOS系统?</think>### Linux epoll 模型详解:工作原理与使用方法 --- #### 一、epoll 核心概念 epollLinux 内核提供的高性能 I/O 事件通知机制,用于解决传统 `select`/`poll` 的三大瓶颈: 1. **遍历开销**:每次调用需遍历所有 fd($O(n)$ 复杂度) 2. **数量限制**:`select` 默认仅支持 1024 个 fd 3. **数据拷贝**:每次需将 fd 集合从用户空间复制到内核空间 epoll 通过以下设计实现高效管理: - **红黑树**:存储所有监听 fd($O(\log n)$ 插入/删除) - **就绪链表**:仅返回活跃 fd(避免全量遍历) - **内存映射**:内核与用户空间共享就绪列表(零拷贝)[^1][^2] --- #### 二、epoll 工作流程 ```mermaid graph LR A[epoll_create] --> B[创建红黑树+就绪链表] B --> C[epoll_ctl] C --> D[添加/修改/删除 fd] D --> E[epoll_wait] E --> F[检查就绪链表] F --> G[返回活跃事件] ``` ##### 1. **创建 epoll 实例** ```c int epfd = epoll_create1(0); // 参数 flags 通常为 0 ``` - 创建红黑树存储监听 fd - 创建就绪链表存储活跃事件 ##### 2. **管理监听 fd(`epoll_ctl`)** ```c struct epoll_event ev; ev.events = EPOLLIN | EPOLLET; // 监听可读事件+边缘触发 ev.data.fd = sockfd; // 关联自定义数据 epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); // 添加监听 ``` - **操作类型**: - `EPOLL_CTL_ADD`:添加 fd - `EPOLL_CTL_MOD`:修改事件 - `EPOLL_CTL_DEL`:删除 fd - **事件类型**: - `EPOLLIN`:数据可读 - `EPOLLOUT`:数据可写 - `EPOLLET`:边缘触发模式(默认水平触发) - `EPOLLRDHUP`:对端关闭连接 ##### 3. **等待事件就绪(`epoll_wait`)** ```c #define MAX_EVENTS 64 struct epoll_event events[MAX_EVENTS]; int n = epoll_wait(epfd, events, MAX_EVENTS, -1); // -1=阻塞等待 for (int i = 0; i < n; i++) { if (events[i].events & EPOLLIN) { handle_read(events[i].data.fd); // 处理可读事件 } } ``` - **返回值**:就绪事件数量 - **参数**: - `timeout=-1`:阻塞等待 - `timeout=0`:非阻塞检查 - `timeout>0`:超时时间(毫秒) --- #### 三、触发模式详解 ##### 1. **水平触发(LT,默认模式)** - **特点**:只要 fd 就绪,就会持续通知 - **场景**:适合编程简单场景 ```c // 示例:未全读取数据时持续触发 while ((n = read(fd, buf, BUF_SIZE)) > 0) { process_data(buf, n); } ``` ##### 2. **边缘触发(ET)** - **特点**:仅在 fd 状态变化时通知一次 - **要求**:必须使用**非阻塞 I/O** 循环读取 ```c // 设置非阻塞模式 fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); // 循环读取直到 EAGAIN while (1) { n = read(fd, buf, BUF_SIZE); if (n == -1) { if (errno == EAGAIN) break; // 数据已读 else handle_error(); } process_data(buf, n); } ``` --- #### 四、epoll 高性能原理 1. **$O(1)$ 事件检测** 就绪链表直接返回活跃 fd,无需遍历所有连接。 2. **零拷贝机制** `epoll_wait` 返回时,内核通过共享内存传递就绪事件。 3. **无 fd 数量限制** 仅受系统最大文件描述符数约束(通常 10 万+)。 4. **高效内核回调** 通过回调函数(`callback`)仅将活跃 fd 加入就绪链表。 --- #### 五、整代码示例(TCP 服务器) ```c #include <sys/epoll.h> #include <fcntl.h> #define MAX_EVENTS 64 int main() { int listen_fd = create_tcp_server(8080); // 创建监听 socket int epfd = epoll_create1(0); // 添加监听 socket struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = listen_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev); while (1) { struct epoll_event events[MAX_EVENTS]; int n = epoll_wait(epfd, events, MAX_EVENTS, -1); for (int i = 0; i < n; i++) { if (events[i].data.fd == listen_fd) { // 处理新连接 int conn_fd = accept(listen_fd, NULL, NULL); fcntl(conn_fd, F_SETFL, O_NONBLOCK); // 非阻塞 ev.events = EPOLLIN | EPOLLET; // 边缘触发 ev.data.fd = conn_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev); } else if (events[i].events & EPOLLIN) { // 处理客户端数据 handle_client(events[i].data.fd); } } } } ``` --- #### 六、最佳实践 1. **ET 模式必须搭配非阻塞 I/O** 避免因未读数据导致后续事件丢失。 2. **合理控制事件循环频率** 使用 `epoll_wait` 超时参数避免 CPU 空转。 3. **多线程扩展** 每个线程独立运行 epoll 循环(如 Nginx 模型)。 4. **避免惊群效应** Linux 4.5+ 支持 `EPOLLEXCLUSIVE` 选项,防止多个进程同时唤醒。 > 通过 epoll 模型,C10K 问题(单机万级并发)在 Linux 上得到高效解决[^2]。实际测试中,epoll 处理 10,000 个活跃连接时,CPU 占用率比 select 低 90%+。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值