常見IO模型和epoll O(1)實現原理

myownstars發表於2014-11-20
同步和阻塞
同步:傳送方傳送請求後,需要收到響應後才能接著傳送下一步請求;
阻塞;通常針對網路套接字socket,呼叫結果返回前,當前執行緒一直掛起等待;
同步針對呼叫方,阻塞針對接受方;
同步非阻塞:傳送方發出請求後一直等待,接受方處理請求時立即返回,不用等待執行結果;
非同步阻塞:傳送方發出請求後馬上返回,接受方處理請求期間一直等待,直到返回執行結果;
nginx工作程式採用了非同步非阻塞,即執行請求時,工作程式和客戶端都無需等待響應;

那麼問題來了,當IO請求完成時,如何通知工作程式?
有兩種方式:
1 worker定期檢查IO執行狀態
2 IO在完成後主動通知worker,即採用事件驅動,允許一個程式同時處理多個請求;


事件驅動
包含事件收集器,事件傳送器和事件處理器,重點是事件處理器,通常有3種模式;
基於程式的,基於執行緒的,基於非阻塞IO的(每接受一個請求,將其放入待處理事件的列表,使用非阻塞IO方式呼叫事件處理器來處理請求,形成事件驅動處理庫)
基於非阻塞IO常見的有select()和epoll(),然而兩者皆有不足,尤其在高併發連線時。
select():描述符總數有限制(1024個);讀/寫/異常各對應一個描述符;O(n)
poll():讀/寫/異常集合到一個描述符中;描述符總數沒有限制;O(n)
而epoll()很大程度上解決了上述問題。


epoll優勢 
為了避免每次掃描所有fd,epoll引入兩個資料結構,
紅黑樹--儲存socket fd
ready list--當socket上有事件發生時,將其加入此列表
epoll每次只掃描ready list,直接跳過那些不符合條件socketfd,演算法複雜度為O(1);
epoll在初始化時呼叫kmem_cache_create申請核心記憶體(採用slab機制),用於建立epitem和eppoll_entry;
epoll對外只提供epoll_create/ctl/wait 3個API,原型如下:
int epoll_create(int size);  
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout); 
epoll_create(): 在核心cache裡建立上述紅黑樹和ready list;
epoll_ctl(): 如果增加socket控制程式碼,則檢查在紅黑樹中是否存在,存在立即返回,不存在則新增到樹幹上;然後向核心註冊回撥函式,告訴核心,如果這個控制程式碼的中斷到了,就把它放到準備就緒list連結串列裡;
epoll_wait(): 與select()等效,即返回ready list中的資料,將fd從核心態複製到使用者態;
客戶端只需呼叫epoll_ctl(),負責把socket fd加入epoll監控;


sockfd-1
  |
  |
sockfd-2                     e
  |                          p  -----------&gt wait_queue(process)
  |           -----------&gt   o
sockfd-3                     o   
  |           -----------&gt   l
  |                          f  -----------&gt ready_queue(ready events)
.....                        d 
sockfd-N-2
  |
  |
sockfd-N-1
  |
  |
sockfd-N


虛擬碼
while true {
active_stream[] = epoll_wait(epollfd)
for i in active_stream[] {
read or write till
}
}
再詳細一點
for( ; ; )
    {
        nfds = epoll_wait(epfd,events,20,500);
        for(i=0;i
        {
            if(events[i].data.fd==listenfd) //有新的連線
            {
                connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept這個連線
                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 = md;     //md為自定義型別,新增資料
                ev.events=EPOLLOUT|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改識別符號,等待下一個迴圈時傳送資料,非同步處理的精髓
            }
            else if(events[i].events&EPOLLOUT) //有資料待傳送,寫socket
            {
                struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;    //取資料
                sockfd = md->fd;
                send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //傳送資料
                ev.data.fd=sockfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改識別符號,等待下一個迴圈時接收資料
            }
            else
            {
                //其他的處理
            }
        }
    }


epoll_event資料結構如下
typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;


struct epoll_event {
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};
注:events有多種型別,包括EPOLLIN/EPOLLOUT/EPOLLERR/EPOLLET


參考資料

http://blog.csdn.net/russell_tao/article/details/7160071

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/15480802/viewspace-1340824/,如需轉載,請註明出處,否則將追究法律責任。

相關文章