1. 简介
epoll 是 Linux 平台下特有的一种 I/O 复用模型实现, 于 2002 年在 Linux kernel 2.5.44 中被引入在 epoll 之前, Unix/Linux 平台下的 I/O 复用模型包含 select 和 poll 两个系统调用随着因特网的发展, 因特网的用户量越来越大, C10K 问题出现基于 select 和 poll 编写的网络服务已经不能满足不能满足用户的需求了, 业界迫切希望更高效的系统调用出现在此背景下, FreeBSD 的 kqueue 和 Linux 的 epoll 被研发了出来 kqueue 和 epoll 的出现, 终结了 C10K 问题, C10K 问题就此作古
因为 Linux 系统的广泛应用, 所以大家在说 I/O 复用时, 更多的是想到了 epoll, 而不是 kqueue, 本文也不例外本篇文章不会涉及 kqueue, 大家有兴趣可以自己看看
2. 基于 epoll 实现 web 服务器
在 Linux 中, epoll 并不是一个系统调用, 而是 epoll_createepoll_ctl 和 epoll_wait 三个系统调用的统称关于这三个系统调用的细节, 这里就不说明了, 大家可以自己去查 man-page 接下来, 我们来直接看一个例子, 这个例子基于 epoll 和 TinyHttpd 实现了一个 I/O 复用版的 HTTP Server 在上代码前, 我们先来演示这个玩具版 HTTP Server 的效果
上面就是玩具版 HTTP Server 的运行效果了, 看起来还行在我第一次把它成功跑起来的时候, 感觉很奇妙好了, 看完效果, 接下来看代码吧, 如下:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #include <sys/sysinfo.h>
- #include <sys/epoll.h>
- #include <signal.h>
- #include <fcntl.h>
- #include <sys/wait.h>
- #include <sys/types.h>
- #include "httpd.h"
- #define DEFAULT_PORT 8080
- #define MAX_EVENT_NUM 1024
- #define INFTIM -1
- void process(int);
- void handle_subprocess_exit(int);
- int main(int argc, char *argv[])
- {
- struct sockaddr_in server_addr;
- int listen_fd;
- int cpu_core_num;
- int on = 1;
- listen_fd = socket(AF_INET, SOCK_STREAM, 0);
- fcntl(listen_fd, F_SETFL, O_NONBLOCK); // 设置 listen_fd 为非阻塞
- setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
- bzero(&server_addr, sizeof(server_addr));
- server_addr.sin_family = AF_INET;
- server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
- server_addr.sin_port = htons(DEFAULT_PORT);
- if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
- perror("bind error, message:");
- exit(1);
- }
- if (listen(listen_fd, 5) == -1) {
- perror("listen error, message:");
- exit(1);
- }
- printf("listening 8080\n");
- signal(SIGCHLD, handle_subprocess_exit);
- cpu_core_num = get_nprocs();
- printf("cpu core num: %d\n", cpu_core_num);
- // 根据 CPU 数量创建子进程, 为了演示惊群现象, 这里多创建一些子进程
- for (int i = 0; i < cpu_core_num * 2; i++) {
- pid_t pid = fork();
- if (pid == 0) { // 子进程执行此条件分支
- process(listen_fd);
- exit(0);
- }
- }
- while (1) {
- sleep(1);
- }
- return 0;
- }
- void process(int listen_fd)
- {
- int conn_fd;
- int ready_fd_num;
- struct sockaddr_in client_addr;
- int client_addr_size = sizeof(client_addr);
- char buf[128];
- struct epoll_event ev, events[MAX_EVENT_NUM];
- // 创建 epoll 实例, 并返回 epoll 文件描述符
- int epoll_fd = epoll_create(MAX_EVENT_NUM);
- ev.data.fd = listen_fd;
- ev.events = EPOLLIN;
- // 将 listen_fd 注册到刚刚创建的 epoll 中
- if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {
- perror("epoll_ctl error, message:");
- exit(1);
- }
- while(1) {
- // 等待事件发生
- ready_fd_num = epoll_wait(epoll_fd, events, MAX_EVENT_NUM, INFTIM);
- printf("[pid %d] 震惊! 我又被唤醒了...\n", getpid());
- if (ready_fd_num == -1) {
- perror("epoll_wait error, message:");
- continue;
- }
- for(int i = 0; i < ready_fd_num; i++) {
- if (events[i].data.fd == listen_fd) { // 有新的连接
- conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr_size);
- if (conn_fd == -1) {
- sprintf(buf, "[pid %d] accept 出错了:", getpid());
- perror(buf);
- continue;
- }
- // 设置 conn_fd 为非阻塞
- if (fcntl(conn_fd, F_SETFL, fcntl(conn_fd, F_GETFD, 0) | O_NONBLOCK) == -1) {
- continue;
- }
- ev.data.fd = conn_fd;
- ev.events = EPOLLIN;
- if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev) == -1) {
- perror("epoll_ctl error, message:");
- close(conn_fd);
- }
- printf("[pid %d] 收到来自 %s:%d 的请求 \ n", getpid(), inet_ntoa(client_addr.sin_addr), client_addr.sin_port);
- } else if (events[i].events & EPOLLIN) { // 某个 socket 数据已准备好, 可以读取了
- printf("[pid %d] 处理来自 %s:%d 的请求 \ n", getpid(), inet_ntoa(client_addr.sin_addr), client_addr.sin_port);
- conn_fd = events[i].data.fd;
- // 调用 TinyHttpd 的 accept_request 函数处理请求
- accept_request(conn_fd, &client_addr);
- close(conn_fd);
- } else if (events[i].events & EPOLLERR) {
- fprintf(stderr, "epoll error\n");
- close(conn_fd);
- }
- }
- }
- }
- void handle_subprocess_exit(int signo)
- {
- printf("clean subprocess.\n");
- int status;
- while(waitpid(-1, &status, WNOHANG) > 0);
- }
上面的代码有点长, 不过还好, 基本上都是模板代码, 没什么特别复杂的逻辑希望大家耐心看一下
上面的代码基于 epoll + 多进程的方式实现, 开始, 主进程会通过系统调用获取 CPU 核心数, 然后根据核心数创建子进程为了演示惊群现象, 这里多创建了一倍的子进程关于惊群现象, 下一章会讲到, 大家先别急哈创建好子进程后, 主进程不需再做什么事了, 核心逻辑都会在子线程中执行首先, 每个子进程都会调用 epoll_create 在内核创建 epoll 实例, 然后再通过 epoll_ctl 将 listen_fd 注册到 epoll 实例中, 由内核进行监控最后, 再调用 epoll_wait 等待感兴趣的事件发生当 listen_fd 中有新的连接时, epoll_wait 会返回此时子进程调用 accept 接受连接, 并把客户端 socket 注册到 epoll 实例中, 等待 EPOLLIN 事件发生当该事件发生后, 即可接受数据, 并根据 HTTP 请求信息返回相应的页面了
这里说明一下, 上面代码中处理 HTTP 请求的逻辑是写在 TinyHttpd 项目中的, TinyHttpd 是一个只有 500 行左右的超轻量型 Http Server, 很适合学习使用为了适应需求, 我对其源码进行了一定的修改, 并添加了一些注释本章的测试代码已经放到了 github 上, 需要的同学自取, 传送门 -> epoll_multiprocess_server.c
3. 惊群及演示
惊群现象是指并发环境下, 多线程或多进程等待同一个 socket 事件, 当这个事件发生时, 多线程 / 多进程被同时唤醒, 这就是惊群现象对应上面的代码, 多个子进程通过调用 epoll_wait 等待 listen_fd 上某个事件发生当有新连接进来时, 多个进程会被同时唤醒去处理这个事件但最终只有一个进程可以去处理事件, 其他进程重新进入等待状态使用上面的代码可以演示惊群现象, 如下:
从上图可以看出, 当 listen_fd 上有新连接事件发生时, 进程 19571 和 19573 被唤醒但最终进程 19573 成功处理了新连接事件, 进程 19571 则失败了
惊群现象会影响服务器性能, 因为多个进程被唤醒, 但最终只有一个进程可以成功处理事件而 CPU 需要为一个事件的发生调度数个进程, 因此会浪费 CPU 资源
对于惊群现象, 处理的思路一般有两种一种是像 Lighttpd 那样, 无视惊群另一种是像 Nginx 那样, 使用全局锁避免惊群简单起见, 本文测试代码采用的是 Lighttpd 的处理方式, 即无视惊群对于这两种思路的细节, 由于本人未读过两个开源软件的代码, 这里就不多说了如果大家有兴趣, 可以参考网上的一些博文
4. 总结
epoll 是 I/O 复用模型重要的一个实现, 性能优异, 应用广泛像 Linux 平台下的 JVM,NIO 部分就是基于 epoll 实现的再如大名鼎鼎 Nginx 也是使用了 epoll 由此可以看出 epoll 的重要性, 因此我们有很有必要去了解 epoll 本文通过一个测试程序简单演示了一个基于 epoll 的 HTTP Server, 总体上也达到了学习 epoll 的目的大家如果有兴趣, 可以下载源码看看当然, 纸上学来终觉浅, 还是要自己动手写才行本文的测试代码是本人现学现卖写的, 仅测试使用, 写的不好的地方望谅解
好了, 本文到此结束, 谢谢阅读!
参考
关于多进程 epoll 与惊群问题 - CSDN
惊群, 看看 nginx 是怎么解决它的 - CSDN
高性能网络编程 (二): 上一个 10 年, 著名的 C10K 并发连接问题
来源: https://www.cnblogs.com/nullllun/p/8492884.html