select,poll,epoll都是IO多路複用中的模型。再介紹他們特色時,先來看看多路複用的 模型。node
同其餘IO的不一樣的是,IO多路複用一次能夠等多個文件描述符。大大提升了等待數據準備好的時間的效率。爲了完成等的效率,系統提供了三個系統調用:select,poll,epoll。這裏再也不講述三者具體實現,只總結三者的優缺點。linux
select的缺點數組
1.單個進程監控的文件描述符有限,一般爲1024*8個文件描述符。固然能夠改進,因爲select採用輪詢方式掃描文件描述符。文件描述符數量越多,性能越差。服務器
2.內核/用戶數據拷貝頻繁,操做複雜。select在調用以前,須要手動在應用程序裏將要監控的文件描述符添加到fed_set集合中。而後加載到內核進行監控。用戶爲了檢測時間是否發生,還須要在用戶程序手動維護一個數組,存儲監控文件描述符。當內核事件發生,在將fed_set集合中沒有發生的文件描述符清空,而後拷貝到用戶區,和數組中的文件描述符進行比對。再調用selecct也是如此。每次調用,都須要了來回拷貝。網絡
3.輪迴時間效率低。select返回的是整個數組的句柄。應用程序須要遍歷整個數組才知道誰發生了變化。輪詢代價大。數據結構
四、select是水平觸發。應用程序若是沒有完成對一個已經就緒的文件描述符進行IO操做。那麼以後select調用仍是會將這些文件描述符返回,通知進程。併發
poll特色
1.poll操做比select稍微簡單點。select採用三個位圖來表示fd_set,poll使用pollfd的指針,pollfd結構包含了要監視的event和發生的evevt,再也不使用select傳值的方法。更方便socket
2.select的缺點依然存在。拿select爲例,加入咱們的服務器須要支持100萬的併發鏈接。則在FD_SETSIZE爲1024的狀況下,咱們須要開闢100個併發的進程才能實現併發鏈接。除了進程上下調度的時間消耗外。從內核到用戶空間的無腦拷貝,數組輪詢等,也是系統難以接受的。所以,基於select實現一個百萬級別的併發訪問是很難實現的。函數
epoll模型
因爲epoll和上面的實現機制徹底不一樣,因此上面的問題將在epoll中不存在。在select/poll中,服務器進程每次調用select都須要把這100萬個鏈接告訴操做系統(從用戶態拷貝到內核態)。讓操做系統檢測這些套接字是否有時間發生。輪詢完以後,再將這些句柄數據複製到操做系統中,讓服務器進程輪詢處理已發生的網絡時間。這一過程耗時耗力,而epoll經過在linux申請一個建議的文件系統,把select調用分爲了三部分。性能
1)調用epoll_create創建一個epoll對象,這個對象包含了一個紅黑樹和一個雙向鏈表。並與底層創建回調機制。
2)調用epoll_ctl向epoll對象中添加這100萬個鏈接的套接字
3)調用epoll_wait收集發生事件的鏈接。
從上面的調用方式就能夠看到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的高效就在於,當咱們調用epoll_ctl往裏塞入百萬個句柄時,epoll_wait仍然能夠飛快的返回,並有效的將發生事件的句柄給咱們用戶。這是因爲咱們在調用epoll_create時,內核除了幫咱們在epoll文件系統裏建了個file結點,在內核cache裏建了個紅黑樹用於存儲之後epoll_ctl傳來的socket外,還會再創建一個list鏈表,用於存儲準備就緒的事件,當epoll_wait調用時,僅僅觀察這個list鏈表裏有沒有數據便可。有數據就返回,沒有數據就sleep,等到timeout時間到後即便鏈表沒數據也返回。因此,epoll_wait很是高效。並且,一般狀況下即便咱們要監控百萬計的句柄,大多一次也只返回不多量的準備就緒句柄而已,因此,epoll_wait僅須要從內核態copy少許的句柄到用戶態而已,如何能不高效?!
就緒list鏈表維護
那麼,這個準備就緒list鏈表是怎麼維護的呢?當咱們執行epoll_ctl時,除了把socket放到epoll文件系統裏file對象對應的紅黑樹上以外,還會給內核中斷處理程序註冊一個回調函數,告訴內核,若是這個句柄的中斷到了,就把它放到準備就緒list鏈表裏。因此,當一個socket上有數據到了,內核在把網卡上的數據copy到內核中後就來把socket插入到準備就緒鏈表裏了。
如此,一顆紅黑樹,一張準備就緒句柄鏈表,少許的內核cache,就幫咱們解決了大併發下的socket處理問題。執行epoll_create時,建立了紅黑樹和就緒鏈表,執行epoll_ctl時,若是增長socket句柄,則檢查在紅黑樹中是否存在,存在當即返回,不存在則添加到樹幹上,而後向內核註冊回調函數,用於當中斷事件來臨時向準備就緒鏈表中插入數據。執行epoll_wait時馬上返回準備就緒鏈表裏的數據便可。
兩種模式LT和ET
最後看看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返回的。
其中涉及到的數據結構:
epoll用kmem_cache_create(slab分配器)分配內存用來存放struct epitem和struct eppoll_entry。
當向系統中添加一個fd時,就建立一個epitem結構體,這是內核管理epoll的基本數據結構:
struct epitem { struct rb_node rbn; //用於主結構管理的紅黑樹 struct list_head rdllink; //事件就緒隊列 struct epitem *next; //用於主結構體中的鏈表 struct epoll_filefd ffd; //這個結構體對應的被監聽的文件描述符信息 int nwait; //poll操做中事件的個數 struct list_head pwqlist; //雙向鏈表,保存着被監視文件的等待隊列,功能相似於select/poll中的poll_table struct eventpoll *ep; //該項屬於哪一個主結構體(多個epitm從屬於一個eventpoll) struct list_head fllink; //雙向鏈表,用來連接被監視的文件描述符對應的struct file。由於file裏有f_ep_link,用來保存全部監視這個文件的epoll節點 struct epoll_event event; //註冊的感興趣的事件,也就是用戶空間的epoll_event }
而每一個epoll fd(epfd)對應的主要數據結構爲:
struct eventpoll { spin_lock_t lock; //對本數據結構的訪問 struct mutex mtx; //防止使用時被刪除 wait_queue_head_t wq; //sys_epoll_wait()使用的等待隊列 wait_queue_head_t poll_wait; //file->poll()使用的等待隊列 struct list_head rdllist; //事件知足條件的鏈表 /*雙鏈表中則存放着將要經過epoll_wait返回給用戶的知足條件的事件*/ struct rb_root rbr; //用於管理全部fd的紅黑樹(樹根)/*紅黑樹的根節點,這顆樹中存儲着全部添加到epoll中的須要監控的事件*/ struct epitem *ovflist; //將事件到達的fd進行連接起來發送至用戶空間 }
struct eventpoll在epoll_create時建立。
這樣說來,內核中維護了一棵紅黑樹,大體的結構以下:
總體而言: