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.
setsid() creates a new process group in the new session, and the session leader’s parent is in a different session.Consider this scenario step by step:
wait().init. The process group becomes orphaned.|
BEFORE (Not Orphaned)
Session 4785 Child’s parent (4827) is in same session (4785) but different process group โ NOT orphaned |
AFTER parent exits (Orphaned)
Session 4785 Child’s parent is now init (different session) โ ORPHANED โ Kernel sends SIGHUP+SIGCONT |
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 |
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]
*/
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().
*/
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.
