多進程編程就是孫悟空拔猴毛--變猢猻

Our bravest and best lessons are not learned through success, but through misadventure.--------我們最好的教訓不是透過成功而學到,而是透過不幸的遭遇。


小黑喜歡把多進程編程看成孫悟空拔毛,拔一根毛就是fork()一下,就會將自己的本領複製到新的孫悟空上,但是又有獨特的地方,然後新的分身就會去幹其他事情。

1, 進程回顧

程序與進程的區別: 正在正常運行的程序及其所佔用的系統資源(CPU時間片、內存等)就叫做進程,一個進程可以執行多個程序。
例如你平常打開的QQ就是一個進程。

想要進一步瞭解進程的同學可以移步

https://blog.csdn.net/weixin_46027505/article/details/104812719

  • 在Linux系統下可以使用

ps aux 命令用來查看所有進程ID
Linux停止一個進程的運行命令: kill [進程ID] 或 killall [進程名]
Linux停止一個進程運行的函數: int kill(pid_t pid, int sig);

獲取進程ID的函數:

pid_t getpid(void);     //獲取自己進程ID
pid_t getppid(void);   //獲取父進程ID     

2, 進程內存空間回顧

這部分小黑另外寫了一篇博客,生疏的同學可以移步:
https://blog.csdn.net/weixin_46027505/article/details/105076010

還有關於殭屍進程的博客:

https://blog.csdn.net/weixin_46027505/article/details/105097361

3, 系統調用fork()

Linux下有兩個基本的系統調用可以用於創建子進程:fork()和vfork()。fork在英文中是"分叉"的意思。爲什麼取這個名字呢? 因爲一個進程在運行中,如果使用了fork,就產生了另一個進程,於是進程就”分叉”了,所以這個名字取得很形象。

  • fork之後,操作系統會複製一個與父進程完全相同的子進程,雖說是父子關係,但是在操作系統看來,他們更像兄弟關係,這 2個進程共享代碼空間,但是數據空間是互相獨立的,子進程數據空間中的內容是父進程的完整拷貝,指令指針也完全相同,子 進程擁有父進程當前運行到的位置(兩進程的程序計數器pc值相同,也就是說,子進程是從fork返回處開始執行的)。
  • 可以這樣想象,兩個進程一直同時運行,而且步調一致,在fork之後,他們分別作不同的工作,也就是分岔了。至於哪一個最先運行,這個與操作系統進程調度算法有關,而且這個問題在實際應用中並不重要,如果需 要父子進程協同,可以通過原語的辦法解決。
#include  <sys/types.h>
#include  <unistd.h>
pid_t         fork(void);

由fork()創建的新進程被稱爲子進程。fork()函數被調用一次,但有兩次返回。
返回值=0: 子進程在運行,fork()返回0
大於0:父進程在運行,fork返回值爲子進程的進程ID。
小於0:出錯

  • 說明
  • 我們在調用fork()後,需要通 過其返回值來判斷當前的代碼是在父進程還是子進程運行,
  • fork 函數調用失敗的原因主要有兩個:
  1. 系統中已經有太多的進程;
  2. 該實際用戶 ID 的進程總數超過了系統限制。
  • 將子進程id返回給父進程的理由是:因爲一個進程的子進程可以多於一個,沒有一個函數使一個進程可以獲得其所有子進程的進程id。
  • 對子進程來說, 之所以fork返回0給它,是因爲它隨時可以調用getpid()來獲取自己的pid;也可以調用getppid()來獲取父進程的id。
  • 一般我們fork()後就會在子進程中exec()去執行其他的事情。
  • 子進程是父進程的副本,它將得到父進程的文本段、數據段、堆和棧副本,這樣父子進程都將繼續執行fork()之後的代碼。但父子進程並不共享這些存儲空間,父子進程只是共享文本段。
  • 新創建的父子進程誰先執行沒有規定,由系統調用決定

下面我們給出示例代碼,進一步理解fork()

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
int main(int argc, char **argv)
{

    pid_t          pid;
    int            a=10;

    printf("before fork\n");
    printf("Parent process PID[%d] start running...\n", getpid() );
    pid = fork();
    if(pid < 0)
    {
        printf("fork() create child process failure: %s\n", strerror(errno));
        return -1;
    }
    
    //下面是子進程空間
    else if( pid == 0 )
    {

        a+=5;
        printf("Child process PID[%d] start running, my parent PID is [%d] \n", getpid(), getppid());
        printf("son a=%d\n",a);
    }
//下面是父進程空間
    else if( pid > 0 )
    {
       sleep(5);  //父進程阻塞在這裏,讓子進程先跑
        a+=10;
        printf("Parent process PID[%d] continue running, and child process PID is [%d]\n", getpid(), pid);
        printf("parent a=%d\n",a);

    }
    printf("after fork....\n");

    return 0;
}

在這裏插入圖片描述

我們觀察運行結果父進程和子進程都是將a在原先的全局變量的值上進行自己的操作,說明a = 10在父子進程間有自己的獨立的空間,互不干擾,
還有我們發現after fork 打印了兩次,是因爲子進程退出運行了一次,父進程退出也運行了。
在這裏插入圖片描述

4, 系統調用vfork()

在實現寫時複製(這個之後會具體介紹)之前,Unix的設計者們就一直很關注在fork後立刻執行exec所造成的地址空間的浪費。
這樣前輩大佬們就想出了vfork()

  • vfork()的函數原型和 fork原型一樣:
#include <unistd.h>
#include <sys/types.h>
pid_t fork(void); 
pid_t vfork(void);

除了子進程必須要立刻執行一次對exec的系統調用,或者調用_exit( )退出,對vfork( )的成功調用所產生的結果和fork( )是一樣的。

5, fork和vfork的區別

fork() vfork()
子進程拷貝父進程的數據段和代碼段 子進程與父進程共享數據段,當需要改變共享數據段中變量的值,則拷貝父進程。
父子進程的執行次序不確定 保證子進程先運行,在調用exec或exit之前與父進程數據是共享的,在它調用exec或exit之後父進程纔可能被調度運行。
父子進程相互獨立 如果在調用這兩個函數之前子進程依賴於父進程的進一步動作,則會導致死鎖。

爲什麼會有vfork,因爲以前的fork很傻, 它創建一個子進程時,將會創建一個新的地址
空間,並且拷貝父進程的資源,而往往在子進程中會執行exec 調用,這樣,前面的拷貝工
作就是白費力氣了,
這種情況下,聰明的人就想出了vfork,它產生的子進程剛開始暫時與
父進程共享地址空間(其實就是線程的概念了),因爲這時候子進程在父進程的地址空間中
運行,所以子進程不能進行寫操作,並且在兒子“霸佔”着老子的房子時候,要委屈老子一
下了,讓他在外面歇着(阻塞),一旦兒子執行了exec 或者exit 後,相於兒子買了自己的
房子了,這時候就相於分家了。
//上面這段是從網上看到的,覺得不錯,具體從哪來的忘了,就無法附上鍊接,如果原作者看到的話,望諒解,可以留言,我後面附上鍊接。

vfork( )是一個歷史遺留產物,Linux本不應該實現它。需要注意的是,即使增加了寫時複製,vfork( )也要比fork( )快,因爲它沒有進行頁表項的複製。然而,寫時複製的出現減少了對於替換fork( )爭論。實際上,直到2.2.0內核,vfork( )只是一個封裝過的fork( )。因爲對vfork( )的需求要小於fork( ),所以vfork( )的這種實現方式是可行的。

6, 寫時複製

Linux採用了寫時複製的方法,以減少fork時對父進程空間進程整體複製帶來的開銷。
fork()之後常會緊跟着調用exec來執行另外一個程序,而exec會拋棄父進程的文本段、數據 段和堆棧等並加載另外一個程序,所以現在的很多fork()實現並不執行一個父進程數據段、堆和棧的完全副本拷貝。

寫時複製是一種採取了惰性優化方法來避免複製時的系統開銷。

它的前提很簡單:如果有多個進程要讀取它們自己的那部門資源的副本,那麼複製是不必要的。每個進程只要保存一個指向這個資源的指針就可以了。只要沒有進程要去修改自己的“副本”,就存在着這樣的幻覺:每個進程好像獨佔那個資源。從而就避免了複製帶來的負擔。如果一個進程要修改自己的那份資源“副本”,那麼就會複製那份資源,並把複製的那份提供給進程。不過其中的複製對進程來說是透明的。這個進程就可以修改複製後的資源了,同時其他的進程仍然共享那份沒有修改過的資源。
所以這就是名稱的由來:在寫入時進行復制。

6.1 寫時複製的好處

寫時複製的主要好處在於:如果進程從來就不需要修改資源,則不需要進行復制。
惰性算法的好處就在於它們儘量推遲代價高昂的操作,直到必要的時刻纔會去執行。

6.2 寫時複製擴展知識

1, 在使用虛擬內存的情況下,寫時複製(Copy-On-Write)是以頁爲基礎進行的。所以,只要進程不修改它全部的地址空間,那麼就不必複製整個地址空間。在fork( )調用結束後,父進程和子進程都相信它們有一個自己的地址空間,但實際上它們共享父進程的原始頁,接下來這些頁又可以被其他的父進程或子進程共享。

2, 寫時複製在內核中的實現非常簡單。與內核頁相關的數據結構可以被標記爲只讀和寫時複製。如果有進程試圖修改一個頁,就會產生一個缺頁中斷。內核處理缺頁中斷的方式就是對該頁進行一次透明覆制。這時會清除頁面的COW屬性,表示着它不再被共享。

3, 現代的計算機系統結構中都在內存管理單元(MMU)提供了硬件級別的寫時複製支持,所以實現是很容易的。在調用fork( )時,寫時複製是有很大優勢的。因爲大量的fork之後都會跟着執行exec,那麼複製整個父進程地址空間中的內容到子進程的地址空間完全是在浪費時間:如果子進程立刻執行一個新的二進制可執行文件的映像,它先前的地址空間就會被交換出去。寫時複製可以對這種情況進行優化。

7,子進程繼承了父進程什麼東西

由子進程自父進程繼承到:
進程的資格
環境(environment)變量 堆棧
內存
打開文件的描述符
等。。。
子進程所獨有:
進程號
不同的父進程號
等。。。
這部分可以自己查閱

8, 其他相關API函數

exec*()

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

上面7種不同的函數都稱爲exec函數,再fork()之後再調用這些函數中的任何一個都會讓新創建的進程執行另外一個程序。

  • 其中execl()參數比較簡單,所以用得較多
    用法:
    execl(“程序所在路徑”, “命令”, “命令的參數”, NULL);

wait()和waitpid()

wait和waitpid函數是用來處理僵死進程的。
徐小黑在這之前寫了一篇相關的博客:
https://blog.csdn.net/weixin_46027505/article/details/105097361

system()(慎用)和popen()

如果我們在程序中,想執行另外一個Linux命令時,可以調用fork()然後再exec執行相應的命令即可,但這樣相對比較麻煩。 Linux系統提供了一個system()庫函數,該庫函數可以快速創建一個進程來執行相應的命令。

int system(const char *command);
  • 例如
    system("ping -c 4 -I eth0 4.2.2.2");
    //執行ping命令

這兩個函數同學們可以另外搜索相關博客學習

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