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