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