1. 引言
同步異步I/O瓷耙,阻塞非阻塞I/O是程序員老生常談的話題了超升,也是自己一直以來懵懵懂懂的一個話題。比如:何為同步異步哺徊?何為阻塞與非阻塞?二者的區(qū)別在哪里乾闰?阻塞在何處落追?為什么會有多種IO模型,分別用來解決問題涯肩?常用的框架采用的是何種I/O模型轿钠?各種IO模型的優(yōu)劣勢在哪里,適用于何種應(yīng)用場景病苗?
簡而言之疗垛,對于I/O的認(rèn)知,不能僅僅停留在字面上認(rèn)識硫朦,了解內(nèi)部玄機(jī)贷腕,才能深刻理解I/O,才能看清I/O相關(guān)問題的本質(zhì)。
2. I/O 的定義
I/O 的全稱是Input/Output泽裳。雖常談及I/O瞒斩,但想必你也一時不能給出一個完整的定義。搜索了谷歌涮总,發(fā)現(xiàn)也盡是些冗長的論述胸囱。要想厘清I/O這個概念,我們需要從不同的視角去理解它瀑梗。
2.1. 計(jì)算機(jī)視角
馮?諾伊曼計(jì)算機(jī)的基本思想中有提到計(jì)算機(jī)硬件組成應(yīng)為五大部分:控制器烹笔,運(yùn)算器,存儲器抛丽,輸入和輸出谤职。其中輸入是指將數(shù)據(jù)輸入到計(jì)算機(jī)的設(shè)備,比如鍵盤鼠標(biāo)铺纽;輸出是指從計(jì)算機(jī)中獲取數(shù)據(jù)的設(shè)備柬帕,比如顯示器;以及既是輸入又是輸出設(shè)備狡门,硬盤陷寝,網(wǎng)卡等。
用戶通過操作系統(tǒng)才能完成對計(jì)算機(jī)的操作其馏。計(jì)算機(jī)啟動時凤跑,第一個啟動的程序是操作系統(tǒng)的內(nèi)核,它將負(fù)責(zé)計(jì)算機(jī)的資源管理和進(jìn)程的調(diào)度叛复。換句話說:操作系統(tǒng)負(fù)責(zé)從輸入設(shè)備讀取數(shù)據(jù)并將數(shù)據(jù)寫入到輸出設(shè)備仔引。
所以I/O之于計(jì)算機(jī),有兩層意思:
- I/O設(shè)備
- 對I/O設(shè)備的數(shù)據(jù)讀寫
對于一次I/O操作褐奥,必然涉及2個參與方咖耘,一個輸入端,一個輸出端撬码,而又根據(jù)參與雙方的設(shè)備類型儿倒,我們又可以分為磁盤I/O,網(wǎng)絡(luò)I/O(一次網(wǎng)絡(luò)的請求響應(yīng)呜笑,網(wǎng)卡)等夫否。
2.2. 程序視角
應(yīng)用程序作為一個文件保存在磁盤中,只有加載到內(nèi)存到成為一個進(jìn)程才能運(yùn)行叫胁。應(yīng)用程序運(yùn)行在計(jì)算機(jī)內(nèi)存中凰慈,必然會涉及到數(shù)據(jù)交換,比如讀寫磁盤文件驼鹅,訪問數(shù)據(jù)庫微谓,調(diào)用遠(yuǎn)程API等等森篷。但我們編寫的程序并不能像操作系統(tǒng)內(nèi)核一樣直接進(jìn)行I/O操作。
因?yàn)闉榱舜_保操作系統(tǒng)的安全穩(wěn)定運(yùn)行堰酿,操作系統(tǒng)啟動后疾宏,將會開啟保護(hù)模式:將內(nèi)存分為內(nèi)核空間(內(nèi)核對應(yīng)進(jìn)程所在內(nèi)存空間)和用戶空間,進(jìn)行內(nèi)存隔離触创。我們構(gòu)建的程序?qū)⑦\(yùn)行在用戶空間坎藐,用戶空間無法操作內(nèi)核空間,也就意味著用戶空間的程序不能直接訪問由內(nèi)核管理的I/O哼绑,比如:硬盤岩馍、網(wǎng)卡等。
但操作系統(tǒng)向外提供API抖韩,其由各種類型的系統(tǒng)調(diào)用(System Call)組成蛀恩,以提供安全的訪問控制。
所以應(yīng)用程序要想訪問內(nèi)核管理的I/O茂浮,必須通過調(diào)用內(nèi)核提供的系統(tǒng)調(diào)用(system call)進(jìn)行間接訪問双谆。
所以I/O之于應(yīng)用程序來說,強(qiáng)調(diào)的通過向內(nèi)核發(fā)起系統(tǒng)調(diào)用完成對I/O的間接訪問席揽。換句話說應(yīng)用程序發(fā)起的一次IO操作實(shí)際包含兩個階段:
- IO調(diào)用階段:應(yīng)用程序進(jìn)程向內(nèi)核發(fā)起系統(tǒng)調(diào)用
- IO執(zhí)行階段:內(nèi)核執(zhí)行IO操作并返回
2.1. 準(zhǔn)備數(shù)據(jù)階段:內(nèi)核等待I/O設(shè)備準(zhǔn)備好數(shù)據(jù)
2.2. 拷貝數(shù)據(jù)階段:將數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到用戶空間緩沖區(qū)
怎么理解準(zhǔn)備數(shù)據(jù)階段呢顽馋?
對于寫請求:等待系統(tǒng)調(diào)用的完整請求數(shù)據(jù),并寫入內(nèi)核緩沖區(qū)幌羞;
對于讀請求:等待系統(tǒng)調(diào)用的完整請求數(shù)據(jù)寸谜;(若請求數(shù)據(jù)不存在于內(nèi)核緩沖區(qū))則將外圍設(shè)備的數(shù)據(jù)讀入到內(nèi)核緩沖區(qū)。
而應(yīng)用程序進(jìn)程在發(fā)起IO調(diào)用至內(nèi)核執(zhí)行IO返回之前属桦,應(yīng)用程序進(jìn)程/線程所處狀態(tài)熊痴,就是我們下面要討論的第二個話題阻塞IO與非阻塞IO。
3. IO 模型之阻塞I/O(BIO)
應(yīng)用程序中進(jìn)程在發(fā)起IO調(diào)用后至內(nèi)核執(zhí)行IO操作返回結(jié)果之前聂宾,若發(fā)起系統(tǒng)調(diào)用的線程一直處于等待狀態(tài)果善,則此次IO操作為阻塞IO。阻塞IO簡稱BIO系谐,Blocking IO岭埠。其處理流程如下圖所示:
從上圖可知當(dāng)用戶進(jìn)程發(fā)起IO系統(tǒng)調(diào)用后,內(nèi)核從準(zhǔn)備數(shù)據(jù)到拷貝數(shù)據(jù)到用戶空間的兩個階段期間用戶調(diào)用線程選擇阻塞等待數(shù)據(jù)返回蔚鸥。
因此BIO帶來了一個問題:如果內(nèi)核數(shù)據(jù)需要耗時很久才能準(zhǔn)備好,那么用戶進(jìn)程將被阻塞许赃,浪費(fèi)性能止喷。為了提升應(yīng)用的性能,雖然可以通過多線程來提升性能混聊,但線程的創(chuàng)建依然會借助系統(tǒng)調(diào)用弹谁,同時多線程會導(dǎo)致頻繁的線程上下文的切換,同樣會影響性能。所以要想解決BIO帶來的問題预愤,我們就得看到問題的本質(zhì)沟于,那就是阻塞二字。
4. IO 模型之非阻塞I/O(NIO)
那解決方案自然也容易想到植康,將阻塞變?yōu)榉亲枞跆蔷褪怯脩暨M(jìn)程在發(fā)起系統(tǒng)調(diào)用時指定為非阻塞,內(nèi)核接收到請求后销睁,就會立即返回供璧,然后用戶進(jìn)程通過輪詢的方式來拉取處理結(jié)果。也就是如下圖所示:
應(yīng)用程序中進(jìn)程在發(fā)起IO調(diào)用后至內(nèi)核執(zhí)行IO操作返回結(jié)果之前冻记,若發(fā)起系統(tǒng)調(diào)用的線程不會等待而是立即返回睡毒,則此次IO操作為非阻塞IO模型。非阻塞IO簡稱NIO冗栗,Non-Blocking IO演顾。
然而,非阻塞IO雖然相對于阻塞IO大幅提升了性能隅居,但依舊不是完美的解決方案钠至,其依然存在性能問題,也就是頻繁的輪詢導(dǎo)致頻繁的系統(tǒng)調(diào)用军浆,會耗費(fèi)大量的CPU資源棕洋。比如當(dāng)并發(fā)很高時,假設(shè)有1000個并發(fā)乒融,那么單位時間循環(huán)內(nèi)將會有1000次系統(tǒng)調(diào)用去輪詢執(zhí)行結(jié)果掰盘,而實(shí)際上可能只有2個請求結(jié)果執(zhí)行完畢,這就會有998次無效的系統(tǒng)調(diào)用赞季,造成嚴(yán)重的性能浪費(fèi)愧捕。有問題就要解決,那NIO問題的本質(zhì)就是頻繁輪詢導(dǎo)致的無效系統(tǒng)調(diào)用申钩。
5. IO模型之IO多路復(fù)用
解決NIO的思路就是降解無效的系統(tǒng)調(diào)用次绘,如何降解呢?我們一起來看看以下幾種IO多路復(fù)用的解決思路撒遣。
5.1. IO多路復(fù)用之select/poll
Select是內(nèi)核提供的系統(tǒng)調(diào)用邮偎,它支持一次查詢多個系統(tǒng)調(diào)用的可用狀態(tài),當(dāng)任意一個結(jié)果狀態(tài)可用時就會返回义黎,用戶進(jìn)程再發(fā)起一次系統(tǒng)調(diào)用進(jìn)行數(shù)據(jù)讀取禾进。換句話說,就是NIO中N次的系統(tǒng)調(diào)用廉涕,借助Select泻云,只需要發(fā)起一次系統(tǒng)調(diào)用就夠了艇拍。其IO流程如下所示:
但是,select有一個限制宠纯,就是存在連接數(shù)限制卸夕,針對于此,又提出了poll婆瓜。其與select相比快集,主要是解決了連接限制。
select/epoll 雖然解決了NIO重復(fù)無效系統(tǒng)調(diào)用用的問題勃救,但同時又引入了新的問題碍讨。問題是:
- 用戶空間和內(nèi)核空間之間,大量的數(shù)據(jù)拷貝
- 內(nèi)核循環(huán)遍歷IO狀態(tài)蒙秒,浪費(fèi)CPU時間
換句話說勃黍,select/poll雖然減少了用戶進(jìn)程的發(fā)起的系統(tǒng)調(diào)用,但內(nèi)核的工作量只增不減晕讲。在高并發(fā)的情況下覆获,內(nèi)核的性能問題依舊。所以select/poll的問題本質(zhì)是:內(nèi)核存在無效的循環(huán)遍歷瓢省。
5.2. IO多路復(fù)用之epoll
針對select/pool引入的問題弄息,我們把解決問題的思路轉(zhuǎn)回到內(nèi)核上,如何減少內(nèi)核重復(fù)無效的循環(huán)遍歷呢勤婚?變主動為被動摹量,基于事件驅(qū)動來實(shí)現(xiàn)。其流程圖如下所示:
epoll相較于select/poll馒胆,多了兩次系統(tǒng)調(diào)用缨称,其中epoll_create建立與內(nèi)核的連接,epoll_ctl注冊事件祝迂,epoll_wait阻塞用戶進(jìn)程睦尽,等待IO事件。
epoll型雳,已經(jīng)大大優(yōu)化了IO的執(zhí)行效率当凡,但在IO執(zhí)行的第一階段:數(shù)據(jù)準(zhǔn)備階段都還是被阻塞的。所以這是一個可以繼續(xù)優(yōu)化的點(diǎn)纠俭。
6. IO 模型之信號驅(qū)動IO(SIGIO)
信號驅(qū)動IO與BIO和NIO最大的區(qū)別就在于沿量,在IO執(zhí)行的數(shù)據(jù)準(zhǔn)備階段,不會阻塞用戶進(jìn)程冤荆。
如下圖所示:當(dāng)用戶進(jìn)程需要等待數(shù)據(jù)的時候朴则,會向內(nèi)核發(fā)送一個信號,告訴內(nèi)核我要什么數(shù)據(jù)匙赞,然后用戶進(jìn)程就繼續(xù)做別的事情去了佛掖,而當(dāng)內(nèi)核中的數(shù)據(jù)準(zhǔn)備好之后,內(nèi)核立馬發(fā)給用戶進(jìn)程一個信號涌庭,說”數(shù)據(jù)準(zhǔn)備好了芥被,快來查收“,用戶進(jìn)程收到信號之后坐榆,立馬調(diào)用recvfrom拴魄,去查收數(shù)據(jù)。
乍一看席镀,信號驅(qū)動式I/O模型有種異步操作的感覺匹中,但是在IO執(zhí)行的第二階段,也就是將數(shù)據(jù)從內(nèi)核空間復(fù)制到用戶空間這個階段豪诲,用戶進(jìn)程還是被阻塞的顶捷。
綜上,你會發(fā)現(xiàn)屎篱,不管是BIO還是NIO還是SIGIO服赎,它們最終都會被阻塞在IO執(zhí)行的第二階段。
那如果能將IO執(zhí)行的第二階段變成非阻塞交播,那就完美了重虑。
7. IO 模型之異步IO(AIO)
異步IO真正實(shí)現(xiàn)了IO全流程的非阻塞。用戶進(jìn)程發(fā)出系統(tǒng)調(diào)用后立即返回秦士,內(nèi)核等待數(shù)據(jù)準(zhǔn)備完成缺厉,然后將數(shù)據(jù)拷貝到用戶進(jìn)程緩沖區(qū),然后發(fā)送信號告訴用戶進(jìn)程IO操作執(zhí)行完畢(與SIGIO相比隧土,一個是發(fā)送信號告訴用戶進(jìn)程數(shù)據(jù)準(zhǔn)備完畢提针,一個是IO執(zhí)行完畢)。其流程如下:
所以次洼,之所以稱為異步IO关贵,取決于IO執(zhí)行的第二階段是否阻塞。因此前面講的BIO卖毁,NIO和SIGIO均為同步IO揖曾。
8. 總結(jié)
梳理完這些IO模型后,之前一直處于懵懂狀態(tài)的阻塞亥啦,非阻塞炭剪,同步異步IO,終于算是有個概念了翔脱。同時也糾正了自己一直以來的誤解奴拦,所以一路走來,愈發(fā)覺得返璞歸真的重要性届吁,只有如此错妖,才能在快速更迭的技術(shù)演進(jìn)中绿鸣,以不變應(yīng)萬變。
本片綜合多方資料寫就暂氯,難免紕漏潮模,但只有寫下來,才能得以指正痴施。所以擎厢,煩請各位看官不吝賜教。
參考資料: