<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          讓我們,從頭到尾,通透I/O模型

          共 3261字,需瀏覽 7分鐘

           ·

          2021-11-23 15:04


          上篇我們已經搞懂了 socket 的通信內幕,也明白了網絡 I/O 確實會有很多阻塞點,阻塞 I/O 隨著用戶數(shù)的增長只能利用增加線程的方式來處理更多的請求,而線程不僅會占用內存資源且太多的線程競爭會導致頻繁地上下文切換產生巨大的開銷。

          因此,阻塞 I/O 已經不能滿足需求,所以后面大佬們不斷地優(yōu)化和演進,提出了多種 I/O 模型。

          在 UNIX 系統(tǒng)下,一共有五種 I/O 模型,今天我們就來盤一盤它!

          不過在介紹 I/O 模型之前,我們需要先了解一下前置知識。

          內核態(tài)和用戶態(tài)

          我們的電腦可能同時運行著非常多的程序,這些程序分別來自不同公司。

          誰也不知道在電腦上跑著的某個程序會不會發(fā)瘋似得做一些奇怪的操作,比如定時把內存清空了。

          因此 CPU 劃分了非特權指令和特權指令,做了權限控制,一些危險的指令不會開放給普通程序,只會開放給操作系統(tǒng)等特權程序。

          你可以理解為我們的代碼調用不了那些可能會產生“危險”操作,而操作系統(tǒng)的內核代碼可以調用。

          這些“危險”的操作指:內存的分配回收,磁盤文件讀寫,網絡數(shù)據(jù)讀寫等等。

          如果我們想要執(zhí)行這些操作,只能調用操作系統(tǒng)開放出來的 API ,也稱為系統(tǒng)調用。

          這就好比我們去行政大廳辦事,那些敏感的操作都由官方人員幫我們處理(系統(tǒng)調用),所以道理都是一樣的,目的都是為了防止我們(普通程序)亂來。

          這里又有兩個名詞:

          • 用戶空間
          • 內核空間。

          我們普通程序的代碼是跑在用戶空間上的,而操作系統(tǒng)的代碼跑在內核空間上,用戶空間無法直接訪問內核空間的。當一個進程運行在用戶空間時就處于用戶態(tài),運行在內核空間時就處于內核態(tài)。

          當處于用戶空間的程序進行系統(tǒng)調用,也就是調用操作系統(tǒng)內核提供的 API 時,就會進行上下文的切換,切換到內核態(tài)中,也時常稱之為陷入內核態(tài)。

          那為什么開頭要先介紹這個知識點呢?

          因為當程序請求獲取網絡數(shù)據(jù)的時候,需要經歷兩次拷貝:

          • 程序需要等待數(shù)據(jù)從網卡拷貝到內核空間。
          • 因為用戶程序無法訪問內核空間,所以內核又得把數(shù)據(jù)拷貝到用戶空間,這樣處于用戶空間的程序才能訪問這個數(shù)據(jù)。

          介紹這么多就是讓你理解為什么會有兩次拷貝,且系統(tǒng)調用是有開銷的,因此最好不要頻繁調用。

          然后我們今天說的 I/O 模型之間的差距就是這拷貝的實現(xiàn)有所不同!

          今天我們就以 read 調用,即讀取網絡數(shù)據(jù)為例子來展開 I/O 模型。

          發(fā)車!

          同步阻塞 I/O

          當用戶程序的線程調用 read 獲取網絡數(shù)據(jù)的時候,首先這個數(shù)據(jù)得有,也就是網卡得先收到客戶端的數(shù)據(jù),然后這個數(shù)據(jù)有了之后需要拷貝到內核中,然后再被拷貝到用戶空間內,這整一個過程用戶線程都是被阻塞的。

          假設沒有客戶端發(fā)數(shù)據(jù)過來,那么這個用戶線程就會一直阻塞等著,直到有數(shù)據(jù)。即使有數(shù)據(jù),那么兩次拷貝的過程也得阻塞等著。

          所以這稱為同步阻塞 I/O 模型。

          它的優(yōu)點很明顯,簡單。調用 read 之后就不管了,直到數(shù)據(jù)來了且準備好了進行處理即可。

          缺點也很明顯,一個線程對應一個連接,一直被霸占著,即使網卡沒有數(shù)據(jù)到來,也同步阻塞等著。

          我們都知道線程是屬于比較重資源,這就有點浪費了。

          所以我們不想讓它這樣傻等著。

          于是就有了同步非阻塞 I/O。

          同步非阻塞 I/O

          從圖中我們可以很清晰的看到,同步非阻塞I/O 基于同步阻塞I/O 進行了優(yōu)化:

          在沒數(shù)據(jù)的時候可以不再傻傻地阻塞等著,而是直接返回錯誤,告知暫無準備就緒的數(shù)據(jù)!

          這里要注意,從內核拷貝到用戶空間這一步,用戶線程還是會被阻塞的

          這個模型相比于同步阻塞 I/O 而言比較靈活,比如調用 read 如果暫無數(shù)據(jù),則線程可以先去干干別的事情,然后再來繼續(xù)調用 read 看看有沒有數(shù)據(jù)。

          但是如果你的線程就是取數(shù)據(jù)然后處理數(shù)據(jù),不干別的邏輯,那這個模型又有點問題了。

          等于你不斷地進行系統(tǒng)調用,如果你的服務器需要處理海量的連接,那么就需要有海量的線程不斷調用,上下文切換頻繁,CPU 也會忙死,做無用功而忙死。

          那怎么辦?

          于是就有了I/O 多路復用。

          I/O 多路復用

          從圖上來看,好像和上面的同步非阻塞 I/O 差不多啊,其實不太一樣,線程模型不一樣。

          既然同步非阻塞 I/O 在太多的連接下頻繁調用太浪費了, 那就招個專員吧。

          這個專員工作就是管理多個連接,幫忙查看連接上是否有數(shù)據(jù)已準備就緒。

          也就是說,可以只用一個線程查看多個連接是否有數(shù)據(jù)已準備就緒。

          具體到代碼上,這個專員就是 ?select ,我們可以往 select 注冊需要被監(jiān)聽的連接,由 select 來監(jiān)控它所管理的連接是否有數(shù)據(jù)已就緒,如果有則可以通知別的線程來 read 讀取數(shù)據(jù),這個 read 和之前的一樣,還是會阻塞用戶線程。

          這樣一來就可以用少量的線程去監(jiān)控多條連接,減少了線程的數(shù)量,降低了內存的消耗且減少了上下文切換的次數(shù),很舒服。

          想必到此你已經理解了什么叫 I/O 多路復用。

          所謂的多路指的是多條連接,復用指的是用一個線程就可以監(jiān)控這么多條連接。

          看到這,你再想想,還有什么地方可以優(yōu)化的?

          信號驅動式I/O

          上面的 select 雖然不阻塞了,但是他得時刻去查詢看看是否有數(shù)據(jù)已經準備就緒,那是不是可以讓內核告訴我們數(shù)據(jù)到了而不是我們去輪詢呢?

          信號驅動 I/O 就能實現(xiàn)這個功能,由內核告知數(shù)據(jù)已準備就緒,然后用戶線程再去 read(還是會阻塞)。

          聽起來是不是比 I/O 多路復用好呀?那為什么好像很少聽到信號驅動 I/O?

          為什么市面上用的都是 I/O 多路復用而不是信號驅動?

          因為我們的應用通常用的都是 TCP 協(xié)議,而 TCP 協(xié)議的 socket 可以產生信號事件有七種。

          也就是說不僅僅只有數(shù)據(jù)準備就緒才會發(fā)信號,其他事件也會發(fā)信號,而這個信號又是同一個信號,所以我們的應用程序無從區(qū)分到底是什么事件產生的這個信號。

          那就麻了呀!

          所以我們的應用基本上用不了信號驅動 I/O,但如果你的應用程序用的是 UDP 協(xié)議,那是可以的,因為 UDP 沒這么多事件。

          因此,這么一看對我們而言信號驅動 I/O 也不太行。

          異步 I/O

          信號驅動 I/O 雖然對 TCP 不太友好,但是這個思路對的:往異步發(fā)展,但是它并沒有完全異步,因為其后面那段 read 還是會阻塞用戶線程,所以它算是半異步。

          因此,我們得想下如何弄成全異步的,也就是把 read 那步阻塞也省了。

          其實思路很清晰:讓內核直接把數(shù)據(jù)拷貝到用戶空間之后再告知用戶線程,來實現(xiàn)真正的非阻塞I/O!

          所以異步 I/O 其實就是用戶線程調用 aio_read ,然后包括將數(shù)據(jù)從內核拷貝到用戶空間那步,所有操作都由內核完成,當內核操作完畢之后,再調用之前設置的回調,此時用戶線程就拿著已經拷貝到用戶控件的數(shù)據(jù)可以繼續(xù)執(zhí)行后續(xù)操作。

          在整個過程中,用戶線程沒有任何阻塞點,這才是真正的非阻塞I/O。

          那么問題又來了:

          為什么常用的還是I/O多路復用,而不是異步I/O?

          因為 Linux 對異步 I/O 的支持不足,你可以認為還未完全實現(xiàn),所以用不了異步 I/O。

          這里可能有人會說不對呀,像 Tomcat 都實現(xiàn)了 AIO的實現(xiàn)類,其實像這些組件或者你使用的一些類庫看起來支持了 AIO(異步I/O),實際上底層實現(xiàn)是用 epoll 模擬實現(xiàn)的。

          而 Windows 是實現(xiàn)了真正的 AIO,不過我們的服務器一般都是部署在 Linux 上的,所以主流還是 I/O 多路復用。

          最后

          至此,想必你已經清晰五種 I/O 模型是如何演進的了。

          下篇,我將講講談到網絡 I/O 經常會伴隨的幾個容易令人混淆的概念:同步、異步、阻塞、非阻塞。

          參考:

          • https://time.geekbang.org/column/article/100307
          • https://zhuanlan.zhihu.com/p/266950886
          瀏覽 32
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  麻豆三级片大全 | 精品国产白色丝袜高跟鞋 | 北条麻妃在线一区二区三区 | 曰韩人妻一区二区 | 在线a久青草视频 |