Linux IO 複用之 epoll 介紹與 epoll 應用(編寫單執行緒多併發的 Web 伺服器)

兀自成霜、珞清殤發表於2020-12-31

一、Linux epoll 介紹

epoll是Linux核心為處理大批量檔案描述符而作了改進的poll,是Linux下多路複用IO介面select/poll的增強版本,它能顯著提高程式在大量併發連線中只有少量活躍的情況下的系統CPU利用率(百度百科)。

  • epoll

    • The epoll API performs a similar task to poll: monitoring multiple file descriptors to see if I/O is possible on any of them. The epoll API can be used either as an edge-triggered or a level-triggered interface and scales well to large numbers of watched file descriptors.

      epoll 執行與 poll 相似的任務:通過監控多個檔案描述符來檢視任何一個(被監控的)檔案描述符是否可能有IO操作。epoll API 要麼通過edge-triggered要麼通過level-triggered介面來使用並且可以很好地擴充套件到大量被監控的檔案描述符(可用於實現Web伺服器的原因)。

  • System calls

    • epoll_create(): creates an epoll instance and returns a file descriptor referring to that instance(建立一個epoll例項並且返回一個指代該例項的檔案描述符).

      • int epoll_create(int size)
        在這裡插入圖片描述
    • epoll_ctl(): Interest in particular file descriptors is then registered via epoll_ctl. The set of file descriptors currently registered on an epoll instance is sometimes called an epoll set.

      感興趣的特定檔案描述符通過epoll_ctl註冊到 epoll 例項。
      當前註冊到一個 epoll 例項的檔案描述符集合有時被稱作 epoll 集合。

      • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
        • epfd: epoll 檔案描述符
        • op: 通過指定op來新增(EPOLL_CTL_ADD)/修改(EPOLL_CTL_MOD)/刪除(EPOLL_CTL_DEL)需要偵聽的檔案描述符及其事件
        • fd: 檔案描述符
        • event: 連結到檔案描述符的事件,struct epoll_event 定義如下
          在這裡插入圖片描述
    • epoll_wait: waits for I/O events(等待IO事件)

      • int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
        • epfd: epoll 檔案描述符
        • events: The memory area pointed to by events will contain the events that will be available for the caller.
        • maxevents: epoll_wait 返回的最大事件數量
        • timeout: timeout為0的時候表示馬上返回,為-1的時候表示一直等下去,直到有事件發生,為正整數(t)的時候表示等待t毫秒。
          在這裡插入圖片描述

二、單執行緒多併發的 Web 伺服器編寫

#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/epoll.h>

#define MAX_EVENTS 10

// 處理get請求
void handle_get(int fd, char path[]){
        char buf[1024];
        int n;
        int filefd;
        if((filefd=open(path+1, O_RDONLY))==-1){
                write(fd, "HTTP/1.0 404 Not Found\r\n\r\n", 26);
                return;
        }
        write(fd, "HTTP/1.0 200 OK\r\n\r\n", 19);
        while((n=read(filefd, buf, sizeof(buf)))>0){
                write(fd, buf, n);
        }
        close(filefd);
}

// 非阻塞設定
void setnonblocking(int fd) {
        int opts;
        opts=fcntl(fd, F_GETFL);
        if(opts<0) {
                perror("fcntl(sock,GETFL)");
                exit(1);
        }
        opts = opts|O_NONBLOCK;
        if(fcntl(fd, F_SETFL, opts)<0) {
                perror("fcntl(sock,SETFL,opts)");
                exit(1);
        }
}


main(int ac, char *av[]){
        struct sockaddr_in addr;
        char buf[1024];
        int i;
        int n;
        char cmd[512];
        char path[512];
        int sockfd;

        if(ac<2){
                printf("Usage cmd port_num\n");
                exit(1);
        }

		struct epoll_event ev, events[MAX_EVENTS];
        int listen_sock, conn_sock, nfds, epollfd;

        signal(SIGPIPE, SIG_IGN);

        listen_sock = socket(AF_INET, SOCK_STREAM, 0);

        addr.sin_family = AF_INET;
        addr.sin_port = htons(atoi(av[1]));
        addr.sin_addr.s_addr = INADDR_ANY;
		
        if(bind(listen_sock, (const struct sockaddr *)&addr, sizeof(struct sockaddr_in))==-1){
                perror("cannot bind");
                exit(1);
        }
		
        listen(listen_sock, 1);
		// 建立 epoll 例項
        epollfd = epoll_create(10);
        if (epollfd == -1) {
                perror("epoll_create");
                exit(EXIT_FAILURE);
        }
        // 註冊 socket 檔案描述符
        ev.events = EPOLLIN;
        ev.data.fd = listen_sock;
		if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
                perror("epoll_ctl: listen_sock");
                exit(EXIT_FAILURE);
        }
		
        while(1){
        		
                nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
                if (nfds == -1) {
                        perror("epoll_pwait");
                        exit(EXIT_FAILURE);
                }
				for (n = 0; n < nfds; ++n) {
						// 主socket通過accept獲取客戶端與伺服器之間的socket檔案描述符並註冊到epoll例項
                        if (events[n].data.fd == listen_sock) {
                        		// 註冊客戶端與伺服器之間的socket檔案描述符
                                conn_sock = accept(listen_sock, NULL, NULL);
                                if (conn_sock == -1) {
                                        perror("accept");
                                        exit(EXIT_FAILURE);
                                }
                                setnonblocking(conn_sock);
                                ev.events = EPOLLIN | EPOLLET; // 處理的事件型別為EPOLLIN
                                ev.data.fd = conn_sock;
                                if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) {
                                        perror("epoll_ctl: conn_sock");
                                        exit(EXIT_FAILURE);
                                }
                        } else { // 客戶端與伺服器之間的socket檔案描述符用於讀取客戶端GET請求(EPOLLIN)
                                sockfd=events[n].data.fd;
                                if ((i = read(sockfd, buf, sizeof(buf))) <= 0) {
                                        close(sockfd);
                                } else {
                                        sscanf(buf, "%s%s", cmd, path);
                                        if(strcmp(cmd, "GET")==0){
                                                handle_get(sockfd, path);
                                        }
                                        close(sockfd);  //close the sockfd
                                }
                        }
                }
		}
}

相關文章