天天看點

【Linux】I/O多路複用-SELECT/POLL/EPOLLI/O多路複用

I/O多路複用

前言

  • 文本相關參考資料及部分内容來源
    • 《Linux高性能伺服器程式設計》
    • 《TCP/IP網絡程式設計》
    • 《Linux/UNIX系統程式設計手冊》
  • I/O多路複用核心思想為,使用一個線程,來處理多個用戶端的請求。
  • 或者說,使用一個特殊的fd,監視多個fd。
  • 使得程式能同時監聽多個檔案描述符,這對提高程式的性能至關重要。

通常,網絡程式在下列情況下需要使用I/O多路複用技術。

  • 用戶端程式需要同時處理多個socket。
  • 用戶端程式要同時處理使用者輸入和網絡連接配接。
  • TCP伺服器要同時處理監聽socket和連接配接socket。這是I/O複用使用最多的場合

  • 伺服器要同時處理TCP請求和UDP請求。
  • 伺服器要同時監聽多個端口,或處理多種服務。

select

select-函數

  • 在一段指定時間内,監聽使用者感興趣的檔案描述符上的可讀、可寫和異常等事件。
  • select
  • 函數原型:
int select(
    	   int nfds, 
           fd_set *readfds, 
           fd_set *writefds, 
           fd_set *exceptfds, 
           struct timeval *timeout
);
           
  • 參數:
    • nfds: 被監聽的檔案描述符的總數。
    • readfds: 将所有關注"是否存在待讀取資料"的檔案描述符注冊到fd_set型變量中(存入fd_set數組),并傳遞其位址值。-fd_set檔案描述符集合,注意傳遞記得備份,因為調用

      select後會将其重置

    • writefds: 将所有關注"是否存在無阻塞資料(可寫入)"的檔案描述符注冊到fd_set型變量中(存入fd_set數組),并傳遞其位址值。
    • exceptfds: 将所有關注"是否發生異常"的檔案描述符注冊到fd_set型變量中,并傳遞其位址值。
    • timeout: 用來設定select的阻塞時間上限。
      • 指定為NULL将會

        一直阻塞

        ,直到某個檔案描述符就緒。
      • 指定為一個timeval結構體,詳見timeval結構體。
  • 傳回值:
    • -1: 表示發生錯誤。
    • 0: 表示逾時。
    • >0: 表示有一個或多個檔案描述符已達到就緒态,傳回值表示處于就緒态的檔案描述符個數。[三個集合中就緒的fd數量總和,也就是說,如果一個fd在三個fd_set數組中,三種事件都就緒了,會存在重複累計(對于同一個fd來說)。]

timeval-結構體

  • 結構體定義
struct timeval{
	long tv_sec; // 秒
	long tv_usec; // 微秒
};
           
  • 如果結構體 timeval 的兩個域都為 0 的話,此時 select()不會阻塞,它隻是簡單地輪詢指定的檔案描述符集合,看看其中是否有就緒的檔案描述符并立刻傳回。
  • 否則,timeout 将為 select指定一個等待時間的上限值。

fd_set-檔案描述符集合

  • 在fd_set變量中各注冊或更改值的操作都由以下四個宏完成。
  • 将fdset所指向的檔案描述符集合初始化為空。
void FD_ZERO(fd_set *fdset); 
           
  • 将檔案描述符fd,從fdset所指向的檔案描述集合中移除。
void FD_CLR(int fd, fd_set *fdset);
           
  • 将檔案描述符fd,添加到fdset所指向的檔案描述集合中。
void FD_SET(int fd, fd_set *fd_set); 
           
  • 檢查指定的檔案描述符fd,是否在fdset所指向的檔案描述集合中。
  • 存在傳回非0,反之傳回0。
int FD_ISSET(int fd, fd_set *fdset);
           

檔案描述符就緒條件

  • 以下情況socket可讀:
    • socket核心接收緩存區中的位元組數大于或等于其低水位标記SO_RCVLOWAT。此時我們可以無阻塞地讀取該socket,并且讀操作将傳回的位元組數大于0。
    • socket通信的對方關閉連接配接。此時對該socket的讀操作将傳回0。
    • 監聽socket上有新的連接配接請求。
    • socket上有未處理的錯誤。此時我們可以使用getsockop來讀取和清除該錯誤。
  • 以下情況socket可寫:
    • socket核心發送緩沖區中的可用位元組數大于或等于其低水位标記SO_SNDLOWAT。此時我們可以無阻塞地寫該socket,并且寫操作傳回的位元組數大于0。
    • socket的寫操作被關閉。對寫操作被關閉的socket執行寫操作将觸發一個SIGPIPE信号。
    • socket使用非阻塞connect連接配接成功或失敗(逾時)之後。
    • socket上有未處理的錯誤,此時我們可以使用getsockopt來讀取和清除該錯誤。
  • 異常情況:
    • 網絡程式中,select能處理的異常情況隻有一種: socket上接收到帶外資料。

示例

  • server.c
#include <sys/types.h> 
#include <sys/socket.h> 
#include <stdio.h> 
#include <netinet/in.h> 
#include <sys/time.h> 
#include <sys/ioctl.h> 
#include <unistd.h> 
#include <stdlib.h>
#include <errno.h>

int errno;
int main(void){
    int server_sockfd,client_sockfd;
    int server_len,client_len;
    struct sockaddr_in server_address;
    struct sockaddr_in client_address;
    int result;
    //兩個檔案描述符集合
    fd_set readfds,testfds;//readfds用于檢測輸出是否就緒的檔案描述符集合

    server_sockfd = socket(AF_INET,SOCK_STREAM,0); // 建立服務端socket
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(9000);
    server_len = sizeof(server_address);
    bind(server_sockfd,(struct sockaddr*)&server_address,server_len);
    listen(server_sockfd,5);// 監聽隊列最多容納5個
    
    FD_ZERO(&readfds);// 清空置0
    FD_SET(server_sockfd,&readfds);// 将服務端socket加入到集合中

    /*
    int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
    */ 

    while(1){
        char ch;
        int fd;
        int nread;
        testfds = readfds;//相當于備份一份,因為調用select後,傳進去的檔案描述符集合會被修改。
        struct timeval my_time;
        my_time.tv_sec = 2;
        my_time.tv_usec = 0;
        printf("server waiting\n");
        // 監視server_sockfd與client_sockfd
        //result = select(FD_SETSIZE,&testfds,(fd_set*)0,(fd_set*)0,(struct timeval* )0); //無限期阻塞,并測試檔案描述符變動
        result = select(FD_SETSIZE,&testfds,(fd_set*)0,(fd_set*)0,&my_time); //根據my_time中設定的時間進行等待,超過繼續往下執行。
        if(result < 0){//有錯誤發生
            perror("select errno"); 
            exit(1);
        }else if(result == 0){//超過等待時間,未響應    
            FD_ZERO(&readfds);// 清空置0
            FD_SET(server_sockfd,&readfds);// 将服務端socket重新加入到集合中
            printf("no connect request \n");
            continue;//沒有響應的就别下去周遊了
        }
        //掃描所有的檔案描述符(周遊所有的檔案句柄),是件很耗時的事情,嚴重拉低效率。
        for(fd = 0;fd<FD_SETSIZE;fd++){
            //找到相關檔案描述符,判斷是否在testfds這個檔案描述符集合中。 
            if(FD_ISSET(fd,&testfds)){
                //判斷是否為伺服器套接字,是則表示為用戶端請求連接配接
                if(fd == server_sockfd){
                    client_len = sizeof(client_address);
                    client_sockfd = accept(server_sockfd,(struct sockaddr*)&client_address,&client_len);
                    FD_SET(client_sockfd,&readfds);//将用戶端socket加入到集合中,用來監聽是否有資料來。
                    printf("adding client on fd %d\n",client_sockfd);;
                }else{// 用戶端來消息了
                    //擷取接收緩存區中的位元組數
                    ioctl(fd,FIONREAD,&nread);//即擷取fd來了多少資料

                    //用戶端資料請求完畢,關閉套接字,并從集合中清除相應的套接字描述符
                    if(nread ==0){
                        close(fd);
                        FD_CLR(fd,&readfds);//去掉關閉的fd
                        printf("removing client on fd %d\n", fd);
                    }else{//處理客戶數請求
                        read(fd,&ch,1);
                        sleep(5);
                        printf("serving client on fd %d\n", fd);
                        ch++;
                        write(fd, &ch, 1);
                    }
                }
            }
        }
    }
    return 0;
}
           
  • client.c
#include <sys/types.h> 
#include <sys/socket.h> 
#include <stdio.h> 
#include <netinet/in.h> 
#include <arpa/inet.h> 
#include <unistd.h> 
#include <stdlib.h>
#include <sys/time.h>

int main(){
    
    int client_sockfd;
    int len;
    struct sockaddr_in address;//伺服器端網絡位址結構體 
    int result;
    char ch = 'A';
    client_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立用戶端socket 
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = inet_addr("127.0.0.1");
    address.sin_port = htons(9000);
    len = sizeof(address);
    result = connect(client_sockfd, (struct sockaddr*)&address, len);
    if (result == -1){
        perror("oops: client2");
        exit(1);
    }
    //第一次讀寫
    write(client_sockfd, &ch, 1);
    read(client_sockfd, &ch, 1);
    printf("the first time: char from server = %c\n", ch);
    sleep(5);

    //第二次讀寫
    write(client_sockfd, &ch, 1);
    read(client_sockfd, &ch, 1);
    printf("the second time: char from server = %c\n", ch);

    close(client_sockfd);

    return 0;
}
           
【Linux】I/O多路複用-SELECT/POLL/EPOLLI/O多路複用

poll

poll函數

  • 與select類似,也是在指定事件内輪詢一定的數量的檔案描述符,看其中是否有就緒的。
  • poll
  • 函數原型:
int poll(
    	 struct pollfd *fds, 
         nfds_t nfds, 
         int timeout
);
           
  • 參數:
    • fds: pollfd類型的數組,它存儲所有我們該興趣的檔案描述符上發生的可讀、可寫和異常事件。結構體定義詳見pollfd結構體。
    • nfds: 數組fds中的元素個數,類型為nfds_t無符号整型。
    • timeout: 逾時等待時間。
      • -1: 一直阻塞,直到某個事件發生。
      • 0: 調用後不等待立即傳回。
  • 傳回值:
    • -1: 表示發生錯誤。
    • 0: 表示逾時。
    • >0: 表示fds中有這麼多個檔案描述符處于就緒态了。即fds中擁有非零revents字段的pollfd結構體數量。

pollfd-結構體

  • 結構體定義:
struct pollfd {
    int fd;          
    short events;     
    short revents;   
};
           
  • 參數:
    • fd:

      檔案描述符

    • events:

      注冊的事件

      。如下圖所示。
    • revents: 實際發生的事件,由

      核心填充

【Linux】I/O多路複用-SELECT/POLL/EPOLLI/O多路複用

示例

  • server.c
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
#include <poll.h>

#define MAX_FD  8192 //最大檔案辨別符
struct pollfd  fds[MAX_FD];
int cur_max_fd = 0;     //目前要監聽的最大檔案描述符+1,減少要周遊的數量。

int main(void){

    int server_sockfd,client_sockfd;
    int server_len,client_len;
    struct sockaddr_in server_address,client_address;
    int result;
    server_sockfd = socket(AF_INET,SOCK_STREAM,0);//服務端socket
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(9000);
    server_len = sizeof(server_address);
    bind(server_sockfd,(struct sockaddr*)&server_address,server_len);
    listen(server_sockfd,5);

    //添加待監測檔案描述符到fds數組中
    fds[server_sockfd].fd = server_sockfd;
    fds[server_sockfd].events = POLLIN;
    fds[server_sockfd].revents = 0;
    
    if(cur_max_fd <= server_sockfd){
        cur_max_fd = server_sockfd+1;
    }

    while(1){
        char ch;
        int i,fd;
        int nread;
        printf("server waiting\n");

        result = poll(fds,cur_max_fd,1000);
        if(result <0){
            perror("server5");
            exit(1);
        }else if(result == 0){
            printf("no connect,end waiting\n");
        }else{//大于0,傳回的是fds中處于就緒态的檔案描述符個數。

        }
        //掃描檔案描述符
        for(i = 0; i < cur_max_fd;i++){
            if(fds[i].revents){//有沒有結果,沒有結果說明該檔案描述符上還未發生事件。
                fd= fds[i].fd;
                //判斷是否為伺服器套接字,是則表示為用戶端請求連接配接
                if(fd == server_sockfd){
                    client_len = sizeof(client_address);
                    client_sockfd = accept(server_sockfd,(struct sockaddr*)&client_address,&client_len);
                    fds[client_sockfd].fd = client_sockfd;
                    fds[client_sockfd].events = POLLIN;
                    fds[client_sockfd].revents = 0;
                    if(cur_max_fd <= client_sockfd){
                        cur_max_fd = client_sockfd + 1;
                    }
                    printf("adding client on fd %d\n",client_sockfd);
                }else{//用戶端socket中有資料請求
                    if(fds[i].revents & POLLIN){//讀
                        nread = read(fd,&ch,1);
                        if(nread == 0){
                            close(fd);
                            memset(&fds[i],0,sizeof(struct pollfd));
                            printf("removing client on fd %d\n",fd);
                        }else{//寫
                            sleep(5);
                            printf("serving client on fd %d,receive: %c\n",fd,ch);
                            ch++;
                            fds[i].events = POLLOUT;//添加一個寫事件監聽
                        }
                    }else if(fds[i].revents & POLLOUT){//寫
                        write(fd,&ch,1);
                        fds[i].events = POLLIN;
                    }

                }
            }
        }

    }
    return 0;
}
           
  • client.c——同select中的執行個體。
【Linux】I/O多路複用-SELECT/POLL/EPOLLI/O多路複用

epoll

  • epoll是

    Linux特有的I/O複用函數

    。它在實作和使用上與select和poll有很大的差異。
  • epoll使用

    一組函數

    來完成任務,而不是

    單個函數

  • epoll把使用者關心的檔案描述符上的事件放在核心的一個

    事件表

    中,不需要像select與poll那樣每次都要重複傳入檔案描述符集合或是事件集。

    epoll需要使用一個額外的檔案描述符,在核心中唯一辨別這個事件表

epoll_create-建立epoll

  • epoll_create
    • 功能: 建立一個epoll執行個體。
  • 函數原型:
int epoll_create(int size); 
           
  • 參數:
    • size: 現已被抛棄,隻是給核心一個提示,告訴它事件表需要多大。
  • 傳回值:

    傳回建立的epoll執行個體檔案描述符,在其它epoll相關函數中指定要通路的核心事件表

epoll_ctl-操作對應核心事件表

  • epoll_ctl
  • 功能: 操作對應epoll的核心事件表,進行添加/删除/修改指定fd的事件。
  • 函數原型:
int epoll_ctl(
   int epfd, 
   int op, 
   int fd, 
   struct epoll_event *event
); 
           
  • 參數:
    • epfd: epoll執行個體,用來指定要通路的核心事件表。
    • op: 用來指定需要執行的操作。
      • EPOLL_CTR_ADD: 往事件表上注冊fd上的事件。
      • EPOLL_CTR_MOD: 修改fd上的注冊事件。
      • EPOLL_CTR_DEL: 删除fd上的注冊事件。
    • fd: 要進行op操作的檔案描述符。
    • event: 為一個指向epoll_event結構體的指針。結構體定義如下event_event-結構體所示:
  • 傳回值:
    • 成功: 傳回0。
    • 失敗: 傳回-1并設定errno。

epoll_event-結構體

  • 結構體定義:
struct epoll_event{
    uint32_t events;
    epoll_data_t data;
};
           
  • 參數:
    • events: epoll事件,如下圖所示。
    • data: 使用者資料,詳見epoll_data_t-結構體。
【Linux】I/O多路複用-SELECT/POLL/EPOLLI/O多路複用

epoll_data_t-結構體

  • 結構體定義:
typedef union epoll_data{
    void *ptr;    // 使用者自定義使用
    int fd;		 // 指定事件檔案描述符
    uint32_t u32;
    uint64_t u64;
}epoll_data_t;
           
  • 參數:
    • ptr: 可用來指定與fd相關的使用者資料。
    • fd: 指定事件所從屬的目标檔案描述符。
  • 注意:
    • epoll_data_t是一個聯合體,是以我們隻能使用fd或ptr其中一個成員。
    • 如果要将檔案描述符和使用者資料關聯起來,以實作快速的資料通路,可以放棄使用epoll_data_t中的fd成員,而是在ptr所指向的自定義使用者資料中包含fd。
  • 補充:
    • 當我們調用epoll_wait後,evlist數組中的epoll_event每個data參數為我們在一開始(即調用epoll_ctl)所指定的内容,比如像上面所說的我們指定了自定義資料ptr,最終某一fd産生了我們監視的事件,我們可以在其對應的epoll_event的data中取到。

      例如下方epoll-簡易web伺服器中的_ConnectStat結構體。

epoll_wait-事件等待

  • epoll_wait
  • 功能: 在一段逾時時間内等待一組檔案描述符上的事件。
  • 函數原型:
int epoll_wait(int epfd, 
               struct epoll_event * evlist, 
               int maxevents, 
               int timeout
); 
           
  • 參數:
    • epfd: epoll檔案描述符,指定核心事件表。
    • evlist: 配置設定好的epoll_event結構體數組,epoll将會把發生的事件複制到evlist數組中。
    • maxevents: 最多監聽多少個時間,必須大于0。
    • timeout: 表示在沒有檢測到事件發生時最多等待的時間(ms)。
      • 0: 将會立即傳回,不會等待。
      • -1: 表示無限期阻塞,直到有事件發生。
      • >0: 阻塞(等待)時間。
  • 傳回值:
    • 成功: 傳回就緒的檔案描述符個數。
    • 失敗: 傳回-1并設定errno。

epoll-簡易web伺服器

  • 使用epoll的一個簡易web伺服器

頭檔案

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/epoll.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<assert.h>
#include<fcntl.h>
#include<unistd.h>
           

自定義儲存資料的結構體

//因為下面的函數指針是以單獨拿出來聲明typedef
typedef struct _ConnectStat  ConnectStat;

typedef void(*response_handler) (ConnectStat * stat);

// 儲存自定義資料的結構體,調用epoll時用epoll_data_t中的ptr存儲
struct _ConnectStat {
	int fd;						//檔案描述符
	char name[64];				//姓名
	char  age[64];				//年齡
	struct epoll_event _ev;		//目前檔案句柄對應epoll事件
	int  status;				//0-未登入,1-已登入
	response_handler handler;	//不同頁面的處理函數
};
           

相關函數聲明與全局變量

// 初始化一個自定義資料存儲結構體
ConnectStat * stat_init(int fd);

// 将新連結進來的用戶端fd放入目前epoll所對應的核心事件表中
void connect_handle(int new_fd);

// 請求響應-指定對應的處理函數
void do_http_respone(ConnectStat * stat);

// 處理http請求
void do_http_request(ConnectStat * stat);

// 響應處理函數——請求連結傳回的内容
void welcome_response_handler(ConnectStat * stat);

// 響應處理函數——commit後傳回的内容
void commit_respone_handler(ConnectStat * stat);

// 将新連結進來的用戶端fd放入目前epoll所對應的核心事件表中
void connect_handle(int new_fd);

// 建立一個監聽套接字 - 略
int startup(char* _ip, int _port);

// 将fd-設定為非阻塞狀态,即給指定fd添加狀态
void set_nonblock(int fd);

// 列印資訊提示ip:port
void usage(const char* argv);
    
// 響應頭
const char *main_header = "HTTP/1.0 200 OK\r\nServer: Xuanxuan Server\r\nContent-Type: text/html\r\nConnection: Close\r\n";

static int epfd = 0;// epoll檔案描述符,對應一張核心事件表
           

初始化一個自定義資料存儲結構體

// 初始化自定義資料存儲結構體
ConnectStat * stat_init(int fd) {
	ConnectStat * temp = NULL;
	temp = (ConnectStat *)malloc(sizeof(ConnectStat));

	if (!temp) {
		fprintf(stderr, "malloc failed. reason: %m\n");
		return NULL;
	}

	memset(temp, '\0', sizeof(ConnectStat));
	temp->fd = fd; 
	temp->status = 0; 
}
           

處理http請求

// 解析http請求
void do_http_request(ConnectStat * stat) {

	//讀取和解析http 請求
	char buf[4096];
	char * pos = NULL;

	ssize_t _s = read(stat->fd, buf, sizeof(buf) - 1);
	if (_s > 0){// 讀取到資料
		buf[_s] = '\0';
		// printf("receive from client:%s\n", buf);//GET / HTTP/1.1
		pos = buf;

		//Demo 僅僅示範效果,不做詳細的協定解析
		if (!strncasecmp(pos, "GET", 3)) {// 是否為Get請求
			stat->handler = welcome_response_handler;// 設定執行函數
		}else if (!strncasecmp(pos, "Post", 4)) {// 是否為POST請求
			//擷取 uri
			//printf("---Post----\n");
			pos += strlen("Post");
			while (*pos == ' ' || *pos == '/') ++pos;

			// POST /commit HTTP/1.1
			if (!strncasecmp(pos, "commit", 6)) {//送出
				int len = 0;

				//printf("post commit --------\n");
				pos = strstr(buf, "\r\n\r\n");//傳回第一次出現\r\n\r\n的位置
				char *end = NULL;
				// 拿到姓名與年齡
				if (end = strstr(pos, "name=")) {
					pos = end + strlen("name=");
					end = pos;
					while (('a' <= *end && *end <= 'z') || ('A' <= *end && *end <= 'Z') || ('0' <= *end && *end <= '9'))	end++;
					len = end - pos;
					if (len > 0) {// 将姓名存入自定義結構體中
						memcpy(stat->name, pos, end - pos);
						stat->name[len] = '\0';
					}
				}
				if (end = strstr(pos, "age=")) {
					pos = end + strlen("age=");
					end = pos;
					while ('0' <= *end && *end <= '9')	end++;
					len = end - pos;
					if (len > 0) {// 将年齡存入自定義結構體中
						memcpy(stat->age, pos, end - pos);
						stat->age[len] = '\0';
					}
				}
				stat->handler = commit_respone_handler;// 設定響應函數
			}
			else {
				stat->handler = welcome_response_handler;// 設定響應函數
			}
		}
		else {
			stat->handler = welcome_response_handler;// 設定響應函數
		}

		stat->_ev.events = EPOLLOUT;	// 修改事件類型
		epoll_ctl(epfd, EPOLL_CTL_MOD, stat->fd, &stat->_ev);   //修改,交給eoill監視。
	}else if (_s == 0){// 沒有讀取到資料,用戶端關閉。
		printf("client: %d close\n", stat->fd);
		epoll_ctl(epfd, EPOLL_CTL_DEL, stat->fd, NULL);// 将對應fd從對應epoll的核心事件表中删除
		close(stat->fd);// 關閉套接字
		free(stat);	// 釋放記憶體
	}else{// read發生錯誤
		perror("read");
	}
}
           

請求響應-根據指定的處理函數

void do_http_respone(ConnectStat * stat) {
	stat->handler(stat);// 調用對應設定的函數
}
           

響應處理函數——請求連結傳回的内容

void welcome_response_handler(ConnectStat * stat) {
	const char * welcome_content = "\
			<html lang=\"zh-CN\">\n\
			<head>\n\
			<meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">\n\
			<title>This is a test</title>\n\
			</head>\n\
			<body>\n\
			<div align=center height=\"500px\" >\n\
			<br/><br/><br/>\n\
			<h2>Hello World</h2><br/><br/>\n\
			<form action=\"commit\" method=\"post\">\n\
			姓名: <input type=\"text\" name=\"name\" />\n\
			<br/>年齡: <input type=\"password\" name=\"age\" />\n\
			<br/><br/><br/><input type=\"submit\" value=\"送出\" />\n\
			<input type=\"reset\" value=\"重置\" />\n\
			</form>\n\
			</div>\n\
			</body>\n\
			</html>";

	char sendbuffer[4096];
	char content_len[64];

	strcpy(sendbuffer, main_header);// 拷貝響應頭
	snprintf(content_len, 64, "Content-Length: %d\r\n\r\n", (int)strlen(welcome_content));
	strcat(sendbuffer, content_len);
	strcat(sendbuffer, welcome_content);
	//printf("send reply to client \n%s", sendbuffer);

	// 寫給用戶端-即發起請求的浏覽器
	write(stat->fd, sendbuffer, strlen(sendbuffer));

	stat->_ev.events = EPOLLIN;		// 修改關心的事件
	//stat->_ev.data.ptr = stat;
	epoll_ctl(epfd, EPOLL_CTL_MOD, stat->fd, &stat->_ev);
}
           

響應處理函數——commit後傳回的内容

void commit_respone_handler(ConnectStat * stat) {
	const char * commit_content = "\
		<html lang=\"zh-CN\">\n\
		<head>\n\
		<meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">\n\
		<title>This is a test</title>\n\
		</head>\n\
		<body>\n\
		<div align=center height=\"500px\" >\n\
		<br/><br/><br/>\n\
		<h2>歡迎&nbsp;%s &nbsp;,年齡&nbsp;%s!</h2><br/><br/>\n\
		</div>\n\
		</body>\n\
		</html>\n";

	char sendbuffer[4096];
	char content[4096];
	char content_len[64];
	int len = 0;

	len = snprintf(content, 4096, commit_content, stat->name, stat->age);
	strcpy(sendbuffer, main_header); //響應頭
	snprintf(content_len, 64, "Content-Length: %d\r\n\r\n", len);
	strcat(sendbuffer, content_len);
	strcat(sendbuffer, content);
	//printf("send reply to client \n%s", sendbuffer);

	write(stat->fd, sendbuffer, strlen(sendbuffer));

	stat->_ev.events = EPOLLIN; // 修改關心的事件

	epoll_ctl(epfd, EPOLL_CTL_MOD, stat->fd, &stat->_ev);// 交給epoll來監視
}
           

列印資訊提示ip:port

void usage(const char* argv){
	printf("%s:[ip][port]\n", argv);
}
           

将fd-設定為非阻塞狀态

void set_nonblock(int fd){
	// 這裡的檔案狀态标志flag即open函數的第二個參數
	int fl = fcntl(fd, F_GETFL);// 擷取設定的flag
	fcntl(fd, F_SETFL, fl | O_NONBLOCK);// 設定flag
	// fcntl函數 				https://blog.csdn.net/zhoulaowu/article/details/14057799
	// O_NONBLOCK https://blog.csdn.net/cjfeii/article/details/115484558
}
           

建立一個監聽套接字

int startup(char* _ip, int _port){
	int sock = socket(AF_INET, SOCK_STREAM, 0);
	if (sock < 0){
		perror("sock");
		exit(2);
	}

	int opt = 1;
	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

	struct sockaddr_in local;
	local.sin_port = htons(_port);
	local.sin_family = AF_INET;
	local.sin_addr.s_addr = inet_addr(_ip);

	if (bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
		perror("bind");
		exit(3);
	}

	if (listen(sock, 5) < 0){
		perror("listen");
		exit(4);
	}
	return sock;    //傳回套接字
}
           

main

#include "epoll_server.h"

int main(int argc, char *argv[]){

	if (argc != 3){//檢查輸入的參數個數是否正确
		usage(argv[0]);
		exit(1);
	}
	
	//建立一個server socket
	int listen_sock = startup(argv[1], atoi(argv[2]));      

	//建立epoll
	epfd = epoll_create(256);
	if (epfd < 0){//建立失敗
		perror("epoll_create");
		exit(5);
	}

	ConnectStat * stat = stat_init(listen_sock);// 自定義資料存儲

	struct epoll_event _ev; 	//epoll事件結構體
	_ev.events = EPOLLIN;    	//設定關心事件為讀事件     
	_ev.data.ptr = stat;    	//接收傳回值
	

    //将listen_sock添加到epfd中,關心讀事件,有用戶端來請求連結
	epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &_ev);

	struct epoll_event revs[64];//接收傳回的産生響應的事件

	int timeout = -1;// -1無限期阻塞
	int num = 0;// 就緒的請求I/O個數

	while (1){
		//檢測事件
		switch ((num = epoll_wait(epfd, revs, 64, timeout))){
		case 0:   //監聽逾時               
			printf("timeout\n");
			break;
		case -1: //出錯     
			perror("epoll_wait");
			break;
		default:{	//>0,即傳回了需要處理事件的數目
			//拿到對應的檔案描述符
			struct sockaddr_in peer;
			socklen_t len = sizeof(peer);

			for (int i = 0; i < num; i++){//
                //拿到該fd相關的連結資訊
				ConnectStat * stat = (ConnectStat *)revs[i].data.ptr;

				int rsock = stat->fd;//拿到對應的fd,進行如下的判斷
				if (rsock == listen_sock && (revs[i].events) && EPOLLIN) {// 有用戶端連結
					int new_fd = accept(listen_sock, (struct sockaddr*)&peer, &len);
					
					if (new_fd > 0){//accept成功
						printf("get a new client:%s:%d\n", inet_ntoa(peer.sin_addr), ntohs(peer.sin_port));	
						connect_handle(new_fd);// 監聽新進來的用戶端fd
					}
				}else {//除server socket 之外的其他fd就緒
					if (revs[i].events & EPOLLIN){//有資料可讀
						do_http_request((ConnectStat *)revs[i].data.ptr);
					}else if (revs[i].events & EPOLLOUT){//寫
						do_http_respone((ConnectStat *)revs[i].data.ptr);// 完成響應後會再次關心EPOLLIN事件,等待下一次請求。					
					}else{
					}
				}
			}
		}
		break;
		}
	}
	return 0;
}
           
  • 運作-通路-示例: ./main 192.168.0.70 8080

LT與ET

  • Level Trigger——水準觸發:
    • 當被監控的檔案描述符上有可讀寫的事件發生時,epoll_wait會通知處理程式去讀寫。如果這次沒有把資料一次性全部讀寫完(如讀寫緩沖區太小),那麼下次有調用epoll_wait時,它還會通知你在上次沒讀寫完的檔案描述符上繼續讀寫,如果你一直不去讀寫它,它就會一直通知你。
    • 如果系統中有大量你不需要讀寫的就緒檔案描述符,而它們每次都會傳回,這樣會大大降低處理程式檢索自己關心的就緒檔案描述符的效率。

    • 應用程式可以不立即處理該事件,因為當下一次調用epoll_wait,epoll_wait還會再次向應用程式通告此事件。
    • 設定方式: 預設即水準觸發。
  • Edge_triggered(邊緣觸發):
    • 當被監控的檔案描述符上有可讀寫的事件發生時,epoll_wait會通知處理程式去讀寫。如果這次沒有把資料全部讀寫完(如讀寫緩沖區太小),它不會通知你,也就是它隻會通知你一次,直到該檔案描述符上出現第二次可讀寫事件才會通知你。
    • 這種模式比水準觸發效率高,系統不會充斥大量你不關心的就緒檔案描述符,很大程度上降低了同一個epoll事件被重複觸發的次數。

    • 同時,應用程式應立即處理該事件,因為後續的epoll_wait調用将不再向應用程式通知這一事件(之後的讀寫事件就會通知,隻是這次的不會了)。

  • 設定方式(epoll):
    • 對應檔案描述符上要監聽的事件設定為,events |= EPOLLET
    • 同時對該檔案描述符設定為非阻塞模式。如上epoll-簡易web伺服器中所示。

EPOLLONESHOT事件

  • 用途:

    保證一個socket連接配接在任一時刻都隻被一個線程處理,進而保證連接配接的完整性,避免了很多可能的競态條件。

  • 可能産生的情景: 一個線程(或程序)在讀取完某個socket上的資料并開始處理時,在處理的過程中該socket上又有新的資料可讀(EPOLLIN被再次觸發),此時喚醒另一個線程來讀取這些新的資料。于是就出現了兩個線程操作一個socket的局面。
  • 使用: 使用epoll_ctrl函數在該socket(檔案描述符)上注冊EPOLLONESHOT事件。
  • 注意:
    • 注冊完EPOLLONESHOT事件的socket一旦被某個線程處理完畢,應立即重置這個socket上的EPOLLONESHOT事件,以確定這個socket下次可讀時,其EPOLLIN事件能被觸發,同時也給其它線程處理這個socket的機會。

    • 用于監聽連結請求的Server_socket是不能注冊EPOLLONESHOT事件的,否則應用程式隻能處理一個客戶連結,因為後續的客戶連結請求将不再觸發Server_socket上的EPOLLIN事件。

    • 如果某一線程處理完成該socket上的請求之後,又在該socket上收到了新的客戶請求,該線程将繼續接觸這個socket。

代碼示例

  • 僅部分核心代碼示例: 完整的可以去《Linux高性能伺服器程式設計》源代碼9-4檢視

主線程中循環監聽事件

while( 1 ){
        int ret = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );
        if ( ret < 0 )break;
    
        for ( int i = 0; i < ret; i++ ){
            int sockfd = events[i].data.fd;
            if ( sockfd == listenfd ){// 有連結請求接入
                struct sockaddr_in client_address;
                socklen_t client_addrlength = sizeof( client_address );
                int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
                addfd( epollfd, connfd, true );
            }
            else if ( events[i].events & EPOLLIN ){
                pthread_t thread;
                fds fds_for_new_worker;
                fds_for_new_worker.epollfd = epollfd;
                fds_for_new_worker.sockfd = sockfd;
                // 建立一個線程去處理
                pthread_create( &thread, NULL, worker, ( void* )&fds_for_new_worker );
            }
            else printf( "something else happened \n" );
        }
    }
           

将指定fd上的某一事件注冊到對應的核心事件表中

void addfd( int epollfd, int fd, bool oneshot ){
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET;
    if( oneshot ){
        event.events |= EPOLLONESHOT;// 注冊EPOLLONESHOT事件
    }
    epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
    setnonblocking( fd );// 設定為非阻塞fd
}
           

設定fd為非阻塞

int setnonblocking( int fd ){
    int old_option = fcntl( fd, F_GETFL );// 拿到之前對該fd的設定屬性
    int new_option = old_option | O_NONBLOCK;// 追加O_NONBLOCK屬性
    fcntl( fd, F_SETFL, new_option );// 設定
    return old_option;// 目前示例Demo傳回無意義,未使用。
}
           

線程工作函數

void* worker( void* arg ){
    int sockfd = ( (fds*)arg )->sockfd;
    int epollfd = ( (fds*)arg )->epollfd;
    printf( "start new thread to receive data on fd: %d\n", sockfd );
    char buf[ BUFFER_SIZE ];
    memset( buf, '\0', BUFFER_SIZE );
    while( 1 ){// 因為是非阻塞的,是以要一次性讀光,即要立即處理,因為epoll_wait隻會提醒一次。
        int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
        if( ret == 0 ){
            close( sockfd );
            printf( "foreiner closed the connection\n" );
            break;
        }
        else if( ret < 0 ){
            if( errno == EAGAIN ){// 讀光啦
                reset_oneshot( epollfd, sockfd );// 重置注冊事件
                printf( "read later\n" );
                break;
            }
        }
        else{
            printf( "get content: %s\n", buf );
            // sleep 5秒,模拟資料處理過程
            sleep( 5 );
        }
    }
    printf( "end thread receiving data on fd: %d\n", sockfd );
}
           

重置fd上注冊的事件

void reset_oneshot( int epollfd, int fd ){
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
    epoll_ctl( epollfd, EPOLL_CTL_MOD, fd, &event );
}

           

三個I/O複用函數的對比

  • select:
    • select的參數類型fd_set,僅僅是個檔案描述符集合,是以select需要3個這種類型的參數來區分可讀、可寫及異常事件。
    • 一方面使得select不能處理更多類型的事件,另一方面核心對fd_set

      線上修改

      ,導緻應用程式下次再調用select前不得不

      重置

      這三個fd_set集合。同時我們也需要在使用前進行

      備份

  • poll:
    • poll參數類型pollfd要聰明一些,将檔案描述符和事件類型定義在一起,調用後修改的是pollfd結構體中的revents成員,為實際檢測到的事件,我們設定的events成員保持不變。再次調用後,revents會被重新置空。
  • select與poll每次調用後,都需要周遊整個使用者關心的事件集合,無論其中的事件是否就緒,是以應用程式檢索就緒檔案描述符的時間複雜度為O(n)。

  • epoll:
    • epoll使用與上面二者完全不同的方式來管理使用者注冊事件,它在核心中維護一個

      事件表

      ,并提供獨立的系統調用epoll_ctl來往其中進行添加、删除、修改事件,而

      無須

      反複地從使用者空間讀入這些事件。
  • epoll_wait系統調用的events參數負責儲存這些就緒的事件,使得應用程式檢索就緒檔案描述符的時間複雜度達到O(1)。

  • 最大支援檔案描述符個數:
    • poll與epoll_wait分别用nfds和maxevents參數來指定最多監聽多少個檔案描述符和事件,這兩個數值都能達到系統允許打開的最大檔案描述符個數——65535。而select允許監聽的最大檔案描述符數量通常有限制。雖然使用者可以修改這個限制,但是這可能會導緻

      不可預期

      的後果。
  • 工作模式:
    • select與poll都隻能工作在相對低效的LT模式,而epoll可以工作在高效的ET模式。
  • 核心實作:
    • select與poll采用的是

      輪詢

      的方式,每次

      掃描整個注冊檔案描述符集合

      ,将就緒的檔案描述符傳回給使用者程式。檢測就緒事件的時間複雜度為O(n)。
    • epoll_wait采用

      回調

      的方式,核心檢測到就緒的檔案描述符時,将觸發回調函數,回調函數将該檔案描述符上對應的事件插入核心就緒事件隊列。

      核心最後在适當的時機将該就緒事件隊列中的内容拷貝到使用者空間。

    • 活動連接配接比較多

      的時候,epoll_wait的效率

      未必

      比select和poll高,因為此時回調函數被觸發得過于頻繁。

      是以epoll_wait适用于連接配接數量多,但活動連接配接較少的情況。

繼續閱讀