溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

I/O模型的相關(guān)問題有哪些

發(fā)布時(shí)間:2022-01-06 09:18:36 來源:億速云 閱讀:147 作者:iii 欄目:云計(jì)算

這篇文章主要講解了“I/O模型的相關(guān)問題有哪些”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“I/O模型的相關(guān)問題有哪些”吧!

一、關(guān)于I/O模型的問題

  最近通過對(duì)ucore操作系統(tǒng)的學(xué)習(xí),讓我打開了操作系統(tǒng)內(nèi)核這一黑盒子,與之前所學(xué)知識(shí)結(jié)合起來,解答了長久以來困擾我的關(guān)于I/O的一些問題。

  1. 為什么redis能以單工作線程處理高達(dá)幾萬的并發(fā)請(qǐng)求?

  2. 什么是I/O多路復(fù)用?為什么redis、nginx、nodeJS以及netty等以高性能著稱的服務(wù)器其底層都利用了I/O多路復(fù)用技術(shù)?

  3. 非阻塞I/O為什么會(huì)流行起來,在許多場景下取代了傳統(tǒng)的阻塞I/O?

  4. 非阻塞I/O真的是銀彈嗎?為什么即使在為海量用戶提供服務(wù)的,追求高性能的互聯(lián)網(wǎng)公司中依然有那么多的服務(wù)器在傳統(tǒng)的阻塞IO模型下工作?

  5. 什么是協(xié)程?為什么Go語言這么受歡迎?

  在這篇博客中,將介紹不同層面、不同I/O模型的原理,并嘗試著給出我對(duì)上述問題的回答。如果你也或多或少的對(duì)上述問題感到疑惑,希望這篇博客能為你提供幫助。

  I/O模型和硬件、操作系統(tǒng)內(nèi)核息息相關(guān),博客中會(huì)涉及到諸如保護(hù)模式、中斷、特權(quán)級(jí)、進(jìn)程/線程、上下文切換、系統(tǒng)調(diào)用等關(guān)于操作系統(tǒng)、硬件相關(guān)的概念。由于計(jì)算機(jī)中的知識(shí)是按照層次組織起來的,如果對(duì)這些相對(duì)底層的概念不是很了解的話可能會(huì)影響對(duì)整體內(nèi)容的理解??梢詤⒖家幌挛谊P(guān)于操作系統(tǒng)、硬件學(xué)習(xí)相關(guān)的博客:x86匯編學(xué)習(xí)、操作系統(tǒng)學(xué)習(xí)(持續(xù)更新中)

二、硬件I/O模型

  軟件的功能總是構(gòu)建在硬件上的,計(jì)算機(jī)中的I/O本質(zhì)上是CPU/內(nèi)存與外設(shè)(網(wǎng)卡、磁盤等)進(jìn)行數(shù)據(jù)的單向或雙向傳輸。

  從外設(shè)讀入數(shù)據(jù)到CPU/內(nèi)存稱作Input輸入,從CPU/內(nèi)存中寫出數(shù)據(jù)到外設(shè)稱作Output輸出。

  要想理解軟件層次上的不同I/O模型,必須先對(duì)其基于的硬件I/O模型有一個(gè)基本的認(rèn)識(shí)。硬件I/O模型大致可以分為三種:程序控制I/O、中斷驅(qū)動(dòng)I/O、使用DMA的I/O。

程序控制I/O:

  程序控制I/O模型中,通過指令控制CPU不斷的輪詢外設(shè)是否就緒,當(dāng)硬件就緒時(shí)一點(diǎn)一點(diǎn)的反復(fù)讀/寫數(shù)據(jù)。

  從CPU的角度來說,程序控制I/O模型是同步、阻塞的(同步指的是I/O操作依然是處于程序指令控制,由CPU主導(dǎo)的;阻塞指的是在發(fā)起I/O后CPU必須持續(xù)輪詢完成狀態(tài),無法執(zhí)行別的指令)。

程序控制I/O的優(yōu)點(diǎn):

  硬件結(jié)構(gòu)簡單,編寫對(duì)應(yīng)程序也簡單。

程序控制I/O的缺點(diǎn):

  十分消耗CPU,持續(xù)的輪訓(xùn)令寶貴的CPU資源無謂的浪費(fèi)在了等待I/O完成的過程中,導(dǎo)致CPU利用率不高。

中斷驅(qū)動(dòng)I/O:

  為了解決上述程序控制I/O模型對(duì)CPU資源利用率不高的問題,計(jì)算機(jī)硬件的設(shè)計(jì)者令CPU擁有了處理中斷的功能。

  在中斷驅(qū)動(dòng)I/O模型中,CPU發(fā)起對(duì)外設(shè)的I/O請(qǐng)求后,就直接去執(zhí)行別的指令了。當(dāng)硬件處理完I/O請(qǐng)求后,通過中斷異步的通知CPU。接到讀取完成中斷通知后,CPU負(fù)責(zé)將數(shù)據(jù)從外設(shè)緩沖區(qū)中寫入內(nèi)存;接到寫出完成中斷通知后,CPU需要將內(nèi)存中后續(xù)的數(shù)據(jù)接著寫出交給外設(shè)處理。

  從CPU的角度來說,中斷驅(qū)動(dòng)I/O模型是同步、非阻塞的(同步指的是I/O操作依然是處于程序指令控制,由CPU主導(dǎo)的;非阻塞指的是在發(fā)起I/O后CPU不會(huì)停下等待,而是可以執(zhí)行別的指令)。

中斷驅(qū)動(dòng)I/O的優(yōu)點(diǎn):

  由于I/O總是相對(duì)耗時(shí)的,比起通過程序控制I/O模型下CPU不停的輪訓(xùn)。在等待硬件I/O完成的過程中CPU可以解放出來執(zhí)行另外的命令,大大提高了I/O密集程序的CPU利用率。

中斷驅(qū)動(dòng)I/O的缺點(diǎn):

  受制于硬件緩沖區(qū)的大小,一次硬件I/O可以處理的數(shù)據(jù)是相對(duì)有限的。在處理一次大數(shù)據(jù)的I/O請(qǐng)求中,CPU需要被反復(fù)的中斷,而處理讀寫中斷事件本身也是有一定開銷的。

使用DMA的I/O:

  為了解決中斷驅(qū)動(dòng)I/O模型中,大數(shù)據(jù)量的I/O傳輸使得CPU需要反復(fù)處理中斷的缺陷,計(jì)算機(jī)硬件的設(shè)計(jì)者提出了基于DMA模式的I/O(DMA Direct Memory Access 直接存儲(chǔ)器訪問)。DMA也是一種處理器芯片,和CPU一樣也可以訪問內(nèi)存和外設(shè),但DMA芯片是被設(shè)計(jì)來專門處理I/O數(shù)據(jù)傳輸?shù)?,因此其成本相?duì)CPU較低。

  在使用DMA的I/O模型中,CPU與DMA芯片交互,指定需要讀/寫的數(shù)據(jù)塊大小和需要進(jìn)行I/O數(shù)據(jù)的目的內(nèi)存地址后,便異步的處理別的指令了。由DMA與外設(shè)硬件進(jìn)行交互,一次大數(shù)據(jù)量的I/O需要DMA反復(fù)的與外設(shè)進(jìn)行交互,當(dāng)DMA完成了整體數(shù)據(jù)塊的I/O后(完整的將數(shù)據(jù)讀入到內(nèi)存或是完整的將某一內(nèi)存塊的數(shù)據(jù)寫出到外設(shè)),再發(fā)起DMA中斷通知CPU。

  從CPU的角度來說,使用DMA的I/O模型是異步、非阻塞的(異步指的是整個(gè)I/O操作并不是由CPU主導(dǎo),而是由DMA芯片與外設(shè)交互完成的;非阻塞指的是在發(fā)起I/O后CPU不會(huì)停下等待,而是可以執(zhí)行別的指令)。

使用DMA的I/O優(yōu)點(diǎn):

  比起外設(shè)硬件中斷通知,對(duì)于一次完整的大數(shù)據(jù)內(nèi)存與外設(shè)間的I/O,CPU只需要處理一次中斷。CPU的利用效率相對(duì)來說是最高的。

使用DMA的I/O缺點(diǎn):

  1. 引入DMA芯片令硬件結(jié)構(gòu)變復(fù)雜,成本較高。

  2. 由于DMA芯片的引入,使得DMA和CPU并發(fā)的對(duì)內(nèi)存進(jìn)行操作,在擁有高速緩存的CPU中,引入了高速緩存與內(nèi)存不一致的問題。

  總的來說,自DMA技術(shù)被發(fā)明以來,由于其極大減少了CPU在I/O時(shí)的性能損耗,已經(jīng)成為了絕大多數(shù)通用計(jì)算機(jī)的硬件標(biāo)配。隨著技術(shù)的發(fā)展又出現(xiàn)了更先進(jìn)的通道I/O方式,相當(dāng)于并發(fā)的DMA,允許并發(fā)的處理涉及多個(gè)不同內(nèi)存區(qū)域、外設(shè)硬件的I/O操作。

三、操作系統(tǒng)I/O模型

  介紹完硬件的I/O模型后,下面介紹這篇博客的重點(diǎn):操作系統(tǒng)I/O模型。

  操作系統(tǒng)幫我們屏蔽了諸多硬件外設(shè)的差異,為應(yīng)用程序的開發(fā)者提供了友好、統(tǒng)一的服務(wù)。為了避免應(yīng)用程序破壞操作系統(tǒng)內(nèi)核,CPU提供了保護(hù)模式機(jī)制,使得應(yīng)用程序無法直接訪問被操作系統(tǒng)管理起來的外設(shè),而必須通過內(nèi)核提供的系統(tǒng)調(diào)用間接的訪問外設(shè)。關(guān)于操作系統(tǒng)I/O模型的討論針對(duì)的就是應(yīng)用程序與內(nèi)核之間進(jìn)行I/O交互的系統(tǒng)調(diào)用模型。

'  操作系統(tǒng)內(nèi)核提供的I/O模型大致可以分為幾種:同步阻塞I/O、同步非阻塞I/O、同步I/O多路復(fù)用、異步非阻塞I/O(信號(hào)驅(qū)動(dòng)I/O用的比較少,就不在這里展開了)。

同步阻塞I/O(Blocking I/O BIO)

  我們已經(jīng)知道,高效的硬件層面I/O模型對(duì)于CPU來說是異步的,但應(yīng)用程序開發(fā)者總是希望在執(zhí)行完I/O系統(tǒng)調(diào)用后能同步的返回,線性的執(zhí)行后續(xù)邏輯(例如當(dāng)磁盤讀取的系統(tǒng)調(diào)用返回后,下一行代碼中就能直接訪問到所讀出的數(shù)據(jù))。但這與硬件層面耗時(shí)、異步的I/O模型相違背(程序控制I/O過于浪費(fèi)CPU),因此操作系統(tǒng)內(nèi)核提供了基于同步、阻塞I/O的系統(tǒng)調(diào)用(BIO)來解決這一問題。

  舉個(gè)例子:當(dāng)線程通過基于BIO的系統(tǒng)調(diào)用進(jìn)行磁盤讀取時(shí),內(nèi)核會(huì)令當(dāng)前線程進(jìn)入阻塞態(tài),讓出CPU資源給其它并發(fā)的就緒態(tài)線程,以便更有效率的利用CPU。當(dāng)DMA完成讀取,異步的I/O中斷到來時(shí),內(nèi)核會(huì)找到先前被阻塞的對(duì)應(yīng)線程,將其喚醒進(jìn)入就緒態(tài)。當(dāng)這個(gè)就緒態(tài)的線程被內(nèi)核CPU調(diào)度器選中再度獲得CPU時(shí),便能從對(duì)應(yīng)的緩沖區(qū)結(jié)構(gòu)中得到讀取到的磁盤數(shù)據(jù),程序同步的執(zhí)行流便能順利的向下執(zhí)行了。(感覺好像線程卡在了那里不動(dòng),過了一會(huì)才執(zhí)行下一行,且指定的緩沖區(qū)中已經(jīng)有了所需的數(shù)據(jù))

  下面的偽代碼示例中參考linux的設(shè)計(jì),將不同的外設(shè)統(tǒng)一抽象為文件,通過文件描述符(file descriptor)來統(tǒng)一的訪問。

BIO偽代碼實(shí)例 :

// 創(chuàng)建TCP套接字并綁定端口8888,進(jìn)行服務(wù)監(jiān)聽
listenfd = serverSocket(8888,"tcp");
while(true){
    // accept同步阻塞調(diào)用
    newfd = accept(listenfd);

    // read會(huì)阻塞,因此使用線程異步處理,避免阻塞accpet(一般使用線程池)
    new thread(()->{
        // 同步阻塞讀取數(shù)據(jù)
        xxx = read(newfd);
        ... dosomething
        // 關(guān)閉連接
        close(newfd);
    });
}

BIO模型的優(yōu)點(diǎn):

  BIO的I/O模型由于同步、阻塞的特性,屏蔽了底層實(shí)質(zhì)上異步的硬件交互方式,令程序員可以編寫出簡單易懂的線性程序邏輯。

BIO模型的缺點(diǎn):

  1. BIO的同步、阻塞特性在簡單易用的同時(shí),也存在一些性能上的缺陷。由于BIO在等待I/O完成的時(shí)間中,線程雖然被阻塞不消耗CPU,但內(nèi)核維護(hù)一個(gè)系統(tǒng)級(jí)線程本身也是有一定的開銷(維護(hù)線程控制塊、內(nèi)核線程棧空間等等)。

  2. 不同線程在調(diào)度時(shí)的上下文切換CPU開銷較大,在如今大量用戶、高并發(fā)的互聯(lián)網(wǎng)時(shí)代越來越成為web服務(wù)器性能的瓶頸。線程上下文切換本身需要需要保存、恢復(fù)現(xiàn)場,同時(shí)還會(huì)清空CPU指令流水線,以及令高速緩存大量失效。對(duì)于一個(gè)web服務(wù)器,如果使用BIO模型,服務(wù)器將至少需要1:1的維護(hù)同等數(shù)量的系統(tǒng)級(jí)線程(內(nèi)核線程),由于持續(xù)并發(fā)的網(wǎng)絡(luò)數(shù)據(jù)交互,導(dǎo)致不同線程由于網(wǎng)絡(luò)I/O的完成事件被內(nèi)核反復(fù)的調(diào)度。

  在著名的C10K問題的語境下,一臺(tái)服務(wù)器需要同時(shí)維護(hù)1W個(gè)并發(fā)的tcp連接和對(duì)等的1W個(gè)系統(tǒng)級(jí)線程。量變引起質(zhì)變,1W個(gè)系統(tǒng)級(jí)線程調(diào)度引起的上下文切換和100個(gè)系統(tǒng)級(jí)線程的調(diào)度開銷完全不同,其將耗盡CPU資源,令整個(gè)系統(tǒng)卡死,崩潰。

BIO交互流程示意圖:

  I/O模型的相關(guān)問題有哪些

同步非阻塞I/O(NonBlocking I/O NIO)

  BIO模型簡單易用,但其阻塞內(nèi)核線程的特性使得其已經(jīng)不適用于需要處理大量(1K以上)并發(fā)網(wǎng)絡(luò)連接場景的web服務(wù)器了。為此,操作系統(tǒng)內(nèi)核提供了非阻塞特性的I/O系統(tǒng)調(diào)用,即NIO(NonBlocking-IO)

  針對(duì)BIO模型的缺陷,NIO模型的系統(tǒng)調(diào)用不會(huì)阻塞當(dāng)前調(diào)用線程。但由于I/O本質(zhì)上的耗時(shí)特性,無法立即得到I/O處理的結(jié)果,NIO的系統(tǒng)調(diào)用在I/O未完成時(shí)會(huì)返回特定標(biāo)識(shí),代表對(duì)應(yīng)的I/O事件還未完成。因此需要應(yīng)用程序按照一定的頻率反復(fù)調(diào)用,以獲取最新的IO狀態(tài)。

NIO偽代碼實(shí)例 :

// 創(chuàng)建TCP套接字并綁定端口8888,進(jìn)行服務(wù)監(jiān)聽
listenfd = serverSocket(8888,"tcp");
clientFdSet = empty_set();
while(true){ // 開啟事件監(jiān)聽循環(huán)
    // accept同步非阻塞調(diào)用,判斷是否接收了新的連接
    newfd = acceptNonBlock(listenfd);

    if(newfd != EMPTY){
        // 如果存在新連接將其加入監(jiān)聽連接集合
        clientFdSet.add(newfd);
    }
    // 申請(qǐng)一個(gè)1024字節(jié)的緩沖區(qū)
    buffer = new buffer(1024);
    for(clientfd in clientFdSet){
        // 非阻塞read讀
        num = readNonBlock(clientfd,buffer);
        if(num > 0){
            // 讀緩沖區(qū)存在數(shù)據(jù)
            data = buffer;
            ... dosomething
            if(needClose(data)){
                // 關(guān)閉連接時(shí),移除當(dāng)前監(jiān)聽的連接
                clientFdSet.remove(clientfd);
            }
        }
        ... dosomething
        // 清空buffer
        buffer.clear();
    }
}

NIO模型的優(yōu)點(diǎn):

  NIO因?yàn)槠浞亲枞奶匦?,使得一個(gè)線程可以處理多個(gè)并發(fā)的網(wǎng)絡(luò)I/O連接。在C10K問題的語境下,理論上可以通過一個(gè)線程處理這1W個(gè)并發(fā)連接(對(duì)于多核CPU,可以創(chuàng)建多個(gè)線程在每個(gè)CPU核心中分?jǐn)傌?fù)載,提高性能)。

NIO模型的缺點(diǎn):

  NIO克服了BIO在高并發(fā)條件下的缺陷,但原始的NIO系統(tǒng)調(diào)用依然有著一定的性能問題。在上述偽代碼示例中,每個(gè)文件描述符對(duì)應(yīng)的I/O狀態(tài)查詢,都必須通過一次NIO系統(tǒng)調(diào)用才能完成。

  由于操作系統(tǒng)內(nèi)核利用CPU提供的保護(hù)模式機(jī)制,使內(nèi)核運(yùn)行在高特權(quán)級(jí),而令用戶程序運(yùn)行在執(zhí)行、訪問受限的低特權(quán)級(jí)。這樣設(shè)計(jì)的一個(gè)好處就是使得應(yīng)用程序無法直接的訪問硬件,而必須由操作系統(tǒng)提供的系統(tǒng)調(diào)用間接的訪問硬件(網(wǎng)卡、磁盤甚至電源等)。執(zhí)行系統(tǒng)調(diào)用時(shí),需要令應(yīng)用線程通過系統(tǒng)調(diào)用陷入內(nèi)核(即提高應(yīng)用程序的當(dāng)前特權(quán)級(jí)CPL,使其能夠訪問受保護(hù)的硬件),并在系統(tǒng)調(diào)用返回時(shí)恢復(fù)為低特權(quán)級(jí),這樣一個(gè)過程在硬件上是通過中斷實(shí)現(xiàn)的。

  通過中斷實(shí)現(xiàn)系統(tǒng)調(diào)用的效率遠(yuǎn)低于應(yīng)用程序本地的函數(shù)調(diào)用,因此原始的NIO模式下通過系統(tǒng)調(diào)用循環(huán)訪問每個(gè)文件描述符I/O就緒狀態(tài)的方式是低效的。

NIO交互流程示意圖:

  I/O模型的相關(guān)問題有哪些

同步I/O多路復(fù)用(I/O Multiplexing)

  為了解決上述NIO模型的系統(tǒng)調(diào)用中,一次事件循環(huán)遍歷進(jìn)行N次系統(tǒng)調(diào)用的缺陷。操作系統(tǒng)內(nèi)核在NIO系統(tǒng)調(diào)用的基礎(chǔ)上提供了I/O多路復(fù)用模型的系統(tǒng)調(diào)用。

  I/O多路復(fù)用相對(duì)于NIO模型的一個(gè)優(yōu)化便是允許在一次I/O狀態(tài)查詢的系統(tǒng)調(diào)用中,一次傳遞復(fù)數(shù)個(gè)文件描述符進(jìn)行批量的I/O狀態(tài)查詢。在一次事件循環(huán)中只需要進(jìn)行一次I/O多路復(fù)用的系統(tǒng)調(diào)用就能得到所傳遞文件描述符集合的I/O狀態(tài),減少了原始NIO模型中不必要的系統(tǒng)調(diào)用開銷。

  多路復(fù)用I/O模型大致可以分為三種實(shí)現(xiàn)(雖然不同操作系統(tǒng)在最終實(shí)現(xiàn)上略有不同,但原理是類似的,示例代碼以linux內(nèi)核舉例):select、poll、epoll。

select多路復(fù)用器介紹

  select I/O多路復(fù)用器允許應(yīng)用程序傳遞需要監(jiān)聽事件變化的文件描述符集合,監(jiān)聽其讀/寫,接受連接等I/O事件的狀態(tài)。

  select系統(tǒng)調(diào)用本身是同步、阻塞的,當(dāng)所傳遞的文件描述符集合中都沒有就緒的I/O事件時(shí),執(zhí)行select系統(tǒng)調(diào)用的線程將會(huì)進(jìn)入阻塞態(tài),直到至少一個(gè)文件描述符對(duì)應(yīng)的I/O事件就緒,則喚醒被select阻塞的線程(可以指定超時(shí)時(shí)間來強(qiáng)制喚醒并返回)。喚醒后獲得CPU的線程在select系統(tǒng)調(diào)用返回后可以遍歷所傳入的文件描述符集合,處理完成了I/O事件的文件描述符。

select偽代碼示例:

// 創(chuàng)建TCP套接字并綁定端口8888,進(jìn)行服務(wù)監(jiān)聽
listenfd = serverSocket(8888,"tcp");
fdNum = 1;
clientFdSet = empty_set();
clientFdSet.add(listenfd);
while(true){ // 開啟事件監(jiān)聽循環(huán)
    // man 2 select(查看linux系統(tǒng)文檔)
    // int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    // 參數(shù)nfds:一共需要監(jiān)聽的readfds、writefds、exceptfds中文件描述符個(gè)數(shù)+1
    // 參數(shù)readfds/writefds/exceptfds: 需要監(jiān)聽讀、寫、異常事件的文件描述符集合
    // 參數(shù)timeout:select是同步阻塞的,當(dāng)timeout時(shí)間內(nèi)都沒有任何I/O事件就緒,則調(diào)用線程被喚醒并返回(ret=0)
    //         timeout為null代表永久阻塞
    // 返回值ret:
    //  1.返回大于0的整數(shù),代表傳入的readfds/writefds/exceptfds中共有ret個(gè)被激活(需要應(yīng)用程序自己遍歷),
    //    2.返回0,在阻塞超時(shí)前沒有任何I/O事件就緒
    //    3.返回-1,出現(xiàn)錯(cuò)誤

    listenReadFd = clientFdSet;
    // select多路復(fù)用,一次傳入需要監(jiān)聽事件的全量連接集合(超時(shí)時(shí)間1s)
    result = select(fdNum+1,listenReadFd,null,null,timeval("1s"));
    if(result > 0){
        // 如果服務(wù)器監(jiān)聽連接存在讀事件
        if(IN_SET(listenfd,listenReadFd)){
            // 接收并建立連接
            newClientFd = accept(listenfd);
            // 加入客戶端連接集合
            clientFdSet.add(newClientFd);       fdNum++;
        }
        
        // 遍歷整個(gè)需要監(jiān)聽的客戶端連接集合
        for(clientFd : clientFdSet){
            // 如果當(dāng)前客戶端連接存在讀事件
            if(IN_SET(clientFd,listenReadFd)){
                // 阻塞讀取數(shù)據(jù)
                data = read(clientfd);
                ... dosomething
                
                if(needClose(data)){
                    // 關(guān)閉連接時(shí),移除當(dāng)前監(jiān)聽的連接
                    clientFdSet.remove(clientfd);            fdNum--;
                }
            }
        }
    }
}

select的優(yōu)點(diǎn):

  1. select多路復(fù)用避免了上述原始NIO模型中無謂的多次查詢I/O狀態(tài)的系統(tǒng)調(diào)用,將其聚合成集合,批量的進(jìn)行監(jiān)聽并返回結(jié)果集。

  2. select實(shí)現(xiàn)相對(duì)簡單,windows、linux等主流的操作系統(tǒng)都實(shí)現(xiàn)了select系統(tǒng)調(diào)用,跨平臺(tái)的兼容性好。

select的缺點(diǎn):

  1. 在事件循環(huán)中,每次select系統(tǒng)調(diào)用都需要從用戶態(tài)全量的傳遞所需要監(jiān)聽的文件描述符集合,并且select返回后還需要全量遍歷之前傳入的文件描述符集合的狀態(tài)。

  2. 出于性能的考量,內(nèi)核設(shè)置了select所監(jiān)聽文件描述符集合元素的最大數(shù)量(一般為1024,可在內(nèi)核啟動(dòng)時(shí)指定),使得單次select所能監(jiān)聽的連接數(shù)受到了限制。

  3. 拋開性能的考慮,從接口設(shè)計(jì)的角度來看,select將系統(tǒng)調(diào)用的參數(shù)與返回值混合到了一起(返回值覆蓋了參數(shù)),增加了使用者理解的困難度。

I/O多路復(fù)用交互示意圖:

  I/O模型的相關(guān)問題有哪些

poll多路復(fù)用器介紹

  poll I/O多路復(fù)用器在使用上和select大同小異,也是通過傳入指定的文件描述符集合以及指定內(nèi)核監(jiān)聽對(duì)應(yīng)文件描述符上的I/O事件集合,但在實(shí)現(xiàn)的細(xì)節(jié)上基于select做了一定的優(yōu)化。

  和select一樣,poll系統(tǒng)調(diào)用在沒有任何就緒事件發(fā)生時(shí)也是同步、阻塞的(可以指定超時(shí)時(shí)間強(qiáng)制喚醒并返回),當(dāng)返回后要判斷是否有就緒事件時(shí),也一樣需要全量的遍歷整個(gè)返回的文件描述符集合。

poll偽代碼示例:

/*
// man 2 poll(查看linux系統(tǒng)文檔)
// 和select不同將參數(shù)events和返回值revents分開了
struct pollfd {
               int   fd;         // file descriptor 對(duì)應(yīng)的文件描述符 
               short events;     // requested events 需要監(jiān)聽的事件
               short revents;    // returned events 返回時(shí),就緒的事件
           };

// 參數(shù)fds,要監(jiān)聽的poolfd數(shù)組集合
// 參數(shù)nfds,傳入fds數(shù)組中需要監(jiān)聽的元素個(gè)數(shù)
// 參數(shù)timeout,阻塞的超時(shí)時(shí)間(傳入-1代表永久阻塞)
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

//events/revents是位圖表示的
//revents & POLLIN == 1 存在就緒的讀事件
//revents & POLLOUT == 1 存在就緒的寫事件
//revents & POLLHUP == 1 存在對(duì)端斷開連接或是通信完成事件
*/

// 創(chuàng)建TCP套接字并綁定端口8888,進(jìn)行服務(wù)監(jiān)聽
listenfd = serverSocket(8888,"tcp");

MAX_LISTEN_SIZE = 100;
struct pollfd fds[MAX_LISTEN_SIZE];
// 設(shè)置服務(wù)器監(jiān)聽套接字(監(jiān)聽讀事件)
fds[0].fd = listenfd;
fds[0].events = POLLIN;
fds[0].revents = 0;
// 客戶端連接數(shù)一開始為0
int clientCount = 0;

while(true){
    // poll同步阻塞調(diào)用(超時(shí)時(shí)間-1表示永久阻塞直到存在監(jiān)聽的就緒事件)
    int ret = poll(fds, clientCount + 1, -1);
        
    for (int i = 0; i < clientCount + 1; i++){
        if(fds[i].fd == listenfd && fds[i].revents & POLLIN){
            // 服務(wù)器監(jiān)聽套接字讀事件就緒,建立新連接
            clientCount++;
            fds[clientCount].fd = conn;
            fds[clientCount].events = POLLIN | POLLRDHUP ;
            fds[clientCount].revents = 0;
        }else if(fds[i].revents & POLLIN){
            // 其他鏈接可讀,進(jìn)行讀取
            read(fds[i].fd);
            ... doSomething
        }else if(fds[i].revents & POLLRDHUP){
            // 監(jiān)聽到客戶端連接斷開,移除該連接
            fds[i] = fds[clientCount];
            i--;
            clientCount--;
            // 關(guān)閉該連接
            close(fd);
        }
    }
}

poll的優(yōu)點(diǎn):

  1. poll解決了select系統(tǒng)調(diào)用受限于內(nèi)核配置參數(shù)的限制問題,可以同時(shí)監(jiān)聽更多文件描述符的I/O狀態(tài)(但不能超過內(nèi)核限制當(dāng)前進(jìn)程所能擁有的最大文件描述符數(shù)目限制)。

  2. 優(yōu)化了接口設(shè)計(jì),將參數(shù)與返回值的進(jìn)行了分離。

poll的缺點(diǎn):

  1. poll優(yōu)化了select,但在處理大量閑置連接時(shí),即使真正產(chǎn)生I/O就緒事件的活躍文件描述符數(shù)量很少,依然免不了線性的遍歷整個(gè)監(jiān)聽的文件描述符集合。每次調(diào)用時(shí),需要全量的將整個(gè)感興趣的文件描述符集合從用戶態(tài)復(fù)制到內(nèi)核態(tài)。

  2. 由于select/poll都需要全量的傳遞參數(shù)以及遍歷返回值,因此其時(shí)間復(fù)雜度為O(n),即處理的開銷隨著并發(fā)連接數(shù)n的增加而增加,而無論并發(fā)連接本身活躍與否。但一般情況下即使并發(fā)連接數(shù)很多,大量連接都產(chǎn)生I/O就緒事件的情況并不多,更多的情況是1W的并發(fā)連接,可能只有幾百個(gè)是處于活躍狀態(tài)的,這種情況下select/poll的性能并不理想,還存在優(yōu)化的空間。

epoll多路復(fù)用器:

  epoll是linux系統(tǒng)中獨(dú)有的,針對(duì)select/poll上述缺點(diǎn)進(jìn)行改進(jìn)的高性能I/O多路復(fù)用器。

  針對(duì)poll系統(tǒng)調(diào)用介紹中的第一個(gè)缺點(diǎn):在每次事件循環(huán)時(shí)都需要從用戶態(tài)全量傳遞整個(gè)需要監(jiān)聽的文件描述符集合

  epoll在內(nèi)核中分配內(nèi)存空間用于緩存被監(jiān)聽的文件描述符集合。通過創(chuàng)建epoll的系統(tǒng)調(diào)用(epoll_create),在內(nèi)核中維護(hù)了一個(gè)epoll結(jié)構(gòu),而在應(yīng)用程序中只需要保留epoll結(jié)構(gòu)的句柄就可對(duì)其進(jìn)行訪問(也是一個(gè)文件描述符)??梢詣?dòng)態(tài)的在epoll結(jié)構(gòu)的內(nèi)核空間中增加/刪除/更新所要監(jiān)聽的文件描述符以及不同的監(jiān)聽事件(epoll_ctl),而不必每次都全量的傳遞需要監(jiān)聽的文件描述符集合。

  針對(duì)select/poll的第二個(gè)缺點(diǎn):在系統(tǒng)調(diào)用返回后通過修改所監(jiān)聽文件描述符結(jié)構(gòu)的狀態(tài),來標(biāo)識(shí)文件描述符對(duì)應(yīng)的I/O事件是否就緒。每次系統(tǒng)調(diào)用返回時(shí),都需要全量的遍歷整個(gè)監(jiān)聽文件描述符集合,而無論是否真的完成了I/O。

  epoll監(jiān)聽事件的系統(tǒng)調(diào)用完成后,只會(huì)將真正活躍的、完成了I/O事件的文件描述符返回,避免了全量的遍歷。在并發(fā)的連接數(shù)很大,但閑置連接占比很高時(shí),epoll的性能大大優(yōu)于select/poll這兩種I/O多路復(fù)用器。epoll的時(shí)間復(fù)雜度為O(m),即處理的開銷不隨著并發(fā)連接n的增加而增加,而是僅僅和監(jiān)控的活躍連接m相關(guān);在某些情況下n遠(yuǎn)大于m,epoll的時(shí)間復(fù)雜度甚至可以認(rèn)為近似的達(dá)到了O(1)。

  通過epoll_wait系統(tǒng)調(diào)用,監(jiān)聽參數(shù)中傳入對(duì)應(yīng)epoll結(jié)構(gòu)中關(guān)聯(lián)的所有文件描述符的對(duì)應(yīng)I/O狀態(tài)。epoll_wait本身是同步、阻塞的(可以指定超時(shí)時(shí)間強(qiáng)制喚醒并返回),當(dāng)epoll_wait同步返回時(shí),會(huì)返回處于活躍狀態(tài)的完成I/O事件的文件描述符集合,避免了select/poll中的無效遍歷。同時(shí)epoll使用了mmap機(jī)制,將內(nèi)核中的維護(hù)的就緒文件描述符集合所在空間映射到了用戶態(tài),令應(yīng)用程序與epoll的內(nèi)核共享這一區(qū)域的內(nèi)存,避免了epoll返回就緒文件描述符集合時(shí)的一次內(nèi)存復(fù)制。

epoll偽代碼示例:

/**
    epoll比較復(fù)雜,使用時(shí)大致依賴三個(gè)系統(tǒng)調(diào)用 (man 7 epoll)
    1. epoll_create 創(chuàng)建一個(gè)epoll結(jié)構(gòu),返回對(duì)應(yīng)epoll的文件描述符 (man 2 epoll_create)
        int epoll_create();
    2. epoll_ctl 控制某一epoll結(jié)構(gòu)(epfd),向其增加/刪除/更新(op)某一其它連接(fd),監(jiān)控其I/O事件(event) (man 2 epoll_ctl)
        op有三種合法值:EPOLL_CTL_ADD代表新增、EPOLL_CTL_MOD代表更新、EPOLL_CTL_DEL代表刪除
        int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    3. epoll_wait 令某一epoll同步阻塞的開始監(jiān)聽(epfd),感興趣的I/O事件(events),所監(jiān)聽fd的最大個(gè)數(shù)(maxevents),指定阻塞超時(shí)時(shí)間(timeout) (man 2 epoll_wait)
        int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
*/

// 創(chuàng)建TCP套接字并綁定端口8888,進(jìn)行服務(wù)監(jiān)聽
listenfd = serverSocket(8888,"tcp");
// 創(chuàng)建一個(gè)epoll結(jié)構(gòu)
epollfd = epoll_create();

ev = new epoll_event();
ev.events = EPOLLIN; // 讀事件
ev.data.fd = listenfd;
// 通過epoll監(jiān)聽服務(wù)器端口讀事件(新連接建立請(qǐng)求)
epoll_ctl(epollfd,EPOLL_CTL_ADD,listenfd,ev);

// 最大監(jiān)聽1000個(gè)連接
MAX_EVENTS = 1000;
listenEvents = new event[MAX_EVENTS];
while(true){
    // 同步阻塞監(jiān)聽事件
    // 最多返回MAX_EVENTS個(gè)事件響應(yīng)結(jié)果
    // (超時(shí)時(shí)間1000ms,標(biāo)識(shí)在超時(shí)時(shí)間內(nèi)沒有任何事件就緒則當(dāng)前線程被喚醒,返回值nfd將為0)
    nfds = epoll_wait(epollfd, listenEvents, MAX_EVENTS, 1 * 1000);
        
    for(n = 0; n < nfds; ++n){
        if(events[n].data.fd == listenfd){
            // 當(dāng)發(fā)現(xiàn)服務(wù)器監(jiān)聽套接字存在可讀事件,建立新的套接字連接
            clientfd = accept(listenfd);

            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = clientfd;
            // 新建立的套接字連接也加入當(dāng)前epoll的監(jiān)聽(監(jiān)聽讀(EPOLLIN)/寫(EPOLLET)事件)
            epoll_ctl(epollfd,EPOLL_CTL_ADD,clientfd,ev);
        } else{
            // 否則是其它連接的I/O事件就緒,進(jìn)行對(duì)應(yīng)的操作
            ... do_something
        }
    }
}

epoll的優(yōu)點(diǎn):

  epoll是目前性能最好的I/O多路復(fù)用器之一,具有I/O多路復(fù)用優(yōu)點(diǎn)的情況下很好的解決了select/poll的缺陷。目前l(fā)inux平臺(tái)中,像nginx、redis、netty等高性能服務(wù)器都是首選epoll作為基礎(chǔ)來實(shí)現(xiàn)網(wǎng)絡(luò)I/O功能的。

epoll的缺點(diǎn):

  1. 常規(guī)情況下閑置連接占比很大,epoll的性能表現(xiàn)的很好。但是也有少部分場景中,絕大多數(shù)連接都是活躍的,那么其性能與select/poll這種基于位圖、數(shù)組等簡單結(jié)構(gòu)的I/O多路復(fù)用器相比,就不那么有優(yōu)勢(shì)了。因?yàn)閟elect/poll被詬病的一點(diǎn)就是通常情況下進(jìn)行了無謂的全量檢查,而當(dāng)活躍連接數(shù)占比一直超過90%甚至更高時(shí),就不再是浪費(fèi)了;相反的,由于epoll內(nèi)部結(jié)構(gòu)比較復(fù)雜,在這種情況下其性能比select/poll還要低一點(diǎn)。

  2. epoll是linux操作系統(tǒng)下獨(dú)有的,使得基于epoll實(shí)現(xiàn)的應(yīng)用程序的跨平臺(tái)兼容性受到了一定影響。

異步非阻塞I/O(Asynchronous I/O AIO)

  windows和linux都支持了select系統(tǒng)調(diào)用,但linux內(nèi)核在之后又實(shí)現(xiàn)了epoll這一更高性能的I/O多路復(fù)用器來改進(jìn)select。

  windows沒有模仿linux,而是提供了被稱為IOCP(Input/Output Completion Port 輸入輸出完成端口)的功能解決select性能的問題。IOCP采用異步非阻塞IO(AIO)的模型,其與epoll同步非阻塞IO的最大區(qū)別在于,epoll調(diào)用完成后,僅僅返回了就緒的文件描述符集合;而IOCP則在內(nèi)核中自動(dòng)的完成了epoll中原本應(yīng)該由應(yīng)用程序主動(dòng)發(fā)起的I/O操作。

  舉個(gè)例子,當(dāng)監(jiān)聽到就緒事件開始讀取某一網(wǎng)絡(luò)連接的請(qǐng)求報(bào)文時(shí),epoll依然需要通過程序主動(dòng)的發(fā)起讀取請(qǐng)求,將數(shù)據(jù)從內(nèi)核中讀入用戶空間。而windows下的IOCP則是通過注冊(cè)回調(diào)事件的方式工作,由內(nèi)核自動(dòng)的將數(shù)據(jù)放入指定的用戶空間,當(dāng)處理完畢后會(huì)調(diào)度激活注冊(cè)的回調(diào)事件,被喚醒的線程能直接訪問到所需要的數(shù)據(jù)。

  這也是為什么BIO/NIO/IO多路復(fù)用被稱為同步I/O,而IOCP被稱為異步I/O的原因。

  同步I/O與異步I/O的主要區(qū)別就在于站在應(yīng)用程序的視角看,真正讀取/寫入數(shù)據(jù)時(shí)是否是由應(yīng)用程序主導(dǎo)的。如果需要用戶程序主動(dòng)發(fā)起最終的I/O請(qǐng)求就被稱為同步I/O;而如果是內(nèi)核自動(dòng)完成I/O后通知用戶程序,則被稱為異步I/O。(可以類比在前面硬件I/O模型中,站在CPU視角的同步、異步I/O模型,只不過這里CPU變成了應(yīng)用程序,而外設(shè)/DMA變成了操作系統(tǒng)內(nèi)核)

AIO的優(yōu)點(diǎn):

  AIO作為異步I/O,由內(nèi)核自動(dòng)的完成了底層一整套的I/O操作,應(yīng)用程序在事件回調(diào)通知中能直接獲取到所需數(shù)據(jù)。內(nèi)核中可以實(shí)現(xiàn)非常高效的調(diào)度、通知框架。擁有前面NIO高性能的優(yōu)點(diǎn),又簡化了應(yīng)用程序的開發(fā)。

AIO的缺點(diǎn):

  由內(nèi)核全盤控制的全自動(dòng)I/O雖然能夠做到足夠高效,但是在一些特定場景下性能并不一定能超過由應(yīng)用程序主導(dǎo)的,經(jīng)過深度優(yōu)化的代碼。像epoll在支持了和select/poll一樣的水平觸發(fā)I/O的同時(shí),還支持了更加細(xì)致的邊緣觸發(fā)I/O,允許用戶自主的決定當(dāng)I/O就緒時(shí),是否需要立即處理或是緩存起來等待稍后再處理。(就像java等支持自動(dòng)內(nèi)存垃圾回收的語言,即使其垃圾收集器經(jīng)過持續(xù)的優(yōu)化,在大多數(shù)情況下性能都很不錯(cuò),但卻依然無法達(dá)到和經(jīng)過開發(fā)人員反復(fù)調(diào)優(yōu),手動(dòng)回收內(nèi)存的C、C++等語言實(shí)現(xiàn)的程序一樣的性能)

  I/O模型的相關(guān)問題有哪些(截圖自《Unix網(wǎng)絡(luò)編程 卷1》)

操作系統(tǒng)I/O模型小結(jié)

  1. 同步I/O包括了同步阻塞I/O和同步非阻塞I/O,而異步I/O中由于異步阻塞I/O模型沒有太大價(jià)值,因此提到異步I/O(AIO)時(shí),默認(rèn)指的就是異步非阻塞I/O。

  I/O模型的相關(guān)問題有哪些

  2. 在I/O多路復(fù)用器的工作中,當(dāng)監(jiān)聽到對(duì)應(yīng)文件描述符I/O事件就緒時(shí),后續(xù)進(jìn)行的讀/寫操作既可以是阻塞的,也可以是非阻塞的。如果是都以阻塞的方式進(jìn)行讀/寫,雖然實(shí)現(xiàn)簡單,但如果某一文件描述符需要讀寫的數(shù)據(jù)量很大時(shí)將耗時(shí)較多,可能會(huì)導(dǎo)致事件循環(huán)中的其它事件得不到及時(shí)處理。因此截圖中的阻塞讀寫數(shù)據(jù)部分并不準(zhǔn)確,需要辯證的看待。

四、非阻塞I/O是銀彈嗎?

  計(jì)算機(jī)技術(shù)的發(fā)展看似日新月異,但本質(zhì)上有兩類目標(biāo)指引著其前進(jìn)。一是盡可能的增強(qiáng)、壓榨硬件的性能,提高機(jī)器效率;二是盡可能的通過持續(xù)的抽象、封裝簡化軟件復(fù)雜度,提高程序員的開發(fā)效率。計(jì)算機(jī)軟件的發(fā)展方向必須至少需要滿足其中一種目標(biāo)。

  從上面關(guān)于操作系統(tǒng)內(nèi)核I/O模型的發(fā)展中可以看到,最初被廣泛使用的是易理解、開發(fā)簡單的BIO模型;但由于互聯(lián)網(wǎng)時(shí)代的到來,web服務(wù)器系統(tǒng)面臨著C10K問題,需要能支持海量的并發(fā)客戶端連接,因此出現(xiàn)了包括NIO、I/O多路復(fù)用、AIO等技術(shù),利用一個(gè)內(nèi)核線程管理成百上千的并發(fā)連接,來解決BIO模型中一個(gè)內(nèi)核線程對(duì)應(yīng)一個(gè)網(wǎng)絡(luò)連接的工作模式中,由于處理大量連接導(dǎo)致內(nèi)核線程上下文頻繁切換,造成CPU資源耗盡的問題。上述的第一條原則指引著內(nèi)核I/O模型的發(fā)展,使得web服務(wù)器能夠獲得更大的連接服務(wù)吞吐量,提高了機(jī)器效率。

  但非阻塞I/O真的是完美無缺的嗎?

  有著非阻塞I/O模型開發(fā)經(jīng)驗(yàn)的程序員都知道,正是由于一個(gè)內(nèi)核線程管理著成百上千個(gè)客戶端連接,因此在整個(gè)線程的執(zhí)行流中不能出現(xiàn)耗時(shí)、阻塞的操作(比如同步阻塞的數(shù)據(jù)庫查詢、rpc接口調(diào)用等)。如果這種操作不可避免,則需要單獨(dú)使用另外的線程異步的處理,而不能阻塞當(dāng)前的整個(gè)事件循環(huán),否則將會(huì)導(dǎo)致其它連接的請(qǐng)求得不到及時(shí)的處理,造成饑餓。

  對(duì)于多數(shù)互聯(lián)網(wǎng)分布式架構(gòu)下處理業(yè)務(wù)邏輯的應(yīng)用程序服務(wù)器來說,在一個(gè)網(wǎng)絡(luò)請(qǐng)求服務(wù)中,可能需要頻繁的訪問數(shù)據(jù)庫或者通過網(wǎng)絡(luò)遠(yuǎn)程調(diào)用其它服務(wù)的接口。如果使用的是基于NIO模型進(jìn)行工作的話,則要求rpc庫以及數(shù)據(jù)庫、中間件等連接的庫是支持異步非阻塞的。如果由于同步阻塞庫的存在,在每次接受連接進(jìn)行服務(wù)時(shí)依然被迫通過另外的線程處理以避免阻塞,則NIO服務(wù)器的性能將退化到和使用傳統(tǒng)的BIO模型一樣的地步。

  所幸的是隨著非阻塞I/O的逐漸流行,上述問題得到了很大的改善,越來越的框架/庫都提供了異步非阻塞的api接口。

非阻塞I/O帶來的新問題

  異步非阻塞庫改變了同步阻塞庫下程序員習(xí)以為常的,線性的思維方式,在編碼時(shí)被迫的以事件驅(qū)動(dòng)的方式思考。邏輯上連貫的業(yè)務(wù)代碼為了適應(yīng)異步非阻塞的庫程序,被迫分隔成多個(gè)獨(dú)立片段嵌套在各個(gè)不同層次的回調(diào)函數(shù)中。對(duì)于復(fù)雜的業(yè)務(wù)而言,很容易出現(xiàn)嵌套為一層層的回調(diào)函數(shù)調(diào)用鏈,形成臭名昭著的callback hell(回調(diào)地獄)。

  最早被callback hell折磨的可能是客戶端程序的開發(fā)人員,因?yàn)榭蛻舳顺绦蛐枰獣r(shí)刻監(jiān)聽著用戶操作事件的產(chǎn)生,通常以基于事件驅(qū)動(dòng)的方式組織異步處理代碼。

callback hell偽代碼示例:

// 由于互相之間有前后的數(shù)據(jù)依賴,按照順序異步的調(diào)用A、B、C、D
A.dosomething((res)->{
    data = res.xxx;
    B.dosomething(data,(res)->{
        data = res.xxx;
        C.dosomething(data,(res)->{
            data = res.xxx
            D.dosomething(data,(res)->{
                // 。。。 有依賴的同步業(yè)務(wù)越復(fù)雜,層次越深,就像一個(gè)無底洞
            })
        })
    })
})

  異步非阻塞庫的使用割裂了代碼的連貫結(jié)構(gòu),使得程序變得難以理解、調(diào)試,這一缺陷在堆積著復(fù)雜晦澀業(yè)務(wù)邏輯的web應(yīng)用程序服務(wù)器程序中顯得難以忍受。這也是為什么如今web服務(wù)器仍然有很大一部分依然使用傳統(tǒng)的同步阻塞的BIO模型進(jìn)行開發(fā)的主要原因。通過分布式、集群的方式分?jǐn)偞罅坎l(fā)的連接,而只在業(yè)務(wù)相對(duì)簡單的API網(wǎng)關(guān)、消息隊(duì)列等I/O密集型的中間件程序中NIO才被廣泛使用(實(shí)在不行,業(yè)務(wù)服務(wù)器集群可以加機(jī)器,保證開發(fā)效率也同樣重要)。

  那么就沒有什么辦法既能夠擁有非阻塞I/O支撐海量并發(fā)、高吞吐量的性能優(yōu)勢(shì);又能夠令程序員以同步方式思考、編寫程序,以提高開發(fā)效率嗎?

  解決辦法當(dāng)然是存在的,且相關(guān)技術(shù)依然在不斷發(fā)展。上述計(jì)算機(jī)技術(shù)發(fā)展的第二個(gè)原則指導(dǎo)著這些技術(shù)發(fā)展,目的是為了簡化代碼復(fù)雜性,提高程序員的效率。

1. 優(yōu)化語法、語言庫以簡化異步編程的難度

  在函數(shù)式編程的領(lǐng)域,就一直有著諸多晦澀的“黑科技”(CPS變換、monad等),能夠簡化callback hell,使得可以以幾乎是同步的方式編寫實(shí)質(zhì)上是異步執(zhí)行的代碼。例如EcmaScript便在EcmaScript6、EcmaScript7中分別引入了promise和async/await來解決這一問題。

2. 在語言級(jí)別支持用戶級(jí)線程(協(xié)程)

  前面提到,傳統(tǒng)的基于BIO模型的工作模式最大的優(yōu)點(diǎn)在于可以同步的編寫代碼,遇到需要等待的耗時(shí)操作時(shí)能夠被阻塞,使用起來簡單易懂。但由于1:1的維護(hù)內(nèi)核線程在處理海量連接時(shí)由于頻繁的內(nèi)核線程上下文切換而力不從心,催生了非阻塞I/O。

  而由于上述非阻塞I/O引起的代碼復(fù)雜度增加的問題,計(jì)算機(jī)科學(xué)家們想到了很早之前就在操作系統(tǒng)概念中提出,但一直沒有被廣泛使用的另一種線程實(shí)現(xiàn)方式:用戶級(jí)線程。

  用戶級(jí)線程顧名思義,就是在用戶級(jí)實(shí)現(xiàn)的線程,操作系統(tǒng)內(nèi)核對(duì)其是無感知的。用戶級(jí)線程在許多方面與大家所熟知的內(nèi)核級(jí)線程相似,都有著自己獨(dú)立的執(zhí)行流,和進(jìn)程中的其它線程共享內(nèi)存空間。

  用戶級(jí)線程與內(nèi)核級(jí)線程最大的一個(gè)區(qū)別就在于由于操作系統(tǒng)對(duì)其無感知,因此無法對(duì)用戶級(jí)線程進(jìn)行基于中斷的搶占式調(diào)度。要使得同一進(jìn)程下的不同用戶級(jí)線程能夠協(xié)調(diào)工作,必須小心的編寫執(zhí)行邏輯,以互相之間主動(dòng)讓渡CPU的形式工作,否則將會(huì)導(dǎo)致一個(gè)用戶級(jí)線程持續(xù)不斷的占用CPU,而令其它用戶級(jí)線程處于饑餓狀態(tài),因此用戶級(jí)線程也被稱為協(xié)程,即互相協(xié)作的線程。

  用戶級(jí)線程無論如何是基于至少一個(gè)內(nèi)核線程/進(jìn)程的,多個(gè)用戶級(jí)線程可以掛載在一個(gè)內(nèi)核線程/進(jìn)程中被內(nèi)核統(tǒng)一的調(diào)度管理。

  I/O模型的相關(guān)問題有哪些

  協(xié)程可以在遇到I/O等耗時(shí)操作時(shí)選擇主動(dòng)的讓出CPU,以實(shí)現(xiàn)同步阻塞的效果,令程序執(zhí)行流轉(zhuǎn)移到另一個(gè)協(xié)程中。由于多個(gè)協(xié)程可以復(fù)用一個(gè)內(nèi)核線程,每個(gè)協(xié)程所占用的開銷相對(duì)內(nèi)核級(jí)線程來說非常小;且協(xié)程上下文切換時(shí)由于不需要陷入內(nèi)核,其切換效率也遠(yuǎn)比內(nèi)核線程的上下文切換高(開銷近似于一個(gè)函數(shù)調(diào)用)。

  最近很流行的Go語言就是由于其支持語言層面的協(xié)程而備受推崇。程序員可以利用一些語言層面提供的協(xié)程機(jī)制編寫高效的web服務(wù)器程序(例如在語句中添加控制協(xié)程同步的關(guān)鍵字)。通過在編譯后的最終代碼中加入對(duì)應(yīng)的協(xié)程調(diào)度指令,由協(xié)程調(diào)度器接手,控制協(xié)程同步時(shí)在耗時(shí)I/O操作發(fā)生時(shí)主動(dòng)的讓出CPU,并在處理完畢后能被調(diào)度回來接著執(zhí)行。Go語言通過語言層面上對(duì)協(xié)程的支持,降低了編寫正確、協(xié)調(diào)工作的協(xié)程代碼的難度。

  Go編寫的高性能web服務(wù)器如果運(yùn)行在多核CPU的linux操作系統(tǒng)中,一般會(huì)創(chuàng)建m個(gè)內(nèi)核線程和n個(gè)協(xié)程(m正比與CPU核心數(shù),n遠(yuǎn)大于m且正比于并發(fā)連接數(shù)),底層每個(gè)內(nèi)核線程依然可以利用epoll IO多路復(fù)用器處理并發(fā)的網(wǎng)絡(luò)連接,并將業(yè)務(wù)邏輯處理的任務(wù)轉(zhuǎn)交給用戶態(tài)的協(xié)程(gorountine)。每個(gè)協(xié)程可以在不同的內(nèi)核線程(CPU核心)中被來回調(diào)度,以獲得最大的CPU吞吐量。

  使用協(xié)程,程序員在開發(fā)時(shí)能夠編寫同步阻塞的耗時(shí)I/O代碼,又不用擔(dān)心高并發(fā)情況下BIO模型中的性能問題。可以說協(xié)程兼顧了程序開發(fā)效率與機(jī)器執(zhí)行效率,因此越來越多的語言也在語言層面或是在函數(shù)庫中提供協(xié)程機(jī)制。

3. 實(shí)現(xiàn)用戶透明的協(xié)程

  在通過虛擬機(jī)作為中間媒介,操作系統(tǒng)平臺(tái)無關(guān)的語言中(比如java),虛擬機(jī)作為應(yīng)用程序與操作系統(tǒng)內(nèi)核的中間層,可以對(duì)應(yīng)用程序進(jìn)行各方面的優(yōu)化,令程序員可以輕松編寫出高效的代碼。

  有大牛在知乎的一篇回答中提到過,其曾經(jīng)領(lǐng)導(dǎo)團(tuán)隊(duì)在阿里巴巴工作時(shí)在java中實(shí)現(xiàn)了透明的協(xié)程。但似乎沒有和官方標(biāo)準(zhǔn)達(dá)成統(tǒng)一因此并沒有對(duì)外開放。

  如果能夠在虛擬機(jī)中提供高效、用戶透明的協(xié)程機(jī)制,使得原本基于BIO多線程的服務(wù)器程序無需改造便自動(dòng)的獲得了支持海量并發(fā)的能力,那真是太強(qiáng)了Orz。

感謝各位的閱讀,以上就是“I/O模型的相關(guān)問題有哪些”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對(duì)I/O模型的相關(guān)問題有哪些這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI