1. 程式人生 > >操作系統和Web服務器那點事兒

操作系統和Web服務器那點事兒

memory ... 還需 不同 圖片 web kill -o 傷感

又一個進程啟動了,操作系統老大嘆了一口氣,畢竟自己的肩頭又多了一份責任。

讓人煩惱的是,新來的家夥們很無知,幾乎就是一張白紙。有些老實本分的會按照自己的規矩來做事,有些刺頭兒喜歡問這問那,時不時還想搞點非法的訪問,想訪問別的進程的地址空間,甚至想訪問內核的代碼和數據! 這時候,我只有把他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技術。

操作系統和Web服務器那點事兒