进程关系

寵の児 提交于 2020-02-25 22:22:53

进程组

每个进程除了有一进程ID外,还属于一个进程组。函数getpgrp用于返回调用进程的进程组ID。

#include <unistd.h>
pid_t getpgrp(void);
返回值:调用进程的进程组ID。

getpgid函数用于获取指定pid的进程的组ID。

#include <unistd.h>
pid_t getpgid(pid_t pid);
返回值:若成功,返回进程组ID;若出错,返回-1

pid是0,返回调用进程的进程组ID,于是getpgid(0);等价于getpgrp();

每个进程组有一个组长进程。组长进程的进程组ID等于其进程ID。进程组组长可以创建一个进程组、创建该组中的进程,然后终止。只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。从进程组创建开始到其中最后一个进程离开为止的时间段为进程组的生命期。某个进程组中的最后一个进程可以终止,也可以转移到另一个进程组。

setpgid函数用于将调用其的进程加入一个现有进程组或是创建一个新进程组。

#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
返回值:若成功,返回0;若出错,返回-1

setpgid函数将pid进程的进程组ID设置为pgid。如果这两个参数相等,则由pid指定的进程变成进程组组长。如果pid是0,则使用调用者的进程ID,另外,如果pgid是0,则由pid指定的进程ID用作进程组ID。

会话

会话是一个或多个进程组的集合。

setsid函数用于创建一个新会话。

#include <unistd.h>
pid_t setsid(void);
返回值:若成功,返回进程组ID;若出错,返回-1

如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新会话。具体会发生以下3件事。

  1. 该进程变成新会话的会话首进程(会话首进程是创建该会话的进程)。此时,该进程是新会话中的唯一进程。
  2. 该进程成为一个新进程组的组长进程。新进程组ID是该调用进程的进程ID。
  3. 该进程没有控制终端。如果在调用setsid之前该进程有一个控制终端,那么这种联系也被切断。

会话首进程总是一个进程组的组长进程。

getsid函数用于获取会话首进程的进程组ID。

#include <unistd.h>
pid_t getsid(pid_t pid);
返回值:若成功,返回会话首进程的进程组ID;若出错,返回-1

如若pid是0,getsid返回调用进程的会话首进程的进程组ID。出于安全方面的考虑,一些实现有如下限制:如若pid并不属于调用者所在的会话,那么调用进程就不能得到该会话首进程的进程组ID。

控制终端

会话和进程组还有如下一些其他特性。

  • 一个会话可以有一个控制终端。这通常是终端设备或伪终端设备。
  • 建立与控制终端连接的会话首进程被称为控制进程。
  • 一个会话中的几个进程可被分成一个前台进程组以及一个或多个后台进程组。
  • 如果一个会话有一个控制终端,则它有一个前台进程组,其他进程组为后台进程组。
  • 无论何时键入终端的中断键,都会将中断信号发至前台进程组的所有进程。
  • 无论何时键入终端的退出键,都会将退出信号发至前台进程组的所有进程。
  • 如果终端接口检测到调制解调器已经断开连接,则将挂断信号发送至控制进程(会话首进程)。

这些特性的关系图如下:

不同实现分配控制终端的方式:

函数tcgetpgrptcsetpgrptcgetsid

tcgetpgrptcsetpgrp分别用于获取会话中的前台进程组和设置前台进程ID。

#include <unistd.h>
pid_t tcgetpgrp(int fd);
返回值:若成功,返回前台进程组ID;若出错,返回-1。
pid_t tcsetpgrp(int fd, pid_t pgrpid);
返回值:若成功,返回0;若出错,返回-1

函数tcgetpgrp返回前台进程组ID,它与fd上打开的终端相关联。如果进程有一个控制终端,则该进程可以调用tcsetpgrp将前台进程组ID设置为pgrpidpgrpid值应当是在同一会话中的一个进程组的ID。fd必须引用该会话的控制终端。

tcgetsid函数用于获取会话首进程的进程组ID,并给出控制TTY的文件描述符。

#include <termios.h>
pid_t tcgetsid(int fd);
返回值:若成功,返回会话首进程的进程组ID;若出错,返回-1

作业控制

作业控制允许在一个终端上启动多个作业(进程组)。作业控制有以下3种形式的支持。

  1. 支持作业控制的shell。
  2. 内核中的终端驱动程序必须支持作业控制。
  3. 内核必须提供对某些作业控制信号的支持。

有3个字符可使终端驱动程序产生信号,并将它们发送至前台进程,它们是:

  • 中断字符产生SIGINT;(一般采用Delete或Ctrl + C)
  • 退出字符产生SIGQUIT;(一般采用Ctrl + \)
  • 挂起字符产生SIGTSTP。(一般采用Ctrl + Z)

测试示例1:

后台进程如何能接收终端输入?

$cat > temp.foo &       在后台启动,但将从标准输入读
[1] 2438
$					    输入回车
[1]+  Stopped                 cat > temp.foo
$fg %1				    使1号作业成为前台作业
cat > temp.foo		    shell告诉我们现在哪个作业在前台
hello, world		    输入一行
^D					    键入文件结束符
$cat temp.foo		    键入该行命令,查看输入已送入文件
hello, world			

因为终端输入只会被前台作业接收,若是后台作业要接收终端输入,则必须将后台作业成为前台作业,这时才可使得后台作业接收终端输入。

测试示例2:

后台作业如何输出到控制终端呢?

$cat temp.foo & 			在后台执行
[1] 2671					
$hello, world				提示符后出现后台作业的输出
						    键入回车
[1]+  Done                    cat temp.foo
$stty tostop				禁止后台作业输出至控制终端
$cat temp.foo &				在后台再试一次
[1] 2673					
$							键入回车,发现作业已停止
[1]+  Stopped                 cat temp.foo
$fg %1						在前台恢复停止的作业
cat temp.foo				shell告诉我们现在哪一个作业在前台
hello, world				这是该作业的输出

以下是对于前台、后台作业以及终端驱动程序的作业控制功能总结。

shell执行程序

测试示例:

Bourne-Again Shell实例,在一条管道中执行3个进程,检验Bourne shell使用的进程控制方式。

$ps -o pid,ppid,pgid,sid,tpgid,comm | cat1 | cat2
   PID   PPID   PGID    SID  TPGID COMMAND
  2406   2405   2406   2406   3096 bash
  3096   2406   3096   2406   3096 ps
  3097   2406   3096   2406   3096 cat1
  3098   2406   3096   2406   3096 cat2

由上面执行的结果可知,管道中的执行三个命令的进程都从同一个父进程创建而来,并可得出如下的关系图:

相关说明:

pid:进程ID

ppid:父进程ID

pgid: 进程组ID

sid: 会话ID

tpgid: 与会话控制终端相关联的进程组ID

comm: 命令

孤儿进程组

与孤儿进程(一个其父进程已终止的进程)类似,进程组也可能成为孤儿进程组

测试示例(创建孤儿进程组):

如上图所示,shell将前台进程放在它(指前台进程)自己的进程组中(指的是11242),shell则留在自己的进程组内(11002)。子进程继承其父进程(11242)的进程组。在fork之后:

  • 父进程睡眠5秒,这是一种让子进程在父进程终止之前运行的一种权宜之计。
  • 子进程为挂断信号(SIGHUP)建立信号处理程序。这样能观察到SIGHUP信号是否已发送给子进程。
  • 子进程用kill函数向其自身发送停止信号(SIGTSTP)。这将停止子进程,类似于用终端挂起字符(Ctrl + Z)停止一个前台作业。
  • 当父进程终止时,该子进程称为孤儿进程,所以其父进程ID成为1,也就是init进程ID。
  • 现在,子进程成为一个孤儿进程组的成员。
  • 因为在父进程终止后,进程组包含一个停止的进程,进程组成为孤儿进程组,POSIX.1要求向新孤儿进程组中处于停止状态的每一个进程发送挂断信号(SIGHUP),接着又向其发送继续信号(SIGCONT)。
  • 在处理了挂断信号后,子进程继续。对挂断信号的系统默认动作是终止该进程,为此必须提供一个信号处理程序以捕捉该信号。
#include "../../include/apue.h"
#include <errno.h>

static void sig_hup(int signo)
{
    printf("SIGHUP received, pid = %ld\n", (long)getpid());
}

static void pr_ids(char *name)
{
    printf("%s: pid = %ld, ppid = %ld, pgrp = %ld, tpgrp = %ld\n", 
           name, (long)getpid(), (long)getppid(), (long)getpgrp(), 
           (long)tcgetpgrp(STDIN_FILENO));
    fflush(stdout);
}

int main(void)
{
    char c;
    pid_t pid;

    pr_ids("parent");
    if((pid = fork()) < 0){
        err_sys("fork error");
    }else if(pid > 0){
        sleep(2);
    }else{
        pr_ids("child");
        signal(SIGHUP, sig_hup);
        kill(getpid(), SIGTSTP);
        pr_ids("child");
        if(read(STDIN_FILENO, &c, 1) != 1)
            printf("read error %d on controlling TTY\n", errno);
    }
    //return 0;
    exit(0);
}

结果如下:

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!