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