從零開始UNIX環境高級編程(8):進程控制

1. 進程標識

1.1 進程ID

由于每個進程ID都是唯一的,Unix使用進程ID作為進程的標識。使用ps命令可以查看進程的ID。

zhanghuamaodeMacBook-Pro:~ zhanghuamao$ ps 
  PID TTY           TIME CMD
  956 ttys000    0:00.01 -bash

系統中還會有一些專用進程ID:

  • ID為0:調度進程(也稱交換進程),是內核的一部分
  • ID為1: init進程,在自舉過程結束時由內核調用
  • ID為2:頁守護進程,負責支持虛擬儲存器系統的分頁操作

1.2 進程描述符

我們在編寫進程相關的程序時,使用一個getpid()就可以獲得當前運行進程的PID,那Linux內核是如何獲取到這個信息的呢?為了對進程標識有更加深入的理解,接下來我們從內核的角度,來看下Linux的進程描述符。

1.2.1 task_struct

為了管理進程,Linux內核使用進程描述符對每個進程所做事情進行記錄,進程描述符對應的數據類型是task_struc結構體,它包含了與一個進程相關的所有信息,例如,進程的優先級、分配的地址空間和允許它訪問的文件等, task_struct結構體定義在include/linux/sched.h中。由于進程描述符中存放了很多信息(從Line1511Line 2009),它的結構也是很復雜的,如下圖所示:

Linux進程描述符 - 圖片來自深入理解Linux內核

進程ID也是存放在task_struct中,新創建的進程ID是前一個進程的PID加1。

pid

PID的值也有一個上限,當內核使用的PID達到上限時,就必須開始循環使用已閑置的最小PID號。

int pid_max = PID_MAX_DEFAULT;
int last_pid;
#define RESERVED_PIDS       300
int pid_max_min = RESERVED_PIDS + 1;
int pid_max_max = PID_MAX_LIMIT;

... ...

int alloc_pidmap(void)
{
    int i, offset, max_scan, pid, last = last_pid;
    pidmap_t *map;
    pid = last + 1;
    if (pid >= pid_max)
        pid = RESERVED_PIDS;
    offset = pid & BITS_PER_PAGE_MASK;
    map = &pidmap_array[pid/BITS_PER_PAGE];
    ... ...
}

1.2.2 獲取當前運行進程的進程描述符

進程在內核的內存區中的儲存內容為:與進程描述符task_struct相關的小數據結構tread_info內核態的進程堆棧

task_struct

內核通過esp寄存器的值可以計算出當前在CPU上正在運行進程的thread_info的地址,這項工作由current_thread_info()函數完成。

tread_info和進程描述符

由于thread_info結構體中task又指向進程描述符task_struct,因此,通過current_thread_info()->task可以獲取當前運行進程的進程描述符,而current_thread_info()->task通常被定義為current宏,只要拿到了進程描述符,和進程相關的信息就都能夠獲取了。

thread_info

#ifndef __ASM_GENERIC_CURRENT_H
#define __ASM_GENERIC_CURRENT_H

include <linux/thread_info.h>

#define get_current() (current_thread_info()->task)
#define current get_current()

#endif /* __ASM_GENERIC_CURRENT_H */

2. 創建進程

2.1 創建進程的場景

有4種主要事件導致進程的創建

  • 系統初始化時

系統初始時會創建若干進程,其中有些是前臺進程,為用戶提供UI界面。其他的是后臺進程,如接收電子郵件的進程,大部分時間都在休眠,當有新郵件到達時就突然被喚醒了,這種停留在后臺處理的進程又被稱為守護進程

  • 正在運行的進程調用了創建進程函數

一個正在運行的進程可以通過系統調用來創建新的進程

  • 用戶請求創建一個新的進程

用戶雙擊一個圖標就可以啟動一個程序,會開始一個新的進程

  • 一個批處理作業的初始化

在大型機的批處理系統中,用戶提交批處理作業時,在操作系統認為有資源可以運行另外一個作業時,就會創建一個新的進程

2.2 創建進程的函數

2.2.1 fork函數

使用fork可以創建一個新的進程。fork函數被調用一次,會返回兩次,一次是從子進程中返回,返回值為0,另外一次從父進程中返回,返回值為子進程的PID。子進程是父進程的一個副本,子進程獲得父進程的數據空間、棧和堆的副本,但是它們并不數據共享,子進程和父進程只共享代碼段。

 #include <unistd.h>

 pid_t
 fork(void);
  • 示例代碼

調用fork創建一個進程,父進程中有兩個變量globvar和var,在子進程中對這兩個變量的值加1,然后分別在父進程和子進程中打印出兩個變量的值和地址。

由于fork之后是父進程先執行還是子進程先執行是不確定的,取決于內核的調度,因此,在父進程中調用sleep休眠1秒鐘,保證能讓子進程先執行。

#include "../inc/apue.h"

int globvar = 6;

int main(int argc, char const *argv[])
{
    int var;
    pid_t pid;

    var = 88;

    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) {
        globvar++;
        var++;
    } else {
        sleep(1);
    }

    printf("pid = %ld, globvar = %d - address = %x, var = %d - address = %x\n",
           (long)getpid(), globvar, &globvar, var, &var);

    return 0;
}
  • 運行結果

從運行結果可以看出,兩個變量在父進程和子進程的地址相同,而值卻不同,驗證了子進程只是父進程的副本,它們并不數據共享。

zhanghuamaodeMacBook-Pro:Chapter8 zhanghuamao$ ./fork_test 
pid = 692, globvar = 7 - address = 7e0d0a0, var = 89 - address = 57df4b5c
pid = 691, globvar = 6 - address = 7e0d0a0, var = 88 - address = 57df4b5c

2.2.2 vfork函數

使用vfork函數也可以創建一個進程,它的返回值與fork相同。vfork和fork的區別有2點:

  • vfork保證子進程先運行,父進程會被掛起,直到子進程調用了exec或exit后,父進程才能運行。

  • 使用vfork創建的子進程,并不復制父進程的地址空間,子進程之間在父進程的地址空間中運行。

 #include <unistd.h>

 pid_t
 vfork(void);
  • 示例代碼

和fork的示例一樣,只是去掉父進程中的sleep函數,在子進程中調用sleep休眠1秒。

#include "../inc/apue.h"

int globvar = 6;

int main(int argc, char const *argv[])
{
    int var;
    pid_t pid;

    var = 88;

    if ((pid = vfork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) {
        globvar++;
        var++;
        sleep(1);
    }

    printf("pid = %ld, globvar = %d - address = %x, var = %d - address = %x\n",
           (long)getpid(), globvar, &globvar, var, &var);

    exit(0);
}
  • 運行結果

雖然子進程休眠了1秒,但是vfork仍然保證了讓子進程先執行,并且父進程中的兩個變量的值,在子進程中被修改了,說明子進程并沒有創建自己的一個副本。

zhanghuamaodeMacBook-Pro:Chapter8 zhanghuamao$ ./fork_test 
pid = 811, globvar = 7 - address = be180a0, var = 89 - address = 53de9b5c
pid = 810, globvar = 7 - address = be180a0, var = 89 - address = 53de9b5c
fork和vfork空間共享區別

2. 進程的執行

2.1 execl函數

子進程創建以后,我們可以使用execl可以執行一個新的程序,新程序從main開始執行。其中參數path表示新程序的路徑,arg0表示傳遞給新程序的參數。

 #include <unistd.h>

 int
 execl(const char *path, const char *arg0, ... /*, (char *)0 */);
  • 示例代碼

先準備一個準備在子進程中運行的新程序child,在child中打印出當前的PID和main中的參數。使用cc child.c -o child命令編譯得到可執行文件child

#include "../inc/apue.h"

int main(int argc, char const *argv[])
{
    int i = 0;
    printf("This is child process, pid = %d\n", getpid());
    for (i = 0; i < argc; i++)
        printf("argv[%d] = %s\n", i, argv[i]);
    exit(0);
}

在child同一目錄下,編寫execl_test.c程序,通過vfork創建一個新的進程,在新進程中通過execl調用child程序,并向child傳遞參數。

#include "../inc/apue.h"


int main(int argc, char const *argv[])
{
    pid_t pid;

    printf("This is parent process, pid = %d\n", getpid());;

    if ((pid = vfork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) {
        if (execl("child", "test1", "test2", "test3", (char *)0) < 0) {
            err_sys("execl error");
        }
    }

    exit(0);
}
  • 運行結果

zhanghuamaodeMacBook-Pro:Chapter8 zhanghuamao$ cc execl_test.c -o execl_test
zhanghuamaodeMacBook-Pro:Chapter8 zhanghuamao$ ./execl_test 
This is parent process, pid = 1096
This is child process, pid = 1097
argv[0] = test1
argv[1] = test2
argv[2] = test3

3. 進程終止

3.1 獲取進程退出狀態

從零開始UNIX環境高級編程(7):進程環境中,我們知道調用exit函數可以讓進程終止。如果想要知道進程終止時的狀態,可以通過在父進程中調用wait函數獲取。wait的返回值為子進程的PID,參數stat_loc是指向為子進程終止狀態的指針,如果不需要獲得終止狀態,可以將其置為NULL。

 #include <sys/wait.h>

 pid_t
 wait(int *stat_loc);

由于有很多種情況會導致進程的終止,因此,系統提供了終止狀態的宏來區分不同的終止情況。
例如,如果進程是調用exit終止的那么WIFEXITED(status)會返回true,其中status的值等于exit傳入的參數。如果進程是被信號終止的,那么WIFSIGNALED會返回true,并且調用WTERMSIG(status)可以打印出信號的值。

 WIFEXITED(status)
         True if the process terminated normally by a call to _exit(2) or exit(3).

 WIFSIGNALED(status)
         True if the process terminated due to receipt of a signal.

 WTERMSIG(status)
         If WIFSIGNALED(status) is true, evaluates to the number of the signal that
         caused the termination of the process.
  • 示例代碼

分別調用exit和abort終止進程,并在父進程中調用wait獲取進程終止時的狀態。

#include "../inc/apue.h"

void print_status(int status)
{
    if (WEXITSTATUS(status))
    {
        printf("exit status : %d\n", WEXITSTATUS(status));
    }
    else if ( WIFSIGNALED(status))
    {
        printf("signal number : %d\n", WTERMSIG(status));
    }
}

int main(int argc, char const *argv[])
{
    int status;
    pid_t pid;

    if ((pid = fork()) < 0)
        err_sys("fork error");
    else if (pid == 0)
        exit(7);

    if (wait(&status) != pid)
        err_sys("wait error");

    print_status(status);

    if ((pid = fork()) < 0)
        err_sys("fork error");
    else if (pid == 0)
        abort();

    if (wait(&status) != pid)
        err_sys("wait error");

    print_status(status);

    return 0;
}
  • 運行結果

abort函數會產生SIGABRT信號,通過kill -l命令可以查看到SIGABRT信號對應的值為6,和通過WTERMSIG打印的結果一致。

zhanghuamaodeMacBook-Pro:chapter8 zhanghuamao$ cc wait_test.c -o wait_test
zhanghuamaodeMacBook-Pro:chapter8 zhanghuamao$ ./wait_test 
exit status : 7
signal number : 6

zhanghuamaodeMacBook-Pro:chapter8 zhanghuamao$ kill -l
1) SIGHUP    2) SIGINT   3) SIGQUIT  4) SIGILL
5) SIGTRAP   6) SIGABRT  7) SIGEMT   8) SIGFPE

3.2 孤兒進程和僵死進程

父進程調用fork創建子進程,如果父進程在子進程前先終止,子進程變成了孤兒進程,它們將交給init進程收養,init進程變成了它們的父進程。相反,如果子進程比父進程先終止,并且父進程沒有調用wait函數去獲取子進程終止時的信息,那么子進程將變成一個僵尸進程,接著我們通過一段代碼來說明。

  • 示例代碼

通過fork創建子進程,子進程調用exit終止自己,父進程休眠60秒,由于父進程沒有調用wait,那么子進程終止后,會變成一個僵尸進程。

#include "../inc/apue.h"

int main(int argc, char const *argv[])
{
    pid_t pid;

    pid = fork();

    if (pid < 0) {
        err_sys("fork error");
    } else if (pid == 0) {
        exit(0);
    } else {
        sleep(60);
    }

    return 0;
}
  • 運行結果

父進程的PID為823,子進程的PID為824,通過ps -o pid,ppid,state,tty,command命令,我們可以查看到子進程的狀態為Z,說明它是一個僵尸進程。

zhanghuamaodeMacBook-Pro:chapter8 zhanghuamao$ cc zomble.c -o zomble_test
zhanghuamaodeMacBook-Pro:chapter8 zhanghuamao$ ./zomble_test &
[2] 823
zhanghuamaodeMacBook-Pro:chapter8 zhanghuamao$ ps -o pid,ppid,state,tty,command
  PID  PPID STAT TTY      COMMAND
  650   649 S    ttys000  -bash
  809   650 S    ttys000  ./zomble_test
  823   650 S    ttys000  ./zomble_test
  824   823 Z    ttys000  (zomble_test)

我們再將上面的代碼修改為:子進程休眠60秒,父進程調用exit退出,那么子進程將變成孤兒進程

#include "../inc/apue.h"

int main(int argc, char const *argv[])
{
    pid_t pid;

    pid = fork();

    if (pid < 0) {
        err_sys("fork error");
    } else if (pid == 0) {
        sleep(60);
    } else {
        exit(0);
    }

    return 0;
}

父進程PID為849,子進程PID為850。當父進程終止后,子進程變成了孤兒進程,子進程的PPID為了1,說明它的父進程是init進程。

zhanghuamaodeMacBook-Pro:chapter8 zhanghuamao$ ./orphan_test &
[1] 849
zhanghuamaodeMacBook-Pro:chapter8 zhanghuamao$ ps -o pid,ppid,state,tty,command
  PID  PPID STAT TTY      COMMAND
  650   649 S    ttys000  -bash
  850     1 S    ttys000  ./orphan_test
[1]+  Done                    ./orphan_test

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 計算機啟動的過程 系統啟動的經過可以匯整成底下的流程的:1、加載 BIOS 的硬件資訊與進行自我測試,并依據配置取...
    hailiu13閱讀 1,260評論 0 1
  • 又來到了一個老生常談的問題,應用層軟件開發的程序員要不要了解和深入學習操作系統呢? 今天就這個問題開始,來談談操...
    tangsl閱讀 4,173評論 0 23
  • Linux 進程管理與程序開發 進程是Linux事務管理的基本單元,所有的進程均擁有自己獨立的處理環境和系統資源,...
    JamesPeng閱讀 2,512評論 1 14
  • 最近覺得自己太急功近利了。做什么都只盯著它帶來的好處,而忽略了要做好事情應該做的積累。 怎么樣才能做的好,怎么樣才...
    訥于文閱讀 400評論 0 1
  • 洞山走過 身后野草叢生 面前是海 紅色,黃色,藍色 東方巨人橫亙其中 昨天確定已經過去 還有你的名字 昨天你出生的...
    崔五四閱讀 212評論 0 0