進程的基本控制

前言:整個操作系統都在圍繞進程這一概念具體展開,所以對於進程的控制就顯得十分重要,這篇文章主要講述以下幾點:

1. 進程創建
2. 進程退出
3. 進程等待
4. 進程程序替換

進程創建

在操作系統中,對於父子進程的概念非常重要,必要linux自帶的bash,對於你在命令行輸入的一些指令,它是不會自己去處理你這些請求的,而是通過創建子進程去處理,它只需要知道子進程返回的消息就好了。

爲什麼要基於這樣的父子進程關係呢?試想一下,我們和操作系統打交道是通過shell內建的bash,如果用戶的什麼請求都經由bash去親自執行,那麼一個bash也不夠用啊,其次,如果一旦請求中出了問題,那麼bash掛掉的話,誰來幫我們向操作系統傳達我們的請求呢?

基於上面提出的種種問題,就引出進程創建子進程的必要性了。

進程的創建方式:

  1. pid_t fork(void);
  2. pid_t vfork(void);

認識fork函數

pid_t fork(void)
返回值:pid_t其實就是一個整型,typedef成pid_t只是爲了一眼看上去知道這是一個進程號
子進程中返回值爲0.
父進程中返回操作系統給子進程分配的pid號。
fork失敗返回-1

進程調用fork,當控制權限轉移到內核中的fork代碼後:

  • 分配新的內存塊和數據結構給子進程
  • 將父進程的大多數數據結構拷貝至子進程
  • 添加子進程到系統進程列表
  • fork返回,操作系統進行進程調度

當一個進程fork出一個子進程後,就有兩個二進制代碼相同的進程,並且運行到相同的地方,但每個進程都將開始執行自己的代碼。

如下:

int main()
{
    printf("Before fork: pid is:%d\n",getpid())//getpid函數爲獲取進程pid
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork error");
        exit(1);
    }
    printf("After fork:pid is %d\n",getpid());

    return 0;
}

這裏寫圖片描述

這裏需要注意的是,先執行子進程還是父進程完全取決於操作系統的進程調度器決定。

fork失敗的原因:

  • 系統中的進程數達到了上限
  • 系統的內存不足
  • 系統不支持,如Windows不支持fork

認識vfork函數

對於vfork來說,其他的都是fork函數用法一樣,只要記住最重要的兩個特性就好。

  1. 子進程一定先於父進程執行。
  2. 子進程調用exec或者exit之後父進程才能執行

進程終止

進程退出的場景:

  1. 代碼運行完,結果正確
  2. 代碼運行完,結果不正確
  3. 代碼異常終止

常見進程退出:

正常終止:
1. main函數返回
2. 調用exit函數
3. 調用_exit函數

exit函數和_exit函數的區別:

  1. exit會進行清理工作,如刷新緩衝區等,而exit直接退出程序
  2. _exit是系統調用,exit最終也會調用_exit。
    異常終止
    CTRL+C/kill -9

具體的程序退出部分,可見博客尾部的鏈接。

進程等待

進程等待是非常重要的,如果父進程對子進程不管不顧的話,那麼可能會產生殭屍進程,從而造成內存泄漏。
並且作爲父進程,創建子進程是讓它完成一些任務的,總要知道它返回的結果,完成的怎麼樣。

wait函數

pid_t wait(int *status)//阻塞式等待
返回值:成功返回等待進程的pid,失敗返回-1
參數:輸出型參數,獲取子進程的退出狀態,不關心可以爲NULL,該參數由操作系統初始化

status
這裏寫圖片描述

所以查看的話,先查看低七位是否爲0 ,如果是0代表程序正常退出,可以查看高八位具體的退出碼。
如果低七爲不爲0,則代表信號終止,高八位就沒有意義了,可以查看低七位的具體信號。
core dump是指進程終止時所記錄的現場。

以下面的代碼爲實例:

#include<stdio.h>
#include<wait.h>

int main()
{
    pid_t id = fork();
    if(id > 0)
    {
        //father
        int status = 0;
        int ret  = wait(&status);
        if(ret > 0 && (status&0x7f) == 0)
        {
            //success
            printf("child exit code:%d\n",(status>>8));
        }
        else
        {
            //signal exit
            printf("signal code:%d\n",(status>>8)&0xff);
        }
    }
    else if(id == 0)
    {
        //child
        sleep(3);
        exit(5);//子進程的退出碼
    }
    else
    {
        perror("fork");
    }
    return 0;
}

當子進程正常退出時,會返回退出碼。
運行結果:
這裏寫圖片描述

接下來,我們直接kill -9掉該進程,結果應該返回9號信號,看如下運行結果:
這裏寫圖片描述
我的Ubuntu是最新的,本應該返回9,但是操作系統將9這個數字,對應成第九個信號的名稱顯示出來。

下面是Linux下的信號:
這裏寫圖片描述

waitpid函數

pid_t waitpid(pid_t pid,int *status,int option)//如果最後一個參數設置了WNOHANG就是非阻塞式等待

返回值:

  1. 正常返回收集子進程的進程ID
  2. 如果設置了選項WNOHANG,而調用waitpid發現沒有已退出的子進程可以回收,則返回0,就是輪詢等待的意思
  3. 如果調用中出錯,返回-1,errno會被設置成相應的值以指示錯誤所在。

參數:
pid:

  • pid = -1,等待任意一個子進程,與wait等效
  • pid > 0,等待其進程ID與pid相等的進程

status:

  • WIFEXITED(status):若爲正常終止子進程返回的狀態,則爲真(相當於上面的檢查低7位爲是否爲0)
  • WEXITSTATUS(status):若WIFIXITED非零,提取子進程退出碼(相當於上面的查看高8位的退出碼)

option:
WNOHANG:若pid指定的子進程沒有結束,則waitpid()函數返回0,不予以等待,若正常結束,則返回該進程的ID

需要注意的是:

  • 如果子進程已經結束,調用wait/waitpid時,函數會直接返回,並且釋放資源,獲得子進程退出信息。(對應的場景就是子進程退出時,父進程在沉睡,如果這時父進程不予以處理子進程則會產生殭屍進程)
  • 如果在任意時刻調用wait/waitpid,子進程存在且正常運行,則進程可能阻塞
  • 如果不存在該子進程,則立即出錯返回

    如下代碼示例:

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


int main()
{
    pid_t id = fork();
    if(id > 0)
    {
        //father
        int status = 0;
        pid_t ret = 0;
        do
        {
            ret = waitpid(-1,&status,WNOHANG);//no-blocking
            if(ret == 0)
            {
                printf("child is running\n");
            }
            sleep(1);
        }
        while(ret == 0);

        if(WIFEXITED(status) && ret == id)
        {
            printf("wait child 3s success,child return code is:%d\n",WEXITSTATUS(status));
        }
        else
        {
            printf("wait child failed,return\n");
            return 1;
        }

    }
    else if(id == 0)
    {
        //child
        printf("child is run ,pid is:%d\n",getpid());
        sleep(3);
        exit(1);
    }
    else
    {
        printf("%s fork error\n",__FUNCTION__);
        return 1;
    }
    return 0;
}

進程程序替換

替換原理

用fork創建子進程後執行的是和父進程相同的程序(但有可能執行不同的代碼分支),子進程一般要調用一種exec函數以執行另一個程序。當進程調用exec函數族時,該進程的用戶空間代碼和數據完全被新的程序替換,從新程序的啓動例程開始執行,調用exec並不創建新進程,所以調用exec前後該進程的pid並未改變。

exec函數族

#include<unistd.h>

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[]);
int execve(const char *path,char *const argv[],char *const envp[]);

函數解釋

  • l(list):表示參數採用列表
  • v(vector):表示參數採用數組
  • p(path):帶p的函數會自動搜索環境變量PATH
  • e(env):表示自己維護環境變量

需要特別注意的時,

  • 如果exec函數族調用成功,則從新程序的啓動代碼開始,所以沒有返回值
  • 如果調用失敗,則返回-1

代碼示例:

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

int main()
{
    const *const argv[] = {"ls","-al",NULL};
    char *const envp[] = {"PATH=/bin:/usr/bin",NULL};//環境變量
execl("/bin/ls","ls","-al".NULL);

//帶p的函數,不必再給出全路徑
execlp("ls","ls","-al",NULL);

//帶e的,需要自己配置環境變量
execle("ls","ls","-al".NULL,envp);

execv("/bin/ls",argv);

//帶p的,不需要給出全路徑
execvp("ls",argv);

//帶e的,需要自己配置環境變量
execve("/bin/ls",argv,envp);

exit(0);
}

這裏寫圖片描述

雖然exec函數族有六個函數,但是隻有execve函數是系統調用,其他幾個函數最終都會調用execve函數。

基於程序替換,可以實現一個簡單的shell。


exit函數詳解http://blog.csdn.net/qq_36528114/article/details/71321390
實現一個簡單的shell:http://blog.csdn.net/qq_36528114/article/details/72582588

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