進程——父子進程共享
一、fork()
1. 在談fork之前,先簡單說一下進程的相關知識點。
(1)進程不同於程式是動態運行在記憶體中的實體,佔用系統資源(CPU、記憶體等),而程式則是存放在磁碟中的靜態的資源,佔用磁碟空間而不佔用系統資源。進程在記憶體中運行,由CPU分配資源。
(2)與進程相關的兩個記憶體:虛擬記憶體和物理記憶體。所謂虛擬記憶體就是我們程式設計師視角下的記憶體,比如int a = 10; &a 所得的值就是虛擬記憶體,是給我們程式設計師看的連續的地址空間。(當我們在程式碼中連續定義幾個local object時,通過&可以觀察到它們的地址是連續的)相對的,物理記憶體才是實實在在的存在於電腦硬體中的記憶體(比如買記憶體條時我們可以參考的4G、8G等容量參數),當執行 a = 20這條語句時,作業系統就會將 a 的虛擬地址送入 CPU的地址轉換單元(MMU),如果a還沒有實際的物理單元,則為a分配物理記憶體,寫入20,反之直接將20寫入a物理記憶體單元。
(3)為什麼會有虛擬記憶體? 虛擬記憶體的產生源自物理記憶體的稀缺,買過SSD或者記憶體條的夥伴都知道,250G的SSD也就是250塊左右,而僅僅8G的記憶體條就要250塊,記憶體的小容量與高價格的反差促使猿們必須節省記憶體的開銷。由此產生了虛擬記憶體技術,32位系統下,CPU會為每個進程分配4G的虛擬地址單元(地址編號為0-4G),分為用戶空間(通常為0-3G)和內核(kernel)空間(3-4G),用戶空間存放該進程的堆棧變數、全局變數等,kernel里存放該進程的進程式控制制塊(PCB,唯一區分每一個進程)。虛擬記憶體單元只有在被進程訪問後才會映射為物理記憶體單元(見(2)a=20的執行過程)。
(4)記憶體使用的一大機制(虛擬記憶體能實現的原因):缺頁中斷(見文章最後)。
2. fork()函數用於創建進程,fork執行完後系統就產生了一個新的進程,成為調用fork的進程的子進程,此時系統中有兩個進程,父進程和子進程。
fork()返回值:
成功:父進程——返回子進程的pid; 子進程——返回0
失敗:返回-1
3. fork()是如何產生子進程的? 上面說了每一個進程都有自己0-4G的虛擬地址空間,因此,fork所做的動作就是在當前進程的基礎上產生一個新進程:(1)複製父進程的0-3G的用戶空間(2)創建新進程(子進程)的PCB
二、父子進程共享
共享的定義是二者可以共同使用一件東西,前提是一件東西,而不是一個東西的兩個副本。
fork()完成的動作是子進程拿到了父進程0-3G用戶空間的副本而不是原件,因此嚴格來說父子進程之間是不共享的,那為什麼這裡還要說到共享呢?前面說到,父進程的0-3G用戶空間是虛擬記憶體空間,只有我們訪問了某個單元時,才會真正在物理記憶體上分配一份地址空間,也就是說,這些虛擬記憶體里總會有一部分被真正的映射到了物理記憶體中,但是為了節省記憶體開銷,fork在複製0-3G的虛擬記憶體空間時並不會將父進程中已經映射到物理記憶體的記憶體單元再複製一份到物理記憶體中,而是遵循「讀時共享,寫時複製」的原則,即僅複製虛擬地址空間。
因此這裡說的「共享」更大意義上存在於 父子進程共同訪問一塊記憶體空間的過程0,比如fork產生的子進程要輸出父進程中變數a的值,那麼子進程只需要共享父進程提供的變數a的物理單元,取出20這個值輸出即可,如果子進程不單要輸出a,還要對a執行+1操作,這個時候子進程才會將父進程中變數a的物理記憶體單元複製一份並執行+1操作,執行完後父進程中的變數a值仍為20,子進程a值為21。
因此可以說fork後父子進程的用戶空間是相同的,kernel空間是不同的。
但是fork後父子進程的用戶空間遵循的是「讀時共享,寫時複製」的原則,類似於「父子進程共享棧變數、全局變數」的說法都是不準確的。
看一個例子吧:
#include <unistd.h> #include <stdio.h> #include <stdlib.h> //global variable int globalVar = 78; int main(){ pid_t pid = getpid(); int k = 0; int i; for(i = 0; i < 5; ++i){ pid = fork(); // 循環生成5個子進程 if(-1 == pid){ perror("FORK"); exit(1); }else if(0 == pid){ break; } } sleep(i); // 保證父進程最後退出 if(i < 5){ ++k; // 寫時複製 --globalVar; //寫時複製 printf("I'm %dth child, pid = %d, parent is %d, k = %d, globalVar = %d\n", i, getpid(), getppid(), k, globalVar); }else{ ++k; --globalVar; printf("I'm parent, pid = %d, k = %d, globalVar = %d\n", getpid(), k, globalVar); } }
三、小福利
缺頁中斷:缺頁中斷的過程其實已經在1.(2)中描述了,缺頁中斷簡單來說就是:先對 待訪問數據的虛擬地址進行段表和頁表的映射,試圖訪問其對應的物理記憶體單元, 然後——待訪問的數據在物理記憶體中嗎 ? (在)訪問/修改該物理記憶體單元 : (不在)申請一段物理記憶體存放該數據並完成與虛擬地址的映射,然後訪問/修改該記憶體單元。 (段表和頁表是段頁式記憶體管理中記錄虛擬地址與物理地址映射關係的表)