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)进程
- 孤儿进程与僵尸进程[总结]