1

详解 Linux 进程组

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

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

  • 进程组可方便进程管理(如:同时杀死多个进程,发送一个信号给多个进程)

    • 每个进程必定属于一个进程组,也只能属于一个进程组
    • 进程除了有 PID 外,还有 PGID (唯一,可变,即某一个进程可以切换进程组)
    • 每个进程组有一个进程组长,进程组长的 PID 和 PGID 相同
> ps -o pgid 19843
PGID 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] 3022
parent = 3022, ppid = 2125, pgid = 3022
new : 3024
new : 3025
new : 3026
new : 3027
new : 3028
tiansong@tiansong:~/Desktop/linux$ child = 3025, ppid = 3022, pgid = 3022
child = 3026, ppid = 3022, pgid = 3022
child = 3027, ppid = 3022, pgid = 3022
child = 3024, ppid = 3022, pgid = 3022
child = 3028, ppid = 3022, pgid = 3022

tiansong@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 ps
tiansong@tiansong:~/Desktop/linux$ kill 3022  // kill 进程组长
[1]+  Terminated              ./a.out
tiansong@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 ps
tiansong@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 = 3434
new: 3435, r = 0
child = 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] 5660
parent = 5660, ppid = 2125, pgid = 5660
new: 5662, r = 0  // r 等于 0, 表示父进程 setpgid 调用成功
child = 5662, ppid = 5660, pgid = 5662
hello 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 = 5917
tiansong@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 相等

image.png

image.png

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

    • 会话首进程建立与控制终端的连接(会话首进程又叫做控制进程)
    • 会话中的进程组可分为:

      • 前台进程组:可接收控制终端中的输入,也可输出数据到控制终端
      • 后台进程组:所有进程后台运行,无法接收终端中的输入,但可输出数据到终端

image.png

其中 getty 用于关联终端

会话与前后台进程组

会话中的前台进程组

image.png

问题

在终端中输入命令后,发生了什么?
  • 当命令行(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 = 2125
new: 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 ps
tiansong@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.out

tiansong@tiansong:~/Desktop/linux$ pstree -p -s -A 7292
systemd(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 = 2125
new: 7948
child = 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
问题:在上述测试中,子进程创建新会话,与当前终端断开,与任何终端都不关联,那么为什么会在当前终端有输出呢?

image.png

尽管 setsid 导致子进程再无相关联的终端,但因为 fork 的关系,子进程的 stdout 仍标记的当前所操作的终端上,因此子进程的打印会在当前终端输出

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


TianSong
734 声望138 粉丝

阿里山神木的种子在3000年前已经埋下,今天不过是看到当年注定的结果,为了未来的自己,今天就埋下一颗好种子吧