微信公眾號(hào)【Java技術(shù)江湖】一位阿里 Java 工程師的技術(shù)小站。(關(guān)注公眾號(hào)后回復(fù)”Java“即可領(lǐng)取 Java基礎(chǔ)沉颂、進(jìn)階促脉、項(xiàng)目和架構(gòu)師等免費(fèi)學(xué)習(xí)資料,更有數(shù)據(jù)庫(kù)代芜、分布式埠褪、微服務(wù)等熱門技術(shù)學(xué)習(xí)視頻,內(nèi)容豐富挤庇,兼顧原理和實(shí)踐钞速,另外也將贈(zèng)送作者原創(chuàng)的Java學(xué)習(xí)指南、Java程序員面試指南等干貨資源)
基本概念說(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é)(從虛擬地址0x00000000到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)銷是非常大的触徐。
IO模型介紹
作者:cooffeelis
鏈接:http://www.reibang.com/p/511b9cffbdac
來(lái)源:簡(jiǎn)書
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán)狐赡,非商業(yè)轉(zhuǎn)載請(qǐng)注明出處撞鹉。
常用的5種IO模型:
blocking IO
nonblocking IO
IO multiplexing
signal driven IO
asynchronous IO
再說(shuō)一下IO發(fā)生時(shí)涉及的對(duì)象和步驟:
對(duì)于一個(gè)network IO (這里我們以read舉例),它會(huì)涉及到兩個(gè)系統(tǒng)對(duì)象:
- 一個(gè)是調(diào)用這個(gè)IO的process (or thread)
- 一個(gè)就是系統(tǒng)內(nèi)核(kernel)
當(dāng)一個(gè)read操作發(fā)生時(shí)颖侄,它會(huì)經(jīng)歷兩個(gè)階段:
- 等待數(shù)據(jù)準(zhǔn)備,比如accept(), recv()等待數(shù)據(jù)
(Waiting for the data to be ready)
- 將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中, 比如 accept()接受到請(qǐng)求,recv()接收連接發(fā)送的數(shù)據(jù)后需要復(fù)制到內(nèi)核,再?gòu)膬?nèi)核復(fù)制到進(jìn)程用戶空間
(Copying the data from the kernel to the process)
對(duì)于socket流而言,數(shù)據(jù)的流向經(jīng)歷兩個(gè)階段:
- 第一步通常涉及等待網(wǎng)絡(luò)上的數(shù)據(jù)分組到達(dá)鸟雏,然后被復(fù)制到內(nèi)核的某個(gè)緩沖區(qū)。
- 第二步把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到應(yīng)用進(jìn)程緩沖區(qū)览祖。
記住這兩點(diǎn)很重要孝鹊,因?yàn)檫@些IO Model的區(qū)別就是在兩個(gè)階段上各有不同的情況。
阻塞 I/O(blocking IO)
在linux中展蒂,默認(rèn)情況下所有的socket都是blocking又活,一個(gè)典型的讀操作流程大概是這樣:
阻塞IO流程
當(dāng)用戶進(jìn)程調(diào)用了recvfrom這個(gè)系統(tǒng)調(diào)用,kernel就開(kāi)始了IO的第一個(gè)階段:準(zhǔn)備數(shù)據(jù)(對(duì)于網(wǎng)絡(luò)IO來(lái)說(shuō)锰悼,很多時(shí)候數(shù)據(jù)在一開(kāi)始還沒(méi)有到達(dá)柳骄。比如,還沒(méi)有收到一個(gè)完整的UDP包松捉。這個(gè)時(shí)候kernel就要等待足夠的數(shù)據(jù)到來(lái))夹界。這個(gè)過(guò)程需要等待馆里,也就是說(shuō)數(shù)據(jù)被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中是需要一個(gè)過(guò)程的隘世。而在用戶進(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)行起來(lái)。
所以纷捞,blocking IO的特點(diǎn)就是在IO執(zhí)行的兩個(gè)階段都被block了痢虹。
非阻塞 I/O(nonblocking IO)
linux下,可以通過(guò)設(shè)置socket使其變?yōu)閚on-blocking主儡。當(dāng)對(duì)一個(gè)non-blocking socket執(zhí)行讀操作時(shí)奖唯,流程是這個(gè)樣子:
非阻塞 I/O 流程
當(dāng)用戶進(jìn)程發(fā)出read操作時(shí),如果kernel中的數(shù)據(jù)還沒(méi)有準(zhǔn)備好糜值,那么它并不會(huì)block用戶進(jìn)程丰捷,而是立刻返回一個(gè)error。從用戶進(jìn)程角度講 寂汇,它發(fā)起一個(gè)read操作后病往,并不需要等待,而是馬上就得到了一個(gè)結(jié)果骄瓣。用戶進(jìn)程判斷結(jié)果是一個(gè)error時(shí)停巷,它就知道數(shù)據(jù)還沒(méi)有準(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)詢問(wèn)kernel數(shù)據(jù)好了沒(méi)有渗磅。
***值得注意的是,此時(shí)的非阻塞IO只是應(yīng)用到等待數(shù)據(jù)上,當(dāng)真正有數(shù)據(jù)到達(dá)執(zhí)行recvfrom的時(shí)候,還是同步阻塞IO來(lái)的, 從圖中的copy data from kernel to user可以看出 ***
I/O 多路復(fù)用( IO multiplexing)
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)程负懦。
I/O 多路復(fù)用流程
這個(gè)圖和blocking IO的圖其實(shí)并沒(méi)有太大的不同,事實(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復(fù)用的實(shí)現(xiàn)方式目前主要有select、poll和epoll败匹。
select和poll的原理基本相同:
- 注冊(cè)待偵聽(tīng)的fd(這里的fd創(chuàng)建時(shí)最好使用非阻塞)
- 每次調(diào)用都去檢查這些fd的狀態(tài)寨昙,當(dāng)有一個(gè)或者多個(gè)fd就緒的時(shí)候返回
- 返回結(jié)果中包括已就緒和未就緒的fd
相比select,poll解決了單個(gè)進(jìn)程能夠打開(kāi)的文件描述符數(shù)量有限制這個(gè)問(wèn)題:select受限于FD_SIZE的限制掀亩,如果修改則需要修改這個(gè)宏重新編譯內(nèi)核舔哪;而poll通過(guò)一個(gè)pollfd數(shù)組向內(nèi)核傳遞需要關(guān)注的事件,避開(kāi)了文件描述符數(shù)量限制槽棍。
此外捉蚤,select和poll共同具有的一個(gè)很大的缺點(diǎn)就是包含大量fd的數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核態(tài)地址空間之間,開(kāi)銷會(huì)隨著fd數(shù)量增多而線性增大炼七。
select和poll就類似于上面說(shuō)的就餐方式缆巧。但當(dāng)你每次都去詢問(wèn)時(shí),老板會(huì)把所有你點(diǎn)的飯菜都輪詢一遍再告訴你情況豌拙,當(dāng)大量飯菜很長(zhǎng)時(shí)間都不能準(zhǔn)備好的情況下是很低效的陕悬。于是,老板有些不耐煩了按傅,就讓廚師每做好一個(gè)菜就通知他捉超。這樣每次你再去問(wèn)的時(shí)候,他會(huì)直接把已經(jīng)準(zhǔn)備好的菜告訴你唯绍,你再去端拼岳。這就是事件驅(qū)動(dòng)IO就緒通知的方式-epoll。
epoll的出現(xiàn)况芒,解決了select惜纸、poll的缺點(diǎn):
- 基于事件驅(qū)動(dòng)的方式,避免了每次都要把所有fd都掃描一遍绝骚。
- epoll_wait只返回就緒的fd耐版。
- epoll使用nmap內(nèi)存映射技術(shù)避免了內(nèi)存復(fù)制的開(kāi)銷。
- epoll的fd數(shù)量上限是操作系統(tǒng)的最大文件句柄數(shù)目,這個(gè)數(shù)目一般和內(nèi)存有關(guān)皮壁,通常遠(yuǎn)大于1024椭更。
目前哪审,epoll是Linux2.6下最高效的IO復(fù)用方式蛾魄,也是Nginx、Node的IO實(shí)現(xiàn)方式。而在freeBSD下滴须,kqueue是另一種類似于epoll的IO復(fù)用方式舌狗。
此外,對(duì)于IO復(fù)用還有一個(gè)水平觸發(fā)和邊緣觸發(fā)的概念:
- 水平觸發(fā):當(dāng)就緒的fd未被用戶進(jìn)程處理后扔水,下一次查詢依舊會(huì)返回痛侍,這是select和poll的觸發(fā)方式。
- 邊緣觸發(fā):無(wú)論就緒的fd是否被處理魔市,下一次不再返回主届。理論上性能更高,但是實(shí)現(xiàn)相當(dāng)復(fù)雜待德,并且任何意外的丟失事件都會(huì)造成請(qǐng)求處理錯(cuò)誤君丁。epoll默認(rèn)使用水平觸發(fā),通過(guò)相應(yīng)選項(xiàng)可以使用邊緣觸發(fā)将宪。
點(diǎn)評(píng):
I/O 多路復(fù)用的特點(diǎn)是通過(guò)一種機(jī)制一個(gè)進(jìn)程能同時(shí)等待多個(gè)文件描述符绘闷,而這些文件描述符(套接字描述符)其中的任意一個(gè)進(jìn)入讀就緒狀態(tài),select()函數(shù)就可以返回较坛。
所以, IO多路復(fù)用印蔗,本質(zhì)上不會(huì)有并發(fā)的功能,因?yàn)槿魏螘r(shí)候還是只有一個(gè)進(jìn)程或線程進(jìn)行工作丑勤,它之所以能提高效率是因?yàn)閟elect\epoll 把進(jìn)來(lái)的socket放到他們的 '監(jiān)視' 列表里面华嘹,當(dāng)任何socket有可讀可寫數(shù)據(jù)立馬處理,那如果select\epoll 手里同時(shí)檢測(cè)著很多socket法竞, 一有動(dòng)靜馬上返回給進(jìn)程處理除呵,總比一個(gè)一個(gè)socket過(guò)來(lái),阻塞等待,處理高效率。
當(dāng)然也可以多線程/多進(jìn)程方式爪喘,一個(gè)連接過(guò)來(lái)開(kāi)一個(gè)進(jìn)程/線程處理颜曾,這樣消耗的內(nèi)存和進(jìn)程切換頁(yè)會(huì)耗掉更多的系統(tǒng)資源。
所以我們可以結(jié)合IO多路復(fù)用和多進(jìn)程/多線程 來(lái)高性能并發(fā)秉剑,IO復(fù)用負(fù)責(zé)提高接受socket的通知效率泛豪,收到請(qǐng)求后,交給進(jìn)程池/線程池來(lái)處理邏輯侦鹏。信號(hào)驅(qū)動(dòng)
上文的就餐方式還是需要你每次都去問(wèn)一下飯菜狀況诡曙。于是,你再次不耐煩了略水,就跟老板說(shuō)价卤,哪個(gè)飯菜好了就通知我一聲吧。然后就自己坐在桌子那里干自己的事情渊涝。更甚者慎璧,你可以把手機(jī)號(hào)留給老板床嫌,自己出門,等飯菜好了直接發(fā)條短信給你胸私。這就類似信號(hào)驅(qū)動(dòng)的IO模型厌处。
流程如下:
- 開(kāi)啟套接字信號(hào)驅(qū)動(dòng)IO功能
- 系統(tǒng)調(diào)用sigaction執(zhí)行信號(hào)處理函數(shù)(非阻塞,立刻返回)
- 數(shù)據(jù)就緒岁疼,生成sigio信號(hào)阔涉,通過(guò)信號(hào)回調(diào)通知應(yīng)用來(lái)讀取數(shù)據(jù)。
此種io方式存在的一個(gè)很大的問(wèn)題:Linux中信號(hào)隊(duì)列是有限制的捷绒,如果超過(guò)這個(gè)數(shù)字問(wèn)題就無(wú)法讀取數(shù)據(jù)瑰排。
異步非阻塞
異步 I/O(asynchronous IO)
linux下的asynchronous IO其實(shí)用得很少。先看一下它的流程:
異步IO 流程
用戶進(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操作完成了套腹。
阻塞IO,非阻塞IO 與 同步IO, 異步IO的區(qū)別和聯(lián)系
阻塞IO VS 非阻塞IO:
概念:
阻塞和非阻塞關(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)前線程尖飞。
例子:你打電話問(wèn)書店老板有沒(méi)有《分布式系統(tǒng)》這本書症副,你如果是阻塞式調(diào)用,你會(huì)一直把自己“掛起”政基,直到得到這本書有沒(méi)有的結(jié)果贞铣,如果是非阻塞式調(diào)用,你不管老板有沒(méi)有告訴你沮明,你自己先一邊去玩了辕坝, 當(dāng)然你也要偶爾過(guò)幾分鐘check一下老板有沒(méi)有返回結(jié)果。在這里阻塞與非阻塞與是否同步異步無(wú)關(guān)荐健。跟老板通過(guò)什么方式回答你結(jié)果無(wú)關(guān)酱畅。
分析:
阻塞IO會(huì)一直block住對(duì)應(yīng)的進(jìn)程直到操作完成琳袄,而非阻塞IO在kernel還準(zhǔn)備數(shù)據(jù)的情況下會(huì)立刻返回。
同步IO VS 異步IO:
概念:
同步與異步同步和異步關(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舉個(gè)通俗的例子:你打電話問(wèn)書店老板有沒(méi)有《分布式系統(tǒng)》這本書,如果是同步通信機(jī)制取劫,書店老板會(huì)說(shuō)匆笤,你稍等,”我查一下"谱邪,然后開(kāi)始查啊查炮捧,等查好了(可能是5秒,也可能是一天)告訴你結(jié)果(返回結(jié)果)惦银。而異步通信機(jī)制寓盗,書店老板直接告訴你我查一下啊,查好了打電話給你璧函,然后直接掛電話了(不返回結(jié)果)傀蚌。然后查好了,他會(huì)主動(dòng)打電話給你蘸吓。在這里老板通過(guò)“回電”這種方式來(lái)回調(diào)善炫。
分析:
在說(shuō)明同步IO和異步IO的區(qū)別之前,需要先給出兩者的定義库继。Stevens給出的定義(其實(shí)是POSIX的定義)是這樣子的:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
兩者的區(qū)別就在于同步IO做”IO operation”的時(shí)候會(huì)將process阻塞箩艺。按照這個(gè)定義窜醉,之前所述的阻塞IO,非阻塞IO ,IO復(fù)用都屬于同步IO艺谆。
有人可能會(huì)說(shuō)榨惰,非阻塞IO 并沒(méi)有被block啊。這里有個(gè)非尘蔡溃“狡猾”的地方琅催,定義中所指的”IO operation”是指真實(shí)的IO操作,就是例子中的recvfrom這個(gè)system call虫给。非阻塞IO在執(zhí)行recvfrom這個(gè)system call的時(shí)候藤抡,如果kernel的數(shù)據(jù)沒(méi)有準(zhǔn)備好,這時(shí)候不會(huì)block進(jìn)程抹估。但是缠黍,當(dāng)kernel中數(shù)據(jù)準(zhǔn)備好的時(shí)候,recvfrom會(huì)將數(shù)據(jù)從kernel拷貝到用戶內(nèi)存中药蜻,這個(gè)時(shí)候進(jìn)程是被block了瓷式,在這段時(shí)間內(nèi),進(jìn)程是被block的语泽。
而異步IO則不一樣贸典,當(dāng)進(jìn)程發(fā)起IO 操作之后,就直接返回再也不理睬了湿弦,直到kernel發(fā)送一個(gè)信號(hào)瓤漏,告訴進(jìn)程說(shuō)IO完成。在這整個(gè)過(guò)程中颊埃,進(jìn)程完全沒(méi)有被block蔬充。
IO模型的形象舉例
最后,再舉幾個(gè)不是很恰當(dāng)?shù)睦觼?lái)說(shuō)明這四個(gè)IO Model:
有A班利,B饥漫,C,D四個(gè)人在釣魚:
A用的是最老式的魚竿罗标,所以呢庸队,得一直守著,等到魚上鉤了再拉桿闯割;
B的魚竿有個(gè)功能彻消,能夠顯示是否有魚上鉤,所以呢宙拉,B就和旁邊的MM聊天宾尚,隔會(huì)再看看有沒(méi)有魚上鉤,有的話就迅速拉桿;
C用的魚竿和B差不多煌贴,但他想了一個(gè)好辦法御板,就是同時(shí)放好幾根魚竿,然后守在旁邊牛郑,一旦有顯示說(shuō)魚上鉤了怠肋,它就將對(duì)應(yīng)的魚竿拉起來(lái);
D是個(gè)有錢人淹朋,干脆雇了一個(gè)人幫他釣魚笙各,一旦那個(gè)人把魚釣上來(lái)了,就給D發(fā)個(gè)短信瑞你。
Select/Poll/Epoll 輪詢機(jī)制
select酪惭,poll希痴,epoll本質(zhì)上都是同步I/O者甲,因?yàn)樗麄兌夹枰谧x寫事件就緒后自己負(fù)責(zé)進(jìn)行讀寫,也就是說(shuō)這個(gè)讀寫過(guò)程是阻塞的
Select/Poll/Epoll 都是IO復(fù)用的實(shí)現(xiàn)方式砌创, 上面說(shuō)了使用IO復(fù)用虏缸,會(huì)把socket設(shè)置成non-blocking,然后放進(jìn)Select/Poll/Epoll 各自的監(jiān)視列表里面嫩实,那么刽辙,他們的對(duì)socket是否有數(shù)據(jù)到達(dá)的監(jiān)視機(jī)制分別是怎樣的?效率又如何甲献?我們應(yīng)該使用哪種方式實(shí)現(xiàn)IO復(fù)用比較好宰缤?下面列出他們各自的實(shí)現(xiàn)方式,效率晃洒,優(yōu)缺點(diǎn):
(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)銷型奥。
Java網(wǎng)絡(luò)編程模型
上文講述了UNIX環(huán)境的五種IO模型〉锞基于這五種模型厢汹,在Java中,隨著NIO和NIO2.0(AIO)的引入谐宙,一般具有以下幾種網(wǎng)絡(luò)編程模型:
- BIO
- NIO
- AIO
BIO
BIO是一個(gè)典型的網(wǎng)絡(luò)編程模型烫葬,是通常我們實(shí)現(xiàn)一個(gè)服務(wù)端程序的過(guò)程,步驟如下:
- 主線程accept請(qǐng)求阻塞
- 請(qǐng)求到達(dá)凡蜻,創(chuàng)建新的線程來(lái)處理這個(gè)套接字搭综,完成對(duì)客戶端的響應(yīng)。
- 主線程繼續(xù)accept下一個(gè)請(qǐng)求
這種模型有一個(gè)很大的問(wèn)題是:當(dāng)客戶端連接增多時(shí)划栓,服務(wù)端創(chuàng)建的線程也會(huì)暴漲兑巾,系統(tǒng)性能會(huì)急劇下降。因此忠荞,在此模型的基礎(chǔ)上蒋歌,類似于 tomcat的bio connector,采用的是線程池來(lái)避免對(duì)于每一個(gè)客戶端都創(chuàng)建一個(gè)線程委煤。有些地方把這種方式叫做偽異步IO(把請(qǐng)求拋到線程池中異步等待處理)堂油。
NIO
JDK1.4開(kāi)始引入了NIO類庫(kù),這里的NIO指的是New IO素标,主要是使用Selector多路復(fù)用器來(lái)實(shí)現(xiàn)称诗。Selector在Linux等主流操作系統(tǒng)上是通過(guò)epoll實(shí)現(xiàn)的。
NIO的實(shí)現(xiàn)流程头遭,類似于select:
- 創(chuàng)建ServerSocketChannel監(jiān)聽(tīng)客戶端連接并綁定監(jiān)聽(tīng)端口寓免,設(shè)置為非阻塞模式。
- 創(chuàng)建Reactor線程计维,創(chuàng)建多路復(fù)用器(Selector)并啟動(dòng)線程袜香。
- 將ServerSocketChannel注冊(cè)到Reactor線程的Selector上。監(jiān)聽(tīng)accept事件鲫惶。
- Selector在線程run方法中無(wú)線循環(huán)輪詢準(zhǔn)備就緒的Key蜈首。
- Selector監(jiān)聽(tīng)到新的客戶端接入,處理新的請(qǐng)求,完成tcp三次握手欢策,建立物理連接课锌。
- 將新的客戶端連接注冊(cè)到Selector上撼短,監(jiān)聽(tīng)讀操作课兄。讀取客戶端發(fā)送的網(wǎng)絡(luò)消息晋修。
- 客戶端發(fā)送的數(shù)據(jù)就緒則讀取客戶端請(qǐng)求,進(jìn)行處理俺孙。
相比BIO辣卒,NIO的編程非常復(fù)雜。
AIO
JDK1.7引入NIO2.0睛榄,提供了異步文件通道和異步套接字通道的實(shí)現(xiàn)荣茫。其底層在windows上是通過(guò)IOCP,在Linux上是通過(guò)epoll來(lái)實(shí)現(xiàn)的(LinuxAsynchronousChannelProvider.java,UnixAsynchronousServerSocketChannelImpl.java)场靴。
- 創(chuàng)建AsynchronousServerSocketChannel啡莉,綁定監(jiān)聽(tīng)端口
- 調(diào)用AsynchronousServerSocketChannel的accpet方法,傳入自己實(shí)現(xiàn)的CompletionHandler憎乙。包括上一步票罐,都是非阻塞的
- 連接傳入叉趣,回調(diào)CompletionHandler的completed方法泞边,在里面,調(diào)用AsynchronousSocketChannel的read方法疗杉,傳入負(fù)責(zé)處理數(shù)據(jù)的CompletionHandler阵谚。
- 數(shù)據(jù)就緒,觸發(fā)負(fù)責(zé)處理數(shù)據(jù)的CompletionHandler的completed方法烟具。繼續(xù)做下一步處理即可梢什。
- 寫入操作類似,也需要傳入CompletionHandler朝聋。
其編程模型相比NIO有了不少的簡(jiǎn)化嗡午。
對(duì)比
. | 同步阻塞IO | 偽異步IO | NIO | AIO |
---|---|---|---|---|
客戶端數(shù)目 :IO線程 | 1 : 1 | m : n | m : 1 | m : 0 |
IO模型 | 同步阻塞IO | 同步阻塞IO | 同步非阻塞IO | 異步非阻塞IO |
吞吐量 | 低 | 中 | 高 | 高 |
編程復(fù)雜度 | 簡(jiǎn)單 | 簡(jiǎn)單 | 非常復(fù)雜 | 復(fù)雜 |
微信公眾號(hào)【黃小斜】作者是螞蟻金服 JAVA 工程師,目前在螞蟻財(cái)富負(fù)責(zé)后端開(kāi)發(fā)工作冀痕,專注于 JAVA 后端技術(shù)棧荔睹,同時(shí)也懂點(diǎn)投資理財(cái),堅(jiān)持學(xué)習(xí)和寫作言蛇,用大廠程序員的視角解讀技術(shù)與互聯(lián)網(wǎng)僻他,我的世界里不只有 coding!關(guān)注公眾號(hào)后回復(fù)”架構(gòu)師“即可領(lǐng)取 Java基礎(chǔ)腊尚、進(jìn)階吨拗、項(xiàng)目和架構(gòu)師等免費(fèi)學(xué)習(xí)資料,更有數(shù)據(jù)庫(kù)、分布式劝篷、微服務(wù)等熱門技術(shù)學(xué)習(xí)視頻哨鸭,內(nèi)容豐富,兼顧原理和實(shí)踐娇妓,另外也將贈(zèng)送作者原創(chuàng)的Java學(xué)習(xí)指南兔跌、Java程序員面試指南等干貨資源