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中。由于進程描述符中存放了很多信息(從Line1511到 Line 2009),它的結構也是很復雜的,如下圖所示:
進程ID也是存放在task_struct中,新創建的進程ID是前一個進程的PID加1。
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和內核態的進程堆棧。
內核通過esp寄存器的值可以計算出當前在CPU上正在運行進程的thread_info的地址,這項工作由current_thread_info()函數完成。
由于thread_info結構體中task又指向進程描述符task_struct,因此,通過current_thread_info()->task可以獲取當前運行進程的進程描述符,而current_thread_info()->task通常被定義為current宏,只要拿到了進程描述符,和進程相關的信息就都能夠獲取了。
#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
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
參考
- UNIX環境高級編程(第3版)第8章 進程控制
- 現代操作系統(第3版)第2章 進程與線程
- 深入理解LINUX內核(第三版) 第3章 進程
- Linux中fork系統調用分析
- Linux 的僵尸(zombie)進程
- 孤兒進程與僵尸進程[總結]