[原]淺談幾種服務器端模型——反應堆模式(epoll 簡介)


引言:上一篇說到了線程池方式來處理服務器端的並發,並給出了一個線程池的方案(半同步,半異步方式)。各有各的好處吧,今天來講講關於非阻塞的異步IO。

說到異步IO,其實現在很難實現真正的異步,大部分情況下仍然需要阻塞在某個多路復用函數,比如select 或者 epoll 上,得到就緒描述符,然后調用注冊在相應描述符上的回調函數。這種方式是現在的反應堆設計的基本思路。我截取一段反應堆模型的圖給大家看看。

這個圖是截取至 python的  twisted 服務器的反應堆文章介紹,但是大致和我們需要的理念一樣。

事件循環阻塞查看描述符是否就緒,當就緒后返回可讀或可寫的描述符,也有可能帶外數據或者出錯等情況。

因為 select 很多文章都介紹了,下面我就以 epoll 為例,貌似是2.4.6還是哪個版本以后加入的IO多路復用方式。

epoll 較select 的一些優點就不多說了,內核采用紅黑樹機制,大大提高了epoll 的性能。著名的 libevent Nginx等內部都采用這個機制。

廢話不多說,看一個簡單的epoll 模式,其實本來不想介紹這個的,因為直接 man epoll 就可以看到一個簡單的demo,但是為了文章的連貫性,還是繼續把這部分介紹一下。

 

epoll 主要有幾個函數:

int epoll_create(int size);

在現在的Linux版本中,size 已不重要,默認的不超過最大值就可以。size 就是描述符數目的最大值。

函數的返回值是一個描述符(句柄),很簡單的就創建了epoll.

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

第一個參數是由 epoll_create 返回的描述符

第二個參數是由宏定義的幾個值

EPOLL_CTL_ADD:類似於 select 的 FD_SET() ,將一個描述符加入到epoll 監聽隊列中
EPOLL_CTL_MOD:修改已經注冊的fd的事件類型
EPOLL_CTL_DEL:將一個描述符從epoll 監聽隊列中刪除

第三個參數是需要加入的描述符

第四個是一個結構體參數,結構是這樣的

struct epoll_event {
  __uint32_t events; 
  epoll_data_t data;  
};
typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;

epoll_event 結構體里面的events 表示的是返回的事件類型或者是加入時候的事件類型。也有可能是帶外數據或者錯誤等,它由幾個宏定義:

EPOLLIN :文件描述符上的讀事件
EPOLLOUT:文件描述符上的寫事件
EPOLLPRI:描述符有緊急的數據可讀(這里應該表示有帶外數據到來);
EPOLLERR:描述符發生錯誤;
EPOLLHUP:描述符被掛斷;
EPOLLET: 邊緣觸發(Edge Triggered)模式
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里

 

值得一說的是,很多文章都沒有提到這個宏其實可能是由你自己改變的,通過 epoll_ctl 或者是在 epoll_wait 返回的時候操作系統改的,因為描述符有可能出錯等。

一般情況下,對於一個描述符,可以使用 | 運算來組合。

添加一個描述符,監聽是否可讀或可寫。

EPOLLIN | EPOLLOUT 

注意一下epoll_data_t中的 ptr 或者 fd 而不是 ptr和fd,這個結構只能包含其中一個,所以在注冊相應的描述符上的事件的時候,要么注冊的是對應的描述符fd,要么注冊的是相應的事件封裝,當然,事件封裝里面必然有fd,不然無法繼續下面的操作。

 

 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

第一個參數是epoll的描述符,第二個參數是一個指向 struct epoll_event  的指針,這里需要傳入的是一個數組,epoll_event 類型.

第三個,最大的監聽事件數組值。第四個是超時時間,對於Nginx或者很多如libevent 的超時時間管理是利用紅黑樹和最小堆來管理的,很巧妙的方式,以后寫一篇博文介紹,這里只需要知道timeout 是 epoll_wait 的阻塞的最大值,如果超過這個值不管是否有事件都返回,0表示立即返回,即有無事件都返回,-1 是永久阻塞。

一個簡單的 epoll demo

 struct epoll_event ev,events[1024];
 epfd=epoll_create(1024);

  

 for( ; ; )
    {
        nfds = epoll_wait(epfd,events,1024,time_value);
        for(i=0;i<nfds;++i)
        {
            if(events[i].data.fd==listenfd) /*如果加入的監聽描述符有事件*/
            {
                connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); /*accept這個連接並得到鏈接描述符,將描述符加入到epoll 監聽事件隊列*/
          setnonblocking(connfd); ev.data.fd=connfd; ev.events=EPOLLIN|EPOLLET; /*讀事件*/ epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); /*將新的fd添加到epoll的監聽隊列中*/ } else if( events[i].events&EPOLLIN ) //接收到數據,讀socket { n = read(sockfd, line, MAXLINE)) < 0 ev.data.ptr = my_ev; //ev 可以是自己定義的事件封裝或者是fd ev.events=EPOLLOUT|EPOLLET; epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);/*修改標識符,等待下一個循環時發送數據*/ } else if(events[i].events&EPOLLOUT) /*對應的描述符可寫,即套接口緩沖區有緩沖區可寫*/ { struct my_event* my_ev= (my_event*)events[i].data.ptr; sockfd = my_ev->fd; send( sockfd, ev->ptr, strlen((char*)my_ev->ptr), 0 ); ev.data.fd=sockfd; ev.events=EPOLLIN|EPOLLET; epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); } else { // } } }

  

epoll 還沒講清楚,其中還有很多需要注意的地方。只是想讓不懂異步事件和反應堆模式的讀者了解這種模式。注意的是這種模式下連接描述符需要設置為非阻塞,然后IO 操作函數應該記錄每次讀寫的狀態,如果緩沖區滿的話需要記錄狀態,下次返回這個描述符的時候繼續上一次的狀態繼續傳輸或讀取,因為一個套接口緩沖區讀取的是應用層數據,而TCP層的數據如果比較大的時候分段的話會導致一次不能完全讀取或寫入全部數據而套接口緩沖區已經滿了。需要選取的模式是LT 水平觸發方式,如果是ET 邊緣觸發方式,一次讀取套接口或者寫入套接口但是緩沖區滿了不能繼續寫后,epoll_wait不會繼續返回,不需要狀態機記錄。ET 方式也是所謂的高速模式。

 

總結:這里只是對epoll 做了一個簡單的介紹,如有錯誤,請指教。希望大牛們不要介意,承前啟后,后面會有一個反應堆的框架的介紹,這里沒有使用到事件封裝和設置回調函數等,只是一個demo,還不是我自己寫的。下一站分享一個 epoll 異步事件封裝。今天就到這里吧


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM