意義是:返璞歸真,我們只是在重復著歷史的循環。提高IO效率的根本方向只有2個:(1)讓內核足夠小;(2)讓內核足夠大。
大背景:隨著網絡的發展,IO密集型業務的增長,操作系統的內核逐漸從“好用的工具”變成“礙事的管家”,于是行業開始思考獨立于內核之外實現更高效率的IO。
以下為方便理解的比喻、簡化模型,不是嚴謹的說明。
思考兩個問題:
- 如果沒有操作系統,如何寫一個超簡單的Request-Response服務?
- 如果DPDK平臺上要跑多個第三方應用,如何公平運行這些應用?如何防止“不靠譜”的惡意應用搞破壞?
我們先只考慮1,如果沒有操作系統的話,我們會怎么寫服務器。
STEP 1:時光退回50年前,假設我們有臺單核服務器,上面只跑1個服務,這個服務非常輕量、通信量充分小、計算負載充分低,而且不存在信任與安全的問題。
最簡單的做法是,當網卡收到請求拋出硬件中斷時,CPU(假設DMA還沒有發明出來)將網卡緩沖區的IP包數據讀出,解析請求,執行請求,再將響應數據寫入網卡緩沖區,發送。
STEP 2:業務開始變復雜,CPU處理請求的時間已經不能忽略不計了,但是硬件中斷必須馬上響應完,否則有可能丟失請求。
為了解決這個問題,我們先寫了一個基礎框架:讓CPU可以交替執行2個程序A和B,如果網卡有數據請求過來,就保存CPU現場立即強制切換到執行A。
程序A的功能是:收數據包但不立即處理請求,而是把收到的數據緩存到內存
程序B的功能是:從內存里取出緩存的數據包,執行處理請求
只要我們的基礎框架和程序A的性能不拉跨,基本不用擔心丟失請求了。
現在開始考慮問題2。
STEP 3:業務更復雜了,我們需要在這臺服務器上跑3個程序A、B、C。A還是上面那個框架,B是我們自研的應用,C是外包給外包商開發的程序,而且這個開發商好像還有點不太靠譜。
為了解決這個問題,我們先把內存分成兩半,一半只允許程序A使用,另一半再劈兩半,分別允許B和C使用。然后再改造一下CPU,讓程序B和C只能訪問各自段的段內內存,如果想跨段訪問,必須調用程序A的接口進行申請,由A來代理訪問。
由于開發程序B的外包商不太靠譜,我們還要防止程序B“偷聽”本來要發給A的請求。于是,我們給IP消息編個號加到頭部,發給B的消息是1號,發給C的消息是2號,再規定所有消息只能由程序A接收。程序B和C向程序A申請提取消息時,程序A會核對編號;程序B和C定時向程序A詢問是否有發給自己的消息,如果有,就把消息從程序A復制到自己的內存,然后再處理執行。我們管這種帶編號的消息起名叫UDP報文。
STEP 4:通信量越來越大,程序A光從網卡取數據就能把CPU累死,于是我們又制造了一個硬件DMA,協助CPU做枯燥無味的拷貝工作。
STEP 5:通信量又大了。消息從程序A拷貝到程序B和C的開銷已經大到不可忽視。程序B和C都覺得程序A“太礙事了”“管得太寬,手伸得太長”,抗議A“濫權”的呼聲越來越高。于是程序A改造了一下,決定“放權”。程序A允許B向A申請一段共享內存,程序A把消息往共享內存里轉圈寫入,B就可以不從A拷貝數據直接看到內容了;同理,B想發什么數據,直接往共享內存里寫入,程序A會轉圈處理程序B寫入的消息。發明這個機制的人姓U,所以人們把這種機制命名為“uring”。
STEP 6:通信量更大了,而且還更頻繁。而且人們發現,程序A、B、C之間互相通信的開銷也非常大。于是程序B和C合計了一下,“A還是太礙事了,咱們把A踢了單干如何?走,去問問網卡同不同意!”
?