從io模型到ppc,tpc,reactor,preactor

李奔三發表於2020-11-22

所有的系統I/O都分為兩個階段:等待就緒和操作.讀就是等待系統可讀和真正的讀;寫就是等待系統可寫和真正的寫.

 

1.網路io模型

emma_1

這是我們常見的一張圖.

1.傳統的bio,就是同步阻塞的.當呼叫socket.read的時候.會阻塞. 知道系統可讀/寫,當真正去執行讀的時候(核心-->使用者),還是阻塞.

2.非阻塞io,當呼叫read時,如果不可讀,那麼直接返回一個標識,告訴你等會再來問.現在不可讀嘞.那麼好的,你就過一會問一下.當可讀時,去呼叫recvFrom系統呼叫.不過此時還是阻塞的.

3. 而多路複用是作業系統提供了一個函式.可以建立一個selector,用於遍歷你傳入的fd(也就是socket,萬物皆檔案).再註冊你關注的事件.同時你會阻塞selector.select().這個會返回你感興趣的事件.例如某個fd可讀了.那麼就是說,這個socket有資料傳送到了讀緩衝區.你可以拿走了,你拿走我的滑塊好滑起來.寫同理

4.前面3種在讀的時候,都是同步讀取的.你必須要不斷的問作業系統,好了沒,好了沒.,那麼Aio是可以使用者程式啥都不管.作業系統會在資料可讀的時候,把資料從核心空間拷貝到使用者空間.並呼叫使用者的回撥函式.執行

1.1 ppc

針對bio模型,在早期的程式設計中,對每個連線可以fork一個子程式(process per connection)

流程:

  1. 父程式接收連線
  2. 當收到一個連線後,fork一個子程式去處理請求
  3. 子程式處理完畢後,close

缺點:

  • fork代價高.建立程式需要很多資源.而且需要copy父程式資源到子程式,例如頁表,記憶體等,而且會阻塞.釋放程式消耗也不小
  • 父子程式通訊很複雜.需要ipc進行通訊.例如子程式告訴父程式處理了幾個請求,耗時等等.
  • cpu一共就那麼幾個核.來100個連線就要有100多個程式.程式切換成本也不小

1.2 tpc

對每個連線建立一個執行緒(thread per connnection).聽起來執行緒比程式代價小,而且程式間通訊也很快,但是缺點仍然很大

流程:

  1. 主執行緒接收連線
  2. 當accpet到一個連線後,建立一個執行緒去處理
  3. 為了避免不斷建立執行緒,可以使用執行緒池來處理

缺點:

  • 實際上和ppc一樣.只不過用的是執行緒,不過很多作業系統(linux).執行緒就是用的程式
  • 雖然使用了連線池,但是一旦池子滿了,還是阻塞的

1.3reactor

 

反應堆,理解為事件反映.就是我們只關注事件,當一個連線來,一個連線可寫,一個連線可讀時,對事件作出反應,分發給handler去處理.Reactor 模式的核心組成部分包括 Reactor 和處理資源池.reactor用來處理連線,接收到連線後,會呼叫資源池去處理這個連線.有些文章也稱之為dispatch模式.

根據reactor和資源池多少可以分為以下幾種型別

1.3.1 單reactor單程式/執行緒

a.reactor用於監聽事件.當監聽到連線事件後,分發給acceptor處理;當監聽到讀寫事件後,分發給handle

b:acceptor接收完連線後,會分配handler處理後續請求.並註冊讀寫到selector

c:當可讀後,handler讀取資料,進行處理,然後傳送給client

缺點:可以看到只有一個執行緒在selector.當可讀後,只有一個執行緒處理請求.無法使用到多核優勢.redis在用.不過redis最近也要搞多執行緒.

     1.3.2單reactor多執行緒

         

事件的監聽還是在主執行緒.不過處理事件,會交給其他執行緒,可以搞一個執行緒池

    1.3.3多reactor和多執行緒

    netty就是這麼實現,有多個reactor在監聽事件.監聽到之後,分配給執行緒池去處理.避免io處理和業務處理相互影響.

         

對比下netty實現,就很容易理解了.

1.一個reactor用來監聽連線事件(boss執行緒).當監聽到連線事件後,呼叫accept.同時把channel註冊到work reactor中(work執行緒),

2.work reactor用來監聽讀寫.當監聽到可讀後,呼叫業務執行緒池,進行處理,處理結束後,呼叫channel.write(msg);此時work reactor監聽到可寫事件.將資料發給client.ending

 

1.4 proactor

copy一個圖,大概意思就是

1.使用者會把事件和回撥函式註冊到核心

2.當有對應的事件時(這裡指io事件,連線/可讀/可寫),會自動執行連線/讀資料/寫資料.並呼叫註冊的回撥函式

想法是好的,不過作業系統沒有實現完美,因此很少有用proactor的

 

2.IO分類

網路io的時候,都是在操作記憶體,如果從磁碟讀寫一個檔案的時候,作業系統又分為哪幾種模式呢

2.1.直接io

    直接io:直接把磁碟資料讀取到使用者態記憶體.

    例如我們在程式碼中讀取一個檔案

    Byte[] byteArray=new Byte[1024];

    byteArray=file.read("/data/test.txt")

    這裡byteArray就是申請一塊使用者態記憶體.

    我們把磁碟資料直接讀到使用者態記憶體,可能耗時很慢.我們知道磁碟的操作和記憶體的操作速度不是一個級別的.尤其如果是隨機讀,那麼會更慢!但是,很多資料庫都會用直接讀寫,為啥呢?它可以自己做控制,

 

直接i

 

2.2快取io(標準 I/O)

    由於從磁碟讀取到使用者記憶體很慢.因此作業系統給我們做了一層優化,那就是再加一層快取.這種設計也是隨處可見的,例如為了解決cpu和記憶體的效能差異,引入了L1,L2,L3快取.

    這就是pageCache.預設情況下,我們讀寫磁碟資料都會使用pageCache,是不是使用直接io一定錯嘞?NO!,總結下直接io的優缺點

 優點:

1.節約記憶體.少了一個pageCache的開銷(可以free 檢視pageCache).

2.可以自己控制.更靈活.例如一些自快取的系統,可以自己設計快取實現,比較常見的是資料庫系統.

缺點:

1.無法直接從pageCache讀取,pageCache是記憶體讀取,比較快,pageCache做了預讀優化,減少讀盤次數.

2.讀的話需要讀取磁碟,慢;寫的話,需要刷盤成功,也慢.

    例如,mysql中寫binlog為什麼很快,其實預設就是寫到了pageCache中,然後通過設定fSync策略,設定什麼時候重新整理到磁碟.pageCache刷盤有一定條件.

    a.使用者程式呼叫sync()或者fsync()

    b.系統呼叫空閒記憶體低於特定閾值

    c.髒頁的資料在記憶體中駐留的時間超過一個特定閾值

3.如果讀大檔案,會浪費,因為大檔案不會讀很多次.浪費記憶體,導致小的檔案也用不了pageCache,而且大檔案都連續儲存,讀磁碟也可以.即使用了pageCache,還是得讀到記憶體,多了一次拷貝

快取io

2.3 mmap

    從快取io中可以看到,使用pageCache多了一次記憶體拷貝.那麼我們能不能優化這次拷貝呢?使用mmap,可以把使用者態的記憶體對映到核心態.回憶一下,使用者虛擬地址的佈局.

 

虛擬地址佈局

    其中在使用者區有一段是用來做記憶體對映的.我們也是基於這段可以申請一段記憶體,和使用者態記憶體對映在同一個地址.

 

mmap

2.4 sendFile

mmap減少了一次核心空間到使用者空間的拷貝.但是還是避免不了程式從使用者態到核心態的切換.

 

sendFile

可以看到,只需要磁碟-->pageCache-->socket緩衝區--->網路卡緩衝區.不需要使用者態/核心態切換.

        其實socketCache和網路卡直接的拷貝,也可以去除.需要用到一個支援收集操作的網路介面。主要的方式是待傳輸的資料可以分散在儲存的不同位置上,而不需要在連續儲存中存放。這樣一來,從檔案中讀出的資料就根本不需要被拷貝到 socket 緩衝區中去,而只是需要將緩衝區描述符傳到網路協議棧中去,之後其在緩衝區中建立起資料包的相關結構,然後通過 DMA 收集拷貝功能將所有的資料結合成一個網路資料包。網路卡的 DMA 引擎會在一次操作中從多個位置讀取包頭和資料。Linux 2.4 版本中的 socket 緩衝區就可以滿足這種條件,這種方法不但減少了因為多次上下文切換所帶來開銷,同時也減少了處理器造成的資料副本的個數。對於使用者應用程式來說,程式碼沒有任何改變。

 

2.5.socket緩衝區

        再說下socket緩衝區,以write為例,當使用者呼叫socket.write時,其實會把資料從使用者空間拷貝到核心空間.也就是socket緩衝區.每個socket建立時,都會分配一定大小的讀緩衝區和寫緩衝區這兩個互不影響.大小可以自由配置.可以參考另一篇https://mp.csdn.net/editor/html/109929715

相關文章