Orphaned Process Groups

 

34.7.4 Orphaned Process Groups
When a process group loses all connection to the rest of its session โ€” SIGHUP, SIGCONT, and EIO
SIGHUP
+ SIGCONT sent
EIO
on terminal read
6
Interview Q&A

Key Concepts
Orphaned process group SIGHUP + SIGCONT EIO on read() Stopped members init adoption Session boundary SIGTTIN / SIGTTOU discarded SIGTSTP discarded tcsetpgrp ENOTTY

What Is an Orphaned Process Group?

The formal SUSv3 definition: a process group is orphaned if the parent of every member is either: in the same process group, OR in a completely different session.

Put more simply: a process group is not orphaned as long as at least one member has a parent that is in the same session but in a different process group. When that last “connection” disappears (because the parent exits), the group becomes orphaned.

The most common scenario: a parent process (process group leader) forks a child, then exits. The child is adopted by init. Since init is in a different session, the child’s process group is now orphaned.

Key insight: By definition, a session leader is always in an orphaned process group, because setsid() creates a new process group in the new session, and the session leader’s parent is in a different session.

Why Orphaned Process Groups Are Dangerous

Consider this scenario step by step:

1
Shell creates a process group with a parent and a child. The child is stopped (via SIGSTOP or SIGTSTP) while the parent is still alive. The shell monitors this job via wait().
2
The parent exits. The shell removes the parent’s process group from its job list. The child is adopted by init. The process group becomes orphaned.
3
The stopped child is now a background process for the terminal. Nobody monitors it with wait(). Nobody will ever send it SIGCONT. It will remain stopped forever โ€” a zombie-like stuck process.
POSIX / SUSv3 Solution: When a process group becomes orphaned and has any stopped members, the kernel automatically sends SIGHUP followed by SIGCONT to all members of that group. SIGHUP informs them they are now disconnected from their session. SIGCONT ensures they resume (so they can handle SIGHUP and exit cleanly). If the group has no stopped members, no signals are sent.

Process Group Orphaning โ€” Visual
BEFORE (Not Orphaned)

Session 4785
โ–ธ Shell PGID=4785 (SID=4785)
โ†ณ Parent PID=4827, PGID=4827
โ†ณ Child PID=4828, PGID=4827 [STOPPED]

Child’s parent (4827) is in same session (4785) but different process group โ†’ NOT orphaned

AFTER parent exits (Orphaned)

Session 4785
โ–ธ Shell PGID=4785
โ†ณ Child PID=4828, PGID=4827 [was STOPPED]
Different session
โ–ธ init PID=1 (adopted child)

Child’s parent is now init (different session) โ†’ ORPHANED โ†’ Kernel sends SIGHUP+SIGCONT

EIO Error on Terminal Read/Write in Orphaned Groups

Normally, if a background process tries to read() from the controlling terminal, it receives SIGTTIN which stops it. If it tries to write() (with TOSTOP set), it receives SIGTTOU.

But for an orphaned process group, sending SIGTTIN or SIGTTOU would stop the process permanently (since there is nobody to send it SIGCONT). So instead, the kernel returns EIO directly from read() or write().

Signal / Action Normal Background Group Orphaned Process Group
read() from terminal SIGTTIN sent (stops process) read() fails with EIO
write() to terminal (TOSTOP set) SIGTTOU sent (stops process) write() fails with EIO
SIGTSTP received (SIG_DFL) Process stopped (resumable) Signal silently discarded
SIGTTIN received (SIG_DFL) Process stopped Signal silently discarded
SIGTTOU received (SIG_DFL) Process stopped Signal silently discarded
tcsetpgrp() call Works normally Fails with ENOTTY
Exception: If a handler is installed for SIGTSTP/SIGTTIN/SIGTTOU, the signal IS delivered even to members of an orphaned process group โ€” it is only discarded when it would stop the process (i.e., when the disposition is SIG_DFL or SIG_DFL-equivalent).

Code Example 1: Demonstrating SIGHUP+SIGCONT on Orphaning (orphaned_pgrp_SIGHUP.c)

Creates a parent and children. The parent then exits, orphaning the children’s process group. Children that were stopped receive SIGHUP + SIGCONT. Children that were not stopped receive nothing.

/* orphaned_pgrp_SIGHUP.c
 * Demonstrates SIGHUP+SIGCONT sent to orphaned process group with stopped members.
 * Compile: gcc -o orphaned_pgrp_SIGHUP orphaned_pgrp_SIGHUP.c
 * Run:     ./orphaned_pgrp_SIGHUP s p
 *   's' = child stops itself (raise SIGSTOP)
 *   'p' = child just pauses (calls pause())
 * Observe: stopped children receive SIGHUP+SIGCONT; pausing children also receive them
 */
#define _GNU_SOURCE
#include <string.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static void handler(int sig)
{
    /* UNSAFE: printf is not async-signal-safe, but OK for demo */
    printf("PID=%ld: caught signal %d (%s)\n",
           (long)getpid(), sig, strsignal(sig));
    fflush(stdout);
}

int main(int argc, char *argv[])
{
    int j;
    struct sigaction sa;

    if (argc < 2) {
        fprintf(stderr, "Usage: %s {s|p} ...\n", argv[0]);
        fprintf(stderr, "  s = child stops itself\n");
        fprintf(stderr, "  p = child just pauses\n");
        return 1;
    }

    /* Make stdout unbuffered so output appears immediately */
    setbuf(stdout, NULL);

    /* Install handler for SIGHUP and SIGCONT */
    sigemptyset(&sa.sa_mask);
    sa.sa_flags   = 0;
    sa.sa_handler = handler;
    if (sigaction(SIGHUP, &sa, NULL) == -1)  { perror("sigaction"); return 1; }
    if (sigaction(SIGCONT, &sa, NULL) == -1) { perror("sigaction"); return 1; }

    printf("parent: PID=%ld, PPID=%ld, PGID=%ld, SID=%ld\n",
           (long)getpid(), (long)getppid(),
           (long)getpgrp(), (long)getsid(0));

    /* Create one child for each command-line argument */
    for (j = 1; j < argc; j++) {
        switch (fork()) {
        case -1:
            perror("fork"); return 1;

        case 0:  /* Child */
            printf("child: PID=%ld, PPID=%ld, PGID=%ld, SID=%ld\n",
                   (long)getpid(), (long)getppid(),
                   (long)getpgrp(), (long)getsid(0));

            if (argv[j][0] == 's') {
                /* This child stops itself */
                printf("PID=%ld stopping itself with SIGSTOP\n", (long)getpid());
                fflush(stdout);
                raise(SIGSTOP);
                /* Execution resumes here after receiving SIGCONT */
                printf("PID=%ld resumed after stop\n", (long)getpid());
            } else {
                /* This child just pauses waiting for signals */
                alarm(60);  /* Safety exit after 60s */
                printf("PID=%ld pausing for signals\n", (long)getpid());
                fflush(stdout);
                pause();
            }
            _exit(EXIT_SUCCESS);

        default: /* Parent continues creating more children */
            break;
        }
    }

    /* Parent: sleep briefly to let children set up, then exit.
     * When parent exits, children's process group becomes orphaned.
     * If any child is stopped, kernel sends SIGHUP+SIGCONT to all. */
    sleep(2);
    printf("parent exiting - this will orphan the children's process group\n");
    exit(EXIT_SUCCESS);
}

/* Expected session (run: ./orphaned_pgrp_SIGHUP s p):
 *
 * $ echo $$
 * 4785
 * $ ./orphaned_pgrp_SIGHUP s p
 * parent: PID=4827, PPID=4785, PGID=4827, SID=4785
 * child:  PID=4828, PPID=4827, PGID=4827, SID=4785   <-- 's' child
 * PID=4828 stopping itself with SIGSTOP
 * child:  PID=4829, PPID=4827, PGID=4827, SID=4785   <-- 'p' child
 * PID=4829 pausing for signals
 * parent exiting - this will orphan the children's process group
 * $                                     <-- shell prompt appears (parent exited)
 * PID=4828: caught signal 18 (Continued)  <-- SIGCONT first
 * PID=4828: caught signal 1 (Hangup)      <-- then SIGHUP
 * PID=4829: caught signal 18 (Continued)
 * PID=4829: caught signal 1 (Hangup)
 *
 * Second run (./orphaned_pgrp_SIGHUP p p) - no stopped children:
 * parent exiting - this will orphan the children's process group
 * [NO SIGNALS SENT - no stopped members in group]
 */

Code Example 2: EIO on Terminal Read in Orphaned Group (orphan_eio.c)

Demonstrates that a member of an orphaned process group receives EIO (not SIGTTIN) when attempting to read() from the controlling terminal.

/* orphan_eio.c
 * Demonstrates EIO on read() from controlling terminal for orphaned process group.
 * Compile: gcc -o orphan_eio orphan_eio.c
 * Run:     ./orphan_eio
 *
 * What happens:
 * 1. Parent forks a child and immediately exits.
 * 2. Child is adopted by init -> its process group becomes orphaned.
 * 3. Child tries to read() from /dev/tty (the controlling terminal).
 * 4. Normally a background process would get SIGTTIN, but since the
 *    group is orphaned, read() returns -1 with errno == EIO.
 */
#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <signal.h>

static void sigttin_handler(int sig)
{
    printf("PID=%ld: received SIGTTIN (unexpected for orphaned group)\n",
           (long)getpid());
    fflush(stdout);
}

int main(void)
{
    pid_t child_pid;

    setbuf(stdout, NULL);

    /* Make child its own process group leader */
    child_pid = fork();
    if (child_pid == -1) { perror("fork"); return 1; }

    if (child_pid != 0) {
        /* Parent: exit immediately to orphan the child's group */
        printf("parent PID=%ld exiting to orphan child PID=%ld\n",
               (long)getpid(), (long)child_pid);
        exit(EXIT_SUCCESS);
    }

    /* ---- Child process ---- */

    /* Move child into its own process group (so parent's exit orphans it) */
    if (setpgid(0, 0) == -1) { perror("setpgid"); _exit(1); }

    /* Give parent time to exit, ensuring orphaning has happened */
    sleep(1);

    printf("child PID=%ld: PPID=%ld (should be 1=init), PGID=%ld\n",
           (long)getpid(), (long)getppid(), (long)getpgrp());
    printf("child: process group is now orphaned\n");

    /* Install SIGTTIN handler so we can detect if it's sent
     * (for an orphaned group, it should NOT be - EIO is returned instead) */
    struct sigaction sa;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags   = 0;
    sa.sa_handler = sigttin_handler;
    sigaction(SIGTTIN, &sa, NULL);

    /* Attempt to read from the controlling terminal.
     * For an orphaned process group, this should fail with EIO. */
    int ttyfd = open("/dev/tty", O_RDONLY);
    if (ttyfd == -1) {
        printf("child: open(/dev/tty) failed: %s\n", strerror(errno));
        _exit(1);
    }

    printf("child: attempting read() from controlling terminal...\n");
    fflush(stdout);

    char buf[64];
    ssize_t n = read(ttyfd, buf, sizeof(buf) - 1);

    if (n == -1) {
        if (errno == EIO) {
            printf("child: read() failed with EIO as expected for orphaned group!\n");
            printf("       (SIGTTIN was NOT sent because group is orphaned)\n");
        } else {
            printf("child: read() failed with unexpected error: %s\n", strerror(errno));
        }
    } else {
        buf[n] = '\0';
        printf("child: read() succeeded unexpectedly, got: %s\n", buf);
    }

    close(ttyfd);
    _exit(EXIT_SUCCESS);
}

/* Expected output:
 * $ ./orphan_eio
 * parent PID=5001 exiting to orphan child PID=5002
 * $                             <-- shell prompt (parent exited)
 * child PID=5002: PPID=1 (should be 1=init), PGID=5002
 * child: process group is now orphaned
 * child: attempting read() from controlling terminal...
 * child: read() failed with EIO as expected for orphaned group!
 *        (SIGTTIN was NOT sent because group is orphaned)
 *
 * Note: PPID may show as the shell's PID briefly before init adopts it.
 * The key result is the EIO error from read().
 */
When is SIGTSTP/SIGTTIN/SIGTTOU silently discarded? Only when the signal’s disposition would cause the process to stop (i.e., disposition is SIG_DFL). If a handler is installed, the signal is delivered normally. This is important for programs like daemons that install handlers for these signals for logging purposes.

Interview Questions

Q1: What is the formal definition of an orphaned process group?

A process group is orphaned if the parent of every member is either in the same process group, or in a completely different session. Equivalently, a process group is NOT orphaned as long as at least one member has a parent in the same session but a different process group. When that last connection is broken (typically by the parent exiting), the group becomes orphaned.

Q2: Why does the kernel send SIGHUP followed by SIGCONT (not just SIGHUP) to an orphaned group?

The scenario being addressed is specifically when the orphaned group has stopped members. If the kernel sent only SIGHUP, a stopped process could not handle it (stopped processes don’t process signals). SIGCONT resumes the stopped process so it can receive and act on SIGHUP. The SIGCONT ensures the stopped child is not permanently stuck. If no members are stopped, neither signal is sent.

Q3: Why does read() return EIO instead of delivering SIGTTIN to an orphaned process group?

Delivering SIGTTIN to an orphaned process group would stop the process, and since no process in a different group within the same session is monitoring it, it would remain stopped forever โ€” which is the exact problem orphaned group handling is trying to prevent. Instead, EIO gives the process a clear error it can handle programmatically, allowing it to exit cleanly.

Q4: Is SIGTSTP always discarded for members of an orphaned process group?

No. SIGTSTP is only discarded (silently) if it would cause the process to stop โ€” that is, when the disposition is SIG_DFL. If the process has installed a signal handler for SIGTSTP, the signal is delivered and the handler runs. The discarding rule applies specifically to the stopping effect, not to signal delivery in general.

Q5: Why is a session leader always considered to be in an orphaned process group?

When setsid() is called, it creates a new session and a new process group in that session. The calling process (the new session leader) is the only member of that new process group. Its parent is in a completely different session. By the formal definition, every member’s parent is in a different session โ€” so the group is orphaned from the moment of creation.

Q6: What happens if tcsetpgrp() is called by a member of an orphaned process group?

It fails with the error ENOTTY. Similarly, tcsetattr(), tcflush(), tcflow(), tcsendbreak(), and tcdrain() all fail with EIO when called by a member of an orphaned process group. This is because an orphaned group has no valid controlling terminal relationship, and allowing these calls would have no meaningful effect or could cause confusion.

Leave a Reply

Your email address will not be published. Required fields are marked *