POSIX 进程组

13

我正在将进程组集成到我的操作系统项目的POSIX子系统中。然而,我对POSIX规范(setsid)(以及维基百科上的进程组页面)感到有些困惑。

我们的终端层将SIGINT发送给前台进程(群组),其ID应等于组长的PID。在这种情况下,前台进程(我们的“登录”应用程序)通过调用setsid成为组长。当用户登录时,程序会分叉并执行用户的shell。在这个阶段,我的理解是,在从分叉出的子进程中调用exec*之前,我需要从分叉出的子进程中调用setpgid。这意味着执行的程序将从一开始就成为进程组的一部分。

如果我想让新分叉的子进程在进程组外运行,我只需在调用exec*之前在分叉的子进程中调用setsid

这是正确的吗?我是否应该检查或执行任何真正模糊的事情?

作为一个后续问题,我相信我已经知道了,fork是否需要传递组成员资格?或者是每次fork调用后必须使用setpgid进行处理的事情?根据POSIX关于fork的定义,进程组是通过fork传递的。

提前感谢您的回答。

2个回答

18

有趣的问题-尤其是因为它没有得到任何部分答案而保持了这么长时间。

POSIX基本定义

摘自POSIX定义部分的一些引用:

3.290进程组

允许信号相关进程的集合。系统中的每个进程都是一个进程组的成员,该进程组由进程组ID标识。新创建的进程加入其创建者的进程组。

3.291进程组ID

在其生命周期内表示进程组的唯一正整数标识符。

注意:请参阅进程ID重用中定义的进程组ID重用。

3.292进程组领导者

其进程ID与其进程组ID相同的进程。

3.293进程组生命周期

从创建进程组开始的时间段,在最后一个剩余进程离开组时结束该时间段,原因是最后一个进程的生命周期结束或最后一个剩余进程调用setsid()或setpgid()函数。

注:setsid()和setpgid()函数在POSIX.1-2008的系统接口卷中有详细定义。

[...]

3.337会话

为作业控制目的建立的进程组集合。每个进程组都是会话的成员。将进程视为其进程组所属会话的成员。新创建的进程加入其创建者的会话。进程可以更改其会话成员身份;请参见setsid()。同一会话中可能有多个进程组。

注意:setsid()函数在POSIX.1-2008的系统接口卷中有详细定义。

3.338会话领导者

创建会话的进程。

注:有关详细信息,请参见POSIX.1-2008的系统接口卷中定义的setsid()函数。

3.339会话生命周期

从会话创建到仍然作为会话成员的所有进程组的生命周期结束之间的时间段。


POSIX系统接口

名称

setsid - 创建会话并设置进程组ID

概述

   #include <unistd.h>

   pid_t setsid(void);

描述

setsid()函数将创建一个新的会话(session),如果调用进程不是进程组(process group)的组长。返回时,调用进程将成为该新会话的会话首领(session leader),将成为新进程组(process group)的进程组首领(process group leader),并且将没有控制终端。调用进程的进程组ID将被设置为调用进程的进程ID。调用进程将是新进程组中唯一的进程,也是新会话中唯一的进程。

名称

setpgid - 为作业控制设置进程组ID

概述

   #include <unistd.h>

   int setpgid(pid_t pid, pid_t pgid);

setpgid()函数可加入现有进程组或在调用进程的会话中创建新的进程组。对于会话领导者的进程组ID不会更改。成功完成后,与pid匹配的进程ID的进程组ID将设置为pgid。特殊情况下,如果pid为0,则使用调用进程的进程ID。此外,如果pgid为0,则使用指定进程的进程ID。

是的,这基本上是正确的。是的,可能还有一些不太常见的事情需要注意。例如,您可能需要考虑控制TTY。

作为后续问题,我相信我已经知道了,fork是否需要传递组成员资格?还是必须在每次fork调用后使用setpgid进行设置?我从POSIX对fork的定义中得出进程组会被传输。

fork()后的子进程属于与父进程相同的一组组(如/etc/group中所示),同时也属于相同的会话和进程组——但它既不是会话领导者,也不是进程组领导者。


2
很好的研究,与我在编写shell时所探索的内容一致,但仅凭那些经验,我不敢对会话和进程组发表权威言论 -- 我认为这个问题没有得到答案是因为大多数人也不理解它! - ephemient
非常感谢您深入的回答 - 这正是我所需要的。 - Matthew Iselin

2

setpgid POSIX C进程组最小示例

我认为玩弄基本API通常是学习新概念的最佳方法,所以让我们试试吧。

这说明了如果子进程没有使用setpgid更改其进程组,则信号确实会被发送到子进程。

main.c

#define _XOPEN_SOURCE 700
#include <assert.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

volatile sig_atomic_t is_child = 0;

void signal_handler(int sig) {
    char parent_str[] = "sigint parent\n";
    char child_str[] = "sigint child\n";
    signal(sig, signal_handler);
    if (sig == SIGINT) {
        if (is_child) {
            write(STDOUT_FILENO, child_str, sizeof(child_str) - 1);
        } else {
            write(STDOUT_FILENO, parent_str, sizeof(parent_str) - 1);
        }
    }
}

int main(int argc, char **argv) {
    pid_t pid, pgid;

    (void)argv;
    signal(SIGINT, signal_handler);
    signal(SIGUSR1, signal_handler);
    pid = fork();
    assert(pid != -1);
    if (pid == 0) {
        /* Change the pgid.
         * The new one is guaranteed to be different than the previous, which was equal to the parent's,
         * because `man setpgid` says:
         * > the child has its own unique process ID, and this PID does not match
         * > the ID of any existing process group (setpgid(2)) or session.
         */
        is_child = 1;
        if (argc > 1) {
            setpgid(0, 0);
        }
        printf("child pid, pgid = %ju, %ju\n", (uintmax_t)getpid(), (uintmax_t)getpgid(0));
        assert(kill(getppid(), SIGUSR1) == 0);
        while (1);
        exit(EXIT_SUCCESS);
    }
    /* Wait until the child sends a SIGUSR1. */
    pause();
    pgid = getpgid(0);
    printf("parent pid, pgid = %ju, %ju\n", (uintmax_t)getpid(), (uintmax_t)pgid);
    /* man kill explains that negative first argument means to send a signal to a process group. */
    kill(-pgid, SIGINT);
    while (1);
}

GitHub上游

使用以下命令进行编译:

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -Wpedantic -o setpgid setpgid.c

不使用 setpgid 运行

如果没有任何 CLI 参数,就不会执行 setpgid 操作:

./setpgid

可能的结果:
child pid, pgid = 28250, 28249
parent pid, pgid = 28249, 28249
sigint parent
sigint child

程序会卡住。

正如我们所看到的,两个进程的pgid相同,因为它在fork过程中被继承。

然后每当你点击:

Ctrl + C

它再次输出:
sigint parent
sigint child

以下内容是关于it技术的翻译:

  • 使用kill(-pgid, SIGINT)向整个进程组发送信号
  • 在终端上按下Ctrl + C默认会向整个进程组发送kill命令

通过向两个进程发送不同的信号,例如使用Ctrl + \和SIGQUIT,退出程序。

使用setpgid运行

如果您使用参数运行程序,例如:

./setpgid 1

然后子进程改变了它的pgid,现在每次只有一个sigint信号从父进程中打印出来:

child pid, pgid = 16470, 16470
parent pid, pgid = 16469, 16469
sigint parent

现在,每当您点击:

Ctrl + C

只有父级元素也会接收到该信号:

sigint parent

您仍然可以像以前一样使用SIGQUIT杀死父进程:

Ctrl + \

然而,该子进程现在具有不同的PGID,并且无法接收到该信号!可以从以下内容中看到:

ps aux | grep setpgid

您需要明确地使用以下命令来终止它:
kill -9 16470

这就清楚了信号组存在的原因:否则我们每次都需要手动清理一堆剩余进程。 在Ubuntu 18.04上进行测试。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接