select和epoll的前世今生
了解IO多路復(fù)用應(yīng)該對epoll和select不陌生吧。首先,select是有缺陷的,就是當(dāng)事件發(fā)生(調(diào)用select)的時候,都需要在用戶態(tài)和內(nèi)核態(tài)之間拷貝fd數(shù)組,要知道用戶態(tài)和內(nèi)核態(tài)之間進行內(nèi)存的拷貝是非常昂貴的,如果有上萬級別的并發(fā)網(wǎng)絡(luò)需要處理的時候,服務(wù)器根本處理不來。這時候,Linux內(nèi)核的開發(fā)者應(yīng)該算是簡單又粗暴的增加了一個內(nèi)核調(diào)用,就是epoll了,有時候簡單粗暴的東西還是能提高效率的。先來看select接口:
int select (int maxfd + 1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval * timeout);
select是用來等待fd狀態(tài)的改變,核心就是定義一組fds,如果fds中的某一個fd的狀態(tài)改變(比如變得可讀、可寫、或者異常等),select就會從等待中返回。
可以理解為這個東西必須要靠一個fd的改變才能讓系統(tǒng)調(diào)用去等待,先別思維跳躍,我們一步一步的分析下去,它的手段我覺得肯定是讓這個系統(tǒng)調(diào)用等在一個等待隊列wait_queue上,在不需要執(zhí)行任務(wù)的時候,我們就讓任務(wù)進程休眠,直到條件改變時,我們再喚醒它。
通俗的說就是:你是餐飲店里唯一的一個的服務(wù)員,當(dāng)?shù)昀餂]有顧客或者有顧客但是沒有請求的時候,你處于空閑狀態(tài),就可以做點自己的事情(比如玩玩手機),當(dāng)有顧客來有需求的時候你再過去服務(wù)。
如果店里來了10個顧客,有10個顧客(10個fd)都需要監(jiān)控處理,哪個顧客有請求就要立即去處理,我們先拋開內(nèi)核是怎么實現(xiàn)的,這時候能想到有兩種辦法:
-
輪詢,但是輪詢就會占用無效的輪詢時間。
-
不輪詢,不輪詢那只能同步等待,如果要保證每一個顧客(fd)的請求都能做到立即處理,就需要安排十個服務(wù)員(10個線程),每個服務(wù)員(線程)分別對應(yīng)一個顧客(fd)。
招10個服務(wù)員對老板來說是需要成本的,所以創(chuàng)建10個線程也是需要成本的。
如果你有兩個核,那么創(chuàng)建10個線程毫無意義,大家都知道線程是有時間片的,如果某一個fd的改變?nèi)ヌ幚碇惶幚淼揭话?,這時候這個線程的時間片用完了,就會切換到另一個線程執(zhí)行,這個切換不僅增加了成本,而且毫無意義。
還不如只創(chuàng)建兩個線程,每個線程只處理一組fds中的一半,處理完一個請求,再去處理另一個請求。不過如果是在用戶態(tài)是做不了這件事的,只有調(diào)度器去搞定。這樣你就只能等待在多個fd上,哪個fd請求,就去處理哪一個,處理完再去看看有沒有下一個fd需要請求。
然而,如果隨著fd的數(shù)量的不斷增加,效率就會變得越來越低。
總之,對于select,應(yīng)該沒有什么好辦法了,應(yīng)該只能做到這樣了,如果你覺得可能某一天,select實現(xiàn)了更高效的算法呢?
我覺得應(yīng)該不會的,select接口已經(jīng)那樣了。我們只能接受select這個接口的缺陷,明明知道會帶來限制,我們就知道去規(guī)避這個缺陷,知道什么情況下使用它。
再來看看epoll接口:
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)
從接口看,和select接口幾乎差不多的,區(qū)別主要是select主要是線性遍歷fd數(shù)組去找就緒的fd,而epoll是把就緒的fd(epollfd)放在一個鏈表里,不需要遍歷全部fd,這樣就減少了不少開銷。
我們來簡單想一下:把原來select的大部分接口封裝在epoll上,其實不是很難,epoll需要調(diào)用epoll_create創(chuàng)建epollfd,那么我們改成select自動創(chuàng)建epollfd,然后調(diào)用epoll_ctl把數(shù)組的fds設(shè)置進去,然后調(diào)用epoll_wait就可以了。
當(dāng)然我只是簡單想一下而已,初衷是想告訴大家:
我們不能只想著別人把接口寫好了,然后我們往上一套,可以用,然后就覺得挺好的,這樣我們只能跟在別人屁股后面。
再從內(nèi)核的角度我們簡單想一下:一開始應(yīng)該會想到epoll和select應(yīng)該是復(fù)用同一個內(nèi)核的吧。實際上,它們都是獨立的,一個在fs/select.c中實現(xiàn),一個在fs/eventpoll.c中實現(xiàn)。
整體來看,select和epoll本質(zhì)是一個東西,epoll有一個比較明顯的改進是增加了兩個對文件描述符的操作的模式:水平觸發(fā)(LT:level trigger)和邊緣觸發(fā)(ET:edge trigger)。
現(xiàn)在,對于select和epoll就會形成一種理解:epoll是對select的升級,在fds比較多的情況下,優(yōu)先考慮使用epoll。
分享一個很久以前看過一篇文章里的內(nèi)容,里面說epoll設(shè)計的并不好,像是個補丁,功能太專一,只是簡單粗暴的增加了一個內(nèi)核調(diào)用,沒有從整個架構(gòu)上考慮,所以內(nèi)核開發(fā)者重新考量了epoll開發(fā)出來之前真正的需求是什么,后面就意識到其實真正的需求是一種內(nèi)核態(tài)到用戶態(tài)之間的事件通知機制,后面就給出了一個解決方案,用戶程序不但可以監(jiān)聽網(wǎng)絡(luò)請求時間,還可以監(jiān)聽像文件修改等各種內(nèi)核事件,后面這個方案也被3大BSD和蘋果的 Mac OSX 內(nèi)核所采用。
當(dāng)我們分析epoll和select的時候,我們不能直接跳躍到內(nèi)核看是怎么實現(xiàn)的,應(yīng)該看它的整個邏輯來分析,腦子里要形成一些疑問,就比如select已經(jīng)存在的缺陷是什么?但是又有什么好處?epoll為什么改進?改進了是不更好了?還有沒有值得優(yōu)化的地方?通過整個分析理解下來就能更加了解epoll和select。