详解 Linux 过程组

  • 每个过程都有一个过程组号 (PGID)

    • 过程组:一个或多个过程的汇合(汇合中的过程并不孤立)
    • 过程组中的过程通常存在父子关系,兄弟关系,或 性能相近

  • 过程组可不便过程治理(如:同时杀死多个过程,发送一个信号给多个过程)

    • 每个过程必然属于一个过程组,也只能属于一个过程组
    • 过程除了有 PID 外,还有 PGID (惟一,可变,即某一个过程能够切换过程组)
    • 每个过程组有一个过程组长,过程组长的 PID 和 PGID 雷同
> ps -o pgid 19843PGID 977> kill -- -977

  • pid_t getpgrp(void); // 获取以后过程的组标识
  • pid_t getpgid(pid_t pid); // 获取指定过程的组标识
  • int setpgid(pid_t pid, pid_t pgid); // 设置过程的组标识

    • pid == pgid, 将 pid 指定的过程设为组长
    • pid == 0, 设置以后过程的组标识为 pgid
    • pid == 0,将 pid 设置为组标识 (行将 pid 所代表的过程设置为过程组长)

过程组示例程序

默认状况下,子过程与父过程属于同一过程组, 是 fork 工作机制的产物(子过程复制以后过程自身)
#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <string.h>#include <unistd.h>int main(void){    int pid = 0;    int i = 0;    printf("parent = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());    while (i < 5) {        if ((pid = fork()) > 0) {            printf("new : %d\n", pid);        }        else if (pid == 0) {            sleep(1);            printf("child = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());            sleep(60);            printf("last == pgid = %d\n", getpgrp());            break;        }        else {            printf("fork error...\n");        }        ++i;    }    if (pid) {        sleep(60);    }    return 0;}
tiansong@tiansong:~/Desktop/linux$ ./a.out &[1] 3022parent = 3022, ppid = 2125, pgid = 3022new : 3024new : 3025new : 3026new : 3027new : 3028tiansong@tiansong:~/Desktop/linux$ child = 3025, ppid = 3022, pgid = 3022child = 3026, ppid = 3022, pgid = 3022child = 3027, ppid = 3022, pgid = 3022child = 3024, ppid = 3022, pgid = 3022child = 3028, ppid = 3022, pgid = 3022tiansong@tiansong:~/Desktop/linux$ ps    PID TTY          TIME CMD   2125 pts/3    00:00:00 bash   3022 pts/3    00:00:00 a.out   3024 pts/3    00:00:00 a.out   3025 pts/3    00:00:00 a.out   3026 pts/3    00:00:00 a.out   3027 pts/3    00:00:00 a.out   3028 pts/3    00:00:00 a.out   3061 pts/3    00:00:00 pstiansong@tiansong:~/Desktop/linux$ kill 3022  // kill 过程组长[1]+  Terminated              ./a.outtiansong@tiansong:~/Desktop/linux$ ps    PID TTY          TIME CMD   2125 pts/3    00:00:00 bash   3024 pts/3    00:00:00 a.out   3025 pts/3    00:00:00 a.out   3026 pts/3    00:00:00 a.out   3027 pts/3    00:00:00 a.out   3028 pts/3    00:00:00 a.out   3149 pts/3    00:00:00 pstiansong@tiansong:~/Desktop/linux$ kill -- -3022  // kill 过程组tiansong@tiansong:~/Desktop/linux$ ps    PID TTY          TIME CMD   2125 pts/3    00:00:00 bash   3163 pts/3    00:00:00 ps

过程组深度分析

深刻了解过程组

  • 过程组长终止,过程组仍然存在(过程组长仅用于创立新过程组)
  • 父过程创立子过程后立刻通过 setpgid() 扭转其组标识(PGID)【当须要将子过程设置到其它过程组时】
  • 同时,子过程也须要通过 setpgid() 扭转本身组标识(PGID)【当须要将子过程设置到其它过程组时】
  • 当子过程调用 exec()

    • 父过程无奈通过 setpgid() 扭转子过程组标识(PGID)
    • 只能本身通过 setpgid() 扭转其组标识 (PGID)

过程组标识设置技巧

试验1:设置子过程为过程组长 为例
#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <string.h>#include <unistd.h>int main(void){    int pid = 0;    printf("parent = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());    if( (pid = fork()) > 0) {        int r = setpgid(pid, pid);    // ① 子过程设置新的过程组        printf("new: %d, r = %d\n", pid, r);    }    else if (pid == 0) {        setpgid(pid, pid);   // ② -> setpgid(0,0) -> setpgid(子过程pid, 用子过程id作为过程组id)        sleep(1);        printf("child = %d, ppid = %d, pgid =%d\n", getpid(), getppid(), getpgrp());    }    else {        printf("fork error ...\n");    }    return 0;}
tiansong@tiansong:~/Desktop/linux$ ./a.out parent = 3434, ppid = 2125, pgid = 3434new: 3435, r = 0child = 3435, ppid = 1, pgid =3435
问:为什么在父子过程都须要调用 setpgid 呢?
答:为了双保险。

fork 实现之后,无奈确认是父过程先执行还是子过程限制性(古代操作系统个别子过程先执行)。
为了确保不让子过程与父过程在“短暂的工夫内”仍呈现在雷同的过程组中,须要在子过程创立进去之后立刻对 “子过程” 进行过程组设置。


试验2:当子过程调用 exec() 后,父过程无奈通过 setpgid() 扭转子过程组标识(PGID)
main.c
#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <string.h>#include <unistd.h>int main(void){    int pid = 0;    printf("parent = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());    if( (pid = fork()) > 0) {        int r = setpgid(pid, pid);        printf("new: %d, r = %d\n", pid, r);    }    else if (pid == 0) {        char *out = "./helloword.out";        char *const ps_argv[] = {out, NULL};        char *const ps_envp[] = {"PATH=/bin:/usr/bin", NULL};        execve(out, ps_argv, ps_envp);    }    else {        printf("fork error ...\n");    }    sleep(60);    return 0;}
helloword.c
#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <unistd.h>int main(void){    printf("child = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());    printf("hello world\n");    sleep(30);    return 0;}
tiansong@tiansong:~/Desktop/linux$ ./a.out &[1] 5660parent = 5660, ppid = 2125, pgid = 5660new: 5662, r = 0  // r 等于 0, 示意父过程 setpgid 调用胜利child = 5662, ppid = 5660, pgid = 5662hello world
批改 main.c
#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <string.h>#include <unistd.h>int main(void){    int pid = 0;    printf("parent = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());    if( (pid = fork()) > 0) {        int r = 0;        sleep(1);  // 确保子过程先执行并调用了 execve        r = setpgid(pid, pid);        printf("new: %d, r = %d\n", pid, r);    }    else if (pid == 0) {        char *out = "./helloword.out";        char *const ps_argv[] = {out, NULL};        char *const ps_envp[] = {"PATH=/bin:/usr/bin", NULL};        execve(out, ps_argv, ps_envp);    }    else {        printf("fork error ...\n");    }    sleep(60);    return 0;}
批改 helloword.c
#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <unistd.h>int main(void){    sleep(5);  // 期待父过程 setpgid 后再进行后续打印    printf("child = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());    printf("hello world\n");    sleep(30);    return 0;}
tiansong@tiansong:~/Desktop/linux$ ./a.out &parent = 5917, ppid = 2125, pgid = 5917tiansong@tiansong:~/Desktop/linux$ new: 5919, r = -1  // r 等于 -1, 示意 setpgid 执行失败child = 5919, ppid = 5917, pgid = 5917  // pgid 也未发生变化hello world

会话与终端的关系

Linux 会话(session)

  • 用户通过终端登录零碎后产生一个会话
  • 会话是一个或多个过程组的汇合
  • 每个会话有一个会话标识 (SID

    • 终端登陆后的第一个过程成为会话首过程,通常是一个 shell/pash
    • 对于会话首过程 (session leader), 其 PID 与 SID 相等

  • 通厂状况下,会话与一个终端(管制终端)相关联用于执行输入输出操作

    • 会话首过程建设与管制终端的连贯(会话首过程又叫做管制过程)
    • 会话中的过程组可分为:

      • 前台过程组:可接管管制终端中的输出,也可输入数据到管制终端
      • 后盾过程组:所有过程后盾运行,无奈接管终端中的输出,但可输入数据到终端

其中 getty 用于关联终端

会话与前后台过程组

会话中的前台过程组

问题

在终端中输出命令后,产生了什么?
  • 当命令行(shell)运行命令后创立一个新的过程组
  • 如果运行的命令中有多个子命令则创立多个过程(新创建的过程处于新建的过程组中)
  • 命令不带 &

    • shell 将新见的过程组设置为前台过程组,并将本人临时设置为后盾过程组
  • 命令带 shell

    • shell 将新见的过程组设置为后盾过程组,本人仍旧是前台过程组

什么是终端过程组标识(TPGID)

  • 标识过程是否处于一个和终端相干的过程组中
  • 前台过程:TPGID == PGID (因为前台过程组可能扭转,TPGID 用于标识以后的前台过程组)
  • 后盾过程:TPGID != PGID
  • 若过程和任何终端无关:TPGID == -1
通过比拟 TPGID 与 PGID 可判断:一个过程属于前台过程组,还是后盾过程组
如果过程组和终端相关联,那么当终端敞开断开连接,过程组的过程将全副完结
tiansong@tiansong:~/Desktop/linux$ ps -ajx | grep TPGID   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND   2125    6933    6932    2125 pts/3       6932 S+    1000   0:00 grep --color=auto TPGID

会话编程深度分析

Linux 会话接口

  • #include <unistd.h>
  • pid_t getsid(pd_t pid); // 获取指定过程的 SID, (pid == 0) → 以后过程
  • pid_t setpid(void); // 用于创立新会话,其中调用过程不能是过程组长,执行了如下动作:

    • 创立新会话, SID == PID,调用过程会成为会话首过程(在创立的会话中是惟一过程)
    • 创立新过程组, PGID == PID, 调用过程成为过程组长 (在创立的过程组中是惟一过程)
    • 调用过程没有管制终端,若调用前关联了管制终端,调用后与管制终端断联
#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <string.h>#include <unistd.h>int main(void){    int pid = 0;    if( (pid = fork()) > 0) {        printf("parent = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid()));        printf("new: %d\n", pid);    }    else if (pid == 0) {        setsid();  // 子过程脱离以后会话,创立新的过程会话,新的过程组        sleep(180);        printf("child = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid()));    }    else {        printf("fork error ...\n");    }    sleep(240);    return 0;}
tiansong@tiansong:~/Desktop/linux$ ./a.out   // 前台运行parent = 7291, ppid = 2125, pgid = 7291, sid = 2125new: 7292^C                                          //  ctrl + c 终止前台过程组中的前台过程tiansong@tiansong:~/Desktop/linux$ ps       // 查看以后终端窗口中运行的根本信息,发现没有 a.out    PID TTY          TIME CMD   2125 pts/3    00:00:00 bash   7361 pts/3    00:00:00 pstiansong@tiansong:~/Desktop/linux$ ps -ajx | grep a.out  // 显示没有管制终端的过程(-x), 搜寻 a.out      1    7292    7292    7292 ?             -1 Ss    1000   0:00 ./a.out  // 7292可知为创立的子过程。? 表明与任何一个终端都不关联,同时 TPGID 为 -1   2125    7383    7382    2125 pts/3       7382 S+    1000   0:00 grep --color=auto a.outtiansong@tiansong:~/Desktop/linux$ pstree -p -s -A 7292systemd(1)---a.out(7292)                    // 父过程终止运行,被初始化过程接管

#include <stdio.h>#include <stdlib.h>#include <fcntl.h>#include <string.h>#include <unistd.h>int main(void){    int pid = 0;    if( (pid = fork()) > 0) {        printf("parent = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid()));        printf("new: %d\n", pid);    }    else if (pid == 0) {        setsid();        sleep(3);   // 批改此处,不便察看打印        printf("child = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid()));    }    else {        printf("fork error ...\n");    }    sleep(240);    return 0;}
tiansong@tiansong:~/Desktop/linux$ ./a.out parent = 7947, ppid = 2125, pgid = 7947, sid = 2125new: 7948child = 7948, ppid = 7947, pgid = 7948, sid = 7948    // pgid(过程组长), sid(会话首过程)为以后过程,合乎预期^C // ctrl + c 终止前台过程组中的前台过程tiansong@tiansong:~/Desktop/linux$ ps -ajx | grep 7948      1    7948    7948    7948 ?             -1 Ss    1000   0:00 ./a.out  // ? 表明与任何一个终端都不关联,同时 TPGID 为 -1   2125    8028    8027    2125 pts/3       8027 S+    1000   0:00 grep --color=auto 7948
问题:在上述测试中,子过程创立新会话,与以后终端断开,与任何终端都不关联,那么为什么会在以后终端有输入呢?

只管 setsid 导致子过程再无相关联的终端,但因为 fork 的关系,子过程的 stdout 仍标记的以后所操作的终端上,因而子过程的打印会在以后终端输入

总结:规范输入输出与终端是“无关”的。只不过在默认状况下,规范输入输出和终端挂接到了一起(能够通过重定向使其断连)。(新会话能够没有管制终端,但还是能够有规范输入输出)