天天看點

28-程序空間與 fork 函數原理

前面對 fork 函數牛刀小試,相信你已基本掌握了簡單的“影分身術”了,不過在篇末,卻為各位留下了一些坑位。為了能夠說明白一些問題,本篇将讨論有關程序的一些必備知識,以及 fork 函數的底層實作。如此一來,也友善加深對其它有關問題的了解。當然,如果你對此完全不感興趣,大可跳過。

本文所讨論的範圍,限制在 32 位的 linux 作業系統。

1. 程序空間

這裡的程序空間,說的就是程序虛拟位址空間。

稍微懂點程式設計和作業系統的同學都應該知道,每個程序都有自己的 4GB 虛拟位址空間,這具有深遠的意義。有個經典的 C 語言的例子,就是程序 A 中往位址 ​

​0x50000000​

​​ 寫入一個 int 類型的整數 100,然後在程序 B 的位址​

​0x50000000​

​ 寫入另一個 int 類型的整數 1000,最後兩個程序再各自通過 printf 函數列印這兩個位址儲存的值,發現程序 A 仍然可以正常列印 100,而程序 B 正常列印出 1000.

這段代碼,可以在windows 系統的 visual studio 模拟(沒有使用 linux 是因為 linux 不直接提供在指定位址上配置設定記憶體的函數,另外在概念上,windows 的程序空間和 linux 是互通的,不會有太大差別)。

  • 程序 A 的代碼
#include <windows.h>

int main()
{
    int *buf = (int*)0x50000000;
    LPVOID res = VirtualAlloc((LPVOID)buf, sizeof(int), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
    if (res != buf) {
        printf("ERROR!\n");
        return 1;
    }

    *buf = 100;

    printf("A = %d\n", *buf);

    Sleep(3000);
    return 0;
}      
  • 程序 B 的代碼
#include <windows.h>

int main()
{
    int *buf = (int*)0x50000000;
    LPVOID res = VirtualAlloc((LPVOID)buf, sizeof(int), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
    if (res != buf) {
        printf("ERROR!\n");
        return 1;
    }

    *buf = 1000;

    printf("B = %d\n", *buf);

    Sleep(3000);
    return 0;
}      

然後開啟了兩個控制台視窗,同時(隻要 A 程序沒結束的情況下啟動 B 程式就行了)運作這兩個程式,結果如下。

28-程式空間與 fork 函數原理

圖 1

形成圖 1 中現象的原因在于程序 A 和程序 B 的位址空間是隔離的,盡管它們都在位址 0x50000000 這個位置寫了不同的值,但是各自都互不影響。這就好像我在我家菜地的5号地種的洋芋,你在你家菜地的5号地種的黃瓜,根本就是誰也挨不着誰。

28-程式空間與 fork 函數原理

圖 2

2 虛拟位址到實體位址的映射

對于程序來說,其實屬于程序那一塊菜地,是虛拟的,是假的。這中間有一步到實體位址的映射過程。就好像上面的菜地,你看到的是菜地也是假的。要怎麼了解這件事情,難道你看到的不是菜地嗎?

2.1 種菜的故事

不妨假設一下,親自下地種菜的人不是你本人,而是由另一個管家代勞。而你,隻負責下達指令就行了。當你來到這個世上,你老爸就告訴你,你有一塊的菜地,被分成了16個小塊,你可以在任意一塊地上随便種,你想種什麼,種在哪塊地上,和管家說一聲就行了,你想從地上摘什麼菜,也和管家說就行了。你爸把菜地大概的樣子畫給你看,就像圖 2 中的那樣。

除此之外,你還有一個弟弟,你老爸也和你弟說了相同的話,你弟弟也有和你一塊一樣大小的地,而且你弟弟要種菜,也隻要給你家的管家下達指令就行。

就這樣,你和你弟弟就在圖 2 那樣的菜地上,各自快樂的種着菜,生活了幾年;可是你并不知道你家菜地真實情況是長啥樣,你弟弟也同樣不知道。你和你弟弟知道的菜地的樣子,僅僅就是你老爸當初給你們畫出來的圖 2 的樣子。

其實對你來說,菜地長啥樣都無所謂了。你的菜地上有什麼,統統問管家,你想在哪塊地上種什麼,也由管家代勞。

2.2 菜地長什麼樣

就這樣,5年過去了。種了 5 年菜的你已經不耐煩了,突然有一天,你想親自看看你的菜地。你不顧一切管家的阻撓,帶着你弟弟,一起來到了所謂的菜地,這時候,你們慌了。你們看到的菜地,實際是圖 3 這樣的。根本就沒有你和你弟弟的菜地,你們種的菜,全都隻是在圖 3 這塊地上。

28-程式空間與 fork 函數原理

圖 3

管家氣喘籲籲的正好趕來了,你和你弟弟剛好和他對簿公堂。隻有一塊地,卻騙了你們 5 年。管家是如何做到的?再三追問下,管家拿出了他的兩本筆記本,說秘密都在這裡了。其中一本記錄了你的菜地使用情況,另一本記錄了你弟弟的菜地使用情況。

管家繼續說到,如果你說要去你的 5 号地取洋芋,他就會翻翻筆記本,看看你的 5 号地對應實際哪塊地,比如現在就對應着實際的 3 号地,管家就會去 3 号地把東西取給你。對你弟弟也是一樣,隻要拿出相應對應的筆記本就可以很快查到。這就好像是下面這個樣子。

28-程式空間與 fork 函數原理

圖 4

2.3 程序的那塊地

看完前面的故事,我希望你能夠觸類旁通,每個程序生來認為自己有 4GB 的虛拟位址,可卻被“管家”(作業系統)欺騙了一生,它沒有你和你弟那麼幸運,最終破解這個分地謎題。程序在自己的土地上心情的揮霍,它意識不到真正的菜地到底有多大(你的實體記憶體),隻要“管家”能夠滿足它的需求就行了(管家甚至做了記憶體交換這種事,因為實際的土地隻有 0 到 20 号,一共 21 塊地,而你和你弟弟的地加起來有 32 塊,萬一土地不夠了,管家就會偷偷的把你們不經常用的土地裡的東西取到硬碟上)。

值得一說的是管家的筆記本,對應到程序這裡就是頁表的概念,有關這一塊的知識,不詳細展開,對細節感興趣的同學,請參考我的另一個筆記《OS學習筆記》。

3. fork 幹了什麼

有了程序空間的概念,就很容易知道 fork 做了什麼事。以前面的菜地為例,再假設你沒有弟弟。fork 的含義有點類似下面這樣:

  1. 你告訴你老爸,給我生個弟弟出來。
  2. 你告訴你的管家,給我弟弟種一片和我的菜地一模一樣的菜地出來。

這一切完成後,菜地應該是這樣的。

28-程式空間與 fork 函數原理

圖 5 你弟弟複制了一份你的菜地

那麼再此之後,你種什麼,你弟弟種什麼就各不相幹了。

相對于程序來說,這兩個程序的程序空間是一模一樣。

4. 上一篇的遺留問題

如果你完成上一篇最後一節總結裡的實驗,你會驚訝的發現,tmp 檔案内容和直接運作 ​

​./myfork​

​ 列印在螢幕上的結果是完全不一樣的。

tmp 檔案裡的内容,應該像下面這樣:

Hello, I'm father
before fork
I'm father 3542; my child is 3543
before fork
I'm child 3543; my father is 3542      

執行 ​

​./myfork​

​ 列印到螢幕上的内容,卻是這樣:

Hello, I'm father
before fork
I'm father 4382; my child is 4383
I'm child 4383; my father is 4382      

你比較感興趣的地方應該在于 tmp 檔案裡的 before fork 竟然出現了兩次!這個謎題在這一篇相信你可以搞清楚,原因在于 printf 這個函數,它是帶緩沖區的!!!

當 printf 接收到字元串後,第一件事情是把字元串複制到一個 char 數組(緩沖區)裡,當這個數組遇到了特定的字元,比如 ‘\n’ 字元,回車或者裝滿等等,就會立即把字元寫到螢幕終端上。

而當我們把 printf 重定向到檔案的時候,如果 printf 函數遇到 ‘\n’ 字元,并不會立即把字元寫到檔案裡,這是 printf 函數将字元定向到螢幕和檔案的重要差別。

是以當 ​

​./myfork > tmp​

​ 這個程序執行到 fork 的時候,printf 裡的緩沖區資料還沒來得及被重新整理到 tmp 檔案裡,就被 fork 函數複制了,同時,printf 的緩沖區也被複制了一模一樣的一份出來。這也就是為什麼子程序裡為什麼子程序也會輸出 before fork 的原因。

5. 關于 fork 函數的傳回值

前一節講了 fork 就是将程序的位址空間完完全全的複制了一份(不是100%複制,除少數幾個地方外),實際在複制完後,作業系統會悄悄的修改複制出來的程序空間裡的 fork 函數的傳回值,把它改成 0(準确的說不是改,而是根本就沒有複制原來的值,直接把它指派為 0)。

這也就是子程序 fork 函數傳回了 0 的原因。

6. 寫時複制技術

到此 fork 并沒結束。早在 linux 0.11 裡,fork 函數就實作了 Copy On Write(COW) 機制,也就是寫時複制技術。什麼意思呢?

你告訴管家給你弟弟種一份一模一樣的菜地時,管家并沒有照做,他幹了下面這樣的事情:

28-程式空間與 fork 函數原理

圖6 讀時共享

不得不說,這管家已經懶到家了……但是也不得不說,管家很聰明。

當你弟弟隻是問問管家,我的 5 号地種了什麼?管家什麼也不用管,直接查查筆記就告訴你弟弟,這是洋芋就行了。

當你和你弟弟任何一方想挖掉在 5 号地上的洋芋了怎麼辦?這豈不是很糟糕?不會的。聰明的管家早就意識到了這個問題。如果你弟弟需要挖掉他 5 号地,管家這時候才會真正的把你的 5 号地再複制一份出來,然後從真正的土地做一個映射,配置設定到你弟弟的 5 号地上,你弟弟愛怎麼挖怎麼挖,都不會影響到你。

28-程式空間與 fork 函數原理

7. 總結

  • 了解程序空間的概念,知道程序的位址是虛拟的
  • 知道程序位址空間之間是隔離的
  • 了解程序虛拟位址和實體位址之間的映射關系
  • 了解 fork 函數的工作原理
  • 了解為什麼子程序的 fork 函數傳回 0
  • 了解寫時複制技術

繼續閱讀