作業系統和 Web 伺服器那點事兒
作業系統老大
又一個程序啟動了,作業系統老大嘆了一口氣,畢竟自己的肩頭又多了一份責任。
讓人煩惱的是,新來的傢伙們很無知,幾乎就是一張白紙。有些老實本分的會按照自己的規矩來做事,有些刺頭兒喜歡問這問那,時不時還想搞點非法的訪問,想訪問別的程序的地址空間,甚至想訪問核心的程式碼和資料! 這時候,我只有把他kill掉祭天,留下一個core dump的屍體讓碼農們去分析。
規矩很重要!
想到此處,老大又看了一眼自己的核心空間,這個機器只有可憐巴巴的4G記憶體,0-3G給各個程序共享使用,自己獨佔了從3G-4G的記憶體空間。
新啟動的程序是一個Web伺服器,自稱小W,這是個喜歡問問題的傢伙,他第一個問題就是: ”老大,你為啥不和群眾打成一片,反而自己要獨佔空間呢?“
“這是為你們好?”
“為我們好? ”
“計算機的硬體資源是有限的,硬碟、記憶體、網絡卡,鍵盤,滑鼠,時鐘...... 如果任由你們這些程序隨意訪問,大家你爭我搶,豈不亂套?”
“再說了,那些底層的硬體、驅動操作是極其麻煩的,讓你們每個程序都去寫那些‘噁心’的程式碼,你們受得了嗎? “
”還有,如果某個惡意的傢伙故意搗亂,那還了得?”
老大的三連問簡直是振聾發聵, 小W立刻覺得氣短了三分。
“所以你就不讓我們直接訪問了?”
“對啊,我就做了一個 抽象層 ,你們必須通過這個抽象層來訪問硬體資源。這個抽象層之下就是我的 核心 ,是我的程式碼和資料,所以我必須得單獨居住,不能和你們混在一起。”
系統呼叫
“那我想訪問一個硬碟上的檔案,到底該怎麼辦?” 小W問道。
“非常簡單,我的抽象層中有對外提供的介面,叫做 系統呼叫 ,例如read、open、close等。 你可以open 一個檔案,read它的內容,讀完了close。”
“聽起來好像是函式呼叫啊!”
“對,就是函式呼叫,但是和你內部的函式呼叫有本質的不同,這種系統呼叫會讓你從 使用者態切換到核心態, 也就是到我的核心程式碼中來執行!”
小W懵懂地點點頭,似乎明白了。
他應該沒有明白,他也明白不了, 作業系統老大心裡想,系統呼叫之複雜遠遠超過他的想象。
首先所有Linux的系統呼叫的引數都是通過暫存器而不是棧來傳遞的,按照慣例暫存器EAX儲存了系統呼叫的編號(例如1表示exit這個系統呼叫,2表示fork,3表示read......),暫存器EBX,ECX,EDX,ESI,EDI可以包含最多6個任意的引數。
比如: write(1,"hello",5);
這就是個系統呼叫, 就是向stdout(控制檯)輸出一個字串,在執行時,必須把暫存器給設定好:
EAX = 4 (4表示系統呼叫的編號)
EBX = 1 (1 表示stdout)
ECX = 那個字串的地址
EDX = 字串的長度
然後呼叫int 0x80 系統中斷,這就進入了核心, 我會取出EAX, 從一個核心的表格中查到第4號對應的系統呼叫處理程式來執行。
對了,我還需要把CPU的特權等級從3置為0,表示核心態。
看看,我容易嗎我! 作業系統心裡略微有點傷感。
read 和 write
這時候小W探出頭來,興奮地說:“hi ,老大,有客戶要訪問咱們硬碟的檔案,我得讀取一下,然後通過socket發出去。是不是需要系統呼叫了?”
“那是肯定的,訪問檔案系統必須得通過我,訪問socket也得通過我,不用系統呼叫怎麼可能? 除去open ,close, 你需要兩個關鍵的系統呼叫:”
// 從檔案(用fd表示)中讀取len長度的內容,放到buffer中
read(fd, buffer, len);
// 把buffer中長度為len 的內容寫入到socket中(用sockfd表示)
write(sockfd, buffer, len);
(注: read和write 應該是sys_read和sys_write的“包裹”函式,我們這裡簡化,認為就是直接的函式呼叫。)
“好滴!” 小W做了一些準備工作,然後便開始read, 然後滿心歡喜地等待資料的到來。
作業系統收到read呼叫,陷入核心,正式進入了核心態,然後毫不客氣地暫停了小W的執行,讓他進入了阻塞佇列(假設小W只有一個執行緒)。
小W表示不滿:“怎麼不讓我運行了?”
“讀取檔案太慢,你先歇會兒,資料來了會通知你的。”
老大使用DMA(Direct Memory Access)的方式把檔案的資料從硬碟複製到了核心的緩衝區, 然後又複製到了使用者的緩衝區,read呼叫完成,返回使用者態 ,小W可以繼續執行了。
小W要通過socket傳送資料,於是又發出了write呼叫,再次陷入核心,進入核心態。
老大把資料又從使用者緩衝區複製到socket緩衝區, write呼叫返回,返回使用者態。
小W問道:“這次這麼快就返回了?資料發出去沒有啊?”
老大說:“這就不用你操心了,網絡卡驅動會在合適的時候傳送的,這是個非同步的操作。”
小W畫了一張圖,試圖理解整個過程,等他把圖畫完,不由得咂舌:“嘖嘖,這麼兩個簡單的系統呼叫,代價竟然如此之高啊。”
(1) 需要進入核心態兩次,返回兩次。
(2) 資料居然發生了三次複製,硬碟-->核心緩衝區-->使用者緩衝區-->socket緩衝區
如果說第一次從硬碟到核心緩衝區必不可少,後面的兩次就太浪費了。
老大說:“你看到了吧,系統呼叫的開銷很大啊,以後要少點呼叫啊。”
小W說:“我覺得你這個核心雖然保護了硬體,但是導致效率很低啊,能不能優化一下,省去使用者態<-->核心態之間的資料複製? 這太浪費了!”
sendfile
老大哈哈一笑, 說道: “我早就料到了這一層,我這裡還有個系統呼叫,叫做sendfile,你可以試試啊,通過這個系統呼叫,可以直接把檔案內容發給socket。 ”
sendfile(socket, file, len);
小W一看,不錯啊,自己只需要呼叫sendfile,進入核心態一次就可以了,老大可以把資料從硬碟複製到核心緩衝區,然後直接複製到socket緩衝區, 完全不用自己介入,就用它了!
可是轉念一想,這從核心緩衝區到socket緩衝區的複製有必要嗎? 那個網絡卡驅動不能直接從核心緩衝區讀資料嗎?
老大似乎看穿了小W的心思,說道:“我知道你在想啥,放心吧, 我早就做了優化了,不會把資料從核心緩衝區複製到socket緩衝區,相反,我只會把一些 位置和資料長度等資訊 複製過去,很省事的。網絡卡驅動可以直接從核心緩衝區讀去資料。”
小W放心了,開始使用這種sendfile的方式,果然,效能大為提升!
這其實就是所謂的 zero copy 技術, 從核心角度看,除了把檔案從硬碟讀出來之外,沒有任何的額外copy。
zero copy技術減少了上下文的切換,避免了資料不斷地在使用者態和核心態搬運,不需要CPU參與資料的複製,提高了系統性能,在ngnix, apache等web 伺服器中都引入了zero copy技術。
(完)