深入理解?Linux?的?epoll?機制
時間:2021-08-19 16:29:22
手機看文章
掃描二維碼
隨時隨地手機看文章
[導(dǎo)讀]堅持思考,就會很酷在Linux系統(tǒng)之中有一個核心武器:epoll池,在高并發(fā)的,高吞吐的IO系統(tǒng)中常常見到epoll的身影。IO多路復(fù)用在Go里最核心的是Goroutine,也就是所謂的協(xié)程,協(xié)程最妙的一個實現(xiàn)就是異步的代碼長的跟同步代碼一樣。比如在Go中,網(wǎng)絡(luò)IO的read,w...
在 Linux 系統(tǒng)之中有一個核心武器:epoll 池,在高并發(fā)的,高吞吐的 IO 系統(tǒng)中常常見到 epoll 的身影。
IO 多路復(fù)用
在 Go 里最核心的是 Goroutine ,也就是所謂的協(xié)程,協(xié)程最妙的一個實現(xiàn)就是異步的代碼長的跟同步代碼一樣。比如在 Go 中,網(wǎng)絡(luò) IO 的
read
,write
看似都是同步代碼,其實底下都是異步調(diào)用,一般流程是:write?(?/*?IO?參數(shù)?*/?)
????請求入隊
????等待完成
????
?后臺?loop?程序
????發(fā)送網(wǎng)絡(luò)請求
????喚醒業(yè)務(wù)方
Go 配合協(xié)程在網(wǎng)絡(luò) IO 上實現(xiàn)了異步流程的代碼同步化。核心就是用 epoll 池來管理網(wǎng)絡(luò) fd 。實現(xiàn)形式上,后臺的程序只需要 1 個就可以負責管理多個 fd 句柄,負責應(yīng)對所有的業(yè)務(wù)方的 IO 請求。這種一對多的 IO 模式我們就叫做 IO 多路復(fù)用。多路是指?多個業(yè)務(wù)方(句柄)并發(fā)下來的 IO 。復(fù)用是指?復(fù)用這一個后臺處理程序。站在 IO 系統(tǒng)設(shè)計人員的角度,業(yè)務(wù)方咱們沒辦法提要求,因為業(yè)務(wù)是上帝,只有你服從的份,他們要創(chuàng)建多個 fd,那么你就需要負責這些 fd 的處理,并且最好還要并發(fā)起來。業(yè)務(wù)方?jīng)]法提要求,那么只能要求后臺 loop 程序了!要求什么呢?快!快!快!這就是最核心的要求,處理一定要快,要給每一個 fd 通道最快的感受,要讓每一個 fd 覺得,你只在給他一個人跑腿。那有人又問了,那我一個 IO 請求(比如 write )對應(yīng)一個線程來處理,這樣所有的 IO 不都并發(fā)了嗎?是可以,但是有瓶頸,線程數(shù)一旦多了,性能是反倒會差的。這里不再對比多線程和 IO 多路復(fù)用實現(xiàn)高并發(fā)之間的區(qū)別,詳細的可以去了解下 nginx 和 redis 高并發(fā)的秘密。
我不用任何其他系統(tǒng)調(diào)用,能否實現(xiàn) IO 多路復(fù)用?可以的。那么寫個
for
循環(huán),每次都嘗試 IO 一下,讀/寫到了就處理,讀/寫不到就 sleep
下。這樣我們不就實現(xiàn)了 1 對多的 IO 多路復(fù)用嘛。while?True:
????for?each?句柄數(shù)組?{
????????read/write(fd,?/*?參數(shù)?*/)
????}
????sleep(1s)
慢著,有個問題,上面的程序可能會被卡死在第三行,使得整個系統(tǒng)不得運行,為什么?默認情況下,我們?create
出的句柄是阻塞類型的。我們讀數(shù)據(jù)的時候,如果數(shù)據(jù)還沒準備好,是會需要等待的,當我們寫數(shù)據(jù)的時候,如果還沒準備好,默認也會卡住等待。所以,在上面?zhèn)未a第三行是可能被直接卡死,而導(dǎo)致整個線程都得到不到運行。舉個例子,現(xiàn)在有 11,12,13 這 3 個句柄,現(xiàn)在 11 讀寫都沒有準備好,只要 read/write(11, /*參數(shù)*/)
就會被卡住,但 12,13 這兩個句柄都準備好了,那遍歷句柄數(shù)組 11,12,13 的時候就會卡死在前面,后面 12,13 則得不到運行。這不符合我們的預(yù)期,因為我們 IO 多路復(fù)用的 loop 線程是公共服務(wù),不能因為一個 fd 就直接癱瘓。那這個問題怎么解決?只需要把 fd 都設(shè)置成非阻塞模式。這樣 read/write
的時候,如果數(shù)據(jù)沒準備好,返回 EAGIN
的錯誤即可,不會卡住線程,從而整個系統(tǒng)就運轉(zhuǎn)起來了。比如上面句柄 11 還未就緒,那么 read/write(11, /*參數(shù)*/)
不會阻塞,只會報個 EAGIN
的錯誤,這種錯誤需要特殊處理,然后 loop 線程可以繼續(xù)執(zhí)行 12,13 的讀寫。以上就是最樸實的 IO 多路復(fù)用的實現(xiàn)了。但好像在生產(chǎn)環(huán)境沒見過這種 IO 多路復(fù)用的實現(xiàn)?為什么?因為還不夠高級。for
循環(huán)每次要定期 sleep 1s
,這個會導(dǎo)致吞吐能力極差,因為很可能在剛好要 sleep
的時候,所有的 fd 都準備好 IO 數(shù)據(jù),而這個時候卻要硬生生的等待 1s,可想而知。。。那有同學(xué)又要質(zhì)疑了,那 for
循環(huán)里面就不 sleep
嘛,這樣不就能及時處理了嗎?及時是及時了,但是 CPU 估計要跑飛了。不加 sleep
,那在沒有 fd 需要處理的時候,估計 CPU 都要跑到 100% 了。這個也是無法接受的。糾結(jié)了,那 sleep
吞吐不行,不 sleep
浪費 cpu,怎么辦?這種情況用戶態(tài)很難有所作為,只能求助內(nèi)核來提供機制協(xié)助來。因為內(nèi)核才能及時的管理這些事件的通知和調(diào)度。我們再梳理下 IO 多路復(fù)用的需求和原理。IO 多路復(fù)用就是 1 個線程處理 多個 fd 的模式。我們的要求是:這個 “1” 就要盡可能的快,避免一切無效工作,要把所有的時間都用在處理句柄的 IO 上,不能有任何空轉(zhuǎn),sleep 的時間浪費。有沒有一種工具,我們把一籮筐的 fd 放到里面,只要有一個 fd 能夠讀寫數(shù)據(jù),后臺 loop 線程就要立馬喚醒,全部馬力跑起來。其他時間要把 cpu 讓出去。能做到嗎?能,但這種需求只能內(nèi)核提供機制滿足你。?2???這事 Linux 內(nèi)核必須要給個說法?
是的,想要不用 sleep 這種辣眼睛的實現(xiàn),Linux 內(nèi)核必須出手了,畢竟 IO 的處理都是內(nèi)核之中,數(shù)據(jù)好沒好內(nèi)核最清楚。內(nèi)核一口氣提供了 3 種工具
select
,poll
,epoll
。為什么有 3 種?歷史不斷改進,矬 -> 較矬 -> 臥槽、高效
的演變而已。epoll_wait
醒來的時候就能精確的拿到就緒的 fd 數(shù)組,不需要任何測試,拿到的就是要處理的。epoll 池原理
下面我們看一下 epoll 池的使用和原理。
?1???epoll 涉及的系統(tǒng)調(diào)用
epoll 的使用非常簡單,只有下面 3 個系統(tǒng)調(diào)用。
epoll_create
epollctl
epollwait
就這?是的,就這么簡單。epollcreate
負責創(chuàng)建一個池子,一個監(jiān)控和管理句柄 fd 的池子;epollctl
負責管理這個池子里的 fd 增、刪、改;epollwait
就是負責打盹的,讓出 CPU 調(diào)度,但是只要有“事”,立馬會從這里喚醒;
?2???epoll 高效的原理
Linux 下,epoll 一直被吹爆,作為高并發(fā) IO 實現(xiàn)的秘密武器。其中原理其實非常樸實:epoll 的實現(xiàn)幾乎沒有做任何無效功。 我們從使用的角度切入來一步步分析下。首先,epoll 的第一步是創(chuàng)建一個池子。這個使用
epoll_create
來做:原型:int?epoll_create(int?size);
示例:epollfd?=?epoll_create(1024);
if?(epollfd?==?-1)?{
????perror("epoll_create");
????exit(EXIT_FAILURE);
}
這個池子對我們來說是黑盒,這個黑盒是用來裝 fd 的,我們暫不糾結(jié)其中細節(jié)。我們拿到了一個 epollfd
,這個 epollfd
就能唯一代表這個 epoll 池。注意,這里又有一個細節(jié):用戶可以創(chuàng)建多個 epoll 池。然后,我們就要往這個 epoll 池里放 fd 了,這就要用到 epoll_ctl
了原型:int?epoll_ctl(int?epfd,?int?op,?int?fd,?struct?epoll_event?*event);
示例:if?(epoll_ctl(epollfd,?EPOLL_CTL_ADD,?11,?