詳解:進程控制那些事兒

一、進程創建

1. fork

  • 頭文件#include <unistd.h>
  • 函數原型pid_t fork(void);
  • 說明:通過複製調用進程創建新進程,調用進程稱爲父進程,創建出來的新進程稱爲子進程,父子進程共用同一個代碼段,但是它們的數據並不共用
  • 返回值對於父進程來說返回值是子進程的PID,對於子進程來說返回值是0,如果創建子進程失敗,則返回-1。我們可以通過返回值來判斷父子進程,從而進行代碼分流。

     當一個進程調用fork時,控制會轉移到內核中去,在內核中會有以下幾個過程:

(1)給子進程分配新的內存塊和內核數據結構
(2)將父進程部分數據結構內容拷貝至子進程
(3)將子進程添加到系統進程列表中去
(4)fork返回,調度器開始調度

     在fork返回後,會出現兩個代碼相同的進程,而且它們運行到相同的地方,此時,這兩個進程將各自開始執行。
實例:

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

int num = 5;	//全局變量num

int main()
{
    printf("Before fork:PID=%d\n", getpid());
    pid_t pid = fork();  //創建一個新進程
    if(pid < 0)
    {   
        printf("fork error\n");
        return -1; 
    }   
    else if(pid == 0) //子進程,返回值是0
    {   
        num = 10;	//子進程修改num值
        printf("This is a child process.  [PID=%d] [pid=%d]	num=%d\n", getpid(), pid, num);
    }   
    else //父進程,返回值是子進程的pid
    {   
        printf("This is a parent process. [PID=%d] [pid=%d]	num=%d\n", getpid(), pid, num);
    }
    while(1)
    {}
    return 0;
}

運行結果:
在這裏插入圖片描述
     通過上面的結果來看,我們可以發現fork之前,父進程獨立執行,而fork之後,父子進程分別執行,而誰先執行完全由調度器決定。此外,我們還發現當子進程修改num值後,父進程的值並不變,說明父子進程的數據並不共享。
     通常,父子進程用的是相同的物理空間,子進程的代碼段、數據段、堆棧段都是指向父進程的物理空間,也就是說,兩者的虛擬地址空間不同,但它們對應的物理空間是同一個。但是,當父子進程任意一方中有更改相應段的行爲發生時,則再爲子進程相應的段分配物理空間。這就是寫實拷貝技術。
     寫時拷貝是一種可以推遲甚至免除拷貝數據的技術。在子進程剛創建完後,內核此時並不複製整個父進程地址空間,而是讓父子進程共享同一個物理空間,只有在需要寫入的時候,數據纔會被複制,從而使各個進程擁有各自的數據空間。這種技術避免了拷貝大量根本就不會被使用的數據,極大地提高了進程快速執行的能力
     有時在實際創建進程中,我們也會fork失敗,這主要有以下兩個原因:

  • 當前的進程數已經達到了系統規定的上限,這時errno的值被設置爲EAGAIN
  • 系統內存不足,這時errno的值被設置爲ENOMEM

2. vfork

  • 頭文件
              #include <sys/types.h>
              #include <unistd.h>
  • 函數原型pid_t vfork(void);
  • 說明:該函數功能和fork一樣,但是兩者還是有區別的
    (1)vfork用於創建一個子進程,但內核並不會像fork那樣給子進程創建獨立虛擬地址空間,而是直接共享父進程的虛擬空間,也就是說,父子進程代碼共享,數據也共享
    (2)vfork保證子進程先運行,在子進程調用 execexit 之後父進程纔可能被調度運行

實例:

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

int num = 5;	//全局變量num

int main()
{
    pid_t pid = vfork();
    if(pid < 0)
    {   
        printf("vfork error\n");
        return -1; 
    }   
    else if(pid == 0)
    {   
        num = 10;	//在子進程中修改num值 
        printf("child num:%d\n", num);
        exit(0);
    }   
    else
    {   
        printf("parent num:%d\n", num);
    }
    while(1)
    {
        sleep(1);
    }
    return 0;
}

運行結果:
在這裏插入圖片描述
     通過上面的結果來看,我們發現,子進程是先於父進程運行的,而且當子進程修改num值後,父進程的num值也跟着變,說明父子進程的數據是共享的。

二、進程終止

1. 進程退出的場景

(1)正常退出

          正確退出:代碼運行完畢,結果正確
          錯誤退出:代碼運行完畢,結果錯誤

(2)異常退出

          代碼異常終止

2. 進程退出的方式

(1)_exit函數

  • 頭文件#include <unistd.h>
  • 函數原型void _exit(int status);
  • 說明立即終止調用進程,屬於該進程的任何打開的文件描述符都被關閉
  • 參數status 作爲進程的退出狀態返回給父進程,父進程可以通過 wait 來獲取該值。不過雖然 statusint型,但是僅有低8位可以被父進程所用

實例:

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

int main()
{
    printf("hello world!");
    _exit(-1);
}

運行結果:
在這裏插入圖片描述
在這裏插入圖片描述
     通過上面的結果來看,我們發現 _exit()什麼都沒有做,而是立即終止程序,並且通過echo $?來查看進程退出碼時,顯示結果並不是-1,而是255,這是因爲父進程只能使用退出碼-1的低8位所導致的。

(2)exit函數

  • 函數原型void exit(int status);
  • 說明exit 最後也是會調用的 _exit 的,不過它在調用_exit之前,還做了其他一些工作:
    (1)執行用戶通過atexit或on_exit定義的清理函數
    (2)關閉所有打開的流,所有的緩存數據均被寫入
    (3)調用_exit

實例:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("hello world!");
    exit(-1);
}

運行結果:
在這裏插入圖片描述
     在這裏,我們可以用下面這幅圖去更加容易地理解 _exitexit 的關係:
在這裏插入圖片描述

(3)return

     return是一種更爲常見的退出進程的方法,但是它只有在main中執行纔會退出進程,並且在main中執行 return n 等同於執行 exit(n)

三、進程等待

1. 進程爲什麼需要等待

     從我們之前接觸的殭屍進程來看,當一個子進程先於父進程退出時,如果父進程沒有關心這個子進程的退出狀態,那麼就有可能形成殭屍進程,進而出現內存泄露的問題。所以爲了避免這種問題的出現,我們需要在子進程退出後,讓父進程通過進程等待的方式,去獲取子進程的退出狀態,接着回收子進程的資源。

2. 進程等待的方法

(1)wait

  • 頭文件
              #include <sys/types.h>
              #include <sys/wait.h>
  • 函數原型pid_t wait(int *status);
  • 參數:輸出型參數,獲取終止子進程的退出狀態,若不關心可設置爲NULL
  • 返回值:成功時,返回終止子進程的PID;出錯時,返回-1
  • 功能等待任意一個子進程退出,若沒有子進程退出,則一直阻塞等待

(2)waitpid

  • 頭文件
              #include <sys/types.h>
              #include <sys/wait.h>
  • 函數原型pid_t waitpid(pid_t pid, int *status, int options);
  • 參數
    (1)pid
                   pid=-1時,等待任意一個子進程退出,與 wait 效果一樣
                   pid>0時,等待一個進程ID與pid相等的子進程退出
    (2)status
                   WIFEXITED(status):若子進程正常終止,則返回true
                   WEXITSTATUS(status):若WIFEXITED爲true,則返回子進程的退出狀態
    (3)options
                   WNOHANG:若指定的子進程沒有結束,則立即返回0,不予以等待;若正常結束,則返回該子進程的PID
  • 返回值:當終止子進程正常結束的時候,返回該子進程的PID;如果options被設置爲 WNOHANG ,並且此時沒有子進程退出時,則返回0;若調用中出錯,則返回 -1,這時 errno 會被設置爲相應的值以指示錯誤所在。
  • 功能可以等待指定的子進程退出,也可以等待任意一個子進程退出

說明

  •      如果終止子進程已經退出,此時調用 wait 或 waitpid 會立即返回,獲得子進程退出信息,並且釋放資源;
  •      如果在任意時刻調用 wait 或 waitpid 時,子進程存在且正常運行,則父進程可能會處於阻塞等待狀態;
  •      如果不存在該子進程,則立即出錯返回。

3. 獲取進程退出狀態碼

     通過上面的介紹後,我們知道,如果父進程不關心子進程的退出狀態信息,那麼可以將 status 設置爲NULL,否則,操作系統會根據 status ,將子進程的退出信息反饋給父進程。
     在這個 status 參數中存儲了子進程的退出原因以及退出碼,而參數中只用了低16位(兩個字節)來存儲這些信息,我們可以把它當做一個位圖來看,如下:
在這裏插入圖片描述

  • 正常退出時高8位存儲的是退出碼,只有子進程運行完畢退出時纔會有,低8位爲0
  • 異常退出時低7位存儲的是導致子進程異常退出的信號值,第8位存儲core dump標誌,只有子進程異常退出時纔會有,高8位爲0

4. 實例

(1)wait

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    pid_t pid = fork(); // 創建進程
    if(pid < 0)
    {   
        printf("fork error!\n");
        return -1; 
    }   
    else if(pid == 0)	// 子進程
    {   
        sleep(10);
        exit(10);
    }   
    else // 父進程
    {   
        int status;
        pid_t ret = wait(&status); // 進程等待
        if(ret>0 && (status & 0x7F)==0) // 正常退出
        {
            printf("exit code:%d\n", (status>>8)&0xFF); // 打印退出狀態碼
        }
        else if(ret > 0) // 異常退出
        {
            printf("signal value:%d\n", status&0x7F); // 打印終止信號值
        }
    }
    
    return 0;
}

運行結果:

  • 等待10s,子進程正常退出:
    在這裏插入圖片描述
  • 在其他終端 kill -9 掉這個子進程,子進程異常退出:
    在這裏插入圖片描述

(2)waitpid

  • 阻塞式等待:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    pid_t pid = fork();
    if(pid < 0)
    {   
        printf("fork error!\n");
        return-1;
    }   
    else if(pid == 0)
    {   
        sleep(10);
        exit(10);
    }   
    else
    {   
        int status;
        pid_t ret = waitpid(-1, &status, 0); // 阻塞式等待,等待10s
        if(ret>0 && WIFEXITED(status)) // 等待成功,子進程正常退出
        {
            printf("exit code:%d\n", WEXITSTATUS(status)); // 打印退出狀態碼
        }
        else // 等待失敗,子進程異常退出
        {
            printf("Waiting for child process to exit failed!\n");
        }
    }
    
    return 0;
}

運行結果:

  • 等待10s,子進程正常退出:
    在這裏插入圖片描述
  • 在其他終端 kill -9 掉這個子進程,子進程異常退出:
    在這裏插入圖片描述
  • 非阻塞式等待:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    pid_t pid = fork();
    if(pid < 0)
    {   
        printf("fork error!\n");
        return-1;
    }   
    else if(pid == 0)
    {   
        sleep(10);
        exit(10);
    }   
    else
    {   
        int status;
        pid_t ret = 0;
        do  
        {
             ret = waitpid(-1, &status, WNOHANG); // 非阻塞式等待,若此時沒有子進程退出,則立即返回0
             printf("The child process is running!\n");
             sleep(1);
        }while(ret == 0);
        if(ret>0 && WIFEXITED(status)) // 等待成功,子進程正常退出
        {
            printf("exit code:%d\n", WEXITSTATUS(status)); // 打印退出狀態碼
        }
        else // 等待失敗,子進程異常退出
        {
            printf("Waiting for child process to exit failed!\n");
        }
    }
    
    return 0;
}

運行結果:

  • 等待10s,子進程正常退出:
    在這裏插入圖片描述
  • 在其他終端 kill -9 掉這個子進程,子進程異常退出:
    在這裏插入圖片描述

四、進程程序替換

1. 替換原理

     大多數時候,我們創建一個進程並不希望子進程跟父進程做相同的事情,而是希望能夠做另一件事,這時候就用到了程序替換,子進程會調用一種 exec 函數以執行另一個程序。要注意的是,調用exec函數並不會創建新進程,所以調用exec函數前後該進程的PID並不會改變
     當進程調用exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,因爲只是替換了內容,所以並不會重新創建虛擬地址空間和頁表,替換後這個進程將從新程序的入口函數開始運行。如果替換成功,則表示該進程運行的代碼段已經不是以前的代碼段了,而是新程序,因此原來代碼exec函數以後的代碼都不會運行,除非替換出錯。

2. 替換函數

(1)exec函數族

  • 頭文件:#include <unistd.h>
  • 函數原型:
    (1)int execl(const char *path, const char *arg, ...);
    (2)int execlp(const char *file, const char *arg, ...);
    (3)int execle(const char *path, const char *arg, ..., char * const envp[]);
    (4)int execv(const char *path, char *const argv[]);
    (5)int execvp(const char *file, char *const argv[]);
    (6)int execve(const char *filename, char *const argv[], char *const envp[]);
  • 返回值:如果調用成功,則加載新的程序從啓動代碼開始執行,不在返回; 如果調用出錯,則返回-1。所以可以說:exec函數只有出錯的返回值,而沒有成功的返回值。

(2)命名理解

  • l(list):參數採用列表格式
  • v(vector):參數採用數組格式
  • p(path):自動搜索環境變量PATH
  • e(env):自己維護環境變量
函數名 參數格式 是否帶路徑 是否使用當前環境變量
execl 列表
execlp 列表 否,自動在PATH中尋找
execle 列表 否,需要自己組裝環境變量
execv 數組
execvp 數組 否,自動在PATH中尋找
execve 數組 否,需要自己組裝環境變量

(3)實例:

  • execl

原型int execl(const char *path, const char *arg, ...);

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

int main()
{
    execl("/bin/ls", "ls", "-l", NULL);
    // 帶全路徑
    // 參數是以列表格式給出
    // 不定參數要以NULL結尾
    return 0;
}

執行結果:
在這裏插入圖片描述

  • execlp

原型int execlp(const char *file, const char *arg, ...);

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

int main()
{
    execlp("ls", "ls", "-l", NULL);
    // 不需要帶上路徑,只需要告訴文件名即可,會自動到環境變量PATH中的路徑下尋找
    // 參數是以列表格式給出
    return 0;
}
  • execle

原型int execle(const char *path, const char *arg, ..., char * const envp[]);

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

int main()
{
    char * const envp[] = {"MYENV=666", NULL};
    execle("/bin/ls", "ls", "-l", NULL, envp);
    // 帶全路徑
    // 參數是以列表格式給出
    // 需要自己組裝環境變量
    return 0;
}
  • execv

原型int execv(const char *path, char *const argv[]);

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

int main()
{
    char * const argv[] = {"ls", "-l", NULL};
    execv("/bin/ls", argv);
    // 帶全路徑
    // 參數是以數組格式給出
    return 0;
}
  • execvp

原型int execvp(const char *file, char *const argv[]);

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

int main()
{
    char * const argv[] = {"ls", "-l", NULL};
    execvp("ls", argv);
    // 不需要帶上路徑,只需要告訴文件名即可,會自動到環境變量PATH中的路徑下尋找
    // 參數是以數組格式給出
    return 0;
}
  • execve

原型int execve(const char *filename, char *const argv[], char *const envp[]);

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

int main()
{
    char * const argv[] = {"ls", "-l", NULL};
    char * const envp[] = {"MYENV=123", NULL};
    execve("/bin/ls", argv, envp);
    // 帶全路徑
    // 參數是以數組格式給出
    // 需要自己組裝環境變量
    return 0;
}

     事實上,只有execve是真正的系統調用,其它五個函數最終都會調用execve(execve在man手冊第2節,其它五個函數在第3節)。
在這裏插入圖片描述

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