Linux 下的五種 IO 模型詳細(xì)介紹
概念說(shuō)明
用戶空間與內(nèi)核空間
現(xiàn)在操作系統(tǒng)都是采用虛擬存儲(chǔ)器,那么對(duì)32位操作系統(tǒng)而言,它的尋址空間(虛擬存儲(chǔ)空間)為4G(2的32次方)。操作系統(tǒng)的核心是內(nèi)核,獨(dú)立于普通的應(yīng)用程序,可以訪問(wèn)受保護(hù)的內(nèi)存空間,也有訪問(wèn)底層硬件設(shè)備的所有權(quán)限。為了保證用戶進(jìn)程不能直接操作內(nèi)核(kernel),保證內(nèi)核的安全,操作系統(tǒng)將虛擬空間劃分為兩部分,一部分為內(nèi)核空間,一部分為用戶空間。針對(duì)linux操作系統(tǒng)而言,將最高的1G字節(jié)(從虛擬地址0xC0000000到0xFFFFFFFF),供內(nèi)核使用,稱為內(nèi)核空間,而將較低的3G字節(jié)(從虛擬地址0×00000000到0xBFFFFFFF),供各個(gè)進(jìn)程使用,稱為用戶空間。
進(jìn)程切換
為了控制進(jìn)程的執(zhí)行,內(nèi)核必須有能力掛起正在CPU上運(yùn)行的進(jìn)程,并恢復(fù)以前掛起的某個(gè)進(jìn)程的執(zhí)行。這種行為被稱為進(jìn)程切換。因此可以說(shuō),任何進(jìn)程都是在操作系統(tǒng)內(nèi)核的支持下運(yùn)行的,是與內(nèi)核緊密相關(guān)的。
從一個(gè)進(jìn)程的運(yùn)行轉(zhuǎn)到另一個(gè)進(jìn)程上運(yùn)行,這個(gè)過(guò)程中經(jīng)過(guò)下面這些變化:
- 保存處理機(jī)上下文,包括程序計(jì)數(shù)器和其他寄存器。
- 更新PCB信息。
- 把進(jìn)程的PCB移入相應(yīng)的隊(duì)列,如就緒、在某事件阻塞等隊(duì)列。 選擇另一個(gè)進(jìn)程執(zhí)行,并更新其PCB。
- 更新內(nèi)存管理的數(shù)據(jù)結(jié)構(gòu)。
- 恢復(fù)處理機(jī)上下文。
進(jìn)程的阻塞
正在執(zhí)行的進(jìn)程,由于期待的某些事件未發(fā)生,如請(qǐng)求系統(tǒng)資源失敗、等待某種操作的完成、新數(shù)據(jù)尚未到達(dá)或無(wú)新工作做等,則由系統(tǒng)自動(dòng)執(zhí)行阻塞原語(yǔ)(Block),使自己由運(yùn)行狀態(tài)變?yōu)樽枞麪顟B(tài)??梢?jiàn),進(jìn)程的阻塞是進(jìn)程自身的一種主動(dòng)行為,也因此只有處于運(yùn)行態(tài)的進(jìn)程(獲得CPU),才可能將其轉(zhuǎn)為阻塞狀態(tài)。當(dāng)進(jìn)程進(jìn)入阻塞狀態(tài),是不占用CPU資源的。
文件描述符
文件描述符(File descriptor)是計(jì)算機(jī)科學(xué)中的一個(gè)術(shù)語(yǔ),是一個(gè)用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一個(gè)非負(fù)整數(shù)。實(shí)際上,它是一個(gè)索引值,指向內(nèi)核為每一個(gè)進(jìn)程所維護(hù)的該進(jìn)程打開(kāi)文件的記錄表。當(dāng)程序打開(kāi)一個(gè)現(xiàn)有文件或者創(chuàng)建一個(gè)新文件時(shí),內(nèi)核向進(jìn)程返回一個(gè)文件描述符。在程序設(shè)計(jì)中,一些涉及底層的程序編寫往往會(huì)圍繞著文件描述符展開(kāi)。但是文件描述符這一概念往往只適用于UNIX、Linux這樣的操作系統(tǒng)。
緩存 IO
緩存 IO 又被稱作標(biāo)準(zhǔn) IO,大多數(shù)文件系統(tǒng)的默認(rèn) IO 操作都是緩存 IO。在 Linux 的緩存 IO 機(jī)制中,操作系統(tǒng)會(huì)將 IO 的數(shù)據(jù)緩存在文件系統(tǒng)的頁(yè)緩存( page cache )中,也就是說(shuō),數(shù)據(jù)會(huì)先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會(huì)從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間。
緩存 IO 的缺點(diǎn):
數(shù)據(jù)在傳輸過(guò)程中需要在應(yīng)用程序地址空間和內(nèi)核進(jìn)行多次數(shù)據(jù)拷貝操作,這些數(shù)據(jù)拷貝操作所帶來(lái)的 CPU 以及內(nèi)存開(kāi)銷是非常大的。
同步與異步 & 阻塞與非阻塞
在進(jìn)行網(wǎng)絡(luò)編程時(shí),我們常常見(jiàn)到同步(Sync)/異步(Async),阻塞(Block)/非阻塞(Unblock)四種調(diào)用方式,先理解一些概念性的東西。
1.同步與異步
同步與異步同步和異步關(guān)注的是消息通信機(jī)制 (synchronous communication/ asynchronous communication)所謂同步,就是在發(fā)出一個(gè)調(diào)用時(shí),在沒(méi)有得到結(jié)果之前,該調(diào)用就不返回。但是一旦調(diào)用返回,就得到返回值了。換句話說(shuō),就是由調(diào)用者主動(dòng)等待這個(gè)調(diào)用的結(jié)果。
而異步則是相反,調(diào)用在發(fā)出之后,這個(gè)調(diào)用就直接返回了,所以沒(méi)有返回結(jié)果。換句話說(shuō),當(dāng)一個(gè)異步過(guò)程調(diào)用發(fā)出后,調(diào)用者不會(huì)立刻得到結(jié)果。而是在調(diào)用發(fā)出后,被調(diào)用者通過(guò)狀態(tài)、通知來(lái)通知調(diào)用者,或通過(guò)回調(diào)函數(shù)處理這個(gè)調(diào)用。
典型的異步編程模型比如Node.js。
2016.4.17更新:
POSIX對(duì)這兩個(gè)術(shù)語(yǔ)的定義:
同步I/O操作:導(dǎo)致請(qǐng)求進(jìn)程阻塞,直到I/O操作完成
異步I/O操作:不導(dǎo)致請(qǐng)求進(jìn)程阻塞
2. 阻塞與非阻塞
阻塞和非阻塞關(guān)注的是程序在等待調(diào)用結(jié)果(消息,返回值)時(shí)的狀態(tài)。
阻塞調(diào)用是指調(diào)用結(jié)果返回之前,當(dāng)前線程會(huì)被掛起。調(diào)用線程只有在得到結(jié)果之后才會(huì)返回。非阻塞調(diào)用指在不能立刻得到結(jié)果之前,該調(diào)用不會(huì)阻塞當(dāng)前線程。
關(guān)于阻塞/非阻塞 & 同步/異步更加形象的比喻
老張愛(ài)喝茶,廢話不說(shuō),煮開(kāi)水。 出場(chǎng)人物:老張,水壺兩把(普通水壺,簡(jiǎn)稱水壺;會(huì)響的水壺,簡(jiǎn)稱響水壺)。
1. 老張把水壺放到火上,立等水開(kāi)。(同步阻塞) 老張覺(jué)得自己有點(diǎn)傻
2. 老張把水壺放到火上,去客廳看電視,時(shí)不時(shí)去廚房看看水開(kāi)沒(méi)有。(同步非阻塞) 老張還是覺(jué)得自己有點(diǎn)傻,于是變高端了,買了把會(huì)響笛的那種水壺。水開(kāi)之后,能大聲發(fā)出嘀~~~~的噪音。
3. 老張把響水壺放到火上,立等水開(kāi)。(異步阻塞) 老張覺(jué)得這樣傻等意義不大
4. 老張把響水壺放到火上,去客廳看電視,水壺響之前不再去看它了,響了再去拿壺。(異步非阻塞) 老張覺(jué)得自己聰明了。
所謂同步異步,只是對(duì)于水壺而言。普通水壺,同步;響水壺,異步。雖然都能干活,但響水壺可以在自己完工之后,提示老張水開(kāi)了。這是普通水壺所不能及的。同步只能讓調(diào)用者去輪詢自己(情況2中),造成老張效率的低下。
所謂阻塞非阻塞,僅僅對(duì)于老張而言。立等的老張,阻塞;看視的老張,非阻塞。情況1和情況3中老張就是阻塞的,媳婦喊他都不知道。雖然3中響水壺是異步的,可對(duì)于立等的老張沒(méi)有太大的意義。所以一般異步是配合非阻塞使用的,這樣才能發(fā)揮異步的效用。
Linux下的五種IO模型
- 阻塞IO(blocking IO)
- 非阻塞IO (nonblocking IO)
- IO復(fù)用(select 和poll) (IO multiplexing)
- 信號(hào)驅(qū)動(dòng)IO (signal driven IO (SIGIO))
- 異步IO (asynchronous IO (the POSIX aio_functions))
前四種都是同步,只有最后一種才是異步IO。
阻塞IO模型
在這個(gè)模型中,應(yīng)用程序(application)為了執(zhí)行這個(gè)read操作,會(huì)調(diào)用相應(yīng)的一個(gè)system call,將系統(tǒng)控制權(quán)交給kernel,然后就進(jìn)行等待(這其實(shí)就是被阻塞了)。kernel開(kāi)始執(zhí)行這個(gè)system call,執(zhí)行完畢后會(huì)向應(yīng)用程序返回響應(yīng),應(yīng)用程序得到響應(yīng)后,就不再阻塞,并進(jìn)行后面的工作。
非阻塞IO
在linux下,應(yīng)用程序可以通過(guò)設(shè)置文件描述符的屬性O(shè)_NONBLOCK,IO操作可以立即返回,但是并不保證IO操作成功。也就是說(shuō),當(dāng)應(yīng)用程序設(shè)置了O_NONBLOCK之后,執(zhí)行write操作,調(diào)用相應(yīng)的system call,這個(gè)system call會(huì)從內(nèi)核中立即返回。但是在這個(gè)返回的時(shí)間點(diǎn),數(shù)據(jù)可能還沒(méi)有被真正的寫入到指定的地方。也就是說(shuō),kernel只是很快的返回了這個(gè) system call(只有立馬返回,應(yīng)用程序才不會(huì)被這個(gè)IO操作blocking),但是這個(gè)system call具體要執(zhí)行的事情(寫數(shù)據(jù))可能并沒(méi)有完成。而對(duì)于應(yīng)用程序,雖然這個(gè)IO操作很快就返回了,但是它并不知道這個(gè)IO操作是否真的成功了,為了知道IO操作是否成功,一般有兩種策略:一是需要應(yīng)用程序主動(dòng)地循環(huán)地去問(wèn)kernel(這種方法就是同步非阻塞IO);二是采用IO通知機(jī)制,比如:IO多路復(fù)用(這種方法屬于異步阻塞IO)或信號(hào)驅(qū)動(dòng)IO(這種方法屬于異步非阻塞IO)。
IO多路復(fù)用(異步阻塞IO)
和之前一樣,應(yīng)用程序要執(zhí)行read操作,因此調(diào)用一個(gè)system call,這個(gè)system call被傳遞給了kernel。但在應(yīng)用程序這邊,它調(diào)用system call之后,并不等待kernel的返回結(jié)果而是立即返回,雖然立即返回的調(diào)用函數(shù)是一個(gè)異步的方式,但應(yīng)用程序會(huì)被像select()、poll和epoll等具有復(fù)用多個(gè)文件描述符的函數(shù)阻塞住,一直等到這個(gè)system call有結(jié)果返回了,再通知應(yīng)用程序。也就是說(shuō),“在這種模型中,IO函數(shù)是非阻塞的,使用阻塞 select、poll、epoll系統(tǒng)調(diào)用來(lái)確定一個(gè) 或多個(gè)IO 描述符何時(shí)能操作?!彼?,從IO操作的實(shí)際效果來(lái)看,異步阻塞IO和第一種同步阻塞IO是一樣的,應(yīng)用程序都是一直等到IO操作成功之后(數(shù)據(jù)已經(jīng)被寫入或者讀?。?,才開(kāi)始進(jìn)行下面的工作。不同點(diǎn)在于異步阻塞IO用一個(gè)select函數(shù)可以為多個(gè)描述符提供通知,提高了并發(fā)性。舉個(gè)例子:假如有一萬(wàn)個(gè)并發(fā)的read請(qǐng)求,但是網(wǎng)絡(luò)上仍然沒(méi)有數(shù)據(jù),此時(shí)這一萬(wàn)個(gè)read會(huì)同時(shí)各自阻塞,現(xiàn)在用select、poll、epoll這樣的函數(shù)來(lái)專門負(fù)責(zé)阻塞同時(shí)監(jiān)聽(tīng)這一萬(wàn)個(gè)請(qǐng)求的狀態(tài),一旦有數(shù)據(jù)到達(dá)了就負(fù)責(zé)通知,這樣就將之前一萬(wàn)個(gè)的各自為戰(zhàn)的等待與阻塞轉(zhuǎn)為一個(gè)專門的函數(shù)來(lái)負(fù)責(zé)與管理。與此同時(shí),異步阻塞IO和第二種同步非阻塞IO的區(qū)別在于:同步非阻塞IO是需要應(yīng)用程序主動(dòng)地循環(huán)去詢問(wèn)是否有操作數(shù)據(jù)可操作,而異步阻塞IO是通過(guò)像select和poll等這樣的IO多路復(fù)用函數(shù)來(lái)同時(shí)檢測(cè)多個(gè)事件句柄來(lái)告知應(yīng)用程序是否可以有數(shù)據(jù)操作。
信號(hào)驅(qū)動(dòng)IO (signal driven IO (SIGIO))
應(yīng)用程序提交read請(qǐng)求的system call,然后,kernel開(kāi)始處理相應(yīng)的IO操作,而同時(shí),應(yīng)用程序并不等kernel返回響應(yīng),就會(huì)開(kāi)始執(zhí)行其他的處理操作(應(yīng)用程序沒(méi)有被IO操作所阻塞)。當(dāng)kernel執(zhí)行完畢,返回read的響應(yīng),就會(huì)產(chǎn)生一個(gè)信號(hào)或執(zhí)行一個(gè)基于線程的回調(diào)函數(shù)來(lái)完成這次 IO 處理過(guò)程。
從理論上說(shuō),阻塞IO、IO復(fù)用和信號(hào)驅(qū)動(dòng)的IO都是同步IO模型。因?yàn)樵谶@三種模型中,IO的讀寫操作都是在IO事件發(fā)生之后由應(yīng)用程序來(lái)完成。而POSIX規(guī)范所定義的異步IO模型則不同。對(duì)異步IO而言,用戶可以直接對(duì)IO執(zhí)行讀寫操作,這些操作告訴內(nèi)核用戶讀寫緩沖區(qū)的位置,以及IO操作完成后內(nèi)核通知應(yīng)用程序的方式。異步IO讀寫操作總是立即返回,而不論IO是否阻塞的,因?yàn)檎嬷鞯淖x寫操作已經(jīng)由內(nèi)核接管。也就是說(shuō),同步IO模型要求用戶代碼自行執(zhí)行IO操作(將數(shù)據(jù)從內(nèi)核緩沖區(qū)讀入用戶緩沖區(qū),或?qū)?shù)據(jù)從用戶緩沖區(qū)寫入內(nèi)核緩沖區(qū)),而異步IO機(jī)制則是由內(nèi)核來(lái)執(zhí)行IO操作(數(shù)據(jù)在內(nèi)核緩沖區(qū)和用戶緩沖區(qū)之間的移動(dòng)是由內(nèi)核在后臺(tái)完成的)。你可以這樣認(rèn)為,同步IO向應(yīng)用程序通知的是IO就緒事件,而異步IO向應(yīng)用程序通知的是IO完成事件。linux環(huán)境下,aio.h頭文件中定義的函數(shù)提供了對(duì)異步IO的支持。
異步IO (asynchronous IO (the POSIX aio_functions))
異步IO與上面的異步概念是一樣的, 當(dāng)一個(gè)異步過(guò)程調(diào)用發(fā)出后,調(diào)用者不能立刻得到結(jié)果,實(shí)際處理這個(gè)調(diào)用的函數(shù)在完成后,通過(guò)狀態(tài)、通知和回調(diào)來(lái)通知調(diào)用者的輸入輸出操作。異步IO的工作機(jī)制是:告知內(nèi)核啟動(dòng)某個(gè)操作,并讓內(nèi)核在整個(gè)操作完成后通知我們,這種模型與信號(hào)驅(qū)動(dòng)的IO區(qū)別在于,信號(hào)驅(qū)動(dòng)IO是由內(nèi)核通知我們何時(shí)可以啟動(dòng)一個(gè)IO操作,這個(gè)IO操作由用戶自定義的信號(hào)函數(shù)來(lái)實(shí)現(xiàn),而異步IO模型是由內(nèi)核告知我們IO操作何時(shí)完成。為了實(shí)現(xiàn)異步IO,專門定義了一套以aio開(kāi)頭的API,如:aio_read.
小結(jié):前四種模型–阻塞IO、非阻塞IO、多路復(fù)用IO和信號(hào)驅(qū)動(dòng)IO都屬于同步模式,因?yàn)槠渲姓嬲腎O操作(函數(shù))都將會(huì)阻塞進(jìn)程,只有異步IO模型真正實(shí)現(xiàn)了IO操作的異步性。
IO復(fù)用
為了解釋這個(gè)名詞,首先來(lái)理解下復(fù)用這個(gè)概念,復(fù)用也就是共用的意思,這樣理解還是有些抽象,為此,咱們來(lái)理解下復(fù)用在通信領(lǐng)域的使用,在通信領(lǐng)域中為了充分利用網(wǎng)絡(luò)連接的物理介質(zhì),往往在同一條網(wǎng)絡(luò)鏈路上采用時(shí)分復(fù)用或頻分復(fù)用的技術(shù)使其在同一鏈路上傳輸多路信號(hào),到這里我們就基本上理解了復(fù)用的含義,即公用某個(gè)“介質(zhì)”來(lái)盡可能多的做同一類(性質(zhì))的事,那IO復(fù)用的“介質(zhì)”是什么呢?為此我們首先來(lái)看看服務(wù)器編程的模型,客戶端發(fā)來(lái)的請(qǐng)求服務(wù)端會(huì)產(chǎn)生一個(gè)進(jìn)程來(lái)對(duì)其進(jìn)行服務(wù),每當(dāng)來(lái)一個(gè)客戶請(qǐng)求就產(chǎn)生一個(gè)進(jìn)程來(lái)服務(wù),然而進(jìn)程不可能無(wú)限制的產(chǎn)生,因此為了解決大量客戶端訪問(wèn)的問(wèn)題,引入了IO復(fù)用技術(shù),即:一個(gè)進(jìn)程可以同時(shí)對(duì)多個(gè)客戶請(qǐng)求進(jìn)行服務(wù)。也就是說(shuō)IO復(fù)用的“介質(zhì)”是進(jìn)程(準(zhǔn)確的說(shuō)復(fù)用的是select和poll,因?yàn)檫M(jìn)程也是靠調(diào)用select和poll來(lái)實(shí)現(xiàn)的),復(fù)用一個(gè)進(jìn)程(select和poll)來(lái)對(duì)多個(gè)IO進(jìn)行服務(wù),雖然客戶端發(fā)來(lái)的IO是并發(fā)的但是IO所需的讀寫數(shù)據(jù)多數(shù)情況下是沒(méi)有準(zhǔn)備好的,因此就可以利用一個(gè)函數(shù)(select和poll)來(lái)監(jiān)聽(tīng)I(yíng)O所需的這些數(shù)據(jù)的狀態(tài),一旦IO有數(shù)據(jù)可以進(jìn)行讀寫了,進(jìn)程就來(lái)對(duì)這樣的IO進(jìn)行服務(wù)。
理解完IO復(fù)用后,我們?cè)趤?lái)看下實(shí)現(xiàn)IO復(fù)用中的三個(gè)API(select、poll和epoll)的區(qū)別和聯(lián)系,select,poll,epoll都是IO多路復(fù)用的機(jī)制,IO多路復(fù)用就是通過(guò)一種機(jī)制,可以監(jiān)視多個(gè)描述符,一旦某個(gè)描述符就緒(一般是讀就緒或者寫就緒),能夠通知應(yīng)用程序進(jìn)行相應(yīng)的讀寫操作。但select,poll,epoll本質(zhì)上都是同步IO,因?yàn)樗麄兌夹枰谧x寫事件就緒后自己負(fù)責(zé)進(jìn)行讀寫,也就是說(shuō)這個(gè)讀寫過(guò)程是阻塞的,而異步IO則無(wú)需自己負(fù)責(zé)進(jìn)行讀寫,異步IO的實(shí)現(xiàn)會(huì)負(fù)責(zé)把數(shù)據(jù)從內(nèi)核拷貝到用戶空間。三者的原型如下所示:
- int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
select
select的第一個(gè)參數(shù)nfds為fdset集合中最大描述符值加1,fdset是一個(gè)位數(shù)組,其大小限制為_(kāi)_FD_SETSIZE(1024),位數(shù)組的每一位代表其對(duì)應(yīng)的描述符是否需要被檢查。第二三四參數(shù)表示需要關(guān)注讀、寫、錯(cuò)誤事件的文件描述符位數(shù)組,這些參數(shù)既是輸入?yún)?shù)也是輸出參數(shù),可能會(huì)被內(nèi)核修改用于標(biāo)示哪些描述符上發(fā)生了關(guān)注的事件,所以每次調(diào)用select前都需要重新初始化fdset。timeout參數(shù)為超時(shí)時(shí)間,該結(jié)構(gòu)會(huì)被內(nèi)核修改,其值為超時(shí)剩余的時(shí)間。
select的調(diào)用步驟如下:
- 使用copy_from_user從用戶空間拷貝fdset到內(nèi)核空間
- 注冊(cè)回調(diào)函數(shù)__pollwait
- 遍歷所有fd,調(diào)用其對(duì)應(yīng)的poll方法(對(duì)于socket,這個(gè)poll方法是sock_poll,sock_poll根據(jù)情況會(huì)調(diào)用到tcp_poll,udp_poll或者datagram_poll)
- 以tcp_poll為例,其核心實(shí)現(xiàn)就是__pollwait,也就是上面注冊(cè)的回調(diào)函數(shù)。
- __pollwait的主要工作就是把current(當(dāng)前進(jìn)程)掛到設(shè)備的等待隊(duì)列中,不同的設(shè)備有不同的等待隊(duì)列,對(duì)于tcp_poll 來(lái)說(shuō),其等待隊(duì)列是sk->sk_sleep(注意把進(jìn)程掛到等待隊(duì)列中并不代表進(jìn)程已經(jīng)睡眠了)。在設(shè)備收到一條消息(網(wǎng)絡(luò)設(shè)備)或填寫完文件數(shù) 據(jù)(磁盤設(shè)備)后,會(huì)喚醒設(shè)備等待隊(duì)列上睡眠的進(jìn)程,這時(shí)current便被喚醒了。
- poll方法返回時(shí)會(huì)返回一個(gè)描述讀寫操作是否就緒的mask掩碼,根據(jù)這個(gè)mask掩碼給fd_set賦值。
- 如果遍歷完所有的fd,還沒(méi)有返回一個(gè)可讀寫的mask掩碼,則會(huì)調(diào)用schedule_timeout是調(diào)用select的進(jìn)程(也就是 current)進(jìn)入睡眠。當(dāng)設(shè)備驅(qū)動(dòng)發(fā)生自身資源可讀寫后,會(huì)喚醒其等待隊(duì)列上睡眠的進(jìn)程。如果超過(guò)一定的超時(shí)時(shí)間(schedule_timeout 指定),還是沒(méi)人喚醒,則調(diào)用select的進(jìn)程會(huì)重新被喚醒獲得CPU,進(jìn)而重新遍歷fd,判斷有沒(méi)有就緒的fd。
- 把fd_set從內(nèi)核空間拷貝到用戶空間。
總結(jié)下select的幾大缺點(diǎn):
(1)每次調(diào)用select,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個(gè)開(kāi)銷在fd很多時(shí)會(huì)很大 (2)同時(shí)每次調(diào)用select都需要在內(nèi)核遍歷傳遞進(jìn)來(lái)的所有fd,這個(gè)開(kāi)銷在fd很多時(shí)也很大 (3)select支持的文件描述符數(shù)量太小了,默認(rèn)是1024
poll
poll與select不同,通過(guò)一個(gè)pollfd數(shù)組向內(nèi)核傳遞需要關(guān)注的事件,故沒(méi)有描述符個(gè)數(shù)的限制,pollfd中的events字段和revents分別用于標(biāo)示關(guān)注的事件和發(fā)生的事件,故pollfd數(shù)組只需要被初始化一次。
poll的實(shí)現(xiàn)機(jī)制與select類似,其對(duì)應(yīng)內(nèi)核中的sys_poll,只不過(guò)poll向內(nèi)核傳遞pollfd數(shù)組,然后對(duì)pollfd中的每個(gè)描述符進(jìn)行poll,相比處理fdset來(lái)說(shuō),poll效率更高。poll返回后,需要對(duì)pollfd中的每個(gè)元素檢查其revents值,來(lái)得指事件是否發(fā)生。
epoll
直到Linux2.6才出現(xiàn)了由內(nèi)核直接支持的實(shí)現(xiàn)方法,那就是epoll,被公認(rèn)為L(zhǎng)inux2.6下性能最好的多路IO就緒通知方法。epoll可以同時(shí)支持水平觸發(fā)和邊緣觸發(fā)(Edge Triggered,只告訴進(jìn)程哪些文件描述符剛剛變?yōu)榫途w狀態(tài),它只說(shuō)一遍,如果我們沒(méi)有采取行動(dòng),那么它將不會(huì)再次告知,這種方式稱為邊緣觸發(fā)),理論上邊緣觸發(fā)的性能要更高一些,但是代碼實(shí)現(xiàn)相當(dāng)復(fù)雜。epoll同樣只告知那些就緒的文件描述符,而且當(dāng)我們調(diào)用epoll_wait()獲得就緒文件描述符時(shí),返回的不是實(shí)際的描述符,而是一個(gè)代表就緒描述符數(shù)量的值,你只需要去epoll指定的一個(gè)數(shù)組中依次取得相應(yīng)數(shù)量的文件描述符即可,這里也使用了內(nèi)存映射(mmap)技術(shù),這樣便徹底省掉了這些文件描述符在系統(tǒng)調(diào)用時(shí)復(fù)制的開(kāi)銷。另一個(gè)本質(zhì)的改進(jìn)在于epoll采用基于事件的就緒通知方式。在select/poll中,進(jìn)程只有在調(diào)用一定的方法后,內(nèi)核才對(duì)所有監(jiān)視的文件描述符進(jìn)行掃描,而epoll事先通過(guò)epoll_ctl()來(lái)注冊(cè)一個(gè)文件描述符,一旦基于某個(gè)文件描述符就緒時(shí),內(nèi)核會(huì)采用類似callback的回調(diào)機(jī)制,迅速激活這個(gè)文件描述符,當(dāng)進(jìn)程調(diào)用epoll_wait()時(shí)便得到通知。
epoll既然是對(duì)select和poll的改進(jìn),就應(yīng)該能避免上述的三個(gè)缺點(diǎn)。那epoll都是怎么解決的呢?在此之前,我們先看一下epoll 和select和poll的調(diào)用接口上的不同,select和poll都只提供了一個(gè)函數(shù)——select或者poll函數(shù)。而epoll提供了三個(gè)函 數(shù),epoll_create,epoll_ctl和epoll_wait,epoll_create是創(chuàng)建一個(gè)epoll句柄;epoll_ctl是注 冊(cè)要監(jiān)聽(tīng)的事件類型;epoll_wait則是等待事件的產(chǎn)生。
對(duì)于第一個(gè)缺點(diǎn),epoll的解決方案在epoll_ctl函數(shù)中。每次注冊(cè)新的事件到epoll句柄中時(shí)(在epoll_ctl中指定 EPOLL_CTL_ADD),會(huì)把所有的fd拷貝進(jìn)內(nèi)核,而不是在epoll_wait的時(shí)候重復(fù)拷貝。epoll保證了每個(gè)fd在整個(gè)過(guò)程中只會(huì)拷貝一次。
對(duì)于第二個(gè)缺點(diǎn),epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對(duì)應(yīng)的設(shè)備等待隊(duì)列中,而只在 epoll_ctl時(shí)把current掛一遍(這一遍必不可少)并為每個(gè)fd指定一個(gè)回調(diào)函數(shù),當(dāng)設(shè)備就緒,喚醒等待隊(duì)列上的等待者時(shí),就會(huì)調(diào)用這個(gè)回調(diào) 函數(shù),而這個(gè)回調(diào)函數(shù)會(huì)把就緒的fd加入一個(gè)就緒鏈表)。epoll_wait的工作實(shí)際上就是在這個(gè)就緒鏈表中查看有沒(méi)有就緒的fd(利用 schedule_timeout()實(shí)現(xiàn)睡一會(huì),判斷一會(huì)的效果,和select實(shí)現(xiàn)中的第7步是類似的)。
對(duì)于第三個(gè)缺點(diǎn),epoll沒(méi)有這個(gè)限制,它所支持的FD上限是最大可以打開(kāi)文件的數(shù)目,這個(gè)數(shù)字一般遠(yuǎn)大于2048,舉個(gè)例子, 在1GB內(nèi)存的機(jī)器上大約是10萬(wàn)左右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來(lái)說(shuō)這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。
總結(jié)
(1)select,poll實(shí)現(xiàn)需要自己不斷輪詢所有fd集合,直到設(shè)備就緒,期間可能要睡眠和喚醒多次交替。而epoll其實(shí)也需要調(diào)用 epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設(shè)備就緒時(shí),調(diào)用回調(diào)函數(shù),把就緒fd放入就緒鏈表中,并喚醒在 epoll_wait中進(jìn)入睡眠的進(jìn)程。雖然都要睡眠和交替,但是select和poll在“醒著”的時(shí)候要遍歷整個(gè)fd集合,而epoll在“醒著”的 時(shí)候只要判斷一下就緒鏈表是否為空就行了,這節(jié)省了大量的CPU時(shí)間,這就是回調(diào)機(jī)制帶來(lái)的性能提升。
(2)select,poll每次調(diào)用都要把fd集合從用戶態(tài)往內(nèi)核態(tài)拷貝一次,并且要把current往設(shè)備等待隊(duì)列中掛一次,而epoll只要 一次拷貝,而且把current往等待隊(duì)列上掛也只掛一次(在epoll_wait的開(kāi)始,注意這里的等待隊(duì)列并不是設(shè)備等待隊(duì)列,只是一個(gè)epoll內(nèi) 部定義的等待隊(duì)列),這也能節(jié)省不少的開(kāi)銷。
感謝閱讀,希望能幫助到大家,謝謝大家對(duì)本站的支持!
相關(guān)文章
linux下C語(yǔ)言實(shí)現(xiàn)寫日志功能
這篇文章給大家分享了linux下C語(yǔ)言實(shí)現(xiàn)寫日志的相關(guān)功能方法以及實(shí)例代碼,有興趣的朋友可以學(xué)習(xí)參考下。2018-07-07Linux安裝Jenkins步驟及各種問(wèn)題解決(頁(yè)面訪問(wèn)初始化密碼)
這篇文章主要介紹了Linux安裝Jenkins步驟及各種問(wèn)題頁(yè)面訪問(wèn)初始化密碼,需要的朋友可以參考下2019-12-12CentOS8 安裝 jdk8 / java8的教程(推薦)
CentOS8上使用 yum 直接安裝,環(huán)境變量自動(dòng)配置好 ,本文主要給大家介紹 CentOS8 安裝 jdk8 / java8的教程,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2019-10-10Vscode遠(yuǎn)程連接Ubuntu出錯(cuò)問(wèn)題的解決方法
這篇文章主要給大家介紹了關(guān)于Vscode遠(yuǎn)程連接Ubuntu出錯(cuò)問(wèn)題的解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10Linux 初始化MySQL 數(shù)據(jù)庫(kù)報(bào)錯(cuò)解決辦法
這篇文章主要介紹了Linux 初始化MySQL 數(shù)據(jù)庫(kù)報(bào)錯(cuò)解決辦法的相關(guān)資料,需要的朋友可以參考下2017-05-05