天天看點

Go的IO管理看這一篇就夠了Go 的IO

文章目錄

  • Go 的IO
    • 首要問題,核心中的緩沖和程序中的緩沖
    • Operating System --- linux的檔案I/O系統
      • 引言
      • 重要概念
    • Go 的IO
      • Go 和 IO的不解之緣
      • Go的IO長什麼樣子
      • IO 的姿勢多種多樣
    • Go的IO具有是怎麼利用核心的IO的呢?

Go 的IO

首要問題,核心中的緩沖和程序中的緩沖

  • 核心中的緩沖

    無論程序是否提供緩沖,核心都是提供緩沖的,系統對磁盤的讀寫都會提供一個緩沖(page/buffer cache),将資料寫入到頁/塊緩沖進行排隊,當頁/塊緩沖達到一定的量時,才能把資料寫入磁盤。

  • 程序中的緩沖

    是指對輸入輸出流進行了改進,提供了流緩沖。當調用一個函數向磁盤寫資料時,先把資料寫入緩沖區,當達到某個條件後,如流緩沖滿了,或者重新整理流緩沖,這時候才會把資料一次送往核心提供的頁/塊緩沖區中,再經頁/塊化重寫入磁盤。

Operating System — linux的檔案I/O系統

引言

  1. 作業系統首先是一個系統,一般由不同的子產品組成,往往主要功能是xxx的增删改查功能。作業系統主要是管理硬體和提供給我們一個舒适的開發環境的作用。
  2. 作業系統可以分為多個子系統(VFS算一個),各個子系統又有多個子產品。
  3. 為什麼處理器設有兩種模式?

    核心态和使用者态,安全第一。

  4. 虛拟記憶體。其實本質上很簡單,就是作業系統将程式常用的資料放到記憶體裡加速通路,不常用的資料放在磁盤上。 這一切對使用者程式來說完全是透明的,使用者程式可以假裝所有資料都在記憶體裡,然後通過虛拟記憶體位址去通路資料。在這背後,作業系統會自動将資料在主存和磁盤之間進行交換。
  5. 作業系統對記憶體的管理機

    突然想到一個哲學問題,跟生活是相關的。像我們平時生活一樣,總是盡可能的把每個東西用盡 用好 用到它的最大極限。同理, Go 語言的記憶體管理是參考 tcmalloc 實作的,它其實就是利用好了 OS 管理記憶體的這些特點,來最大化記憶體配置設定性能的。

  6. Go 語言是如何利用底層的這些特性來優化記憶體的?
    • 記憶體配置設定大多時候都是在使用者态完成的,不需要頻繁進入核心态。
    • 每個 P 都有獨立的 span cache,多個 CPU 不會并發讀寫同一塊記憶體,進而減少 CPU L1 cache 的 cacheline 出現 dirty 情況,增大 cpu cache 命中率。
    • 記憶體碎片的問題,Go 是自己在使用者态管理的,在 OS 層面看是沒有碎片的,使得作業系統層面對碎片的管理壓力也會降低。
    • mcache 的存在使得記憶體配置設定不需要加鎖。

重要概念

  • Linux中VFS —> 檔案IO系統
Go的IO管理看這一篇就夠了Go 的IO

​ 如上圖所示,page cache的本質是由 Linux 核心管理的記憶體區域。我們通過 mmap 以及 buffered IO 将檔案讀取到記憶體空間。實際上,都是讀取到 page cache中。

  • page cache
    除了 Direct IO,與磁盤相關的檔案讀寫都有使用到 page cache技術
    1. kernel 2.6以後(引入了虛拟記憶體),page cache 面向檔案
    2. 檔案I/O (系統I/O)操作隻和page cache互動
    3. 用在所有以檔案為單元的場景中,比如網絡檔案系統
    4. address_space 作為檔案系統和頁緩存的中間擴充卡,用來訓示一個檔案在page cache中已經緩存了的實體頁
    • 如何檢視OS的 page cache?

      通過讀取 /proc/meminfo 檔案,能夠實時擷取系統記憶體情況:

      $ cat /proc/meminfo
      ...
      Buffers:            1224 kB
      Cached:           111472 kB
      SwapCached:        36364 kB
      Active:          6224232 kB
      Inactive:         979432 kB
      Active(anon):    6173036 kB
      Inactive(anon):   927932 kB
      Active(file):      51196 kB
      Inactive(file):    51500 kB
      ...
      Shmem:             10000 kB
      ...
      SReclaimable:      43532 kB
      ...
                 
      根據上面的資料,你可以簡單得出這樣的公式:
      page cache = buffers + cached + swap = active + inactive + share + swap
                 
    • 為什麼 swap 和 buffers 也是 page cache的一部分?

      因為當匿名頁 Inactive(anon)以及 Active(anon) 先被交換到磁盤(out)後,然後再加載回(in)記憶體中,由于讀入到記憶體後原來地 swap file 還在,是以 swap cached 也可以認為是 File-backed page,即屬于 page cache。過程如下圖所示:

      Go的IO管理看這一篇就夠了Go 的IO
    • page 和 page cache

      page 是核心記憶體管理配置設定的基本機關(4KB)

      page cache 由多個page構成(4KB的整數倍)

      注意:并不是所有 page 都被組織成為 page cache

      Linux 系統上供使用者可通路的記憶體分為兩個類型:

      1. File-backed pages: 檔案備份頁,也就是page cache中的 page,對應于磁盤上的若幹資料塊;對于這些頁最大的問題是髒頁回盤
      2. Anonymous pages: 匿名頁不對應磁盤上的任何磁盤資料塊,他們是程序的運作記憶體空間,比如方法棧、局部變量等屬性
      兩種類型在 swap 機制下的性能比較:
      1. File-backed pages的記憶體回收代價較低。page cache 通常對應于一個檔案上的若幹順序塊, 是以可以通過順序IO的方式落盤。另一方面,如果 page cache 上沒有進行寫操作(即沒有髒頁),甚至不會将page cache 回盤,因為資料的内容完全可以通過再次讀取磁盤檔案得到。

        page cache 的主要難點在于髒頁回盤,怎麼個難法呢?

      2. Anonymous pages 的記憶體會後代價較高。因為這種pages 通常随機地寫入持久化交換裝置。另一方面,無論是否有寫操作,為了確定資料不丢失,Anonymous pages在swap時必須持久化到磁盤。
    • swap 與 缺頁中斷

      swap 機制指地是當實體記憶體不夠用,記憶體管理單元(MMU)需要提供排程算法來回收相關記憶體空間,然後将清理出來地記憶體空間給目前記憶體申請方。

      swap 存在地本質原因是 Linux 系統提供了虛拟記憶體管理機制。每個程序都認為自己獨占記憶體空間,是以所有程序地記憶體空間之和遠遠大于實體記憶體。所有程序的記憶體空間之和超過實體記憶體的部分就需要交換到磁盤上。

      OS以 page 為機關管理記憶體,當程序發現需要通路的資料不在記憶體時,OS可能會将資料以頁的方式加載到記憶體中,上述過程被稱為缺頁中斷。當OS發生缺頁中斷時,就會通過系統調用将 page 再次讀到記憶體中。

      但主記憶體的空間時有限的,當主記憶體中不包含可以使用的空間時,OS會選擇合适的實體記憶體逐頁驅逐回磁盤,為新的記憶體頁讓出位置,選擇待驅逐頁的過程在OS中叫做頁面替換(page replacement),替換操作又會觸發 swap 機制。

      如果實體記憶體足夠大,那麼可能不需要 swap 機制,但是 swap 在這種情況下還是有一定優勢:對于有發生記憶體洩漏幾率的應用程式(程序), swap交換分區更是重要,這可以確定記憶體洩漏不至于導緻實體記憶體不夠用,最終導緻系統崩潰。但記憶體洩漏會引起頻繁的 swap,此時會非常影響OS的性能。

      Linux 通過一個 swappiness 參數來控制 swap 機制:這個參數可為 0~100,控制系統swap的優先級。

      1. 高數值,較高頻率的swap,程序不活躍時将其轉換出實體記憶體
      2. 低數值,較低頻率的swap,這可以確定互動式不因為記憶體空間頻繁地交換到磁盤而提高響應延遲。
    • page cache 和 buffer cache
      ~ free -m
                   total       used       free     shared    buffers     cached
      Mem:        128956      96440      32515          0       5368      39900
      -/+ buffers/cache:      51172      77784
      Swap:        16002          0      16001
                 

      其中,cached 表示目前的頁緩存(page cache)占用量,用于緩存檔案的頁資料;頁是邏輯上的概念,是以page cache 是與檔案系統同級的

      buffers 表示目前的塊緩存(buffer cache)占用量,用于緩存塊裝置的塊資料;塊是實體上的概念,是以buffer cache是與塊裝置驅動程式同級的。

      page cache 和 buffer cache的共同目的都是加速資料IO。寫資料時,首先寫到緩存,将寫入的頁标記為 dirty,然後向外部存儲 flush,也就是緩存寫機制中的 write-back(另一種是 write-through,Linux預設情況下不采用);讀資料時,首先讀取緩存,如果命中,再去外部存儲讀取,并且将讀取來的資料頁加入緩存。OS總是積極地将所有空閑記憶體都用做 page cache和 buffer cache,當記憶體不夠用時,也會用LRU等算法淘汰緩存頁。

      在Linux 2.4 核心之前,page cache 和 buffer cache是完全分離的。但是,塊裝置大多是磁盤,磁盤上的資料又大多通過檔案系統來組織,這種設計導緻很多資料被緩存了兩次,浪費記憶體。是以,**在2.4版本之後,兩塊緩存近似融合在了一起,如果一個檔案的頁加載到了 page cache,那麼同時 buffer cache隻需要維護塊指向頁的指針就可以了。**隻有那麼沒有用檔案表示的塊,或者繞過了OS直接操作的塊(如dd指令),才會真正放到 buffer cache中。是以,我們現在提起 page cache,基本上都同時指 page cache 和 buffer cache 兩者。

      下圖近似地給出 32位 Linux系統中可能地一種 page cache結構:

      Go的IO管理看這一篇就夠了Go 的IO

      block size = 1KB

      page size = 4KB

      page cache中的每個檔案都是一棵基數樹(radix tree,本質上是多差搜尋樹),樹的每個節點都是一個頁。根據檔案内的偏移量就可以快速定位到所在的頁,如下圖所示。

      Go的IO管理看這一篇就夠了Go 的IO
    • page cache與預讀

      OS為基于page cache的讀緩存機制提供預讀機制(PAGE_READAHEAD),eg:

      1. 使用者線程僅僅請求讀取磁盤上檔案A的offset為0-3KB範圍内的資料,由于磁盤的基本讀寫機關為 block = 4KB,于是OS至少會讀0-4KB的内容,這恰好可以在一個page中裝下。
      2. 但是OS處于局部性原理會選擇将磁盤塊 offset[4KB,8KB)、[8KB,12KB)、[12KB,16KB)都加載到記憶體,于是額外在記憶體中申請了3個page。如下如圖所示OS的預讀機制:
      Go的IO管理看這一篇就夠了Go 的IO
    • page cache與檔案持久化的一緻性

      現代 Linux 的page cache正如其名,是對磁盤上的 page 的記憶體緩存,同時可以用于讀/寫操作。任何系統引入緩存,就會引發一緻性問題:記憶體中的資料與磁盤中的資料不一緻,如最常見後端架構中的redis緩存與mysql資料庫就存在一緻性的問題。

      Linux 提供多種機制來保證資料一緻性,但無論是單機上的記憶體與磁盤一緻性,還是分布式元件中節點1與節點2、3的資料一緻性問題,了解的關鍵是 trade-off:吞吐量與資料一緻性保證是一對沖突。

      首先,需要我們了解一下檔案的資料,檔案 = 中繼資料+資料。

      中繼資料=檔案大小+建立時間+通路時間+屬主屬組等資訊
      Linux 采用以下兩種方式實作檔案一緻性:
      1. Write Through(寫穿):向使用者層提供特定接口,應用程式可主動調用接口來保證檔案一緻性;

        以犧牲系統IO吞吐量為代價,向上層應用確定一旦寫入,資料就已經落盤,不會丢失。

      2. Write Back(寫回):系統中存在定期任務(表現形式為核心線程),周期性地同步檔案系統中檔案髒資料塊,這就是預設的Linux一緻性方案;

        在系統發生當機的情況下無法確定資料已經落盤,是以存在資料丢失的問題。不過,在程式挂了,如被 kill -9, OS會確定page cache 中的資料落盤。

      兩種方法都依賴系統調用,主要分為三種系統調用,可以分别由使用者程序與核心程序發起:
      1. fsync(int fd),将fd 代表的檔案的髒資料和髒中繼資料全部重新整理到磁盤中
      2. fdatasync(int fd),将 fd 代表的檔案的髒資料重新整理至磁盤,同時對必要的(檔案大小,而檔案修改時間等不屬于必要資訊)中繼資料重新整理至磁盤中。
      3. sync(),對系統中所有的髒的檔案資料、中繼資料都重新整理至磁盤中
      描述一下核心線程的相關特性:
      1. 建立的針對回寫任務的核心線程數由系統中持久儲存設備決定,為每個儲存設備建立單獨的重新整理線程;
      2. 關于多線程的架構問題,Linux核心采取了 Lighthttp 的做法。即系統中存在一個管理線程和多個重新整理線程(每個持久儲存設備對應一個重新整理線程)。管理線程監控裝置上的髒頁面情況,若裝置一段時間内沒有産生髒頁面,就銷毀裝置上的重新整理線程;若監測到裝置上有髒頁面需要回寫且尚未為該裝置建立重新整理線程,那麼建立重新整理線程處理髒頁面回寫。而重新整理線程的任務較為單調,隻負責将裝置中的髒頁面回寫至持久儲存設備中。
      3. 重新整理線程重新整理裝置上髒頁面大緻設計如下:

        每個裝置儲存髒檔案連結清單,儲存的是該裝置上存儲的髒檔案的inode節點。所謂的回寫檔案髒頁面即回寫該 inode 連結清單上的某些檔案的髒頁面;

        系統中存在多個回寫時機。第一,應用程式主動調用回寫接口;第二,管理線程周期性地喚醒裝置上的回寫線程進行回寫;第三,某些應用程式/核心任務發現記憶體不足時要回收部分緩存頁面而事先進行髒頁面回寫,設計一個統一的架構來管理這些回寫任務非常有必要。

    • 優勢,好處,特點,獨特。。。
      1. 加快資料通路
      2. 減少IO次數,提高系統磁盤IO吞吐量

        得益于 page cache的緩存以及預讀能力,而程式又往往符合局部性原理。

    • 劣勢,缺點,不足。。。
      1. 最直接的缺點就是需要占用額外實體記憶體空間,實體記憶體在比較緊張的時候可能導緻頻繁的 swap 操作,最終導緻系統的磁盤IO負載上升。(還是那個觀點,工業4.0時代,多用點而記憶體會帶來人們的美好生活)
      2. 對應用層沒有提供很好的管理API,幾乎是透明管理。應用層即使想優化 page cache的使用政策也很難進行。因為一些應用選擇在使用者空間實作自己的 page 管理,而不使用 page cache,例如 mysql 的 innoDB存儲引擎以 16KB的頁進行管理。(事實是提供了,但某人覺得滿足不了他的美好生活,看來人們的美好生活是日益增長的)
      3. 某些場景下比 Direct IO多一次磁盤IO
  • 零拷貝

    曆史變遷:

    • ​ 沒有任何優化技術的資料四次拷貝與四次上下文切換
      Go的IO管理看這一篇就夠了Go 的IO
    • DMA參與下的資料四次拷貝

      DMA 也有其局限性,DMA 僅僅能用于裝置之間交換資料時進行資料拷貝,但是系統内部的資料拷貝還需要 CPU 進行,例如 CPU 需要負責核心空間資料與使用者空間資料之間的拷貝(記憶體内部的拷貝) 。

      read buffer == page cache

      socket buffer == socket 緩沖區

    Go的IO管理看這一篇就夠了Go 的IO
    • 不同的零拷貝技術适用于不同的應用場景
      • DMA 技術回顧:DMA 負責記憶體與其他元件之間的資料拷貝,CPU 僅需負責管理,而無需負責全程的資料拷貝;
      • 使用 page cache 的 zero copy:
        1. sendfile:一次代替 read/write 系統調用,通過使用 DMA 技術以及傳遞檔案描述符,實作了 zero copy
        2. mmap:僅代替 read 系統調用,将核心空間位址映射為使用者空間位址,write 操作直接作用于核心空間。通過 DMA 技術以及位址映射技術,使用者空間與核心空間無須資料拷貝,實作了 zero copy
      • 不使用 page cache 的 Direct I/O:讀寫操作直接在磁盤上進行,不使用 page cache 機制,通常結合使用者空間的使用者緩存使用。通過 DMA 技術直接與磁盤/網卡進行資料互動,實作了 zero copy

    零拷貝思想( 不是不進行拷貝,而是 CPU 不再全程負責資料拷貝時的搬運工作 )的一個具體實作。

    一種記憶體映射檔案的方法,實作檔案磁盤位址和程序虛拟位址空間中一段虛拟位址的一一映射關系。

    零拷貝的特點是 CPU 不全程負責記憶體中的資料寫入其他元件,CPU 僅僅起到管理的作用 。

    具體實作:

    • sendfile( 使用者從磁盤讀取一些檔案資料後不需要經過任何計算與處理就通過網絡傳輸出去 )
      Go的IO管理看這一篇就夠了Go 的IO
    • mmap,利用

      mmap()

      替換

      read()

      ,配合

      write()

      調用的整個流程如下:
      1. 使用者程序調用

        mmap()

        ,從使用者态陷入核心态,将核心緩沖區映射到使用者緩存區;
      2. DMA 控制器将資料從硬碟拷貝到核心緩沖區(可見其使用了 Page Cache 機制);
      3. mmap()

        傳回,上下文從核心态切換回使用者态;
      4. 使用者程序調用

        write()

        ,嘗試把檔案資料寫到核心裡的套接字緩沖區,再次陷入核心态;
      5. CPU 将核心緩沖區中的資料拷貝到的套接字緩沖區;
      6. DMA 控制器将資料從套接字緩沖區拷貝到網卡完成資料傳輸;
      7. write()

        傳回,上下文從核心态切換回使用者态。
    • splice
    • 直接 Direct I/O(自緩存應用程式,資料庫管理系統就是這類的一個代表)
      Go的IO管理看這一篇就夠了Go 的IO
    不同的零拷貝技術适用于不同的應用場景 。
    • 把mmap單獨拿出來說

      使用者空間mmap—>核心空間mmap—>缺頁異常

      對比,從核心檔案系統看檔案讀寫過程

    • 案例
      1. kafaka

        使用 mmap 來對接收到的資料進行持久化,使用 sendfile 從持久化媒體中讀取資料然後對外發送是一對常用的組合。但是注意,你無法利用 sendfile 來持久化資料,利用 mmap 來實作 CPU 全程不參與資料搬運的資料拷貝。

      2. mysql 的零拷貝技術

    總而言之,正常檔案操作需要從磁盤到頁緩存再到使用者主存的兩次資料拷貝。而mmap操控檔案,隻需要從磁盤到使用者主存的一次資料拷貝過程。

    說白了,mmap的關鍵點是實作了使用者空間和核心空間的資料直接互動而省去了不同空間資料不通的繁瑣過程 。

  • mmap

    一種記憶體映射檔案的方法。将一個檔案或者其它對象映射到程序的位址空間,實作檔案磁盤位址和程序虛拟位址空間中一段虛拟位址的一一對應關系。實作這樣的映射關系後,程序就可以采用指針的方式讀寫這一段記憶體,而OS會自動回寫髒頁面到對應的檔案磁盤上,即完成了對檔案的操作而不必再調用read、write等系統調用函數。相反,核心空間對這段區域的修改也直接反映到使用者空間,進而可以實作不同程序間的檔案共享。

    Go的IO管理看這一篇就夠了Go 的IO
    • 特點
      1. mmap 向應用程式提供的記憶體通路接口是記憶體位址連續的,但是對應的磁盤檔案的 block 可以不是位址連續的
      2. mmap 提供的記憶體空間是虛拟空間,而不是實體空間,是以完全可以配置設定遠遠大于實體記憶體大小的虛拟空間,如16G記憶體主機可以配置設定1000G的mmap記憶體空間
      3. mmap 負責映射檔案邏輯上一段連續的資料(實體上可以不連續存儲)映射為連續記憶體,而這裡的檔案可以是磁盤檔案、驅動假造出來的檔案以及裝置
      4. mmap 由OS負責管理,對同一個檔案位址的映射将被所有線程共享,OS確定線程安全及線程可見性
      mmap 的設計很有啟發性。基于磁盤的讀寫機關是 block(4KB),而基于記憶體的讀寫單元是位址。換言之,CPU進行一次磁盤讀寫操作涉及的資料量至少是4KB。但是,進行一次記憶體操作涉及的資料量是基于位址的,也就是通常的 64bit 。
    • 模型
      Go的IO管理看這一篇就夠了Go 的IO
      1. 利用 DMA 技術來取代 CPU 來在記憶體與其他元件之間的資料拷貝,例如從磁盤到記憶體,從記憶體到網卡;
      2. 使用者空間的 mmap file 使用虛拟記憶體,實際上并不占據實體記憶體,隻有在核心空間的 kernel buffer cache 才占據實際的實體記憶體;
      3. mmap()

        函數需要配合

        write()

        系統調動進行配合操作,這與

        sendfile()

        函數有所不同,後者一次性代替了

        read()

        以及

        write()

        ;是以 mmap 也至少需要 4 次上下文切換;
      4. mmap 僅僅能夠避免核心空間到使用者空間的全程 CPU 負責的資料拷貝,但是核心空間内部還是需要全程 CPU 負責的資料拷貝
    • 流程
      1. 使用者程序調用

        mmap()

        ,從使用者态陷入核心态,将核心緩沖區映射到使用者緩存區;
      2. DMA 控制器将資料從硬碟拷貝到核心緩沖區(可見其使用了 Page Cache 機制);
      3. mmap()

        傳回,上下文從核心态切換回使用者态;
      4. 使用者程序調用

        write()

        ,嘗試把檔案資料寫到核心裡的套接字緩沖區,再次陷入核心态;
      5. CPU 将核心緩沖區中的資料拷貝到的套接字緩沖區;
      6. DMA 控制器将資料從套接字緩沖區拷貝到網卡完成資料傳輸;
      7. write()

        傳回,上下文從核心态切換回使用者态。
    • 優勢
      1. 簡化使用者程序程式設計

        基于缺頁異常的懶加載

        資料一緻性由OS確定

      2. 讀寫效率提高:避免核心空間到使用者空間的資料拷貝
      3. 避免隻讀操作時的swap操作
      4. 節約記憶體

        使用者空間與核心空間實際上公用同一份資料

    • 适用場景,非常受限
      1. 多個線程以隻讀的方式同時通路一個檔案,這是因為 mmap 機制下多線程共享了同一實體記憶體空間,是以節約了記憶體;
      2. mmap 非常适合用于程序間通信,這是因為對同一檔案對應的 mmap 配置設定的實體記憶體天然多線程共享,并可以依賴于作業系統的同步原語;
      3. mmap 雖然比 sendfile 等機制多了一次 CPU 全程參與的記憶體拷貝,但是使用者空間與核心空間并不需要資料拷貝,是以在正确使用情況下并不比 sendfile 效率差;
    • 不适合的場景
      1. 由于 mmap 使用時必須實作指定好記憶體映射的大小,是以 mmap 并不适合變長檔案;
      2. 如果更新頻繁,mmap 避免兩态拷貝的優勢就被攤還,最終還是落在了大量的髒頁回寫及由此引發的随機 I/O 上,是以在随機寫很多的情況下,mmap 方式在效率上不一定會比帶緩沖區的一般寫快;
      3. 讀/寫小檔案(例如 16K 以下的檔案),mmap 與通過 read 系統調用相比有着更高的開銷與延遲;同時 mmap 的刷盤由系統全權控制,但是在小資料量的情況下由應用本身手動控制更好;
      4. mmap 受限于作業系統記憶體大小:例如在 32-bits 的作業系統上,虛拟記憶體總大小也就 2GB,但由于 mmap 必須要在記憶體中找到一塊連續的位址塊,此時你就無法對 4GB 大小的檔案完全進行 mmap,在這種情況下你必須分多塊分别進行 mmap,但是此時位址記憶體位址已經不再連續,使用 mmap 的意義大打折扣,而且引入了額外的複雜性;
    • 參考

      認真分析mmap: 是什麼 為什麼 怎麼用

      Linux中 mmap() 函數的記憶體映射問題了解?

      When should I use mmap for file access

      Linux IO原理和Zero-copy技術全面揭秘

Go 的IO

它的 io 和 bufio 是程序中(也可以說是使用者态)的緩沖。

Go 和 IO的不解之緣

Go 是一種高性能的編譯型語言,天然支援高并發,使用者級别封裝協程,号稱支援百萬的協程并發,這個量級不是線程可比的。

  • 那Go支援如此高并發的秘訣在于?

    執行體排程得當。CPU不停的在不同執行體(goroutine)之間反複橫跳。CPU一直在裝填和運作不同執行體的指令,G1 不行就搞G2,一刻都不能停,這樣才能使得大量的執行體齊頭并進,系統才能完成如此高并發的吞吐。

  • 那Go适合CPU密集型的程式,還是IO密集型的程式呢?

    **IO密集型。**首先,反推邏輯,CPU密集型就意味着每個執行體都是急需CPU的,G1都吃不飽,切到G2去幹嘛,是以CPU密集型的程式最好的情況就是不排程,綁核都來不及呢。想要提高這種程式的性能,就是加錢,買核。

    IO裝置和CPU是不同的獨立裝置。這兩者之間的處理可以是并行運作的。Go程式的協程排程可以很好的利用這個關系。讓CPU執行程式指令,隻負責發送IO,一旦IO被裝置接收,CPU不等待完成,就可以處理其他的指令,IO的完成以異步事件的形式觸發。這樣,IO裝置的處理過程和CPU的執行就并行起來了。

  • 任何IO都适配Go麼?

    Go 語言級别把網絡IO做了異步化,但是檔案IO還是同步的調用

    1. 網絡fd可以用epoll池來管理事件,實作異步IO
    2. 檔案fd不能用epoll池來管理事件,隻能同步IO
    檔案想要實作異步IO,目前Linux下有兩種方式:
    • AIO: 但Go沒有封裝實作
    • io_uring(io use ring): 核心版本要求高(>= 5.1)

Go的IO長什麼樣子

  • IO接口描述

    io/io.go

    不涉及具體的IO實作,隻有語義接口
    type Reader interface {
     Read(p []byte) (n int, err error)
    }
    
    type Writer interface {
     Write(p []byte) (n int, err error)
    }
               
    按照接口的定義次元,大緻可以分為3大類:
    • 基礎類型

      Reader、Writer、Closer。。。等,描述了最原始的Go的IO的樣子。如果你寫代碼的時候,要實作這些接口,千萬要把标準庫裡的注釋讀三遍。

    • 組合類型

      往往把最基本的接口組合起來,使用Go的embeding文法糖,比如:ReaderCloser、WriteCloser等

    • 進階類型

      基于基礎接口,加上一些有趣的實作。比如:TeeReader、LimitReader、MultiReader

  • IO 通用函數

    io庫還有一些基于以上接口的函數,

    • Copy
    • CopyN
    • CopyBuffer
  • io/ioutil

    顧名思義,這是一個工具類型的庫,util嘛 啥都要有,相當于平時的快捷鍵。

    這就是個工具庫,應付一些簡單的場景:

    ReadFile、WriteFile、ReadDir…

IO 的姿勢多種多樣

哈哈,這位部落客的了解很特别,Go 标準io庫定義了基礎的語義接口,那具體實作呢?

  1. 位元組數組可以是 Reader / Writer ?
  2. 記憶體結構體可以是 Reader 嗎?
  3. 檔案可以是 Reader / Writer 嗎?
  4. 字元串可以是 Reader ?
  5. IO 能聚合來提高效率嗎?

都可以!Go幫我們做好了一切!

水友也做過相關測試

io庫的拓撲

IO行為都是以io庫為中心發散的。

Go的IO管理看這一篇就夠了Go 的IO
  • io 和 位元組的故事: bytes 庫

    一句話,記憶體塊可以作為讀寫的資料流。

    bytes.Reader 可以把[]byte轉換成Reader

    bytes.Buffer可以把[]byte轉換成Reader、Writer

    buffer := make([]byte, 1024)
        readerFromBytes := bytes.NewReader(buffer)
        n, err := io.Copy(ioutil.Discard, readerFromBytes)
        // n == 1024, err == nil
        fmt.Printf("n=%v,err=%v\n",n, err)
               
  • io和字元串的故事:strings庫

    strings.Reader能夠把字元串轉換成Reader, 這個也特别有意思,直接能把字元串作為讀源。

    data := "hello world"
        readerFromBytes := strings.NewReader(data)
        n, err := io.Copy(ioutil.Discard, readerFromBytes)
        fmt.Printf("n=%v,err=%v\n",n, err)
               
  • io和網絡的故事:net庫

    網絡可以作為讀寫源,抽象成Reader、Writer的形式。

    服務端:

    func handleConn(conn net.Conn) {
     defer conn.Close()
     buf := make([]byte, 4096)
     conn.Read(buf)
     conn.Write([]byte("pong: "))
     conn.Write(buf)
    }
     
    func main() {
     server, err := net.Listen("tcp", ":9999")
     if err != nil {
      log.Fatalf("err:%v", err)
     }
     for {
      c, err := server.Accept()
      if err != nil {
       log.Fatalf("err:%v", err)
      }
      go handleConn(c)
     }
    }
               
    說明:
    1. net.Listen 建立一個監聽套接字,在Go裡面封裝成了 net.Listener類型
    2. Accept 函數傳回一個 net.Conn,代表一條網絡連接配接,net.Conn 即是Reader,又是Writer,到了之後各自處理即可
    用戶端:
    func main() {
        conn, err := net.Dial("tcp", ":9999")
        if err != nil {
            panic(err)
        }
        conn.Write([]byte("hello world\n"))
        io.Copy(os.Stdout, conn)
    }
               
    說明:
    1. net.Dail 傳入伺服器端位址和網絡協定類型,即可傳回一條和服務端通信的網絡連接配接,傳回的結構為 net.Conn
    2. net.Conn既可作為讀端,也可為寫端
    以上無論是net.Listener,還是net.Conn 都是基于系統調用 socket 之上的一層封裝。底層使用的是類似的系統調用:
    • syscall.Socket
    • syscall.Connect
    • syscall.Listen
    • syscall.GetsocketInt
    Go 針對網絡fd都會做哪些封裝呢?
    1. 建立還是用 socket 調用建立的 fd,建立出來就會立馬設定為 nonblock 模式,Go的網絡fd天然要使用IO多路複用的方式來走IO
    2. 把 socket fd 丢到 epoll 池裡(通過poll.runtime_pollOpen 把 socket 套接字加到epoll池裡,底層調用的還是epollctl),監聽事件
    3. 封裝好讀寫事件到來的函數回調
  • io和檔案的故事: os庫

    檔案IO,這個是我們最常見的IO,檔案可以作為讀端,也可以作為寫端。

    // 如下,把 test.data 的資料讀出來丢到垃圾桶
        fd, err := os.OpenFile("test.data", os.O_RDWR, 0)
        if err != nil {
            panic(err)
        }
        io.Copy(ioutil.Discard, fd)
               
    這裡傳回了一個File類型,不難想象這個是基于檔案fd的一層封裝。這裡面做了什麼呢?
    • 調用系統調用 syscall.Open 拿到檔案的fd,順便設定了垃圾回收時候的析構函數
    • stdin、stdout、stderr

      Go把這三個也都抽象成了讀寫源,這三個類型的變量其實就是File類型的變量,定義在源碼 src/os/file.go中

      var (
       Stdin  = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
       Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
       Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
      )
                 
      标準輸入就可以和友善的作為讀端(

      Reader

      ),标準輸出可以作為寫端(

      Writer

      eg:用一行代碼實作一個最簡單的 echo 回顯的程式

      func main() {
          // 一行代碼實作回顯
          io.Copy(os.Stdout, os.Stdin)
      }
                 
  • 緩存io的故事: bufio庫

    Reader/Writer 可以是緩沖 IO 的資料流

    Go緩沖IO是在底層IO之上的一層buffer

    形象描述的話,可以說是使用者記憶體空間的page cache

    在 c 語言,有人肯定用過

    fopen

    打開的檔案(所謂的标準IO):
    FILE * fopen ( const char * filename, const char * mode );
               
    C 語言的緩沖IO有三種模式:
    • 全緩沖: 隻有填滿 buffer,才會真正的調用底層IO
    • 行緩沖:不用等填滿buffer,遇到換行符,就會把IO下發下去
    • 不緩沖: bypass的模式,每次都是直接掉底層IO
  • 四種方式,将資料寫入檔案
    • os包 f.Write([]byte)
      var f *os.File
      var wireteString = "你好,tcy"
      var d1 = []byte(wireteString)
      f, err3 := os.Create("./output.txt") //建立檔案
      n2, err3 := f.Write(d1) //寫入檔案(位元組數組)  os方式
                 
    • io包的io.WriteString(f, wireteString)
    var wireteString = "你好,tcy"
    n, err1 := io.WriteString(f, wireteString) //寫入檔案(字元串),io方式,n為幾個位元組
    f.Sync()  //Sync遞交檔案的目前内容進行穩定的存儲。一般來說,這表示将檔案系統的最近寫入的資料在記憶體中的拷貝重新整理到硬碟中穩定儲存
               
    • ioutil包的ioutil.WriteFile
    var st = []byte(wireteString)
    err2 := ioutil.WriteFile("./output.txt", st, 0666) //寫入檔案(位元組數組),如果檔案不存在将按給出的權限建立檔案,否則在寫入資料之前清空檔案。
    
               
    • bufio包中的bufio.NewWriter(f)
    var f *os.File
    w := bufio.NewWriter(f) //建立新的 Writer 對象,NewReader建立一個具有預設大小緩沖、從r讀取的*Reader
    n4, err3 := w.WriteString("你好,tcy")
    fmt.Printf("寫入 %d 個位元組n", n4)
    w.Flush() //Flush方法将緩沖中的資料寫入下層的io.Writer接口。
    f.Close()
               

Go的IO具有是怎麼利用核心的IO的呢?

  • 檔案 io 沒有特别的優化,bufio是模拟page cache麼?
  • 檔案 io 包是走了系統調用麼?
  • 底層是否也用到了我們感覺不到的page cache呢?
  • Go中使用mmap的案例有麼?

繼續閱讀