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