Unix/Linux编程:进程组

理论

引入

每个进程都会属于一个进程组(process group),每个进程组中可以包含多个进程。进程组会有一个进程组领导进程 (process group leader),领导进程的PID (PID见Linux进程基础)成为进程组的ID (process group ID, PGID),以识别进程组。

一般情况下,一个进程组是由一个进程 fork 出来的,之后,它的子进程再去 fork ,最后,得到了一个进程组。当然,单个的进程也是一个进程组。进程组有进程组 id ,它通常是第一个进程的 pid ,也就是组长进程的 pid (以识别进程组)。

$ps -o pid,pgid,ppid,comm | cat
 PID  PGID  PPID COMMAND
17763 17763 17751 bash
18534 18534 17763 ps
18535 18534 17763 cat

PID为进程自身的ID,PGID为进程所在的进程组的ID, PPID为进程的父进程ID。从上面的结果,我们可以推测出如下关系:
在这里插入图片描述

图中箭头表示父进程通过fork和exec机制产生子进程。ps和cat都是bash的子进程。进程组的领导进程的PID成为进程组ID。领导进程可以先终结。此时进程组依然存在,并持有相同的PGID,直到进程组中最后一个进程终结

创建进程组的目的是用于简化向组内所有进程发送信号的操作,即如果一个信号是发给一个进程组,则这个组内的所有进程都会受到该信号【方便管理】。

API

每个进程都拥有一个以数字表示的进程组 ID,表示该进程所属的进程组。新进程会继承其父进程的进程组 ID,使用 getpgrp()能够获取一个进程的进程组 ID

#include <unistd.h>
/***********************
 *功能:获取进程组ID
 *返回值:成功返回进程组ID,失败返回-1并设置errno
 * *********************/
pid_t getpgrp(void);

如果getpgrp()的返回值与调用进程的进程 ID 匹配的话就说明该调用进程是其进程组的首进程

通过调用setpgid,可以让一个进程加入一个存在的进程组或创建一个新的进程组

/*****************************
 * 功能: 设置进程组ID
 * 参数: pid进程ID
 *       pgid进程组ID
 *           如果pid为0,setpgid(0, 5)等价于setpgid(getpid(), 5)
 *           如果pgid为0,setpgid( 5, 0)等价于setpgid( 5, getpid())
 * 返回值:成功返回0,错误返回-1
 * 注意:  一个进程只能为它自己或它的子进程设置进程组ID
 *        在它的子进程调用了exec后,它就不再能改变该子进程的进程组ID
 ****************************/
int setpgid(pid_t pid, pid_t pgid);
  • 如果 pid 和 pgid 参数指定了同一个进程,那么就会创建一个新进程组,并且指定的进程会成为这个新组的首进程
  • 如果两个参数的值不同,那么 setpgid()调用会将一个进程从一个进程组中移到另一个进程组中

调用setpgid时存在如下限制:

  • pid参数可以仅指定调用进程或者其中一个子进程。违反这条规则会导致 ESRCH 错误
  • 在组之间移动进程时,调用进程、由 pid 指定的进程(可能是另外一个进程,也可能就是调用进程)以及目标进程组必须要属于同一个会话。违反这条规则会导致EPERM 错误
  • pid 参数所指定的进程不能是会话首进程。违反这条规则会导致 EPERM 错误
  • 一个进程在其子进程已经执行 exec()后就无法修改该子进程的进程组 ID 了。违反这条规则会导致 EACCES 错误。之所以会有这条约束条件的原因是在一个进程开始执行之后再修改其进程组 ID 的话会使程序变得混乱

注:在BSD后代系统的早期版本,getpgrp函数接受一个pid参数并返回这个进程的进程组。SUS定义getpgid函数作为一个XSI扩展来效仿这种行为。

/**************************
 * 功能:获取进程GID
 * 参数:pid ---- 进程ID
 * 		   如果pid为0,返回调用进程的进程组ID。因而getpgid(0)等价于getpgrp();
 * 返回值:成功返回进程组ID,失败返回-1并设置errno
 * ***********************/
pid_t getpgid(pid_t pid);

进程组和shell任务控制

shell执行的每个程序都会在一个新进程内发起。比如,shell创建了个进程来执行以下的管道命令(在当前的工作目录下,根据文件大小对文件进行排序并显示)

$ ls -l | sort -k5n | less

除 Bourne shell 以外,几乎所有的主流 shell 都提供了一种交互式特性,名为任务控制

  • 该特性允许用户同时指定并操纵多条命令或管道。
  • 在支持任务控制的shell中,会将管道内的所有进程置于一个新进程组或任务中(如果情况很简单,shell命令行只包含一条命令,那么就会创建一个只包含单个进程的新进程组)。
  • 进程组中的每个进程都具有相同的检查组标识符(以整数形式),其中就是进程组中某个进程(也称为进程组组长 process group leader)的进程 ID

内核可对进程组中的所有成员执行各种动作,尤其是信号的传递。支持任务控制的shell会利用这一特性,以挂起或恢复管道中的所有进程

在作业控制shell中使用setpgid

一个进程在其子进程已经执行exec()之后就无法修改该子进程的进程组ID的约束条件会影响基于shell的作业控制程序设计,即需要满足下列条件

  • 一个任务(即一个命令或一组以管道符连接的命令)中的所有进程必须被放置在一个进程组中。这一步允许 shell 使用killpg(或使用负的 pid 值来调用 kill())来同时向进程组中的所有成员发送作业控制信号。一般来讲,这一步需要在发送任意作业控制信号来完成
  • 每个子进程在执行程序之前必须要被分配到进程组中,因为程序本身是不清楚如何操作进程组ID的

对于任务中的每个进程来讲,父进程和子进程都可以使用setpgid()来修改子进程的进程组ID。但是,由于在父进程fork()之后子进程与父进程之间的调度顺序是无法确定的,因此无法依靠父进程在子进程执行 exec()之前来改变子进程的进程组 ID,同样也
无法依靠子进程在父进程向其发送任意作业控制信号之前修改其进程组 ID。因此,在编写作业控制 shell 程序时需要让父进程和子进程在 fork()调用之后立即调用 setpgid()来将子进程的进程组 ID 设置为同样的值,并且父进程需要忽略在 setpgid()调用中出现的所有 EACCES 错误。换句话说,在一个作业控制 shell 程序中可能会出现像下面:
在这里插入图片描述

在处理由管道符连接起来的命令时事情会变得比程序清单 34-1 更加复杂一点,父 shell 需要记录管道中第一个进程的进程 ID 并使用这个值作为该组中所有进程的进程组 ID(pipelinePgid)

实践

kill 进程组

一个进程所创建的子进程,都会被包含到一个进程组中。

所以,我们可以用进程组杀死某个进程及其fork出的所有子进程。

$ ps -o pgid 19843
 PGID
  977
$ kill -- -977
$ kill -SIGTERM -- -977

posix中的进程组

显示子进程与父进程的进程组id

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    pid_t pid;
    printf("start fork......\n");
    if((pid=fork())<0){
        perror("fork");
        exit(1);
    }else if(pid==0){
        printf("The child process PID is %d.\n",getpid());
        printf("The Group ID is %d.\n",getpgrp());
        printf("The Group ID is %d.\n",getpgid(0));
        printf("The Group ID is %d.\n",getpgid(getpid()));
    }else{
        sleep(3);
        printf("The parent process PID is %d.\n",getpid());
        printf("The Group ID is %d.\n",getpgrp());
    }
    exit(0);
}

在这里插入图片描述

下面子进程使用setpgid()创建了一个新的进程组,并且进程组的组长就是自己。从而脱离了父进程的进程组

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    pid_t pid;
    printf("start fork......\n");
    if((pid=fork())<0){
        perror("fork");
        exit(1);
    }else if(pid==0){
        printf("Child:before setpgid,my pid=%d,My pgid=%d\n",getpid(),getpgrp());
        setpgid(getpid(),getpid());
        printf("Child:after setpgid,my pid=%d,My pgid=%d\n",getpid(),getpgrp());
    }else{
        wait(NULL);
        printf("child exit\n");
        printf("Father:My pid=%d,My pgid=%d\n",getpid(),getpgid(getpid()));
    }
    exit(0);
}
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    pid_t pid;
    printf("start fork......\n");
    if((pid=fork())<0){
        perror("fork");
        exit(1);
    }else if(pid==0){
        printf("Child:before setpgid,my pid=%d,My pgid=%d\n",getpid(),getpgrp());
        sleep(2);
        printf("Child:after setpgid,my pid=%d,My pgid=%d\n",getpid(),getpgrp());
    }else{
        sleep(1);
        setpgid(pid,pid);
        wait(NULL);
        printf("child exit\n");
        printf("Father:My pid=%d,My pgid=%d\n",getpid(),getpgid(getpid()));
    }
    exit(0);
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值