从零开始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

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • 计算机启动的过程 系统启动的经过可以汇整成底下的流程的:1、加载 BIOS 的硬件资讯与进行自我测试,并依据配置取...
    hailiu13阅读 1,133评论 0 1
  • 又来到了一个老生常谈的问题,应用层软件开发的程序员要不要了解和深入学习操作系统呢? 今天就这个问题开始,来谈谈操...
    tangsl阅读 4,085评论 0 23
  • 2016-02-02 进程控制 进程标识 每个进程都有一个非负整型的唯一的进程id,因为进程id表示服总是唯一的,...
    千里山南阅读 427评论 0 0
  • Linux 进程管理与程序开发 进程是Linux事务管理的基本单元,所有的进程均拥有自己独立的处理环境和系统资源,...
    JamesPeng阅读 2,445评论 1 14
  • 最近觉得自己太急功近利了。做什么都只盯着它带来的好处,而忽略了要做好事情应该做的积累。 怎么样才能做的好,怎么样才...
    讷于文阅读 373评论 0 1