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)都是采用虛擬存儲(chǔ)器,對(duì)于32位的操作系統(tǒng)而言,它的尋址空間(虛擬存儲(chǔ)空間)為4G(2的32次方).
在這4G的存儲(chǔ)空間里面,Linux把最高的1G字節(jié)(從虛擬地址0xC0000000到0xFFFFFFFF)乘综,供內(nèi)核使用憎账,稱為內(nèi)核空間;而將較低的3G字節(jié)(從虛擬地址0x00000000到0xBFFFFFFF),供各個(gè)進(jìn)程使用卡辰,稱為用戶空間胞皱。操作系統(tǒng)的核心就是內(nèi)核,它獨(dú)立于普通的應(yīng)用程序九妈,它可以訪問受保護(hù)的內(nèi)存空間反砌,也有訪問底層硬件設(shè)備的所有權(quán)限。而用戶進(jìn)程不能直接訪問內(nèi)核萌朱。
用戶態(tài)和內(nèi)核態(tài)
CPU將指令等級(jí)分為內(nèi)核態(tài)和用戶態(tài)宴树,其中內(nèi)核態(tài)的指令可以訪問內(nèi)存所有數(shù)據(jù)以及外圍設(shè)備如網(wǎng)卡;而用戶態(tài)的指令只能訪問自己的進(jìn)程空間內(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)建一個(gè)堆棧,以此表明需要系統(tǒng)提供的服務(wù)锄弱;
- 用戶態(tài)程序執(zhí)行陷阱指令CPU切換到內(nèi)核態(tài)考蕾,并跳到位于內(nèi)存指定位置的指令,其中陷阱指令是系統(tǒng)的一部分, 他們具有內(nèi)存保護(hù),不可被用戶態(tài)程序訪問棵癣,他們會(huì)讀取程序放入內(nèi)存的數(shù)據(jù)參數(shù)辕翰,并執(zhí)行程序請(qǐng)求的服務(wù);
- 系統(tǒng)調(diào)用完成后, 操作系統(tǒng)會(huì)重置CPU為用戶態(tài)并返回系統(tǒng)調(diào)用的結(jié)果
進(jìn)程切換
主流的CPU核心在同一時(shí)間內(nèi)只能運(yùn)行一個(gè)線程狈谊,當(dāng)一個(gè)進(jìn)程用完時(shí)間片或者被更高優(yōu)先級(jí)的進(jìn)程搶占后,它會(huì)備份到CPU的任務(wù)隊(duì)列中,同時(shí)調(diào)度其他待運(yùn)行進(jìn)程在CPU上運(yùn)行河劝。這里有兩個(gè)概念:
任務(wù)隊(duì)列:每個(gè)CPU都會(huì)維持一個(gè)任務(wù)隊(duì)列壁榕,調(diào)度器會(huì)不停地根據(jù)進(jìn)程的優(yōu)先級(jí)從隊(duì)列中調(diào)度出新進(jìn)程給CPU進(jìn)行運(yùn)行,并將當(dāng)前正在運(yùn)行的進(jìn)程放到任務(wù)隊(duì)列中赎瞎;任務(wù)隊(duì)列中任務(wù)的多少反映出現(xiàn)目前CPU的負(fù)載情況牌里;
進(jìn)程切換:目前整個(gè)進(jìn)程切換過程是通過中斷技術(shù)來實(shí)現(xiàn),即當(dāng)CPU調(diào)度器獲得了待運(yùn)行進(jìn)程的控制塊后务甥,立即用軟中斷指令來中止當(dāng)前進(jìn)程的運(yùn)行牡辽,并保存當(dāng)前進(jìn)程的PC值和PSW值。其后使用壓棧指令把CPU其他寄存器的值壓入進(jìn)程私有堆棧敞临。然后再從待運(yùn)行進(jìn)程的進(jìn)程控制塊中取出私有堆棧指針的值并存入處理器的寄存器SP态辛,至此SP就指向了待運(yùn)行進(jìn)程的私有堆棧,于是下面就自待運(yùn)行進(jìn)程的私有堆棧中彈出上下文進(jìn)人處理器挺尿。最后奏黑,利用中斷返回指令來實(shí)現(xiàn)自待運(yùn)行進(jìn)程的私有堆棧中彈出PSW值和PC值,從而完成整個(gè)切換编矾。
- 保存處理機(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á)或無新工作做等传泊,則由系統(tǒng)自動(dòng)執(zhí)行阻塞原語(Block)鼠渺,使自己由運(yùn)行狀態(tài)變?yōu)樽枞麪顟B(tài)【煜福可見拦盹,進(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資源的。
緩存I/O
緩存 I/O 又被稱作標(biāo)準(zhǔn) I/O沼侣,大多數(shù)文件系統(tǒng)的默認(rèn) I/O 操作都是緩存 I/O祖能。在 Linux 的緩存 I/O 機(jī)制中,操作系統(tǒng)會(huì)將 I/O 的數(shù)據(jù)緩存在文件系統(tǒng)的頁緩存( page cache )中蛾洛,也就是說养铸,數(shù)據(jù)會(huì)先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會(huì)從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間轧膘。
缺點(diǎn) :
數(shù)據(jù)在傳輸過程中需要在應(yīng)用程序地址空間和內(nèi)核進(jìn)行多次數(shù)據(jù)拷貝操作钞螟,這些數(shù)據(jù)拷貝操作所帶來的 CPU 以及內(nèi)存開銷是非常大的。
IO模型
阻塞 IO
在linux中谎碍,默認(rèn)情況下所有的socket都是blocking鳞滨,一個(gè)典型的讀操作流程大概是這樣:
當(dāng)用戶進(jìn)程調(diào)用了recvfrom這個(gè)系統(tǒng)調(diào)用,kernel就開始了IO的第一個(gè)階段:準(zhǔn)備數(shù)據(jù)(對(duì)于網(wǎng)絡(luò)IO來說椿浓,很多時(shí)候數(shù)據(jù)在一開始還沒有到達(dá)太援。比如,還沒有收到一個(gè)完整的UDP包扳碍。這個(gè)時(shí)候kernel就要等待足夠的數(shù)據(jù)到來)提岔。這個(gè)過程需要等待,也就是說數(shù)據(jù)被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中是需要一個(gè)過程的笋敞。而在用戶進(jìn)程這邊碱蒙,整個(gè)進(jìn)程會(huì)被阻塞(當(dāng)然,是進(jìn)程自己選擇的阻塞)夯巷。當(dāng)kernel一直等到數(shù)據(jù)準(zhǔn)備好了赛惩,它就會(huì)將數(shù)據(jù)從kernel中拷貝到用戶內(nèi)存,然后kernel返回結(jié)果趁餐,用戶進(jìn)程才解除block的狀態(tài)喷兼,重新運(yùn)行起來。
所以后雷,blocking IO的特點(diǎn)就是在IO執(zhí)行的兩個(gè)階段都被block了季惯。
非阻塞IO
linux下,可以通過設(shè)置socket使其變?yōu)閚on-blocking臀突。當(dāng)對(duì)一個(gè)non-blocking socket執(zhí)行讀操作時(shí)勉抓,流程是這個(gè)樣子:
當(dāng)用戶進(jìn)程發(fā)出read操作時(shí),如果kernel中的數(shù)據(jù)還沒有準(zhǔn)備好候学,那么它并不會(huì)block用戶進(jìn)程藕筋,而是立刻返回一個(gè)error。從用戶進(jìn)程角度講 梳码,它發(fā)起一個(gè)read操作后隐圾,并不需要等待伍掀,而是馬上就得到了一個(gè)結(jié)果。用戶進(jìn)程判斷結(jié)果是一個(gè)error時(shí)翎承,它就知道數(shù)據(jù)還沒有準(zhǔn)備好硕盹,于是它可以再次發(fā)送read操作符匾。一旦kernel中的數(shù)據(jù)準(zhǔn)備好了叨咖,并且又再次收到了用戶進(jìn)程的system call,那么它馬上就將數(shù)據(jù)拷貝到了用戶內(nèi)存啊胶,然后返回甸各。
所以,nonblocking IO的特點(diǎn)是用戶進(jìn)程需要不斷的主動(dòng)詢問kernel數(shù)據(jù)好了沒有焰坪。
IO多路復(fù)用
IO multiplexing就是我們說的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)是通過一種機(jī)制一個(gè)進(jìn)程能同時(shí)等待多個(gè)文件描述符,而這些文件描述符(套接字描述符)其中的任意一個(gè)進(jìn)入讀就緒狀態(tài)砖第,select()函數(shù)就可以返回撤卢。
這個(gè)圖和blocking IO的圖其實(shí)并沒有太大的不同,事實(shí)上梧兼,還更差一些放吩。因?yàn)檫@里需要使用兩個(gè)system call (select 和 recvfrom),而blocking IO只調(diào)用了一個(gè)system call (recvfrom)羽杰。但是渡紫,用select的優(yōu)勢(shì)在于它可以同時(shí)處理多個(gè)connection到推。
所以,如果處理的連接數(shù)不是很高的話惕澎,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好莉测,可能延遲還更大。select/epoll的優(yōu)勢(shì)并不是對(duì)于單個(gè)連接能處理得更快唧喉,而是在于能處理更多的連接捣卤。)
在IO multiplexing Model中,實(shí)際中八孝,對(duì)于每一個(gè)socket董朝,一般都設(shè)置成為non-blocking,但是干跛,如上圖所示子姜,整個(gè)用戶的process其實(shí)是一直被block的。只不過process是被select這個(gè)函數(shù)block楼入,而不是被socket IO給block哥捕。
異步IO
inux下的asynchronous IO其實(shí)用得很少。先看一下它的流程:
用戶進(jìn)程發(fā)起read操作之后,立刻就可以開始去做其它的事。而另一方面竖螃,從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操作完成了碧磅。
多路復(fù)用IO
簡(jiǎn)介
select碘箍,poll,epoll都是IO多路復(fù)用的機(jī)制鲸郊。I/O多路復(fù)用就是通過一種機(jī)制丰榴,一個(gè)進(jìn)程可以監(jiān)視多個(gè)描述符,一旦某個(gè)描述符就緒(一般是讀就緒或者寫就緒)秆撮,能夠通知程序進(jìn)行相應(yīng)的讀寫操作四濒。但select,poll,epoll本質(zhì)上都是同步I/O盗蟆,因?yàn)樗麄兌夹枰谧x寫事件就緒后自己負(fù)責(zé)進(jìn)行讀寫戈二,也就是說這個(gè)讀寫過程是阻塞的,而異步I/O則無需自己負(fù)責(zé)進(jìn)行讀寫喳资,異步I/O的實(shí)現(xiàn)會(huì)負(fù)責(zé)把數(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ù)會(huì)阻塞宏赘,直到有描述副就緒(有數(shù)據(jù) 可讀绒北、可寫黎侈、或者有except)察署,或者超時(shí)(timeout指定等待時(shí)間,如果立即返回設(shè)為null即可)峻汉,函數(shù)返回贴汪。當(dāng)select函數(shù)返回后,可以 通過遍歷fdset休吠,來找到就緒的描述符扳埂。
select目前幾乎在所有的平臺(tái)上支持,其良好跨平臺(tái)支持也是它的一個(gè)優(yōu)點(diǎn)瘤礁。select的一 個(gè)缺點(diǎn)在于單個(gè)進(jìn)程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制阳懂,在Linux上一般為1024,可以通過修改宏定義甚至重新編譯內(nèi)核的方式提升這一限制柜思,但 是這樣也會(huì)造成效率的降低岩调。
epoll
使用epoll作為IO處理的程序一般會(huì)做三件事,第一赡盘,在程序啟動(dòng)或者某個(gè)時(shí)刻号枕,調(diào)用epoll_create方法創(chuàng)建一個(gè)epoll句柄(其數(shù)據(jù)結(jié)構(gòu)如下eventpoll);第二陨享,當(dāng)有新的IO到來時(shí)葱淳,epoll會(huì)調(diào)用epoll_ctl對(duì)其進(jìn)行管理,epoll會(huì)把該IO對(duì)應(yīng)的fd添加到eventpoll的rbr紅黑樹中抛姑,然后對(duì)其注冊(cè)回調(diào)函數(shù)赞厕,該回調(diào)函數(shù)的作用是當(dāng)fd發(fā)生中斷(比如網(wǎng)卡的數(shù)據(jù)到了),內(nèi)核會(huì)把把數(shù)據(jù)從外部設(shè)備(網(wǎng)卡)復(fù)制到內(nèi)核中定硝,并且把該fd添加到rdlist中皿桑;第三,程序會(huì)不停的循環(huán)遍歷rblist鏈表,當(dāng)rblist里面有數(shù)據(jù)會(huì)返回?cái)?shù)據(jù)(準(zhǔn)備好的fd)給用戶態(tài)進(jìn)程唁毒,否則會(huì)等待一個(gè)timeout時(shí)間蒜茴,有新數(shù)據(jù)到來時(shí)返回新數(shù)據(jù),timeout過后沒數(shù)據(jù)也返回
struct eventpoll {
spin_lock_t lock; //對(duì)本數(shù)據(jù)結(jié)構(gòu)的訪問
struct mutex mtx; //防止使用時(shí)被刪除
wait_queue_head_t wq; //sys_epoll_wait() 使用的等待隊(duì)列
wait_queue_head_t poll_wait; //file->poll()使用的等待隊(duì)列
struct list_head rdllist; //事件滿足條件的鏈表
struct rb_root rbr; //用于管理所有fd的紅黑樹(樹根)
struct epitem *ovflist; //將事件到達(dá)的fd進(jìn)行鏈接起來發(fā)送至用戶空間
}
epoll操作需要三個(gè)接口:
int epoll_create(int size)浆西;
創(chuàng)建一個(gè)epoll的句柄粉私,size用來告訴內(nèi)核這個(gè)監(jiān)聽的數(shù)目一共有多大,這個(gè)參數(shù)不同于select()中的第一個(gè)參數(shù)近零,給出最大監(jiān)聽的fd+1的值诺核,參數(shù)size并不是限制了epoll所能監(jiān)聽的描述符最大個(gè)數(shù),只是對(duì)內(nèi)核初始分配內(nèi)部數(shù)據(jù)結(jié)構(gòu)的一個(gè)建議久信。
當(dāng)創(chuàng)建好epoll句柄后窖杀,它就會(huì)占用一個(gè)fd值,在linux下如果查看/proc/進(jìn)程id/fd/裙士,是能夠看到這個(gè)fd的入客,所以在使用完epoll后,必須調(diào)用close()關(guān)閉腿椎,否則可能導(dǎo)致fd被耗盡桌硫。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函數(shù)是對(duì)指定描述符fd執(zhí)行op操作啃炸。
- epfd:是epoll_create()的返回值铆隘。
- op:表示op操作,用三個(gè)宏來表示:添加EPOLL_CTL_ADD南用,刪除EPOLL_CTL_DEL膀钠,修改EPOLL_CTL_MOD。分別添加裹虫、刪除和修改對(duì)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可以是以下幾個(gè)宏的集合:
EPOLLIN :表示對(duì)應(yīng)的文件描述符可以讀(包括對(duì)端SOCKET正常關(guān)閉)恒界;
EPOLLOUT:表示對(duì)應(yīng)的文件描述符可以寫睦刃;
EPOLLPRI:表示對(duì)應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來);
EPOLLERR:表示對(duì)應(yīng)的文件描述符發(fā)生錯(cuò)誤十酣;
EPOLLHUP:表示對(duì)應(yīng)的文件描述符被掛斷涩拙;
EPOLLET: 將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式,這是相對(duì)于水平觸發(fā)(Level Triggered)來說的耸采。
EPOLLONESHOT:只監(jiān)聽一次事件兴泥,當(dāng)監(jiān)聽完這次事件之后,如果還需要繼續(xù)監(jiān)聽這個(gè)socket的話虾宇,需要再次把這個(gè)socket加入到EPOLL隊(duì)列里
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待epfd上的io事件搓彻,最多返回maxevents個(gè)事件。
參數(shù)events用來從內(nèi)核得到事件的集合,maxevents告之內(nèi)核這個(gè)events有多大旭贬,這個(gè)maxevents的值不能大于創(chuàng)建epoll_create()時(shí)的size怔接,參數(shù)timeout是超時(shí)時(shí)間(毫秒,0會(huì)立即返回稀轨,-1將不確定扼脐,也有說法說是永久阻塞)。該函數(shù)返回需要處理的事件數(shù)目奋刽,如返回0表示已超時(shí)瓦侮。
總結(jié)
在 select/poll中,進(jìn)程只有在調(diào)用一定的方法后佣谐,內(nèi)核才對(duì)所有監(jiān)視的文件描述符進(jìn)行掃描肚吏,而epoll事先通過epoll_ctl()來注冊(cè)一 個(gè)文件描述符,一旦基于某個(gè)文件描述符就緒時(shí)狭魂,內(nèi)核會(huì)采用類似callback的回調(diào)機(jī)制罚攀,迅速激活這個(gè)文件描述符,當(dāng)進(jìn)程調(diào)用epoll_wait() 時(shí)便得到通知趁蕊。(此處去掉了遍歷文件描述符坞生,而是通過監(jiān)聽回調(diào)的的機(jī)制。這正是epoll的魅力所在掷伙。)
epoll的優(yōu)點(diǎn)主要是一下幾個(gè)方面:
- 監(jiān)視的描述符數(shù)量不受限制,它所支持的FD上限是最大可以打開文件的數(shù)目又兵,這個(gè)數(shù)字一般遠(yuǎn)大于2048,舉個(gè)例子,在1GB內(nèi)存的機(jī)器上大約是10萬左 右任柜,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來說這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。select的最大缺點(diǎn)就是進(jìn)程打開的fd是有數(shù)量限制的沛厨。這對(duì) 于連接數(shù)量比較大的服務(wù)器來說根本不能滿足宙地。雖然也可以選擇多進(jìn)程的解決方案( Apache就是這樣實(shí)現(xiàn)的),不過雖然linux上面創(chuàng)建進(jìn)程的代價(jià)比較小逆皮,但仍舊是不可忽視的宅粥,加上進(jìn)程間數(shù)據(jù)同步遠(yuǎn)比不上線程間同步的高效,所以也不是一種完美的方案电谣。
- IO的效率不會(huì)隨著監(jiān)視fd的數(shù)量的增長而下降秽梅。epoll不同于select和poll輪詢的方式,而是通過每個(gè)fd定義的回調(diào)函數(shù)來實(shí)現(xiàn)的剿牺。只有就緒的fd才會(huì)執(zhí)行回調(diào)函數(shù)企垦。
- 如果沒有大量的idle -connection或者dead-connection,epoll的效率并不會(huì)比select/poll高很多晒来,但是當(dāng)遇到大量的idle- connection钞诡,就會(huì)發(fā)現(xiàn)epoll的效率大大高于select/poll。