程式——父子程式共享

_程式兔發表於2020-07-15

一、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)中描述了,缺頁中斷簡單來說就是:先對 待訪問資料的虛擬地址進行段表和頁表的對映,試圖訪問其對應的實體記憶體單元, 然後——待訪問的資料在實體記憶體中嗎 ? (在)訪問/修改該實體記憶體單元 : (不在)申請一段實體記憶體存放該資料並完成與虛擬地址的對映,然後訪問/修改該記憶體單元。 (段表和頁表是段頁式記憶體管理中記錄虛擬地址與實體地址對映關係的表)

  

 

  

相關文章