Process · Linux userspace / kernel ABI

process group, session, controlling terminal

Explains why shell job control sends signals to a process group rather than one PID and changes the terminal's foreground group.

Series
09 / 38
Build
cc -std=c17 -Wall -Wextra -O2 job_ids.c -o job_ids
Run
./job_ids
Kernel
Linux 6.18.37 LTS

Which process receives SIGINT when Ctrl-C is pressed?

The terminal driver does not send SIGINT to the one shell process that read the key. It sends the signal to the entire foreground process group of the controlling terminal. To treat a pipeline as one job, the shell must put all children in the same PGID.

A session is the parent unit of process groups, and a session leader can acquire a controlling terminal. setsid creates a new session and process group and disconnects any existing controlling-terminal association.

Structure diagram

Figure 1. Session, process-group, and controlling-terminal hierarchy
Session SID=2100session leader=shell · controlling tty=/dev/pts/3

Foreground PGID=2400

  • grep PID 2400
  • sort PID 2401
  • terminal VINTR → SIGINT

Background PGID=2500

  • sleep PID 2500
  • tty read → SIGTTIN
  • job table entry

Shell PGID=2100

  • job control owner
  • tcsetpgrp
  • waitpid(-PGID)

A terminal foreground target is a process group, not one PID. Processes in the same pipeline share one PGID.

Call path

Figure 2. From userspace code to observable results
shell pipeline fork
setpgid form job PGID
tcsetpgrp set foreground
terminal key VINTR/VSUSP
group signal deliver to the whole job

Recording PID, PGID, SID, and foreground PGID in one table makes daemonization and shell job-control problems much easier to distinguish.

Figure 3. Major points along the kernel-internal path
tty input line discipline
tty_signal foreground pgrp lookup
kill_pgrp group signal queue
get_signal select each thread
handler/default stop/terminate

This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.

Linux 6.18.37 LTS source locations

Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.

FileFunction / structureWhat to inspect
kernel/sys.c ksys_setsid(), setpgid() Rules for creating sessions and process groups
drivers/tty/tty_jobctrl.c tty_check_change(), __tty_check_change() Check terminal access by a background group
drivers/tty/n_tty.c isig(), n_tty_receive_signal_char() Convert a terminal control character into a group signal

Complete runnable example

The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.

Buildcc -std=c17 -Wall -Wextra -O2 job_ids.c -o job_ids
01#define _POSIX_C_SOURCE 200809L
02#include <errno.h>
03#include <stdio.h>
04#include <termios.h>
05#include <unistd.h>
06
07int main(void)
08{
09    pid_t pid = getpid();
10    pid_t pgid = getpgrp();
11    pid_t sid = getsid(0);
12    pid_t foreground = tcgetpgrp(STDIN_FILENO);
13
14    printf("pid=%ld pgid=%ld sid=%ld\n",
15           (long)pid, (long)pgid, (long)sid);
16    if (foreground >= 0)
17        printf("tty foreground pgid=%ld%s\n", (long)foreground,
18               foreground == pgid ? " (our group)" : "");
19    else if (errno == ENOTTY)
20        puts("stdin is not a controlling terminal");
21    else
22        perror("tcgetpgrp");
23    return 0;
24}

Code notes

Source line 10pid_t pgid = getpgrp

Gets the process-group ID of the calling process. A pipeline commonly uses the PID of its first child as the PGID.

Source line 11getsid(0)

0 means the calling process itself. A session ID is equal to the PID of its session leader.

Source line 12tcgetpgrp

Reads the foreground PGID of the terminal connected to stdin. ENOTTY is normal when stdin is a plain redirection or pipe.

Source line 18foreground == pgid

If the current job is in the foreground, it can read the terminal and receive terminal-generated signals normally.

Source line 19errno == ENOTTY

For a daemon or pipeline, classify the absence of a controlling terminal as a distinct execution environment rather than only an error message.

Detailed behavior

01

Pipeline creation has a parent-child race

The shell chooses the first child's PID as the PGID and calls setpgid in both parent and child so the group is formed regardless of which runs first. After the child execs, setpgid may fail with EACCES.

While waiting for a foreground job, the shell adjusts its own handling of job-control signals such as SIGTTOU.

02

Background terminal access is restricted

A background process group that reads from its controlling terminal can be stopped by SIGTTIN. With the TOSTOP flag, writes can also receive SIGTTOU.

Before diagnosing a process whose logging suddenly stops as deadlocked, check state T plus PGID and TPGID in ps.

03

Conditions for calling setsid

A process-group leader cannot call setsid. Traditional daemon code calls setsid in a forked child to ensure that the child PID differs from the existing PGID.

Under a modern service manager, it is preferable to run in the foreground with explicit fd and signal protocols instead of breaking the supervisor relationship with a double fork.

Objects and lifetimes

ObjectCreation and releaseValues to inspect
process groupCreated with setpgid and removed when its last member leavesPGID and orphaned status
sessionCreated with setsid and ends when all contained process groups disappearSID, leader, tty
controlling ttyAttached by the session leader and disconnected on hangup/revokeforeground pgrp, termios

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
A background read stopsSIGTTIN job-control stopps state, TPGID, signal disposition
setsid returns EPERMThe caller is a process-group leaderCompare PID and PGID
Ctrl-C reaches only some childrenThe pipeline PGID was not configuredps -o pid,pgid,sid,tpgid

Verify it yourself

  1. Run the program in a terminal, through a pipe, and in the background, then compare the foreground PGID and ENOTTY results.
  2. Run sleep 100 | cat in a shell and inspect the pipeline group with ps -o pid,ppid,pgid,sid,tpgid,stat.
  3. Write a small parent that places two children in the same PGID and terminates the whole group with kill(-pgid, SIGTERM).
Run./job_ids
Tracestrace -e trace=getpid,getpgid,getsid,ioctl ./job_ids

Primary sources