淺談操作系統(tǒng) IO 模式
IO

IO (Input/Output,輸入/輸出)即數(shù)據(jù)的讀取(接收)或?qū)懭?發(fā)送)操作,通常用戶進(jìn)程中的一個(gè)完整IO分為兩階段:用戶進(jìn)程空間<-->內(nèi)核空間、內(nèi)核空間<-->設(shè)備空間(磁盤(pán)、網(wǎng)絡(luò)等)。IO有內(nèi)存IO、網(wǎng)絡(luò)IO和磁盤(pán)IO三種,通常我們說(shuō)的IO指的是后兩者。
LINUX中進(jìn)程無(wú)法直接操作I/O設(shè)備,其必須通過(guò)系統(tǒng)調(diào)用請(qǐng)求kernel來(lái)協(xié)助完成I/O動(dòng)作;內(nèi)核會(huì)為每個(gè)I/O設(shè)備維護(hù)一個(gè)緩沖區(qū)。
對(duì)于一個(gè)輸入操作來(lái)說(shuō),進(jìn)程IO系統(tǒng)調(diào)用后,內(nèi)核會(huì)先看緩沖區(qū)中有沒(méi)有相應(yīng)的緩存數(shù)據(jù),沒(méi)有的話再到設(shè)備中讀取,因?yàn)樵O(shè)備IO一般速度較慢,需要等待;內(nèi)核緩沖區(qū)有數(shù)據(jù)則直接復(fù)制到進(jìn)程空間。
所以,對(duì)于一個(gè)網(wǎng)絡(luò)輸入操作通常包括兩個(gè)不同階段:
(1)等待網(wǎng)絡(luò)數(shù)據(jù)到達(dá)網(wǎng)卡→讀取到內(nèi)核緩沖區(qū),數(shù)據(jù)準(zhǔn)備好;
(2)從內(nèi)核緩沖區(qū)復(fù)制數(shù)據(jù)到進(jìn)程空間。
關(guān)鍵概念理解
- 同步:發(fā)起一個(gè)調(diào)用,得到結(jié)果才返回。
- 異步:調(diào)用發(fā)起后,調(diào)用直接返回;調(diào)用方主動(dòng)詢問(wèn)被調(diào)用方獲取結(jié)果,或被調(diào)用方通過(guò)回調(diào)函數(shù)。
- 阻塞:調(diào)用是指調(diào)用結(jié)果返回之前,當(dāng)前線程會(huì)被掛起。調(diào)用線程只有在得到結(jié)果之后才會(huì)返回。
- 非阻塞:調(diào)用指在不能立刻得到結(jié)果之前,該調(diào)用不會(huì)阻塞當(dāng)前線程。
同步才有阻塞和非阻塞之分;
阻塞與非阻塞關(guān)乎如何對(duì)待事情產(chǎn)生的結(jié)果(阻塞:不等到想要的結(jié)果我就不走了)
明確進(jìn)程狀態(tài)
理解進(jìn)程的狀態(tài)轉(zhuǎn)換
- 就緒狀態(tài) -> 運(yùn)行狀態(tài):處于就緒狀態(tài)的進(jìn)程被調(diào)度后,獲得CPU資源(分派CPU時(shí)間片),于是進(jìn)程由就緒狀態(tài)轉(zhuǎn)換為運(yùn)行狀態(tài)。
- 運(yùn)行狀態(tài) -> 就緒狀態(tài):處于運(yùn)行狀態(tài)的進(jìn)程在時(shí)間片用完后,不得不讓出CPU,從而進(jìn)程由運(yùn)行狀態(tài)轉(zhuǎn)換為就緒狀態(tài)。此外,在可剝奪的操作系統(tǒng)中,當(dāng)有更高優(yōu)先級(jí)的進(jìn)程就 、 緒時(shí),調(diào)度程度將正執(zhí)行的進(jìn)程轉(zhuǎn)換為就緒狀態(tài),讓更高優(yōu)先級(jí)的進(jìn)程執(zhí)行。
- 運(yùn)行狀態(tài) -> 阻塞狀態(tài):當(dāng)進(jìn)程請(qǐng)求某一資源(如外設(shè))的使用和分配或等待某一事件的發(fā)生(如I/O操作的完成)時(shí),它就從運(yùn)行狀態(tài)轉(zhuǎn)換為阻塞狀態(tài)。進(jìn)程以系統(tǒng)調(diào)用的形式請(qǐng)求操作系統(tǒng)提供服務(wù),這是一種特殊的、由運(yùn)行用戶態(tài)程序調(diào)用操作系統(tǒng)內(nèi)核過(guò)程的形式。
- 阻塞狀態(tài) -> 就緒狀態(tài):當(dāng)進(jìn)程等待的事件到來(lái)時(shí),如I/O操作結(jié)束或中斷結(jié)束時(shí),中斷處理程序必須把相應(yīng)進(jìn)程的狀態(tài)由阻塞狀態(tài)轉(zhuǎn)換為就緒狀態(tài)。

從操作系統(tǒng)層面執(zhí)行應(yīng)用程序理解 IO 模型
阻塞IO模型:
- 簡(jiǎn)介:進(jìn)程會(huì)一直阻塞,直到數(shù)據(jù)拷貝完成應(yīng)用程序調(diào)用一個(gè)IO函數(shù),導(dǎo)致應(yīng)用程序阻塞,等待數(shù)據(jù)準(zhǔn)備好。 如果數(shù)據(jù)沒(méi)有準(zhǔn)備好,一直等待….數(shù)據(jù)準(zhǔn)備好了,從內(nèi)核拷貝到用戶空間,IO函數(shù)返回成功指示。 我們 第一次接觸到的網(wǎng)絡(luò)編程都是從 listen()、send()、recv()等接口開(kāi)始的。使用這些接口可以很方便的構(gòu)建服務(wù)器 /客戶機(jī)的模型。
- 阻塞I/O模型圖:在調(diào)用recv()/recvfrom()函數(shù)時(shí),發(fā)生在內(nèi)核中等待數(shù)據(jù)和復(fù)制數(shù)據(jù)的過(guò)程。

當(dāng)調(diào)用recv()函數(shù)時(shí),系統(tǒng)首先查是否有準(zhǔn)備好的數(shù)據(jù)。如果數(shù)據(jù)沒(méi)有準(zhǔn)備好,那么系統(tǒng)就處于等待狀態(tài)。當(dāng)數(shù)據(jù)準(zhǔn)備好后,將數(shù)據(jù)從系統(tǒng)緩沖區(qū)復(fù)制到用戶空間,然后該函數(shù)返回。在套接應(yīng)用程序中,當(dāng)調(diào)用recv()函數(shù)時(shí),未必用戶空間就已經(jīng)存在數(shù)據(jù),那么此時(shí)recv()函數(shù)就會(huì)處于等待狀態(tài)。
阻塞模式給網(wǎng)絡(luò)編程帶來(lái)了一個(gè)很大的問(wèn)題,如在調(diào)用 send()的同時(shí),線程將被阻塞,在此期間,線程將無(wú)法執(zhí)行任何運(yùn)算或響應(yīng)任何的網(wǎng)絡(luò)請(qǐng)求。這給多客戶機(jī)、多業(yè)務(wù)邏輯的網(wǎng)絡(luò)編程帶來(lái)了挑戰(zhàn)。這時(shí),我們可能會(huì)選擇多線程的方式來(lái)解決這個(gè)問(wèn)題。
應(yīng)對(duì)多客戶機(jī)的網(wǎng)絡(luò)應(yīng)用,最簡(jiǎn)單的解決方式是在服務(wù)器端使用多線程(或多進(jìn)程)。多線程(或多進(jìn)程)的目的是讓每個(gè)連接都擁有獨(dú)立的線程(或進(jìn)程),這樣任何一個(gè)連接的阻塞都不會(huì)影響其他的連接。
具體使用多進(jìn)程還是多線程,并沒(méi)有一個(gè)特定的模式。傳統(tǒng)意義上,進(jìn)程的開(kāi)銷要遠(yuǎn)遠(yuǎn)大于線程,所以,如果需要同時(shí)為較多的客戶機(jī)提供服務(wù),則不推薦使用多進(jìn)程;如果單個(gè)服務(wù)執(zhí)行體需要消耗較多的 CPU 資源,譬如需要進(jìn)行大規(guī)?;蜷L(zhǎng)時(shí)間的數(shù)據(jù)運(yùn)算或文件訪問(wèn),則進(jìn)程較為安全。
非阻塞IO模型
- 簡(jiǎn)介:非阻塞IO通過(guò)進(jìn)程反復(fù)調(diào)用IO函數(shù)(多次系統(tǒng)調(diào)用,并馬上返回);在數(shù)據(jù)拷貝的過(guò)程中,進(jìn)程是阻塞的; 我們把一個(gè)SOCKET接口設(shè)置為非阻塞就是告訴內(nèi)核,當(dāng)所請(qǐng)求的I/O操作無(wú)法完成時(shí),不要將進(jìn)程睡眠,而是返回一個(gè)錯(cuò)誤。這樣我們的I/O操作函數(shù)將不斷的測(cè)試數(shù)據(jù)是否已經(jīng)準(zhǔn)備好,如果沒(méi)有準(zhǔn)備好,繼續(xù)測(cè)試,直到數(shù)據(jù)準(zhǔn)備好為止。在這個(gè)不斷測(cè)試的過(guò)程中,會(huì)大量的占用CPU的時(shí)間。

IO復(fù)用模型:
- 簡(jiǎn)介:IO multiplexing就是我們說(shuō)的select,poll,epoll,有些地方也稱這種IO方式為event driven IO。select/epoll的好處就在于單個(gè)process就可以同時(shí)處理多個(gè)網(wǎng)絡(luò)連接的 IO。它的基本原理就是select,poll,epoll這個(gè)function會(huì)不斷的輪詢所負(fù)責(zé)的所有socket,當(dāng)某個(gè)socket有數(shù)據(jù)到達(dá)了,就通知用戶進(jìn)程。

當(dāng)用戶進(jìn)程調(diào)用了select,那么整個(gè)進(jìn)程會(huì)被block,而同時(shí),kernel會(huì)“監(jiān)視”所有select負(fù)責(zé)的socket,當(dāng)任何一個(gè)socket中的數(shù)據(jù)準(zhǔn)備好了,select就會(huì)返回。這個(gè)時(shí)候用戶進(jìn)程再調(diào)用read操作,將數(shù)據(jù)從kernel拷貝到用戶進(jìn)程。
所以,I/O 多路復(fù)用的特點(diǎn)是通過(guò)一種機(jī)制一個(gè)進(jìn)程能同時(shí)等待多個(gè)文件描述符,而這些文件描述符(套接字描述符)其中的任意一個(gè)進(jìn)入讀就緒狀態(tài),select()函數(shù)就可以返回。
異步IO模型
- 簡(jiǎn)介:用戶進(jìn)程發(fā)起read操作之后,立刻就可以開(kāi)始去做其它的事。而另一方面,從kernel的角度,當(dāng)它受到一個(gè)asynchronous read之后,首先它會(huì)立刻返回,所以不會(huì)對(duì)用戶進(jìn)程產(chǎn)生任何block。然后,kernel會(huì)等待數(shù)據(jù)準(zhǔn)備完成,然后將數(shù)據(jù)拷貝到用戶內(nèi)存,當(dāng)這一切都完成之后,kernel會(huì)給用戶進(jìn)程發(fā)送一個(gè)signal,告訴它read操作完成了。

區(qū)別IO多路復(fù)用中的select poll epoll
select
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); select 函數(shù)監(jiān)視的文件描述符分3類,分別是writefds、readfds、和exceptfds。調(diào)用后select函數(shù)會(huì)阻塞,直到有描述符就緒(有數(shù)據(jù) 可讀、可寫(xiě)、或者有except),或者超時(shí)(timeout指定等待時(shí)間,如果立即返回設(shè)為null即可),函數(shù)返回。當(dāng)select函數(shù)返回后,可以 通過(guò)遍歷fdset,來(lái)找到就緒的描述符
poll
int poll (struct pollfd *fds, unsigned int nfds, int timeout); 不同與select使用三個(gè)位圖來(lái)表示三個(gè)fdset的方式,poll使用一個(gè) pollfd的指針實(shí)現(xiàn)。pollfd并沒(méi)有最大數(shù)量限制(但是數(shù)量過(guò)大后性能也是會(huì)下降)。 和select函數(shù)一樣,poll返回后,需要輪詢pollfd來(lái)獲取就緒的描述符。
epoll
epoll是通過(guò)事件的就緒通知方式,調(diào)用epoll_create創(chuàng)建實(shí)例,調(diào)用epoll_ctl添加或刪除監(jiān)控的文件描述符,調(diào)用epoll_wait阻塞住,直到有就緒的文件描述符,通過(guò)epoll_event參數(shù)返回就緒狀態(tài)的文件描述符和事件。
epoll操作過(guò)程需要三個(gè)接口,分別如下: int epoll_create(int size);//創(chuàng)建一個(gè)epoll的句柄,size用來(lái)告訴內(nèi)核這個(gè)監(jiān)聽(tīng)的數(shù)目一共有多大 生成一個(gè) epoll 專用的文件描述符,其實(shí)是申請(qǐng)一個(gè)內(nèi)核空間,用來(lái)存放想關(guān)注的 socket fd 上是否發(fā)生以及發(fā)生了什么事件。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
控制某個(gè) epoll 文件描述符上的事件:注冊(cè)、修改、刪除。其中參數(shù) epfd 是 epoll_create() 創(chuàng)建 epoll 專用的文件描述符。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待 I/O 事件的發(fā)生;返回發(fā)生事件數(shù)。參數(shù)說(shuō)明:
epfd: 由 epoll_create() 生成的 Epoll 專用的文件描述符;
epoll_event: 用于回傳代處理事件的數(shù)組;
maxevents: 每次能處理的事件數(shù);
timeout: 等待 I/O 事件發(fā)生的超時(shí)值;
區(qū)別總結(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)拷貝一次,epoll 通過(guò) mmap 把內(nèi)核空間和用戶空間映射到同一塊內(nèi)存,省去了拷貝的操作。
應(yīng)用舉例
- Tornado:
- 使用單線程的方式,避免線程切換的性能開(kāi)銷,同時(shí)避免在使用一些函數(shù)接口時(shí)出現(xiàn)線程不安全的情況
- 支持異步非阻塞網(wǎng)絡(luò)IO模型,避免主進(jìn)程阻塞等待。
tornado 的 IOLoop 模塊 是異步機(jī)制的核心,它包含了一系列已經(jīng)打開(kāi)的文件描述符和每個(gè)描述符的處理器 (handlers)。這些 handlers 就是對(duì) select, poll , epoll等的封裝。(所以本質(zhì)上說(shuō)是 IO 復(fù)用)
- Django
沒(méi)有用異步,通過(guò)使用多進(jìn)程的WSGI server(比如uWSGI)來(lái)實(shí)現(xiàn)并發(fā),這也是WSGI普遍的做法。
Linux后端服務(wù)器開(kāi)發(fā)要學(xué)關(guān)于IO的哪些知識(shí)點(diǎn)
網(wǎng)絡(luò)IO是網(wǎng)絡(luò)通信的血管,數(shù)據(jù)是血液。血液的流動(dòng)是不能離開(kāi)血管的。
源碼實(shí)現(xiàn)
- 服務(wù)器IO核心— epoll編程實(shí)戰(zhàn)
- 客戶端多網(wǎng)絡(luò)連接機(jī)制poll
- 文件IO管理select實(shí)戰(zhàn)
框架實(shí)戰(zhàn)
- 高性能的時(shí)間循環(huán) libev
- 跨平臺(tái)異步I/O libuv
- 跨平臺(tái)的C++庫(kù) Boost.Asio
- 事件通知庫(kù) libevent
理論詳解
- 阻塞型 BIO
- 異步IO AIO
- 非阻塞型IO NIO