Linux IO
IO
我的理解是 IO是指內(nèi)存和設(shè)備之間的數(shù)據(jù)交換,比如內(nèi)存和硬盤的數(shù)據(jù)交換,內(nèi)存和網(wǎng)卡的數(shù)據(jù)交換
用戶空間和內(nèi)核空間
現(xiàn)在的操作系統(tǒng)都是采用虛擬存儲器,對于32位的操作系統(tǒng)而言,它的尋址空間(虛擬存儲空間)為4G(2的32次方).
在這4G的存儲空間里面,Linux把最高的1G字節(jié)(從虛擬地址0xC0000000到0xFFFFFFFF),供內(nèi)核使用,稱為內(nèi)核空間;而將較低的3G字節(jié)(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱為用戶空間。操作系統(tǒng)的核心就是內(nèi)核,它獨立于普通的應(yīng)用程序,它可以訪問受保護的內(nèi)存空間,也有訪問底層硬件設(shè)備的所有權(quán)限。而用戶進程不能直接訪問內(nèi)核。
用戶態(tài)和內(nèi)核態(tài)
CPU將指令等級分為內(nèi)核態(tài)和用戶態(tài),其中內(nèi)核態(tài)的指令可以訪問內(nèi)存所有數(shù)據(jù)以及外圍設(shè)備如網(wǎng)卡;而用戶態(tài)的指令只能訪問自己的進程空間內(nèi)存,不允許直接訪問外圍設(shè)備,用戶態(tài)和內(nèi)核態(tài)的指令分別占用不同的CPU。
如果用戶的程序需要訪問內(nèi)核的資源(寫文件,訪問網(wǎng)絡(luò)等),可以通過“系統(tǒng)調(diào)用”來完成一次用戶態(tài)與內(nèi)核態(tài)之間的切換,即執(zhí)行一次陷阱指令,主要的工作流程如下:
- 用戶態(tài)程序?qū)⒁恍?shù)據(jù)值放在寄存器中,或使用參數(shù)創(chuàng)建一個堆棧,以此表明需要系統(tǒng)提供的服務(wù);
- 用戶態(tài)程序執(zhí)行陷阱指令CPU切換到內(nèi)核態(tài),并跳到位于內(nèi)存指定位置的指令,其中陷阱指令是系統(tǒng)的一部分, 他們具有內(nèi)存保護,不可被用戶態(tài)程序訪問,他們會讀取程序放入內(nèi)存的數(shù)據(jù)參數(shù),并執(zhí)行程序請求的服務(wù);
- 系統(tǒng)調(diào)用完成后, 操作系統(tǒng)會重置CPU為用戶態(tài)并返回系統(tǒng)調(diào)用的結(jié)果
進程切換
主流的CPU核心在同一時間內(nèi)只能運行一個線程,當一個進程用完時間片或者被更高優(yōu)先級的進程搶占后,它會備份到CPU的任務(wù)隊列中,同時調(diào)度其他待運行進程在CPU上運行。這里有兩個概念:
任務(wù)隊列:每個CPU都會維持一個任務(wù)隊列,調(diào)度器會不停地根據(jù)進程的優(yōu)先級從隊列中調(diào)度出新進程給CPU進行運行,并將當前正在運行的進程放到任務(wù)隊列中;任務(wù)隊列中任務(wù)的多少反映出現(xiàn)目前CPU的負載情況;
進程切換:目前整個進程切換過程是通過中斷技術(shù)來實現(xiàn),即當CPU調(diào)度器獲得了待運行進程的控制塊后,立即用軟中斷指令來中止當前進程的運行,并保存當前進程的PC值和PSW值。其后使用壓棧指令把CPU其他寄存器的值壓入進程私有堆棧。然后再從待運行進程的進程控制塊中取出私有堆棧指針的值并存入處理器的寄存器SP,至此SP就指向了待運行進程的私有堆棧,于是下面就自待運行進程的私有堆棧中彈出上下文進人處理器。最后,利用中斷返回指令來實現(xiàn)自待運行進程的私有堆棧中彈出PSW值和PC值,從而完成整個切換。
- 保存處理機上下文,包括程序計數(shù)器和其他寄存器。
- 更新PCB信息。
- 把進程的PCB移入相應(yīng)的隊列,如就緒、在某事件阻塞等隊列。
- 選擇另一個進程執(zhí)行,并更新其PCB。
- 更新內(nèi)存管理的數(shù)據(jù)結(jié)構(gòu)。
- 恢復處理機上下文。
進程的阻塞
正在執(zhí)行的進程,由于期待的某些事件未發(fā)生,如請求系統(tǒng)資源失敗、等待某種操作的完成、新數(shù)據(jù)尚未到達或無新工作做等,則由系統(tǒng)自動執(zhí)行阻塞原語(Block),使自己由運行狀態(tài)變?yōu)樽枞麪顟B(tài)??梢?,進程的阻塞是進程自身的一種主動行為,也因此只有處于運行態(tài)的進程(獲得CPU),才可能將其轉(zhuǎn)為阻塞狀態(tài)。當進程進入阻塞狀態(tài),是不占用CPU資源的。
緩存I/O
緩存 I/O 又被稱作標準 I/O,大多數(shù)文件系統(tǒng)的默認 I/O 操作都是緩存 I/O。在 Linux 的緩存 I/O 機制中,操作系統(tǒng)會將 I/O 的數(shù)據(jù)緩存在文件系統(tǒng)的頁緩存( page cache )中,也就是說,數(shù)據(jù)會先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間。
缺點 :
數(shù)據(jù)在傳輸過程中需要在應(yīng)用程序地址空間和內(nèi)核進行多次數(shù)據(jù)拷貝操作,這些數(shù)據(jù)拷貝操作所帶來的 CPU 以及內(nèi)存開銷是非常大的。
IO模型
阻塞 IO
在linux中,默認情況下所有的socket都是blocking,一個典型的讀操作流程大概是這樣:

當用戶進程調(diào)用了recvfrom這個系統(tǒng)調(diào)用,kernel就開始了IO的第一個階段:準備數(shù)據(jù)(對于網(wǎng)絡(luò)IO來說,很多時候數(shù)據(jù)在一開始還沒有到達。比如,還沒有收到一個完整的UDP包。這個時候kernel就要等待足夠的數(shù)據(jù)到來)。這個過程需要等待,也就是說數(shù)據(jù)被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中是需要一個過程的。而在用戶進程這邊,整個進程會被阻塞(當然,是進程自己選擇的阻塞)。當kernel一直等到數(shù)據(jù)準備好了,它就會將數(shù)據(jù)從kernel中拷貝到用戶內(nèi)存,然后kernel返回結(jié)果,用戶進程才解除block的狀態(tài),重新運行起來。
所以,blocking IO的特點就是在IO執(zhí)行的兩個階段都被block了。
非阻塞IO
linux下,可以通過設(shè)置socket使其變?yōu)閚on-blocking。當對一個non-blocking socket執(zhí)行讀操作時,流程是這個樣子:

當用戶進程發(fā)出read操作時,如果kernel中的數(shù)據(jù)還沒有準備好,那么它并不會block用戶進程,而是立刻返回一個error。從用戶進程角度講 ,它發(fā)起一個read操作后,并不需要等待,而是馬上就得到了一個結(jié)果。用戶進程判斷結(jié)果是一個error時,它就知道數(shù)據(jù)還沒有準備好,于是它可以再次發(fā)送read操作。一旦kernel中的數(shù)據(jù)準備好了,并且又再次收到了用戶進程的system call,那么它馬上就將數(shù)據(jù)拷貝到了用戶內(nèi)存,然后返回。
所以,nonblocking IO的特點是用戶進程需要不斷的主動詢問kernel數(shù)據(jù)好了沒有。
IO多路復用
IO multiplexing就是我們說的select,poll,epoll,有些地方也稱這種IO方式為event driven IO。select/epoll的好處就在于單個process就可以同時處理多個網(wǎng)絡(luò)連接的IO。它的基本原理就是select,poll,epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有數(shù)據(jù)到達了,就通知用戶進程。

當用戶進程調(diào)用了select,那么整個進程會被block,而同時,kernel會“監(jiān)視”所有select負責的socket,當任何一個socket中的數(shù)據(jù)準備好了,select就會返回。這個時候用戶進程再調(diào)用read操作,將數(shù)據(jù)從kernel拷貝到用戶進程。
所以,I/O 多路復用的特點是通過一種機制一個進程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒狀態(tài),select()函數(shù)就可以返回。
這個圖和blocking IO的圖其實并沒有太大的不同,事實上,還更差一些。因為這里需要使用兩個system call (select 和 recvfrom),而blocking IO只調(diào)用了一個system call (recvfrom)。但是,用select的優(yōu)勢在于它可以同時處理多個connection。
所以,如果處理的連接數(shù)不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll的優(yōu)勢并不是對于單個連接能處理得更快,而是在于能處理更多的連接。)
在IO multiplexing Model中,實際中,對于每一個socket,一般都設(shè)置成為non-blocking,但是,如上圖所示,整個用戶的process其實是一直被block的。只不過process是被select這個函數(shù)block,而不是被socket IO給block。
異步IO
inux下的asynchronous IO其實用得很少。先看一下它的流程:

用戶進程發(fā)起read操作之后,立刻就可以開始去做其它的事。而另一方面,從kernel的角度,當它受到一個asynchronous read之后,首先它會立刻返回,所以不會對用戶進程產(chǎn)生任何block。然后,kernel會等待數(shù)據(jù)準備完成,然后將數(shù)據(jù)拷貝到用戶內(nèi)存,當這一切都完成之后,kernel會給用戶進程發(fā)送一個signal,告訴它read操作完成了。
多路復用IO
簡介
select,poll,epoll都是IO多路復用的機制。I/O多路復用就是通過一種機制,一個進程可以監(jiān)視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應(yīng)的讀寫操作。但select,poll,epoll本質(zhì)上都是同步I/O,因為他們都需要在讀寫事件就緒后自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現(xiàn)會負責把數(shù)據(jù)從內(nèi)核拷貝到用戶空間。
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ù)會阻塞,直到有描述副就緒(有數(shù)據(jù) 可讀、可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回設(shè)為null即可),函數(shù)返回。當select函數(shù)返回后,可以 通過遍歷fdset,來找到就緒的描述符。
select目前幾乎在所有的平臺上支持,其良好跨平臺支持也是它的一個優(yōu)點。select的一 個缺點在于單個進程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制,在Linux上一般為1024,可以通過修改宏定義甚至重新編譯內(nèi)核的方式提升這一限制,但 是這樣也會造成效率的降低。
epoll
使用epoll作為IO處理的程序一般會做三件事,第一,在程序啟動或者某個時刻,調(diào)用epoll_create方法創(chuàng)建一個epoll句柄(其數(shù)據(jù)結(jié)構(gòu)如下eventpoll);第二,當有新的IO到來時,epoll會調(diào)用epoll_ctl對其進行管理,epoll會把該IO對應(yīng)的fd添加到eventpoll的rbr紅黑樹中,然后對其注冊回調(diào)函數(shù),該回調(diào)函數(shù)的作用是當fd發(fā)生中斷(比如網(wǎng)卡的數(shù)據(jù)到了),內(nèi)核會把把數(shù)據(jù)從外部設(shè)備(網(wǎng)卡)復制到內(nèi)核中,并且把該fd添加到rdlist中;第三,程序會不停的循環(huán)遍歷rblist鏈表,當rblist里面有數(shù)據(jù)會返回數(shù)據(jù)(準備好的fd)給用戶態(tài)進程,否則會等待一個timeout時間,有新數(shù)據(jù)到來時返回新數(shù)據(jù),timeout過后沒數(shù)據(jù)也返回
struct eventpoll {
spin_lock_t lock; //對本數(shù)據(jù)結(jié)構(gòu)的訪問
struct mutex mtx; //防止使用時被刪除
wait_queue_head_t wq; //sys_epoll_wait() 使用的等待隊列
wait_queue_head_t poll_wait; //file->poll()使用的等待隊列
struct list_head rdllist; //事件滿足條件的鏈表
struct rb_root rbr; //用于管理所有fd的紅黑樹(樹根)
struct epitem *ovflist; //將事件到達的fd進行鏈接起來發(fā)送至用戶空間
}
epoll操作需要三個接口:
int epoll_create(int size);
創(chuàng)建一個epoll的句柄,size用來告訴內(nèi)核這個監(jiān)聽的數(shù)目一共有多大,這個參數(shù)不同于select()中的第一個參數(shù),給出最大監(jiān)聽的fd+1的值,參數(shù)size并不是限制了epoll所能監(jiān)聽的描述符最大個數(shù),只是對內(nèi)核初始分配內(nèi)部數(shù)據(jù)結(jié)構(gòu)的一個建議。
當創(chuàng)建好epoll句柄后,它就會占用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll后,必須調(diào)用close()關(guān)閉,否則可能導致fd被耗盡。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函數(shù)是對指定描述符fd執(zhí)行op操作。
- epfd:是epoll_create()的返回值。
- op:表示op操作,用三個宏來表示:添加EPOLL_CTL_ADD,刪除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分別添加、刪除和修改對fd的監(jiān)聽事件。
- fd:是需要監(jiān)聽的fd(文件描述符)
- epoll_event:是告訴內(nèi)核需要監(jiān)聽什么事,struct epoll_event結(jié)構(gòu)如下:
struct epoll_event {
__uint32_t events; /* Epoll events /
epoll_data_t data; / User data variable */
};
//events可以是以下幾個宏的集合:
EPOLLIN :表示對應(yīng)的文件描述符可以讀(包括對端SOCKET正常關(guān)閉);
EPOLLOUT:表示對應(yīng)的文件描述符可以寫;
EPOLLPRI:表示對應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來);
EPOLLERR:表示對應(yīng)的文件描述符發(fā)生錯誤;
EPOLLHUP:表示對應(yīng)的文件描述符被掛斷;
EPOLLET: 將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式,這是相對于水平觸發(fā)(Level Triggered)來說的。
EPOLLONESHOT:只監(jiān)聽一次事件,當監(jiān)聽完這次事件之后,如果還需要繼續(xù)監(jiān)聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待epfd上的io事件,最多返回maxevents個事件。
參數(shù)events用來從內(nèi)核得到事件的集合,maxevents告之內(nèi)核這個events有多大,這個maxevents的值不能大于創(chuàng)建epoll_create()時的size,參數(shù)timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。該函數(shù)返回需要處理的事件數(shù)目,如返回0表示已超時。
總結(jié)
在 select/poll中,進程只有在調(diào)用一定的方法后,內(nèi)核才對所有監(jiān)視的文件描述符進行掃描,而epoll事先通過epoll_ctl()來注冊一 個文件描述符,一旦基于某個文件描述符就緒時,內(nèi)核會采用類似callback的回調(diào)機制,迅速激活這個文件描述符,當進程調(diào)用epoll_wait() 時便得到通知。(此處去掉了遍歷文件描述符,而是通過監(jiān)聽回調(diào)的的機制。這正是epoll的魅力所在。)
epoll的優(yōu)點主要是一下幾個方面:
- 監(jiān)視的描述符數(shù)量不受限制,它所支持的FD上限是最大可以打開文件的數(shù)目,這個數(shù)字一般遠大于2048,舉個例子,在1GB內(nèi)存的機器上大約是10萬左 右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來說這個數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。select的最大缺點就是進程打開的fd是有數(shù)量限制的。這對 于連接數(shù)量比較大的服務(wù)器來說根本不能滿足。雖然也可以選擇多進程的解決方案( Apache就是這樣實現(xiàn)的),不過雖然linux上面創(chuàng)建進程的代價比較小,但仍舊是不可忽視的,加上進程間數(shù)據(jù)同步遠比不上線程間同步的高效,所以也不是一種完美的方案。
- IO的效率不會隨著監(jiān)視fd的數(shù)量的增長而下降。epoll不同于select和poll輪詢的方式,而是通過每個fd定義的回調(diào)函數(shù)來實現(xiàn)的。只有就緒的fd才會執(zhí)行回調(diào)函數(shù)。
- 如果沒有大量的idle -connection或者dead-connection,epoll的效率并不會比select/poll高很多,但是當遇到大量的idle- connection,就會發(fā)現(xiàn)epoll的效率大大高于select/poll。