Linux accept()/epoll_wait()惊群问题与解决方案

本文探讨了TCP服务器中“惊群”现象的成因及其对性能的影响,并提出了多种解决方案,包括使用锁机制、改进子进程与主进程间的工作模式以及利用SO_REUSEPORT特性。

问题的来源:

参考《UNP 第三版》第30章“客户/服务器设计范式”中“30.6 TCP预先派生子进程服务器程序”

// 为便于说明问题,代码已简化
int main(int argc, char **argv)
{
    int listenfd = Tcp_Listen();

    for (int i = 0; i < nchildren; i++){
        if ((pid = fork()) == 0){
            child_main(i, listenfd, addrlen);
        }
    }

    for(;;){
        pause();
    }
}

pid_t child_main(int i, int listenfd, int addrlen)
{
    struct sockaddr clientAddr;
    socklen_t clientAddrLen;
    for(;;)
    {
        int connfd = accept(listenfd, &clientAddr, &clientAddrLen);
        web_child(connfd);
        close(connfd);
    }
}

在如上所示的代码中,主进程创建了用于监听的socket描述符listenfd,每个子进程阻塞调用accept(listenfd, ),等待获取客户端的新建连接connfd,并放入web_child(connfd)中执行。
因为多个子进程被同时阻塞在同一个监听socket上,当有客户端的新建连接接入时,所有被阻塞的子进程都被唤醒,但是只有执行最快的那个子进程调用accept(listenfd, )能够得到客户连接connfd,其它子进程调用accept(listenfd, )只会等到EGAIN。这种多个子进程被唤醒后又无事可做的状态被称为“惊群”,因为每次进程调度的系统开销相对较大,所以对于高并发服务器,频繁的“惊群”必然导致的服务器性能低下。

针对30.6样例中存在的问题,30.7和30.8给出了两种方案分别使用“文件锁”和“线程互斥锁”保护accept()。简单的说,就是所有的子进程在调用accept()之前,先获取一个,只有得到的进程才能继续执行accept()函数,其它进程都被锁阻塞了。并且,通过验证,这种加锁的方式对于解决“惊群”问题是有效的。

for(;;) 
{    
    lock(); // 文件锁或互斥锁    
    int client = accept(...);    
    unlock();    

    if (client < 0) continue;    
    ...  
}  

另外,30.9中还给出了一个“主进程aceept客户连接,再将客户连接描述符传递给子进程处理”的方案。不过,通过实验证明:和相比,进程间传递描述符的效率更低。这算是题外话吧。

其实,如果只考虑Linux系统,在Linux 2.6版本以后,内核内核已经解决了accept()函数的“惊群”问题,大概的处理方式就是,当内核接收到一个客户连接后,只会唤醒等待队列上的第一个进程或线程。所以,如果服务器采用accept阻塞调用方式,在最新的Linux系统上,已经没有“惊群”的问题了。

但是,对于实际工程中常见的服务器程序,大都使用select、poll或epoll机制,此时,服务器不是阻塞在accept,而是阻塞在select、poll或epoll_wait,这种情况下的“惊群”仍然需要考虑。接下来以epoll为例分析:

在早期的Linux版本中,内核对于阻塞在epoll_wait的进程,也是采用全部唤醒的机制,所以存在和accept相似的“惊群”问题。新版本的的解决方案也是只会唤醒等待队列上的第一个进程或线程,所以,新版本Linux 部分的解决了epoll的“惊群”问题。所谓部分的解决,意思就是:对于部分特殊场景,使用epoll机制,已经不存在“惊群”的问题了,但是对于大多数场景,epoll机制仍然存在“惊群”。

1、场景一:epoll_create()在fork子进程之前:

如果epoll_create()调用在fork子进程之前,那么epoll_create()创建的epfd 会被所有子进程继承。接下来,所有子进程阻塞调用epoll_wait(),等待被监控的描述符(包括用于监听客户连接的监听描述符)出现新事件。如果监听描述符发生可读事件,内核将阻塞队列上排在第一位的进程/线程唤醒,被唤醒的进程/线程继续执行accept()函数,得到新建立的客户连接描述符connfd。这种情况下,任何一个子进程被唤醒并执行accept()函数都是没有问题的。

但是,接下来,子进程的工作如果是调用epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);将新建连接的描述符connfd加入到epfd中统一监控的话,因为,当前的epfd是在fork之前创建的,此时系统中只有一个epoll监控文件,即所有子进程共享一个epoll监控文件。任何一个进程(父进程或子进程)向epoll监控文件添加、修改和删除文件描述符时,都会影响到其它进程的epoll_wait。
后续,当connfd描述符上接收到客户端信息时,内核也无法保证每次都是唤醒同一个进程/线程,来处理这个连接描述符connfd上的读写信息(其它进程可能根本就不认识connfd;或者在不同进程中,相同的描述符对应不同的客户端连接),最终导致连接处理错误。(另外,不同的线程处理同一个连接描述符,也会导致发送的信息乱序)

所以,应该避免epoll_create()在fork子进程之前。关于这一点,据说libevent的文档中有专门的描述。

2、场景二:epoll_create()在fork子进程之后:

如果epoll_create()在fork子进程之后,则每个进程都有自己的epoll监控文件(当某个进程将新建连接的描述符connfd加入到本进程的epfd中统一监控,不会影响其它进程的epoll_wait),但是为了实现并发监听,所有的子进程都会调用
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
监听描述符加入到监控文件中,也就是说所有子进程都在通过epoll机制轮询同一个监听描述符。如果有新的客户端请求接入,监听描述符出现POLLIN事件(表示描述符可读,有新连接接入),此时内核会唤醒所有的进程,所以“惊群”的问题依然存在。

对于这种情况下的“惊群”问题,Nginx的解决方案和《UNP 第三版》第30章中30.7和30.8给出的加锁方案类似,大概就是通过互斥锁对每个进程从epoll_wait到accept之间的处理通过互斥量保护。需要注意的是,对于这种加锁操作,每次只有一个子进程能执行epoll_wait和accept,具体哪个进程得到执行,要看内核调度。所以,为了解决负载均衡的问题,Nginx的解决方案中,每个进程有一个当前连接计数,如果当前连接计数超过最大连接的7/8,该进程就停止接收新的连接。

lock()    
epoll_wait(...);   
accept(...);    
unlock(...);   

另外,我在想,如果不考虑多进程,而是用多线程实现,因为,线程调度的开销比进程要小很多,那么在多线程下,是否就可以不用考虑惊群的问题,当然这个结论需要具体的测试数据,后面有空准备测试一下。

3、利用SO_REUSEPORT解决epoll的惊群问题

网上这方面的内容也非常多,大家可以自行搜索。因为这是利用内核新版本新特性的解决方案,应该算是终极解决方案吧。
利用SO_REUSEPORT解决epoll的惊群问题

. ├── bin │ ├── common.o │ ├── epoll_serv │ ├── epoll_serv.o │ ├── process_serv │ ├── process_serv.o │ ├── pthread_serv │ └── pthread_serv.o ├── include │ └── common.h ├── Makefile ├── src │ ├── common.c │ ├── epoll_serv.c │ ├── process_serv.c │ └── pthread_serv.c └── web ├── About.html ├── Contact.html ├── css │ ├── reset.css │ ├── style.css │ └── style_mobile.css ├── data │ └── contact.json ├── images │ ├── About_baby.png │ ├── About_ball.png │ ├── About_be.png │ ├── About_bird.png │ ├── About_content.png │ ├── About_message.png │ ├── About_@.png │ ├── body.png │ ├── footer_baby.png │ ├── footer_ball.png │ ├── footer_be.png │ ├── footer_bird.png │ ├── footer_message.png │ ├── footer_@.png │ ├── footer.png │ ├── header.png │ └── Thumbs.db ├── img │ ├── 1.jpg │ ├── 2.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ ├── 6.jpg │ └── Thumbs.db ├── Index.html ├── js │ ├── action.js │ └── jquery-1.9.1.min.js ├── test.html └── Work.html 以上是我的文件目录数,web文件里面的内容是实验提供的, ├── include │ └── common.h ├── Makefile ├── src │ ├── common.c │ ├── epoll_serv.c │ ├── process_serv.c │ └── pthread_serv.c 这几个是我自己写的,代码如下: common.h #ifndef __COMMON_H__ #define __COMMON_H__ #define _XOPEN_SOURCE 700 // 消除sigaction相关报错 #include <stdio.h> // 标准输入输出 #include <stdlib.h> // 标准库函数 #include <string.h> // 字符串处理 #include <unistd.h> // UNIX标准函数 #include <sys/socket.h> // 套接字接口 #include <netinet/in.h> // 网络地址结构 #include <arpa/inet.h> // 地址转换函数 #include <fcntl.h> // 文件控制 #include <sys/stat.h> // 文件状态 #include <sys/sendfile.h> // 高效文件传输 #include <sys/types.h> // 系统类型 #include <dirent.h> // 目录操作 #include <errno.h> // 错误码 #include <regex.h> // 正则表达式 #include <sys/wait.h> // 关闭子进程 #include <signal.h> // 信号处理 #include <sys/epoll.h> // epoll I/O多路复用 #include <pthread.h> // 多线程头文件 #include <semaphore.h> // 信号量 #define PORT 80 // 监听端口 #define MAX_EVENTS 1024 // epoll最大事件数 #define BUFFER_SIZE 4096 // 请求缓冲区大小 #define ROOT_DIR "./web" // Web根目录,根据执行可执行文件的路径来设置 #define MAX_MATCHES 30 // 最大匹配数 /* 根据文件扩展名获取MIME类型 */ const char *get_mime_type(const char *path); /* 发送HTTP响应 */ void send_response(int fd, int status, const char *status_msg, const char *content_type, const char *body, int body_len); /* 发送文件内容 */ void send_file(int fd, const char *path); /* 处理GET请求 */ void handle_get_request(int fd, const char *path); /* 处理POST请求 */ void handle_post_request(int fd, const char *path, const char *body, int body_len); /* 处理HTTP请求 */ void handle_request(int fd, const char *request); #endif process_serv.c #include "common.h" #define WORKER_PROCESSES 1 /* 工作进程数量 */ /* 信号处理函数,用于销毁子进程 */ void read_childproc(int sig) { /* 入参检查*/ if (sig < 0) { return; } pid_t pid; int status; while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { if (WIFEXITED(status)) { printf("Normal removed proc id: %d\n", pid); } else { printf("Abnormal removed proc id: %d\n", pid); } } } /* 进程工作 */ void worker_process(int server_fd) { /* 入参检查*/ if (server_fd <= 2) { return; } while (1) { struct sockaddr_in client_addr; socklen_t addrlen = sizeof(client_addr); int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addrlen); if (client_fd < 0) { if (errno == EINTR) continue; /* 被信号中断,重试 */ perror("accept"); continue; } printf("[Worker %d] New connection: %s:%d\n", getpid(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); /* 读取请求 */ char buffer[BUFFER_SIZE]; ssize_t bytes_read = read(client_fd, buffer, BUFFER_SIZE - 1); if (bytes_read <= 0) { close(client_fd); continue; } buffer[bytes_read] = '\0'; fprintf(stdout, "======buffer======\n%s============\nclient_fd:%d\n", buffer, client_fd); /* 处理请求 */ handle_request(client_fd, buffer); /* 关闭连接 */ close(client_fd); } } /** * 主函数:设置服务器并创建工作进程 */ int main() { int server_fd; struct sockaddr_in addr; socklen_t addrlen = sizeof(addr); /* 创建套接字 */ if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket failed"); exit(EXIT_FAILURE); } /* 设置套接字选项(允许地址重用) */ int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) { perror("setsockopt failed"); close(server_fd); exit(EXIT_FAILURE); } /* 绑定地址 */ memset(&addr, 0, addrlen); addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr *)&addr, addrlen) < 0) { perror("bind failed"); close(server_fd); exit(EXIT_FAILURE); } /* 开始监听 */ if (listen(server_fd, SOMAXCONN) < 0) { perror("listen failed"); close(server_fd); exit(EXIT_FAILURE); } printf("Web server running on port %d...\n", PORT); printf("Root directory: %s\n", ROOT_DIR); /* 设置信号处理 */ struct sigaction act; act.sa_handler = read_childproc; sigemptyset(&act.sa_mask); act.sa_flags = SA_NOCLDSTOP | SA_RESTART; /* 注册信号 */ if (sigaction(SIGCHLD, &act, 0) < 0) { perror("sigaction failed"); close(server_fd); exit(EXIT_FAILURE); } /* 创建工作进程 */ for (int i = 0; i < WORKER_PROCESSES; i++) { pid_t pid = fork(); if (pid == 0) { /* 子进程 */ worker_process(server_fd); exit(EXIT_SUCCESS); } else if (pid < 0) { perror("fork failed"); exit(EXIT_FAILURE); } } /* 主进程等待 */ while (1) { pause(); /* 等待信号 */ } close(server_fd); return 0; } epoll_serv.c #include "common.h" /* 客户端数据结构 */ typedef struct { int fd; /* 客户端套接字描述符 */ char buffer[BUFFER_SIZE]; /* 请求数据缓冲区 */ int buffer_len; /* 当前缓冲区数据长度 */ int status; /* 客户端状态(0:等待,1:读取中,2:写入中) */ } client_data; /** * 工作进程函数(处理客户端连接) */ void worker_process(int server_fd) { /* 入参检查*/ if (server_fd <= 2) { return; } while (1) { struct sockaddr_in client_addr; socklen_t addrlen = sizeof(client_addr); int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addrlen); if (client_fd < 0) { if (errno == EINTR) continue; /* 被信号中断,重试 */ perror("accept"); continue; } printf("[Worker %d] New connection: %s:%d\n", getpid(), inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); /* 读取请求 */ char buffer[BUFFER_SIZE]; ssize_t bytes_read = read(client_fd, buffer, BUFFER_SIZE - 1); if (bytes_read <= 0) { close(client_fd); continue; } buffer[bytes_read] = '\0'; fprintf(stdout, "======buffer======\n%s============\n", buffer); /* 处理请求 */ handle_request(client_fd, buffer); /* 关闭连接 */ close(client_fd); } } /** * 主函数:设置服务器并运行事件循环 */ int main() { int server_fd, epoll_fd; struct sockaddr_in addr; socklen_t addrlen = sizeof(addr); /* 1. 创建服务器套接字 */ if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket failed"); exit(EXIT_FAILURE); } /* 设置套接字选项(允许地址重用) */ int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) { perror("setsockopt failed"); close(server_fd); exit(EXIT_FAILURE); } /* 2. 配置服务器地址 */ addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; /* 监听所有网络接口 */ addr.sin_port = htons(PORT); /* 监听端口 */ /* 3. 绑定套接字 */ if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { perror("bind failed"); close(server_fd); exit(EXIT_FAILURE); } /* 4. 开始监听 */ if (listen(server_fd, SOMAXCONN) < 0) { perror("listen failed"); close(server_fd); exit(EXIT_FAILURE); } printf("Web server running on port %d...\n", PORT); printf("Root directory: %s\n", ROOT_DIR); /* 5. 创建epoll实例 */ if ((epoll_fd = epoll_create1(0)) < 0) { perror("epoll_create1 failed"); close(server_fd); exit(EXIT_FAILURE); } /* 6. 添加服务器套接字到epoll */ struct epoll_event ev; ev.events = EPOLLIN; /* 监听读事件 */ ev.data.fd = server_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) < 0) { perror("epoll_ctl failed"); close(server_fd); close(epoll_fd); exit(EXIT_FAILURE); } printf("Web server running on port %d...\n", PORT); printf("Root directory: %s\n", ROOT_DIR); /* 7. 分配客户端数据存储空间 */ struct epoll_event events[MAX_EVENTS]; client_data *clients = calloc(MAX_EVENTS, sizeof(client_data)); /* 8. 主事件循环 */ while (1) { /* 等待事件发生 */ int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); /* 一直等到事件发生 */ if (nfds < 0) { perror("epoll_wait"); continue; } /* 处理所有就绪事件 */ for (int i = 0; i < nfds; i++) { int fd = events[i].data.fd; /* 处理新连接 */ if (fd == server_fd) { /* 接受新连接 */ int client_fd = accept(server_fd, (struct sockaddr *)&addr, &addrlen); if (client_fd < 0) { perror("accept"); continue; } /* 设置非阻塞模式 */ fcntl(client_fd, F_SETFL, fcntl(client_fd, F_GETFL, 0) | O_NONBLOCK); /* 添加新客户端到epoll */ ev.events = EPOLLIN | EPOLLET; /* 边缘触发模式 */ ev.data.fd = client_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) < 0) { perror("epoll_ctl client"); close(client_fd); } /* 初始化客户端数据 */ clients[client_fd].fd = client_fd; clients[client_fd].buffer_len = 0; clients[client_fd].status = 0; printf("New connection: %s:%d\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port)); } else { /* 处理现有客户端数据 */ client_data *client = &clients[fd]; /* 读取客户端数据 */ int bytes_read = read(fd, client->buffer + client->buffer_len, BUFFER_SIZE - client->buffer_len); if (bytes_read <= 0) { /* 连接关闭或错误 */ epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL); close(fd); memset(client, 0, sizeof(client_data)); continue; } /* 更新缓冲区 */ client->buffer_len += bytes_read; client->buffer[client->buffer_len] = '\0'; /* 检查是否收到完整HTTP请求(以空行结束) */ if (strstr(client->buffer, "\r\n\r\n") != NULL) { /* 处理请求 */ handle_request(fd, client->buffer); /* 重置缓冲区 */ client->buffer_len = 0; memset(client->buffer, 0, BUFFER_SIZE); } } } } /* 清理资源(实际不会执行到这里) */ free(clients); close(server_fd); close(epoll_fd); return 0; } pthread.c #include "common.h" #include <pthread.h> #include <errno.h> /* 线程工作函数 */ void *worker_pthread(void *arg) { /* 入参检查*/ if (NULL == arg) { return NULL; } /* 从参数中获取客户端文件描述符 */ int client_fd = *(int *)arg; free(arg); /* 释放动态分配的内存 */ printf("------------------------- work process ----------------------------\n"); char buffer[BUFFER_SIZE]; ssize_t bytes_read = read(client_fd, buffer, BUFFER_SIZE - 1); if (bytes_read < 0) { perror("read error"); close(client_fd); return NULL; } buffer[bytes_read] = '\0'; /* 确保字符串正确终止 */ /* 修复格式字符串:参数顺序应格式匹配 */ fprintf(stdout, "======buffer======\nclient_fd:%d\t\n%s============\n", client_fd, buffer); /* ======buffer====== client_fd:5 GET /images/footer.png HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0 Accept: Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Referer: http://localhost/css/style_mobile.css Connection: keep-alive ============ */ /* 处理请求 */ if (strlen(buffer) > 0) { handle_request(client_fd, buffer); } /* 关闭连接 */ close(client_fd); printf("______________________________________________________\n"); return NULL; } /** * 主函数:设置服务器并创建回收线程 */ int main() { int server_fd; struct sockaddr_in addr, client_addr; socklen_t client_len = sizeof(client_addr); /* 创建套接字 */ if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket creation failed"); exit(EXIT_FAILURE); } /* 设置套接字选项(允许地址重用) */ int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) { perror("setsockopt failed"); close(server_fd); exit(EXIT_FAILURE); } /* 绑定地址 */ memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { perror("bind failed"); close(server_fd); exit(EXIT_FAILURE); } /* 开始监听 */ if (listen(server_fd, SOMAXCONN) < 0) { perror("listen failed"); close(server_fd); exit(EXIT_FAILURE); } printf("Web server running on port %d...\n", PORT); printf("Root directory: %s\n", ROOT_DIR); while (1) { /* 接受新连接 */ int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len); if (client_fd < 0) { perror("accept failed"); continue; } /* 动态分配内存传递client_fd(避免线程间竞争) */ int *client_ptr = malloc(sizeof(int)); if (!client_ptr) { perror("malloc failed"); close(client_fd); continue; } *client_ptr = client_fd; /* 创建新线程处理连接 */ pthread_t t_id; if (pthread_create(&t_id, NULL, worker_pthread, client_ptr) != 0) { perror("pthread_create failed"); free(client_ptr); close(client_fd); continue; } /* 分离线程(自动回收资源) */ pthread_detach(t_id); printf("New connection: %s:%d (thread %lu)\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), (unsigned long)t_id); } close(server_fd); return 0; } 实验要求如下,帮我写一个实验报告
08-16
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值