Linux中fork創建兄弟子進程,驗證進程之間全局變量不共享,exec函數族



簡 述: 本篇主要講解如下知識點:

  • 並行和併發的區別
  • 進程控制塊 PCB 和進程的五種狀態
  • 使用 fork() 創建子進程,創建多個兄弟子進程(不含孫進程)
  • 驗證進程之間沒有共享全局變量
  • exec() 函數族的使用:execl()execlp()

編程環境:

💻: uos20 📎 gcc/g++ 8.3 📎 gdb8.0

💻: MacOS 10.14 📎 gcc/g++ 9.2 📎 gdb8.3


並行和併發:

併發:一個時間段,處理請求的個數

eg:在一個時間段內, 處理的請求個數。 只有一個 cpu 對任務進行處理。 (每個人都吃一口飯,但是所有人都沒有吃飽,短時間內讓大量的快速吃,然後這樣輪循環。)

並行:多個進程同時進行任務分配:

可以看做,有多個 cpu 對多個任務進行同時處理。


PCB 和 進程的五種狀態:

進程控制塊 PCB:

此部分了解即可: 每一個進程在內核中都有一個進程控制塊 PCB 來維護進程相關的信息,其是有一個 400 多行的結構體組成,其中主要的需要了解的部分如下:

  • 進程 id。系統中每一個進程有唯一的一個 id,在 C 語言中,用 pid_t 類型表示。(凡是 xxx_t 一般都是 #define 重新定一個類型, 這裏實際就上課非負整數)
  • 進程的狀態,有就緒,運行,掛起,停止 等狀態。(還有一個初始態
  • 進程切換時候,需要保存和恢復一些的 CPU 寄存器
  • 描述虛擬地址空間的信息(每啓動一個進程,就對應一個虛擬地址空間)
  • 描述控制終端的信息
  • 當前的工作目錄
  • umask 掩碼(執行 umask)
  • 文件描述符表,包含很多執行 file 結構體的指針(一個進程最多打開 1024 個)
  • 和信號相關的信息
  • 用戶 uid 和組 gid (用stat xxx 可以查看)
  • 會話(Session)和進程組。(會話就是多個進程組)
  • 進程可以使用的資源上限(Resource Limit)。(使用 ulimit -a 可看)

進程的五種狀態:

看着如下的這張圖,想起裏了我之前初看的計算機操作系統的相關知識的時候,感慨頗多。

  • 狀態切換的關鍵:
    • 獲得 cpu
    • cpu 執行

進程控制塊,父進程創建子進程的分析 🎃:

父子進程空間,其虛擬地址空間是完全一樣的(用戶區的所有數據都是一樣的,也是會運行的相同的一段代碼;系統區也是一樣的,唯一不一樣的就是系統區的 id 號)。

創建一個子進程,是使用 int fork(void) 函數來創建,在執行完這個 fork 函數之後,將用戶區的內容全部拷貝(包括接下來要執行的代碼)過去。且該 fork 函數有兩個返回值,執行完 fork() 後,父進程會返回子進程的 id(大於 0),子進程會返回一個 0,後面執行相同的代碼,依靠這個來區分父子進程。 下面寫一個代碼片來表明:

對照上面的兩個圖,可以思考如下問題:

  • fork 之後,函數的返回值?
    • fork() 返回值 > 0,就是父進程;fork() 返回值 == 0,就是子進程
  • 子進程創建成功之後,代碼的執行位置?
    • 父進程執行哪裏(才創建子進程),子進程就重哪裏裏開始執行。
  • 父子進程的執行順序?
    • 不一定,自己去搶佔 cpu 資源。執行順序是程序員無法控制的。(cpu 輪循執行)
  • 如何區分父子進程?
    • 通過 fork() 返回值
  • 如何獲取當前進程和父進程的 id?
    • 獲取進程 id:獲取當前進程的 PID:getpid() ;獲取當前進程的父進程的 PID: getppid()

對與上面的問題,寫如下代碼片段進程測試,main.cpp 裏面的代碼是:代碼下載 main.cpp

#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    for (int i = 0; i < 4; ++i) {
        printf("++++i = %d\n", i);
    }

    pid_t pid = -1;

    pid = fork();

    if (pid > 0)
        printf("this is a parent process, pid = %d\n", getpid());
    else if (pid == 0)
        printf("this is a child process, pid = %d  ppid = %d\n", getpid(), getppid());

    for (int i = 0; i < 4; ++i) {
       printf("----i = %d\n", i);
    }

    return 0;
}

其運行結果如下:


父進程如何創建多個兄弟子進程(不含孫進程)🎃:

看了上面的小的例子之後,發現這樣可以創建子進程;進一步,怎麼創建多個子進程,但是不要孫創建進程?有怎麼判斷子進程的順序編號?怎麼得到最開始父進程?

代碼如下: 代碼下載 childProcess.cpp

#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    pid_t pid = -1;

    int i = 0;

    for (i = 0; i < 3; ++i) {
        pid = fork();

        if (pid == 0)  //如果當前進程是子進程,就跳出此循環,不在進行 fork() 創建新的子進程
            break;
    }

    if (i == 0)
        printf("this is a child process (i == 0), pid = %d  ppid = %d\n", getpid(), getppid());

    if (i == 1)
        printf("this is a child process (i == 1), pid = %d  ppid = %d\n", getpid(), getppid());

    if (i == 2)
        printf("this is a child process (i == 2), pid = %d  ppid = %d\n", getpid(), getppid());

    if (i == 3)
        printf("this is a parent process (i == 3), pid = %d  ppid = %d\n", getpid(), getppid());

    return 0;
}

分析代碼片:

若是沒有上面的 13、14 行代碼,那麼創建的就不止 3 個子進程;就是如下圖片所示的結果; 使用 fork() 創建子進程的時候,是進行虛擬地址控件的拷貝(可在草稿紙上面畫圖理解,是一個地址塊所有的代碼和變量值都複製一份),他們的當時 i 是 1 的時候,創建的新的子進程的 i 也是 1,不會是初值 0;同理,當 i 是 2 時候創建新進程,其新進程的 i 值是 2,而不是 1; 但是禁止子進程創建孫進程,那麼就可以使用 for() 循環生成多個兄弟子進程,且沒有任何的孫進程。

運行結果:

可以自己每運行行代碼,標記一下 i 的值,以及此 i 的值時刻複製當前進程的虛擬地址空間,給新的子進程;所以 可以通過編號 i 的數值,來判斷兄弟子進程的先後順序;最後編號最大的那個,就是最初的父進程。


進程之間是否共享全局變量:🎃

  • 讀時共享(同一個物理內存的上面區域,內存地址爲同一個變量)

  • 寫的時候複製

  • 父子進程之間能否使用全局變量通信?

    • 不能。因爲兩個進程之間內存不共享(讀時共享,寫時複製)

    代碼驗證,改寫上面的代碼,添加一個全局變量 int g_num = 200,進行驗證:

    代碼下載 sharedGlobalvariable.cpp

    #include <stdio.h>
    #include <unistd.h>
    
    
    int g_num = 200;
    
    int main(int argc, char *argv[])
    {
        pid_t pid = -1;
    
        int i = 0;
    
        for (i = 0; i < 3; ++i) {
            pid = fork();
    
            if (pid == 0)  //如果當前進程是子進程,就跳出此循環,不在進行 fork() 創建新的子進程
                break;
        }
    
    
        if (i == 0) {
            g_num += 5;
            printf("this is a child process (i == 2), pid = %d  ppid = %d\n", getpid(), getppid());
        }
    
    
        if (i == 1){
            g_num += 5;
            printf("this is a child process (i == 2), pid = %d  ppid = %d\n", getpid(), getppid());
        }
    
        if (i == 2) {
            g_num += 5;
            printf("this is a child process (i == 2), pid = %d  ppid = %d\n", getpid(), getppid());
        }
    
        if (i == 3) {
            g_num += 100;
            printf("this is a parent process (i == 3), pid = %d  ppid = %d\n", getpid(), getppid());
        }
    
        printf("g_nmu = %d\n", g_num);
    
        return 0;
    }
    

    運行截圖:

    草稿圖分析:


顯示當前進程的狀態 ps:

  • ps aus 顯示不依賴於終端的進程(終端是用來和用戶進行交互的)
  • ps aux | grep 查找的進程名 使用管道查找指定的進程名稱
  • ps ajx 顯示更多的進程信息; PID、PPID、PGID、SID(進程,父進程,組進程,會話)(會話就是多個組進程)
  • ps ajx | grep 查找的進程名 若是沒有找到,也會有一行顯示信息,顯示的是 grep 進程相關的信息

刪除執行中的程序或工作 kill:

Linux kill命令用於刪除執行中的程序或工作。

kill可將指定的信息送至程序。預設的信息爲SIGTERM(15),可將指定程序終止。若仍無法終止該程序,可使用SIGKILL(9)信息嘗試強制刪除程序。程序或工作的編號可利用ps指令或jobs指令查看。

  • kill -9 將被殺死的進程 -9 爲 殺死信號
  • kill -l 顯示所有,1-31 通常使用;32 和 33 沒有; 34-51 號通常爲系統預留

使用 kill 命令之後,有一點困惑,使用 kill -l 命令之後;

  • 在 Ubuntu 18.4 顯示如下, 和 MacOS 顯示有區別, 有清楚的,可以在下面留言幫我解答一下也可以哦。

  • 只有第9種信號(SIGKILL)纔可以無條件終止進程,其他信號進程都有權利忽略。 下面是常用的信號:

    HUP     1    終端斷線
    INT     2    中斷(同 Ctrl + C)
    QUIT    3    退出(同 Ctrl + \)
    KILL    9    強制終止
    TERM   15    終止
    CONT   18    繼續(與STOP相反, fg/bg命令)
    STOP   19    暫停(同 Ctrl + Z)
    

exec 函數族 🎃:

作用: 讓父子進程執行不相干的操作;能夠替換進程空間中源代碼的 .txt 段;當前程序中調用另外一個應用程序 (調用 exec 之前,需要 fork );

exec() 函數族, 也不要判斷返回值:若是函數執行成功,不返回(.text 的代碼立即被全部替換,後面的代碼也不會執行);若是執行失敗,可以調用 perror(“xx 提示:”) 來打印錯誤信息,退出當前進程;


執行指定目錄下的程序 execl():

int execl(const char *path, const char *arg, ... /*, (char *)0 */);
  • path: 需要執行的程序的絕對路徑(推薦絕對路經)

  • 變參 arg: 要執行的程序的需要的參數

    • 第一個 arg: 佔位(內容隨便寫什麼,但不可爲空)
    • 後邊的 arg: 命令的參數
  • 參數寫完之後: NULL (作用是哨兵,表示該命令的參數已經輸入完畢)

  • 一般是執行自己寫的程序(也可以執行系統自帶的命令)

  • 寫一個例子驗證一下:代碼下載

    • 代碼如下:
    #include <stdio.h>
    #include <unistd.h>
    
    int main(int argc, char *argv[])
    {
        for (int i = 0; i < 3; i++)
            printf("-----i = %d\n", i);
    
        pid_t pid = fork();  //創建子進程
    
        if (pid == 0)
            execl("/bin/ls", "佔位參數", "-al", NULL);  //ls 程序使用子進程的地址空間
        
        for (int i = 0; i < 3; i++)         //這段打印只會(父進程)被執行一遍。
            printf("+++++i = %d\n", i);   
    
        return 0;
    }
    
    • 過程分析:

      第一段打印"-----i =" 內容,是隻會被父進程執行一遍的,子進程不會執行該段代碼(分析見上);結束時候打印 “+++++i =” 內容時候,是隻會執行一遍的,還是被父進程執行的,而子進程不執行 的原因是,當第 12 行的代碼execl(“ls”) 執行結束之後,其子進程的虛擬地址空間的以用戶區域的 .text 代碼段的二進制代碼被替換爲 /bin/ls 這個程序的代碼;其後面面也都是運行 ls 程序的代碼內容。

    • 運行結果:


執行 PATH 目錄下的程序 execlp():

int execlp(const char *file, const char *arg, ... /*, (char *)0 */);
  • file: 需要執行的程序的名字
  • 變參 arg: 要執行的程序的需要的參數
    • 第一個 arg: 佔位(內容隨便寫什麼,但不可爲空)
    • 後邊的 arg: 命令的參數
  • 參數寫完之後: NULL (作用是哨兵,表示該命令的參數已經輸入完畢)
  • 一般是執行系統自帶的命令(也就是 /bin 下的程序)
    • execlp 執行自定義程序的程序: file 參數絕對路徑

系列地址:

https://github.com/xmuli/linuxExample

歡迎 star 和 fork 這個系列的 linux 學習,附學習進階的路線圖。

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