从零开始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中。由于进程描述符中存放了很多信息(从Line1511到 Line 2009),它的结构也是很复杂的,如下图所示:

从零开始UNIX环境高级编程(8):进程控制_第1张图片
Linux进程描述符 - 图片来自深入理解Linux内核

进程ID也是存放在task_struct中,新创建的进程ID是前一个进程的PID加1。

从零开始UNIX环境高级编程(8):进程控制_第2张图片
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内核态的进程堆栈

从零开始UNIX环境高级编程(8):进程控制_第3张图片
task_struct

内核通过esp寄存器的值可以计算出当前在CPU上正在运行进程的thread_info的地址,这项工作由current_thread_info()函数完成。

从零开始UNIX环境高级编程(8):进程控制_第4张图片
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 

#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 

 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 

 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
从零开始UNIX环境高级编程(8):进程控制_第5张图片
fork和vfork空间共享区别

2. 进程的执行

2.1 execl函数

子进程创建以后,我们可以使用execl可以执行一个新的程序,新程序从main开始执行。其中参数path表示新程序的路径,arg0表示传递给新程序的参数。

 #include 

 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 

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

你可能感兴趣的:(从零开始UNIX环境高级编程(8):进程控制)