大家知道,用戶程序進(jìn)行IO讀寫(xiě),依賴于操作系統(tǒng)底層的IO讀寫(xiě),基本上會(huì)用到底層的read&write兩大系統(tǒng)調(diào)用。
read系統(tǒng)調(diào)用,并不是直接從物理設(shè)備把數(shù)據(jù)讀取到內(nèi)存中,write系統(tǒng)調(diào)用,也不是把數(shù)據(jù)直接寫(xiě)入到物理設(shè)備 上層應(yīng)用無(wú)論是調(diào)用操作系統(tǒng)的read,還是調(diào)用操作系統(tǒng)的write,都會(huì)涉及緩沖區(qū)。具體來(lái)說(shuō),調(diào)用操作系統(tǒng)的read,是把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到進(jìn)程緩沖區(qū);而write系統(tǒng)調(diào)用,是把數(shù)據(jù)從進(jìn)程緩沖區(qū)復(fù)制到內(nèi)核緩沖區(qū)。 上圖顯示了塊數(shù)據(jù)如何從外部源(例如硬盤(pán))移動(dòng)到正在運(yùn)行的進(jìn)程(例如RAM)內(nèi)部的存儲(chǔ)區(qū)的簡(jiǎn)化“邏輯”圖。 首先,該進(jìn)程通過(guò)進(jìn)行read系統(tǒng)調(diào)用來(lái)填充其緩沖區(qū)。 read讀取調(diào)用會(huì)導(dǎo)致內(nèi)核向磁盤(pán)控制器硬件發(fā)出命令以從磁盤(pán)獲取數(shù)據(jù)。 磁盤(pán)控制器通過(guò)DMA將數(shù)據(jù)直接寫(xiě)入內(nèi)核內(nèi)存緩沖區(qū)。 磁盤(pán)控制器完成緩沖區(qū)的填充后,內(nèi)核將數(shù)據(jù)從內(nèi)核空間中的臨時(shí)緩沖區(qū)復(fù)制到進(jìn)程指定的緩沖區(qū)中。 緩沖區(qū)的目的,是為了減少頻繁地與設(shè)備之間的物理交換。大家都知道,外部設(shè)備的直接讀寫(xiě),涉及操作系統(tǒng)的中斷。發(fā)生系統(tǒng)中斷時(shí),需要保存之前的進(jìn)程數(shù)據(jù)和狀態(tài)等信息,而結(jié)束中斷之后,還需要恢復(fù)之前的進(jìn)程數(shù)據(jù)和狀態(tài)等信息。為了減少這種底層系統(tǒng)的時(shí)間損耗、性能損耗,于是出現(xiàn)了內(nèi)存緩沖區(qū)。 有了內(nèi)存緩沖區(qū),上層應(yīng)用使用read系統(tǒng)調(diào)用時(shí),僅僅把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到上層應(yīng)用的緩沖區(qū)(進(jìn)程緩沖區(qū));上層應(yīng)用使用write系統(tǒng)調(diào)用時(shí),僅僅把數(shù)據(jù)從進(jìn)程緩沖區(qū)復(fù)制到內(nèi)核緩沖區(qū)中。底層操作會(huì)對(duì)內(nèi)核緩沖區(qū)進(jìn)行監(jiān)控,等待緩沖區(qū)達(dá)到一定數(shù)量的時(shí)候,再進(jìn)行IO設(shè)備的中斷處理,集中執(zhí)行物理設(shè)備的實(shí)際IO操作,這種機(jī)制提升了系統(tǒng)的性能。至于什么時(shí)候中斷(讀中斷、寫(xiě)中斷),由操作系統(tǒng)的內(nèi)核來(lái)決定,用戶程序則不需要關(guān)心 從數(shù)量上來(lái)說(shuō),在Linux系統(tǒng)中,操作系統(tǒng)內(nèi)核只有一個(gè)內(nèi)核緩沖區(qū)。而每個(gè)用戶程序(進(jìn)程),有自己獨(dú)立的緩沖區(qū),叫作進(jìn)程緩沖區(qū)。所以,用戶程序的IO讀寫(xiě)程序,在大多數(shù)情況下,并沒(méi)有進(jìn)行實(shí)際的IO操作,而是在進(jìn)程緩沖區(qū)和內(nèi)核緩沖區(qū)之間直接進(jìn)行數(shù)據(jù)的交換 文件句柄,也叫文件描述符。在Linux系統(tǒng)中,文件可分為:普通文件、目錄文件、鏈接文件和設(shè)備文件。文件描述符(File Descriptor)是內(nèi)核為了高效管理已被打開(kāi)的文件所創(chuàng)建的索引,它是一個(gè)非負(fù)整數(shù)(通常是小整數(shù)),用于指代被打開(kāi)的文件。所有的IO系統(tǒng)調(diào)用,包括socket的讀寫(xiě)調(diào)用,都是通過(guò)文件描述符完成的。 4種主要的IO模型介紹4種IO模型之前要先介紹兩組概念
阻塞IO,指的是需要內(nèi)核IO操作徹底完成后,才返回到用戶空間執(zhí)行用戶的操作。阻塞指的是用戶空間程序的執(zhí)行狀態(tài)。傳統(tǒng)的IO模型都是同步阻塞IO。在Java中,默認(rèn)創(chuàng)建的socket都是阻塞的
同步IO,是一種用戶空間與內(nèi)核空間的IO發(fā)起方式。同步IO是指用戶空間的線程是主動(dòng)發(fā)起IO請(qǐng)求的一方,內(nèi)核空間是被動(dòng)接受方。異步IO則反過(guò)來(lái),是指系統(tǒng)內(nèi)核是主動(dòng)發(fā)起IO請(qǐng)求的一方,用戶空間的線程是被動(dòng)接受方 在Java應(yīng)用程序進(jìn)程中,默認(rèn)情況下,所有的socket連接的IO操作都是同步阻塞IO(Blocking IO)。 在阻塞式IO模型中,Java應(yīng)用程序從IO系統(tǒng)調(diào)用開(kāi)始,直到系統(tǒng)調(diào)用返回,在這段時(shí)間內(nèi),Java進(jìn)程是阻塞的。返回成功后,應(yīng)用進(jìn)程開(kāi)始處理用戶空間的緩存區(qū)數(shù)據(jù)。 從Java啟動(dòng)IO讀的read系統(tǒng)調(diào)用開(kāi)始,用戶線程就進(jìn)入阻塞狀態(tài)。 當(dāng)系統(tǒng)內(nèi)核收到read系統(tǒng)調(diào)用,就開(kāi)始準(zhǔn)備數(shù)據(jù)。一開(kāi)始,數(shù)據(jù)可能還沒(méi)有到達(dá)內(nèi)核緩沖區(qū)(例如,還沒(méi)有收到一個(gè)完整的socket數(shù)據(jù)包),這個(gè)時(shí)候內(nèi)核就要等待。 內(nèi)核一直等到完整的數(shù)據(jù)到達(dá),就會(huì)將數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到用戶緩沖區(qū)(用戶空間的內(nèi)存),然后內(nèi)核返回結(jié)果(例如返回復(fù)制到用戶緩沖區(qū)中的字節(jié)數(shù))。 直到內(nèi)核返回后,用戶線程才會(huì)解除阻塞的狀態(tài),重新運(yùn)行起來(lái)。
應(yīng)用的程序開(kāi)發(fā)非常簡(jiǎn)單;在阻塞等待數(shù)據(jù)期間,用戶線程掛起。在阻塞期間,用戶線程基本不會(huì)占用CPU資源。
一般情況下,會(huì)為每個(gè)連接配備一個(gè)獨(dú)立的線程;反過(guò)來(lái)說(shuō),就是一個(gè)線程維護(hù)一個(gè)連接的IO操作。在并發(fā)量小的情況下,這樣做沒(méi)有什么問(wèn)題。但是,當(dāng)在高并發(fā)的應(yīng)用場(chǎng)景下,需要大量的線程來(lái)維護(hù)大量的網(wǎng)絡(luò)連接,內(nèi)存、線程切換開(kāi)銷(xiāo)會(huì)非常巨大。因此,基本上阻塞IO模型在高并發(fā)應(yīng)用場(chǎng)景下是不可用的。 在內(nèi)核數(shù)據(jù)沒(méi)有準(zhǔn)備好的階段,用戶線程發(fā)起IO請(qǐng)求時(shí),立即返回。所以,為了讀取到最終的數(shù)據(jù),用戶線程需要不斷地發(fā)起IO系統(tǒng)調(diào)用。 內(nèi)核數(shù)據(jù)到達(dá)后,用戶線程發(fā)起系統(tǒng)調(diào)用,用戶線程阻塞。內(nèi)核開(kāi)始復(fù)制數(shù)據(jù),它會(huì)將數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到用戶緩沖區(qū)(用戶空間的內(nèi)存),然后內(nèi)核返回結(jié)果(例如返回復(fù)制到的用戶緩沖區(qū)的字節(jié)數(shù))。 用戶線程讀到數(shù)據(jù)后,才會(huì)解除阻塞狀態(tài),重新運(yùn)行起來(lái)。也就是說(shuō),用戶進(jìn)程需要經(jīng)過(guò)多次的嘗試,才能保證最終真正讀到數(shù)據(jù),而后繼續(xù)執(zhí)行。
應(yīng)用程序的線程需要不斷地進(jìn)行IO系統(tǒng)調(diào)用,輪詢數(shù)據(jù)是否已經(jīng)準(zhǔn)備好,如果沒(méi)有準(zhǔn)備好,就繼續(xù)輪詢,直到完成IO系統(tǒng)調(diào)用為止。
每次發(fā)起的IO系統(tǒng)調(diào)用,在內(nèi)核等待數(shù)據(jù)過(guò)程中可以立即返回。用戶線程不會(huì)阻塞,實(shí)時(shí)性較好。
不斷地輪詢內(nèi)核,這將占用大量的CPU時(shí)間,效率低下 總體來(lái)說(shuō),在高并發(fā)應(yīng)用場(chǎng)景下,同步非阻塞IO也是不可用的。一般Web服務(wù)器不使用這種IO模型。這種IO模型一般很少直接使用,而是在其他IO模型中使用非阻塞IO這一特性。在Java的實(shí)際開(kāi)發(fā)中,也不會(huì)涉及這種IO模型 如何避免同步非阻塞IO模型中輪詢等待的問(wèn)題呢?這就是IO多路復(fù)用模型 在IO多路復(fù)用模型中,引入了一種新的系統(tǒng)調(diào)用,查詢IO的就緒狀態(tài)。在Linux系統(tǒng)中,對(duì)應(yīng)的系統(tǒng)調(diào)用為select/epoll系統(tǒng)調(diào)用。通過(guò)該系統(tǒng)調(diào)用,一個(gè)進(jìn)程可以監(jiān)視多個(gè)文件描述符,一旦某個(gè)描述符就緒(一般是內(nèi)核緩沖區(qū)可讀/可寫(xiě)),內(nèi)核能夠?qū)⒕途w的狀態(tài)返回給應(yīng)用程序。隨后,應(yīng)用程序根據(jù)就緒的狀態(tài),進(jìn)行相應(yīng)的IO系統(tǒng)調(diào)用。 目前支持IO多路復(fù)用的系統(tǒng)調(diào)用,有select、epoll等等。select系統(tǒng)調(diào)用,幾乎在所有的操作系統(tǒng)上都有支持,具有良好的跨平臺(tái)特性。epoll是在Linux 2.6內(nèi)核中提出的,是select系統(tǒng)調(diào)用的Linux增強(qiáng)版本。 在IO多路復(fù)用模型中通過(guò)select/epoll系統(tǒng)調(diào)用,單個(gè)應(yīng)用程序的線程,可以不斷地輪詢成百上千的socket連接,當(dāng)某個(gè)或者某些socket網(wǎng)絡(luò)連接有IO就緒的狀態(tài),就返回對(duì)應(yīng)的可以執(zhí)行的讀寫(xiě)操作 舉個(gè)例子來(lái)說(shuō)明IO多路復(fù)用模型的流程。發(fā)起一個(gè)多路復(fù)用IO的read讀操作的系統(tǒng)調(diào)用,流程如下: 選擇器注冊(cè)。在這種模式中,首先,將需要read操作的目標(biāo)socket網(wǎng)絡(luò)連接,提前注冊(cè)到select/epoll選擇器中,Java中對(duì)應(yīng)的選擇器類(lèi)是Selector類(lèi)。然后,才可以開(kāi)啟整個(gè)IO多路復(fù)用模型的輪詢流程。 就緒狀態(tài)的輪詢。通過(guò)選擇器的查詢方法,查詢注冊(cè)過(guò)的所有socket連接的就緒狀態(tài)。通過(guò)查詢的系統(tǒng)調(diào)用,內(nèi)核會(huì)返回一個(gè)就緒的socket列表。當(dāng)任何一個(gè)注冊(cè)過(guò)的socket中的數(shù)據(jù)準(zhǔn)備好了,內(nèi)核緩沖區(qū)有數(shù)據(jù)(就緒)了,內(nèi)核就將該socket加入到就緒的列表中。 當(dāng)用戶進(jìn)程調(diào)用了select查詢方法,那么整個(gè)線程會(huì)被阻塞掉。 用戶線程獲得了就緒狀態(tài)的列表后,根據(jù)其中的socket連接,發(fā)起read系統(tǒng)調(diào)用,用戶線程阻塞。內(nèi)核開(kāi)始復(fù)制數(shù)據(jù),將數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到用戶緩沖區(qū)。 復(fù)制完成后,內(nèi)核返回結(jié)果,用戶線程才會(huì)解除阻塞的狀態(tài),用戶線程讀取到了數(shù)據(jù),繼續(xù)執(zhí)行。
涉及兩種系統(tǒng)調(diào)用(System Call), 一種是select/epoll(就緒查詢) 一種是IO操作。 和NIO模型相似,多路復(fù)用IO也需要輪詢。負(fù)責(zé)select/epoll狀態(tài)查詢調(diào)用的線程,需要不斷地進(jìn)行select/epoll輪詢,查找出達(dá)到IO操作就緒的socket連接。
與一個(gè)線程維護(hù)一個(gè)連接的阻塞IO模式相比,使用select/epoll的最大優(yōu)勢(shì)在于,一個(gè)選擇器查詢線程可以同時(shí)處理成千上萬(wàn)個(gè)連接(Connection)。系統(tǒng)不必創(chuàng)建大量的線程,也不必維護(hù)這些線程,從而大大減小了系統(tǒng)的開(kāi)銷(xiāo)。
本質(zhì)上,select/epoll系統(tǒng)調(diào)用是阻塞式的,屬于同步IO。都需要在讀寫(xiě)事件就緒后,由系統(tǒng)調(diào)用本身負(fù)責(zé)進(jìn)行讀寫(xiě),也就是說(shuō)這個(gè)讀寫(xiě)過(guò)程是阻塞的 如果要徹底地解除線程的阻塞,就必須使用異步IO模型 異步IO模型(Asynchronous IO,簡(jiǎn)稱為AIO)。AIO的基本流程是:用戶線程通過(guò)系統(tǒng)調(diào)用,向內(nèi)核注冊(cè)某個(gè)IO操作。內(nèi)核在整個(gè)IO操作(包括數(shù)據(jù)準(zhǔn)備、數(shù)據(jù)復(fù)制)完成后,通知用戶程序,用戶執(zhí)行后續(xù)的業(yè)務(wù)操作。 舉個(gè)例子。發(fā)起一個(gè)異步IO的read讀操作的系統(tǒng)調(diào)用,流程如下: 當(dāng)用戶線程發(fā)起了read系統(tǒng)調(diào)用,立刻就可以開(kāi)始去做其他的事,用戶線程不阻塞。 內(nèi)核就開(kāi)始了IO的第一個(gè)階段:準(zhǔn)備數(shù)據(jù)。等到數(shù)據(jù)準(zhǔn)備好了,內(nèi)核就會(huì)將數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到用戶緩沖區(qū)(用戶空間的內(nèi)存)。 內(nèi)核會(huì)給用戶線程發(fā)送一個(gè)信號(hào)(Signal),或者回調(diào)用戶線程注冊(cè)的回調(diào)接口,告訴用戶線程read操作完成了。 用戶線程讀取用戶緩沖區(qū)的數(shù)據(jù),完成后續(xù)的業(yè)務(wù)操作。
在內(nèi)核等待數(shù)據(jù)和復(fù)制數(shù)據(jù)的兩個(gè)階段,用戶線程都不是阻塞的。用戶線程需要接收內(nèi)核的IO操作完成的事件,或者用戶線程需要注冊(cè)一個(gè)IO操作完成的回調(diào)函數(shù)。正因?yàn)槿绱?,異步IO有的時(shí)候也被稱為信號(hào)驅(qū)動(dòng)IO
應(yīng)用程序僅需要進(jìn)行事件的注冊(cè)與接收,其余的工作都留給了操作系統(tǒng),也就是說(shuō),需要底層內(nèi)核提供支持。 理論上來(lái)說(shuō),異步IO是真正的異步輸入輸出,它的吞吐量高于IO多路復(fù)用模型的吞吐量 就目前而言,Windows系統(tǒng)下通過(guò)IOCP實(shí)現(xiàn)了真正的異步IO。而在Linux系統(tǒng)下,異步IO模型在2.6版本才引入,目前并不完善,其底層實(shí)現(xiàn)仍使用epoll,與IO多路復(fù)用相同,因此在性能上沒(méi)有明顯的優(yōu)勢(shì)。 大多數(shù)的高并發(fā)服務(wù)器端的程序,一般都是基于Linux系統(tǒng)的。因而,目前這類(lèi)高并發(fā)網(wǎng)絡(luò)應(yīng)用程序的開(kāi)發(fā),大多采用IO多路復(fù)用模型 |
|
來(lái)自: timtxu > 《時(shí)尚科技》