Linux 進程的創建

 目錄

進程創建

fork()函數

fork返回值

fork寫時拷貝

fork失敗原因

fork用法


進程創建

Linux 中我們可以說一個進程就是一個PCB, 即 一個task_struct, 那麼創建進程也就是創建PCB, 即是創建task_struct

Linux 中說到進程創建,  就不得不提到 fork()函數. fork()在Lnux下是非常重要的一個函數 .

fork()函數

從已存在進程中創建一個新進程。新進程爲子進程,而原進程爲父進程

fork()在函數內部會調用clone這個系統調用接口

pid_t  fork ()

頭文件: unistd.h

fork返回值

fork函數返回值 : (返回值類型爲pid_t, 實際等同於int)

  • 子進程在執行fork()時返回 0
  • 父進程在執行fork()時, fork()創建子進程, 返回子進程的PID (PID是一個大於0的整數)
  • 父進程在用fork()創建子進程失敗時返回 -1

因爲fork運行有多種結果, 所以往往fork之後要根據fork的返回值進行分流(例如用 if 寫多個分支), 來看個例子 .

testfork.c 如下

#include<stdio.h>
#include<unistd.h>
int main(){
    pid_t pid = fork();
    if(pid == -1){
        perror("fork error");
    }
    else if(pid == 0){
        printf("子進程\n");
    }
    else{
        printf("父進程\n");
    }
    return 0;
}

編譯執行如下, 可以看到, 當父進程用fork() 創建子進程成功後, 返回了其子進程的pid, 然後繼續執行, 直到執行打印語句後子進程才執行, 如下 :

但並不是父進程創建了子進程, 父進程就一定會先執行完,才執行子進程, 也可能是父進程執行到一半, 甚至剛調用fork()創建完子進程後, 就立即轉而執行子進程. 這取決於CPU的調度. 比如說下面這段代碼 .

#include<stdio.h>
#include<unistd.h>
int main(){
    pid_t pid = fork();
    if(pid == -1){
        perror("fork error");
    }
    else if(pid == 0){
        printf("子進程執行\n子進程pid:%d\n", getpid());
    }
    else{
        printf("父進程開始執行\n");
        sleep(5);
        printf("父進程執行\n父進程pid:%d\n", getpid());
        printf("父進程運行結束\n");
    }
    return 0;
}

可以看到, 父進程執行到一半開始執行子進程了, 就此次運行結果分析, 由於父進程中sleep()函數, 致使父進程進入睡眠狀態

(sleeping)(這種睡眠是可中斷的, 當sleep()執行完, 就會中斷睡眠, 進入就緒狀態(或者說進入運行隊列), 等待分配時間片), 子進程

當被創建後, 一直處於就緒狀態(一直處於運行隊列中), 等待分配時間片, 當父進程睡眠時, 子進程拿到了時間片, 子進程執行 . 當子

進程執行完, 父進程拿到時間片後, 父進程繼續運行 . 

所以就有, fork創建子進程之前, 父進程獨立運行, 創建子進程之後, 誰先運行取決於調度器的調度

進程調用fork,  內核會做出以下操作

  • 分配新的內存塊和內核數據結構給子進程
  • 將父進程部分數據結構內容拷貝至子進程 ( 此時已經創建了子進程的PCB即Linux下的task_struct )
  • 添加子進程到系統進程列表當中 ( 即添加子進程PCB)
  • fork返回,調度器開始調度

fork寫時拷貝

fork 創建子進程採取分時拷貝的策略 .

即,  父進程創建子進程時,只創建了子進程的task_struct(PCB), 並沒有直接給子進程開闢內存來拷貝數據,而是跟父進程

一樣映射到同一位置,但是如果父進程或子進程有一方想要修改內存中的數據時,那麼對於改變的這塊內存,需要重新給

子進程開闢內存,並且更新子進程頁表信息. 這樣做, 提高創建子進程的性能, 並且能節省內存 . 如下圖 :

                      圖1. 父子進程共享代碼與數據                                                     圖2. 父子進程代碼共享, 數據獨立

圖裏涉及到了虛擬內存與分頁式內存管理的內容, 簡單說一下, 分頁式內存管理可以將一段程序加載到不連續的物理空間上,但是

從虛擬地址空間來看依舊是連續的, 用以解決內存使用率低的問題 .

mm_struct結構體也叫內存描述符,其中記錄虛擬內存各個段的起始地址, 結束地址, 通過這種方式描述了進程的虛擬地址空間, 

每一個進程都會有唯一的mm_struct結構體, mm_struct記錄在task_struct中.

頁表: 頁表中存儲的是虛擬地址和物理地址的映射關係, 即頁號到物理塊的地址映射. 通過虛擬地址得到頁號與頁內地址(或者叫頁內偏移),  在頁表中通過頁號找到物理塊號.  然後,  物理地址 = 物理塊號 x 頁面大小 + 頁內偏移  就得到了物理地址

來段代碼感受一下

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
    pid_t pid = fork();
    int data = 0;
    if(pid == -1){
        perror("fork erro");
    }
    else if(pid == 0){
        printf("子進程執行\n");
        data = 10;
        printf("data = %d\n", data);
        printf("data地址: %p\n", &data);
    }
    else{
        sleep(2);
        printf("父進程執行\n");
        printf("data = %d\n", data);
        printf("data地址: %p\n", &data);
    }
    return 0;
}

代碼中, 先讓父進程睡上2秒, 這時會執行子進程, 子進程修改了data的值爲10, 但子進程結束後, 父進程繼續執行打印出的data

還是0, 兩個進程所打印的值不同,  但父子進程中data的地址都是一樣的.  我們知道數據不同, 則數據一定存儲在不同的物理地址

上, 打印的的變量地址依舊相同, 這是因爲取地址&得到的並不是物理地址(在所有有關地址的操作中, 我們只能接觸到虛擬地址),

而是虛擬地址, 虛擬地址雖然相同, 但是父子進程有着不同的mm_struct, 即有着不同的頁表, 這父子進程的data相同的(虛擬)地址

通過不同的頁表映射到不同的物理地址上.

運行結果如下圖:

fork失敗原因

  • 系統進程數達到太多, 達到上限(系統會有一個進程數的限定, 可以修改)
  • 內存不足 (fork創建子進程需要創建新的PCB, 寫時拷貝可能還會分配新的數據空間)

fork用法

fork()函數當然不是爲了創建子進程而創建子進程, 創建子進程目的有兩種:

  • 一個父進程希望複製自己,使父子進程同時執行不同的代碼段.  

    例如: 父進程等待客戶端請求,生成子進程來處理請求。
     
  • 一個進程要執行一個不同的程序 . 例如: 子進程從fork返回後,調用exec函數(用來做進程替換的函數, 下面說)替換子進程. 比

    如, 我們用的Shell就是一個程序,   一般爲默認爲bash, 我們執行一些非Shell內建命令時, 實際就是一個Shell創建子進程, 再進

    行進程替換的過程

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章