參考鏈接:http://c4fun.cn/blog/2013/11/19/linux-io-reuse-interface/
http://blog.chinaunix.net/uid-17299695-id-3059110.html
http://blog.csdn.net/kkxgx/article/details/7717125
I/O複用是Linux中的I/O模型之一。所謂I/O複用,指的是進程預先告訴內核,使得內核一旦發現進程指定的一個或多個I/O條件就緒,就通知進程進行處理,從而不會在單個I/O上導致阻塞。
在Linux中,提供了select、poll、epoll三類接口來實現I/O複用。
select函數接口
select中主要就是一個select函數,用於監聽指定事件的發生,原型如下:
|
|
其中各參數的含義如下:
maxfd:最大文件描述符加1,比它小的從0開始的描述符都將被監視,它的值不能超過系統中定義的FD_SETSIZE(通常是1024)。
rset,wset,eset:分別表示監視的讀、寫、錯誤的描述符位數組,通常是一個整數數組,每一個整數可以表示32個描述符是否被監視。需要注意的是這幾個參數都是值-結果參數,在調用select後這幾個參數將表示哪些描述符就緒了。通過以下幾個宏可以很方便的操作fset數組:
|
|
timeout:超時時間,即select最長等待多久就返回,爲NULL時表示等到有操作符準備就緒後才返回。該時間可以精確到微秒,其結構如下:
|
|
描述符就緒條件
對於普通數據的讀寫,描述符就緒顯而易見,但仍有一些特殊情況時描述符會讀寫就緒,UNP中對描述符的讀寫就緒條件進行了說明。1)滿足以下4個條件時,描述符準備好讀
a)套接字接收緩衝區中的數據字節數大於套接字接收緩衝區低水位標記的當前大小(默認爲1),讀將會返回大於0的數。
b)該連接的讀半部關閉,讀將會返回0。
c)套接字上有一個錯誤待處理,讀將返回-1。
d)該套接字是一個監聽套接字並且已完成連接數不爲0。2)滿足以下4個條件時,描述符準備好寫
a)套接字發送緩衝區中的可用空間字節數大於等於套接字發送緩衝區低水位標記的當前大小(默認2048),寫將會返回大於0的數。
b)該連接的寫半部關閉,寫將會返回EPIPE。
c)套接字上有一個錯誤待處理,寫將返回-1。
d)使用非阻塞式connect的套接字建立有結果返回。
poll函數接口
poll中的主要函數也只有一個poll,與select作用類似,但參數有所不同,函數原型如下:
|
|
其中各參數的含義如下:
fdarray:是一個指向pollfd結構數組的指針,維護着描述符以及事件信息,該結構體是poll裏比較核心的結構體,結構如下:
|
|
該結構體通過兩個變量區分關注的事件和發生的事件,從而避免了使用值-結果參數。events和revents可選的標誌位如下:
|
|
nfds:指定結構體數組中元素的個數。
timeout:每次調用poll最大等待的毫秒數,負值代表等待到直到有事件觸發。
epoll函數接口
epoll主要有三個函數,函數原型如下:
|
|
epoll_create(int size)
size:能監聽多少個描述符,返回一個epoll描述符。注意使用完epoll後要關閉該描述符。
epoll_ctl(int efd, int op, int fd, struct epoll_event *event)
efd:epoll_create返回的epoll描述符
op:表示動作,可以在以下三個宏裏選擇一個
|
|
fd:要監聽的fd
event:告訴內核要監聽什麼事件,其結構如下:
|
|
其中events表示epoll事件,可選的標誌位如下:
|
|
而epoll_data_t使用了union來存儲數據,用戶可以使用data來存放一些關於該fd的額外內容。
標誌位中比較特殊的是EPOLLET這個選項,這個選項將EPOLL設置爲邊緣觸發模式,EPOLL有EPOLLET和EPOLLLT兩種工作模式。
EPOLLLT(Level Triggered,水平觸發模式):默認工作模式,支持block和no-block socket,內核通知你描述符事件後,如果不進行操作,會一直通知。
EPOLLET(Edge Triggered,邊緣觸發模式):高速工作模式,只支持no-block socket,只會在描述符狀態由未就緒轉爲就緒時會通知一次,使用該模式時,如果程序編寫的不夠健全,是很容易出現問題的。
epoll_wait(int efd, struct epoll_event *events, int maxevents, int timeout);
該函數與select和poll函數的功能類似,監視指定事件的發生並返回給用戶。
efd:epoll_create返回的opoll描述符。
events:用來從內核得到事件的集合。
maxevents:用來告知內核events數組的大小。
timeout:超時時間,-1將阻塞直到有事件發生,否則表示最多等待多少毫秒後函數就返回。
select,poll,epoll比較
select
- select能監控的描述符個數由內核中的FD_SETSIZE限制,僅爲1024,這也是select最大的缺點,因爲現在的服務器併發量遠遠不止1024。即使能重新編譯內核改變FD_SETSIZE的值,但這並不能提高select的性能。
- 每次調用select都會線性掃描所有描述符的狀態,在select結束後,用戶也要線性掃描fd_set數組才知道哪些描述符準備就緒,等於說每次調用複雜度都是O(n)的,在併發量大的情況下,每次掃描都是相當耗時的,很有可能有未處理的連接等待超時。
- 每次調用select都要在用戶空間和內核空間裏進行內存複製fd描述符等信息。
poll
- poll使用pollfd結構來存儲fd,突破了select中描述符數目的限制。
- 與select的後兩點類似,poll仍然需要將pollfd數組拷貝到內核空間,之後依次掃描fd的狀態,整體複雜度依然是O(n)的,在併發量大的情況下服務器性能會快速下降。
epoll
- epoll維護的描述符數目不受到限制,而且性能不會隨着描述符數目的增加而下降。
- 服務器的特點是經常維護着大量連接,但其中某一時刻讀寫的操作符數量卻不多。epoll先通過epoll_ctl註冊一個描述符到內核中,並一直維護着而不像poll每次操作都將所有要監控的描述符傳遞給內核;在描述符讀寫就緒時,通過回掉函數將自己加入就緒隊列中,之後epoll_wait返回該就緒隊列。也就是說,epoll基本不做無用的操作,時間複雜度僅與活躍的客戶端數有關,而不會隨着描述符數目的增加而下降。
- epoll在傳遞內核與用戶空間的消息時使用了內存共享,而不是內存拷貝,這也使得epoll的效率比poll和select更高。
epoll使用起來很清晰,首先要調用epoll_create建立一個epoll對象。參數size是內核保證能夠正確處理的最大句柄數,多於這個最大數時內核可不保證效果。
epoll_ctl可以操作上面建立的epoll,例如,將剛建立的socket加入到epoll中讓其監控,或者把 epoll正在監控的某個socket句柄移出epoll,不再監控它等等。
epoll_wait在調用時,在給定的timeout時間內,當在監控的所有句柄中有事件發生時,就返回用戶態的進程。
從上面的調用方式就可以看到epoll比select/poll的優越之處:因爲後者每次調用時都要傳遞你所要監控的所有socket給select/poll系統調用,這意味着需要將用戶態的socket列表copy到內核態,如果以萬計的句柄會導致每次都要copy幾十幾百KB的內存到內核態,非常低效。而我們調用epoll_wait時就相當於以往調用select/poll,但是這時卻不用傳遞socket句柄給內核,因爲內核已經在epoll_ctl中拿到了要監控的句柄列表。
所以,實際上在你調用epoll_create後,內核就已經在內核態開始準備幫你存儲要監控的句柄了,每次調用epoll_ctl只是在往內核的數據結構裏塞入新的socket句柄。
在內核裏,一切皆文件。所以,epoll向內核註冊了一個文件系統,用於存儲上述的被監控socket。當你調用epoll_create時,就會在這個虛擬的epoll文件系統裏創建一個file結點。當然這個file不是普通文件,它只服務於epoll。
epoll在被內核初始化時(操作系統啓動),同時會開闢出epoll自己的內核高速cache區,用於安置每一個我們想監控的socket,這些socket會以紅黑樹的形式保存在內核cache裏,以支持快速的查找、插入、刪除。這個內核高速cache區,就是建立連續的物理內存頁,然後在之上建立slab層,簡單的說,就是物理上分配好你想要的size的內存對象,每次使用時都是使用空閒的已分配好的對象。
epoll的高效就在於,當我們調用epoll_ctl往裏塞入百萬個句柄時,epoll_wait仍然可以飛快的返回,並有效的將發生事件的句柄給我們用戶。這是由於我們在調用epoll_create時,內核除了幫我們在epoll文件系統裏建了個file結點,在內核cache裏建了個紅黑樹用於存儲以後epoll_ctl傳來的socket外,還會再建立一個list鏈表,用於存儲準備就緒的事件,當epoll_wait調用時,僅僅觀察這個list鏈表裏有沒有數據即可。有數據就返回,沒有數據就sleep,等到timeout時間到後即使鏈表沒數據也返回。所以,epoll_wait非常高效。
而且,通常情況下即使我們要監控百萬計的句柄,大多一次也只返回很少量的準備就緒句柄而已,所以,epoll_wait僅需要從內核態copy少量的句柄到用戶態而已,如何能不高效?!
那麼,這個準備就緒list鏈表是怎麼維護的呢?當我們執行epoll_ctl時,除了把socket放到epoll文件系統裏file對象對應的紅黑樹上之外,還會給內核中斷處理程序註冊一個回調函數,告訴內核,如果這個句柄的中斷到了,就把它放到準備就緒list鏈表裏。所以,當一個socket上有數據到了,內核在把網卡上的數據copy到內核中後就來把socket插入到準備就緒鏈表裏了。
如此,一顆紅黑樹,一張準備就緒句柄鏈表,少量的內核cache,就幫我們解決了大併發下的socket處理問題。執行epoll_create時,創建了紅黑樹和就緒鏈表,執行epoll_ctl時,如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹幹上,然後向內核註冊回調函數,用於當中斷事件來臨時向準備就緒鏈表中插入數據。執行epoll_wait時立刻返回準備就緒鏈表裏的數據即可。
最後看看epoll獨有的兩種模式LT和ET。無論是LT和ET模式,都適用於以上所說的流程。區別是,LT模式下,只要一個句柄上的事件一次沒有處理完,會在以後調用epoll_wait時次次返回這個句柄,而ET模式僅在第一次返回。
這件事怎麼做到的呢?當一個socket句柄上有事件時,內核會把該句柄插入上面所說的準備就緒list鏈表,這時我們調用epoll_wait,會把準備就緒的socket拷貝到用戶態內存,然後清空準備就緒list鏈表,最後,epoll_wait幹了件事,就是檢查這些socket,如果不是ET模式(就是LT模式的句柄了),並且這些socket上確實有未處理的事件時,又把該句柄放回到剛剛清空的準備就緒鏈表了。所以,非ET的句柄,只要它上面還有事件,epoll_wait每次都會返回。而ET模式的句柄,除非有新中斷到,即使socket上的事件沒有處理完,也是不會次次從epoll_wait返回的。
程序示例
分別使用select,poll和epoll實現了簡單的回顯服務器程序,客戶端使用select來實現。其中select和poll程序主要參考unp的實現,只是Demo程序,對一些異常情況沒有進行處理。
客戶端程序
使用select來監聽終端輸入和連接服務器的流輸入,這樣可以保證客戶端不在某一個輸入流上死等。
|
|
select服務器
|
|
poll服務器
|
|
epoll服務器
回顯服務器使用了ET高速模式。在該模式下,最好所有的操作都是非阻塞的,程序中套接字都設置爲了non-socket,並且使用了緩衝區,在讀到數據時先將數據存到緩衝區中,下次可寫時纔將數據從緩衝區寫回客戶端。
另外,在ET模式下,accept、read、write時都要使用循環直到讀到EAGAIN才能說明沒有數據了。
|
|