1. 程式人生 > >《深入理解計算機系統》Tiny服務器4——epoll類型IO復用版Tiny

《深入理解計算機系統》Tiny服務器4——epoll類型IO復用版Tiny

[] 用戶數據 nts tin 服務 監視 結束 col 結構

  前幾篇博客分別講了基於多進程、select類型的IO復用、poll類型的IO復用以及多線程版本的Tiny服務器模型,並給出了主要的代碼。至於剩下的epoll類型的IO復用版,本來打算草草帶過,畢竟和其他兩種IO復用模型差不太多。但今天在看Michael Kerrisk的《Linux/UNIX系統編程手冊》時,看到了一章專門用來講解epoll函數,及其IO復用模型。於是,自己也就動手把Tiny改版了一下。感興趣的同學可以參考上述手冊的下冊1113頁,有對於epoll比較詳細的講解。

  前邊針對IO多路復用,我們已經有了很相似的select()函數和poll()函數,那為什麽還需要一個epoll()函數呢?肯定是因為前兩個在某些情況下不能滿足人們的要求吧。我們首先就來分析一下前兩種IO多路復用模型所存在的問題:

  (1) 每次調用select()或poll(),內核都必須檢查所有被指定的文件描述符,看它們是否處於就緒態。當檢查大量處於密集範圍內的文件描述符時,該操作耗費的時間將大大超過接下來的操作。

  (2) 每次調用select()或poll()時,程序都必須傳遞一個表示所有需要被檢查的文件描述符的數據結構到內核,內核檢查過描述符後,修改這個數據結構並返回給程序。

  (3) select()或poll()調用結束後,程序必須檢查返回的數據結構中的每個元素,以此查明哪個文件描述符處於就緒態了。

  所以,隨著帶檢查的文件描述符數量的增加,select()和poll()所占用的CPU時間也會隨之增加,所以才出現了適合於大量文件描述符處理的epoll()。在書中有一張表,記錄了三種IO復用模型隨著處理文件描述符的增多,其花費時間的比較:

技術分享

  epoll類型的IO復用模型主要由三個相關函數組成,分別為

  • epoll_create():創建一個epoll實例,返回代表該實例的文件描述符
  • epoll_ctl():操作同epoll實例相關聯的興趣列表,通過這個函數,我們可以增加新的描述符到列表中,將已有的文件描述符從該列表中移除,以及修改代表文件描述符上事件類型的位掩碼
  • epoll_wait():返回與epoll實例相關聯的就緒列表中的成員

  下面我們依次簡單介紹一下,更具體的論述請參考manpage。

  首先是epoll_create()函數,其原型為:

int epoll_create(int size)                                         //成功返回創建的文件描述符,失敗返回-1

這個函數只有一個參數size,但自從Linux2.6.8以來,這個參數就被忽略不用。只要我們輸入一個正值就行。在程序結束時,可以通過調用close()函數,將返回的這個描述符關閉。

  第二個函數epoll_ctl()比較復雜,其原型為:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev)    //成功返回0,失敗返回-1

這個函數有四個參數,分別為

  • epfd——與epoll實例相關聯的文件描述符,通過epoll_create()產生
  • op ——需要執行的操作,可以是EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL,分別是將第三個參數fd加入epfd、修改fd上設定的事件和移除fd
  • fd ——指明了要修改興趣列表中哪一個文件描述符的設定
  • ev ——指向結構體epoll_event的指針

結構體epoll_event的定義為

struct epoll_event {
    uint32_t         events;        //epoll事件,位掩碼
    epoll_data_t     data;          //用戶數據
}

其中,epoll_data_t是一個聯合,其定義為

typedef union epoll_data {
    void           *ptr;
    int             fd;
    uint32_t        u32;
    uint64_t        u64;
} epoll_data_t;

  最後一個相關函數是epoll_wait(),它與select()和poll()類似,用來得到監視結果。其原型為

int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout)      //成功時返回已就緒的描述符的個數,失敗 返回-1

其中,evlist也是指向結構體epoll_event鏈表的指針,需要通過動態申請內存獲得。

  在簡單介紹了三個函數之後,我們就可以開始改進我們的Tiny了。首先介紹我們的連接池的結構體聲明:

typedef struct epoll_event SE;         //為了縮短代碼的長度

typedef struct {
    int   epfd;                        //epoll的文件描述符
    SE    ev;                          //event結構
    SE   *ev_list;                     //指向event結構鏈表的頭指針
} pool;    

  接下來就是幾個相關操作的參數,如初始化、析構函數、添加客戶端、移除客戶端:

 1 void init_pool(int listenfd, pool *p)   //初始化連接池
 2 {
 3     p->epfd = epoll_create(EPOLL_SIZE);   //建立epoll描述符
 4     p->ev_list = malloc(sizeof(SE)*EPOLL_SIZE);   //為鏈表動態分配內存
 5     p->ev.data.fd = listenfd;                     //設置監聽套件字
 6     p->ev.events = EPOLLIN;                       //設置感興趣的監視類型為輸入
 7     if (epoll_ctl(p->epfd, EPOLL_CTL_ADD, listenfd, &(p->ev)) != 0)   //將listenfd寫入epoll描述符
 8     {
 9         fprintf(stderr, "epoll_ctl error\n");
10         exit(1);
11     }
12 }
13 
14 void free_pool(pool *p)                //清空連接池
15 {
16     free(p->ev_list);
17     close(p->epfd);
18 }
19 
20 void add_client(int connfd, pool *p)   //添加客戶端描述符
21 {
22     p->ev.data.fd = connfd;
23     p->ev.events = EPOLLIN;
24     if (epoll_ctl(p->epfd, EPOLL_CTL_ADD, connfd, &(p->ev)) != 0)
25     {
26         fprintf(stderr, "epoll_ctl error\n");
27         exit(1);
28     }
29 }
30 
31 void del_client(int connfd, pool *p)    //刪除客戶端描述符
32 {
33     epoll_ctl(p->epfd, EPOLL_CTL_DEL, connfd, NULL);
34     close(connfd);
35 }

  最後,給出主函數的框架:

 1 int main(int argc, char *argv[])
 2 {
 3     int listenfd, connfd;
 4         int err, i, ev_cnt;
 5     static pool mypool;
 6     //...其余參數聲明
 7     
 8     listenfd = open_listenfd(argv[1]);
 9     init_pool(listenfd, &mypool);                                 //初始化連接池
10     
11     while (1) {
12         //Wait for listening/connected descriptor(s) to become ready
13         ev_cnt = epoll_wait(mypool.epfd, mypool.ev_list, EPOLL_SIZE, -1);
14         if (ev_cnt == -1)
15         {
16             fprintf(stderr, "epoll_wait error\n");
17             exit(1);
18         }
19         for (i = 0; i < ev_cnt; i++)
20         {
21             if (mypool.ev_list[i].data.fd == listenfd) {          //客戶端請求建立連接
22                 clientlen = sizeof(clientaddr);
23                 connfd = accept(listenfd, (SA *)&clientaddr, &clientlen);
24                 add_client(connfd, &mypool);                      //將新建立連接的套接字加入epoll實例描述符
25             }
26             else {                                                //已連接的客戶端請求數據
27                 doit(&(mypool.ev_list[i].data.fd), &mypool);
28                 del_client(mypool.ev_list[i].data.fd, &mypool);   //將已處理完的描述符清除
29             }
30                 
31         }
32     }  
33     close(listenfd);
34     free_pool(&mypool);
35     return 0;
36 }
37     

  對比前邊的基於select()函數和poll()函數的IO復用模型,我們可以看到,epoll()的IO復用模型也沒有復雜多少,三者的大體框架都是一樣的。但epoll在處理高並發的業務時有比另外兩個更好的性能,我們要繼續掌握它更深層次的使用。這裏只是舉例說明了epoll最簡單的一個用法,感興趣的同學請自行鉆研。

《深入理解計算機系統》Tiny服務器4——epoll類型IO復用版Tiny