1. 硬核圖解網(wǎng)絡IO模型!

        共 5313字,需瀏覽 11分鐘

         ·

        2022-02-25 21:02

        前言

        文章會同步到個人網(wǎng)站,方便閱讀:https://xiaoflyfish.cn/

        • 網(wǎng)站最近豐富了很多內(nèi)容,都是滿滿的干貨!

        • 微信搜索:月伴飛魚,交個朋友,進面試交流群!

        • 公眾號后臺回復666,可以獲得免費電子書籍!

        覺得不錯,希望點贊,在看,轉(zhuǎn)發(fā)支持一下,謝謝

        背景介紹

        • 在互聯(lián)網(wǎng)的時代下,絕大部分數(shù)據(jù)都是通過網(wǎng)絡來進行獲取的。

        • 在服務端的架構(gòu)中,絕大部分數(shù)據(jù)也是通過網(wǎng)絡來進行交互的。

        而且作為服務端的開發(fā)工程師來說,都會進行一系列服務設計、開發(fā)以及能力開放,而服務能力開放也是需要通過網(wǎng)絡來完成的,因此對網(wǎng)絡編程以及網(wǎng)絡IO模型都不會太陌生。

        由于有很多優(yōu)秀的框架(比如Netty、HSF、Dubbo、Thrift等)已經(jīng)把底層網(wǎng)絡IO給封裝了,通過提供的API能力或者配置就能完成想要的服務能力開發(fā),因此大部分工程師對網(wǎng)絡IO模型的底層不夠了解。

        本文系統(tǒng)的講解了Linux內(nèi)核的IO模型、Java網(wǎng)絡IO模型以及兩者之間的關(guān)系!

        什么是IO

        我們都知道在Linux的世界,一切皆文件。

        而文件就是一串二進制流,不管Socket、FIFO、管道還是終端,對我們來說,一切都是流。

        • 在信息的交換過程中,我們都是對這些流進行數(shù)據(jù)收發(fā)操作,簡稱為I/O操作。

        • 往流中讀取數(shù)據(jù),系統(tǒng)調(diào)用Read,寫入數(shù)據(jù),系統(tǒng)調(diào)用Write。

        通常用戶進程的一個完整的IO分為兩個階段:

        磁盤IO:

        網(wǎng)絡IO:

        操作系統(tǒng)和驅(qū)動程序運行在內(nèi)核空間,應用程序運行在用戶空間,兩者不能使用指針傳遞數(shù)據(jù),因為Linux使用的虛擬內(nèi)存機制,必須通過系統(tǒng)調(diào)用請求內(nèi)核來完成IO動作。

        IO有內(nèi)存IO、網(wǎng)絡IO和磁盤IO三種,通常我們說的IO指的是后兩者!

        為什么需要IO模型

        如果使用同步的方式來通信的話,所有的操作都在一個線程內(nèi)順序執(zhí)行完成,這么做缺點是很明顯的:

        • 因為同步的通信操作會阻塞同一個線程的其他任何操作,只有這個操作完成了之后,后續(xù)的操作才可以完成,所以出現(xiàn)了同步阻塞+多線程(每個Socket都創(chuàng)建一個線程對應),但是系統(tǒng)內(nèi)線程數(shù)量是有限制的,同時線程切換很浪費時間,適合Socket少的情況。

        因該需要出現(xiàn)IO模型。

        Linux的IO模型

        在描述Linux IO模型之前,我們先來了解一下Linux系統(tǒng)數(shù)據(jù)讀取的過程:

        以用戶請求index.html文件為例子說明

        基本概念

        用戶空間和內(nèi)核空間

        操作系統(tǒng)的核心是內(nèi)核,獨立于普通的應用程序,可以訪問受保護的內(nèi)存空間,也有訪問底層硬件設備的所有權(quán)限。

        • 為了保證內(nèi)核的安全,用戶進程不能直接操作內(nèi)核,操作系統(tǒng)將虛擬空間劃分為兩部分,一部分為內(nèi)核空間,一部分為用戶空間。

        進程切換

        為了控制進程的執(zhí)行,內(nèi)核必須有能力掛起正在CPU上運行的進程,并恢復以前掛起的某個進程的執(zhí)行。

        這種行為被稱為進程切換。

        因此可以說,任何進程都是在操作系統(tǒng)內(nèi)核的支持下運行的,是與內(nèi)核緊密相關(guān)的。

        進程的阻塞

        正在執(zhí)行的進程,由于期待的某些事件未發(fā)生,如請求系統(tǒng)資源失敗、等待某種操作的完成、新數(shù)據(jù)尚未到達或無新工作做等,則由系統(tǒng)自動執(zhí)行阻塞原語(Block),使自己由運行狀態(tài)變?yōu)樽枞麪顟B(tài)。

        可見,進程的阻塞是進程自身的一種主動行為,也因此只有處于運行態(tài)的進程(獲得CPU),才可能將其轉(zhuǎn)為阻塞狀態(tài)。

        當進程進入阻塞狀態(tài),是不占用CPU資源的。

        文件描述符

        文件描述符(File Descriptor)是計算機科學中的一個術(shù)語,是一個用于表述指向文件的引用的抽象化概念。

        文件描述符在形式上是一個非負整數(shù),實際上,它是一個索引值,指向內(nèi)核為每一個進程所維護的該進程打開文件的記錄表。

        • 當程序打開一個現(xiàn)有文件或者創(chuàng)建一個新文件時,內(nèi)核向進程返回一個文件描述符。

        緩存IO

        大多數(shù)文件系統(tǒng)的默認 IO 操作都是緩存 IO。

        其讀寫過程如下:

        • 讀操作:操作系統(tǒng)檢查內(nèi)核的緩沖區(qū)有沒有需要的數(shù)據(jù),如果已經(jīng)緩存了,那么就直接從緩存中返回;否則從磁盤、網(wǎng)卡等中讀取,然后緩存在操作系統(tǒng)的緩存中;
        • 寫操作:將數(shù)據(jù)從用戶空間復制到內(nèi)核空間的緩存中。這時對用戶程序來說寫操作就已經(jīng)完成,至于什么時候再寫到磁盤、網(wǎng)卡等中由操作系統(tǒng)決定,除非顯示地調(diào)用了 sync 同步命令。

        假設內(nèi)核空間緩存無需要的數(shù)據(jù),用戶進程從磁盤或網(wǎng)絡讀數(shù)據(jù)分兩個階段:

        • 階段一: 內(nèi)核程序從磁盤、網(wǎng)卡等讀取數(shù)據(jù)到內(nèi)核空間緩存區(qū);
        • 階段二: 用戶程序從內(nèi)核空間緩存拷貝數(shù)據(jù)到用戶空間。

        緩存 IO 的缺點:

        數(shù)據(jù)在傳輸過程中需要在應用程序地址空間和內(nèi)核空間進行多次數(shù)據(jù)拷貝操作,這些數(shù)據(jù)拷貝操作所帶來的CPU以及內(nèi)存開銷非常大。

        同步阻塞

        用戶空間的應用程序執(zhí)行一個系統(tǒng)調(diào)用,這會導致應用程序阻塞,什么也不干,直到數(shù)據(jù)準備好,并且將數(shù)據(jù)從內(nèi)核復制到用戶進程,最后進程再處理數(shù)據(jù),在等待數(shù)據(jù)到處理數(shù)據(jù)的兩個階段,整個進程都被阻塞,不能處理別的網(wǎng)絡IO。

        • 調(diào)用應用程序處于一種不再消費 CPU 而只是簡單等待響應的狀態(tài),因此從處理的角度來看,這是非常有效的。

        這也是最簡單的IO模型,在通常FD較少、就緒很快的情況下使用是沒有問題的。

        同步非阻塞

        非阻塞的系統(tǒng)調(diào)用調(diào)用之后,進程并沒有被阻塞,內(nèi)核馬上返回給進程,如果數(shù)據(jù)還沒準備好,此時會返回一個error。

        • 進程在返回之后,可以干點別的事情,然后再發(fā)起系統(tǒng)調(diào)用。

        • 重復上面的過程,循環(huán)往復的進行系統(tǒng)調(diào)用。這個過程通常被稱之為輪詢。

        • 輪詢檢查內(nèi)核數(shù)據(jù),直到數(shù)據(jù)準備好,再拷貝數(shù)據(jù)到進程,進行數(shù)據(jù)處理。

        • 需要注意,拷貝數(shù)據(jù)整個過程,進程仍然是屬于阻塞的狀態(tài)。

        • 這種方式在編程中對Socket設置O_NONBLOCK即可。

        IO多路復用

        IO多路復用,這是一種進程預先告知內(nèi)核的能力,讓內(nèi)核發(fā)現(xiàn)進程指定的一個或多個IO條件就緒了,就通知進程。

        使得一個進程能在一連串的事件上等待。

        IO復用的實現(xiàn)方式目前主要有Select、Poll和Epoll。

        偽代碼描述IO多路復用:

        while(status?==?OK)?{?//?不斷輪詢
        ?ready_fd_list?=?io_wait(fd_list);?//內(nèi)核緩沖區(qū)是否有準備好的數(shù)據(jù)
        ?for(fd?in?ready_fd_list)?{
        ??data?=?read(fd)?//?有準備好的數(shù)據(jù)讀取到用戶緩沖區(qū)
        ??process(data)
        ?}
        }

        信號驅(qū)動

        首先我們允許Socket進行信號驅(qū)動IO,并安裝一個信號處理函數(shù),進程繼續(xù)運行并不阻塞。

        當數(shù)據(jù)準備好時,進程會收到一個SIGIO信號,可以在信號處理函數(shù)中調(diào)用I/O操作函數(shù)處理數(shù)據(jù)。

        流程如下:

        • 開啟套接字信號驅(qū)動IO功能
        • 系統(tǒng)調(diào)用Sigaction執(zhí)行信號處理函數(shù)(非阻塞,立刻返回)
        • 數(shù)據(jù)就緒,生成Sigio信號,通過信號回調(diào)通知應用來讀取數(shù)據(jù)

        此種IO方式存在的一個很大的問題:Linux中信號隊列是有限制的,如果超過這個數(shù)字問題就無法讀取數(shù)據(jù)

        異步非阻塞

        異步IO流程如下所示:

        • 當用戶線程調(diào)用了aio_read系統(tǒng)調(diào)用,立刻就可以開始去做其它的事,用戶線程不阻塞
        • 內(nèi)核就開始了IO的第一個階段:準備數(shù)據(jù)。當內(nèi)核一直等到數(shù)據(jù)準備好了,它就會將數(shù)據(jù)從內(nèi)核內(nèi)核緩沖區(qū),拷貝到用戶緩沖區(qū)
        • 內(nèi)核會給用戶線程發(fā)送一個信號,或者回調(diào)用戶線程注冊的回調(diào)接口,告訴用戶線程Read操作完成了
        • 用戶線程讀取用戶緩沖區(qū)的數(shù)據(jù),完成后續(xù)的業(yè)務操作

        相對于同步IO,異步IO不是順序執(zhí)行。

        用戶進程進行aio_read系統(tǒng)調(diào)用之后,無論內(nèi)核數(shù)據(jù)是否準備好,都會直接返回給用戶進程,然后用戶態(tài)進程可以去做別的事情。

        等到數(shù)據(jù)準備好了,內(nèi)核直接復制數(shù)據(jù)給進程,然后從內(nèi)核向進程發(fā)送通知。

        對比信號驅(qū)動IO,異步IO的主要區(qū)別在于:

        • 信號驅(qū)動由內(nèi)核告訴我們何時可以開始一個IO操作(數(shù)據(jù)在內(nèi)核緩沖區(qū)中),而異步IO則由內(nèi)核通知IO操作何時已經(jīng)完成(數(shù)據(jù)已經(jīng)在用戶空間中)。

        異步IO又叫做事件驅(qū)動IO,在Unix中,為異步方式訪問文件定義了一套庫函數(shù),定義了AIO的一系列接口。

        • 使用aio_read或者aio_write發(fā)起異步IO操作,使用aio_error檢查正在運行的IO操作的狀態(tài)。

        目前Linux中AIO的內(nèi)核實現(xiàn)只對文件IO有效,如果要實現(xiàn)真正的AIO,需要用戶自己來實現(xiàn)。

        目前有很多開源的異步IO庫,例如libevent、libev、libuv。

        Java網(wǎng)絡IO模型

        BIO

        BIO是一個典型的網(wǎng)絡編程模型,是通常我們實現(xiàn)一個服務端程序的方法,對應Linux內(nèi)核的同步阻塞IO模型,發(fā)送數(shù)據(jù)和接收數(shù)據(jù)的過程如下所示:

        步驟如下:

        • 主線程accept請求
        • 請求到達,創(chuàng)建新的線程來處理這個套接字,完成對客戶端的響應
        • 主線程繼續(xù)accept下一個請求

        服務端處理偽代碼如下所示:

        這是經(jīng)典的一個連接對應一個線程的模型,之所以使用多線程,主要原因在于socket.accept()、socket.read()、socket.write()三個主要函數(shù)都是同步阻塞的。

        當一個連接在處理I/O的時候,系統(tǒng)是阻塞的,如果是單線程的話必然就阻塞,但CPU是被釋放出來的,開啟多線程,就可以讓CPU去處理更多的事情。

        其實這也是所有使用多線程的本質(zhì):

        利用多核,當I/O阻塞時,但CPU空閑的時候,可以利用多線程使用CPU資源。

        當面對十萬甚至百萬級連接的時候,傳統(tǒng)的BIO模型是無能為力的。

        隨著移動端應用的興起和各種網(wǎng)絡游戲的盛行,百萬級長連接日趨普遍,此時,必然需要一種更高效的I/O處理模型。

        NIO

        JDK1.4開始引入了NIO類庫,主要是使用Selector多路復用器來實現(xiàn)。

        Selector在Linux等主流操作系統(tǒng)上是通過IO復用Epoll實現(xiàn)的。

        NIO的實現(xiàn)流程,類似于Select:

        • 創(chuàng)建ServerSocketChannel監(jiān)聽客戶端連接并綁定監(jiān)聽端口,設置為非阻塞模式
        • 創(chuàng)建Reactor線程,創(chuàng)建多路復用器(Selector)并啟動線程
        • 將ServerSocketChannel注冊到Reactor線程的Selector上,監(jiān)聽Accept事件
        • Selector在線程run方法中無線循環(huán)輪詢準備就緒的Key
        • Selector監(jiān)聽到新的客戶端接入,處理新的請求,完成TCP三次握手,建立物理連接
        • 將新的客戶端連接注冊到Selector上,監(jiān)聽讀操作,讀取客戶端發(fā)送的網(wǎng)絡消息
        • 客戶端發(fā)送的數(shù)據(jù)就緒則讀取客戶端請求,進行處理

        簡單處理模型是用一個單線程死循環(huán)選擇就緒的事件,會執(zhí)行系統(tǒng)調(diào)用(Linux 2.6之前是Select、Poll,2.6之后是Epoll,Windows是IOCP),還會阻塞的等待新事件的到來。

        新事件到來的時候,會在Selector上注冊標記位,標示可讀、可寫或者有連接到來,簡單處理模型的偽代碼如下所示:

        NIO由原來的阻塞讀寫(占用線程)變成了單線程輪詢事件,找到可以進行讀寫的網(wǎng)絡描述符進行讀寫。

        除了事件的輪詢是阻塞的(沒有可干的事情必須要阻塞),剩余的I/O操作都是純CPU操作,沒有必要開啟多線程。

        并且由于線程的節(jié)約,連接數(shù)大的時候因為線程切換帶來的問題也隨之解決,進而為處理海量連接提供了可能。

        AIO

        JDK1.7引入NIO2.0,提供了異步文件通道和異步套接字通道的實現(xiàn)。

        • 其底層在Windows上是通過IOCP實現(xiàn),在Linux上是通過IO復用Epoll來模擬實現(xiàn)的。

        在JAVA NIO框架中,Selector它負責代替應用查詢中所有已注冊的通道到操作系統(tǒng)中進行IO事件輪詢、管理當前注冊的通道集合,定位發(fā)生事件的通道等操作。

        但是在JAVA AIO框架中,由于應用程序不是輪詢方式,而是訂閱-通知方式,所以不再需要Selector(選擇器)了,改由Channel通道直接到操作系統(tǒng)注冊監(jiān)聽 。

        JAVA AIO框架中,只實現(xiàn)了兩種網(wǎng)絡IO通道:

        • AsynchronousServerSocketChannel(服務器監(jiān)聽通道)

        • AsynchronousSocketChannel(Socket套接字通道)。

        具體過程如下所示:

        • 創(chuàng)建AsynchronousServerSocketChannel,綁定監(jiān)聽端口
        • 調(diào)用AsynchronousServerSocketChannel的accpet方法,傳入自己實現(xiàn)的CompletionHandler,包括上一步,都是非阻塞的
        • 連接傳入,回調(diào)CompletionHandler的completed方法,在里面,調(diào)用AsynchronousSocketChannel的read方法,傳入負責處理數(shù)據(jù)的CompletionHandler
        • 數(shù)據(jù)就緒,觸發(fā)負責處理數(shù)據(jù)的CompletionHandler的completed方法,繼續(xù)做下一步處理即可
        • 寫入操作類似,也需要傳入CompletionHandler

        最后

        覺得有收獲,希望幫忙點贊,轉(zhuǎn)發(fā)下哈,謝謝,謝謝

        微信搜索:月伴飛魚,交個朋友

        公眾號后臺回復666,可以獲得免費電子書籍

        瀏覽 43
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 天天日天天摸天天搞 | 丰满人妻一区二区三区性色 | 麻豆电影免费 | 国产操逼的视频 | jk美女被爆操 |