天天看點

基于epoll實作簡單的web伺服器

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_create、epoll_ctl 和 epoll_wait 三個系統調用的統稱。關于這三個系統調用的細節,這裡就不說明了,大家可以自己去查 man-page。接下來,我們來直接看一個例子,這個例子基于 epoll 和

TinyHttpd

實作了一個 I/O 複用版的 HTTP Server。在上代碼前,我們先來示範這個玩具版 HTTP Server 的效果。

基于epoll實作簡單的web伺服器

上面就是玩具版 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 是一個隻有 500 行左右的超輕量型Http Server,很适合學習使用。為了适應需求,我對其源碼進行了一定的修改,并添加了一些注釋。本章的測試代碼已經放到了 github 上,需要的同學自取,傳送門 ->

epoll_multiprocess_server.c

3. 驚群及示範

“驚群現象”是指并發環境下,多線程或多程序等待同一個 socket 事件,當這個事件發生時,多線程/多程序被同時喚醒,這就是“驚群現象”。對應上面的代碼,多個子程序通過調用 epoll_wait 等待 listen_fd 上某個事件發生。當有新連接配接進來時,多個程序會被同時喚醒去處理這個事件。但最終隻有一個程序可以去處理事件,其他程序重新進入等待狀态。使用上面的代碼可以示範驚群現象,如下:

基于epoll實作簡單的web伺服器

從上圖可以看出,當 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 的目的。大家如果有興趣,可以下載下傳源碼看看。當然,紙上學來終覺淺,還是要自己動手寫才行。本文的測試代碼是本人現學現賣寫的,僅測試使用,寫的不好的地方望諒解。

好了,本文到此結束,謝謝閱讀!

參考

本文在知識共享許可協定 4.0 下釋出,轉載需在明顯位置處注明出處

作者:coolblog

本文同步釋出在我的個人部落格:

http://www.coolblog.xyz
基于epoll實作簡單的web伺服器

本作品采用

知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協定

進行許可。