QUESTION
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
Structure diagram
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
Call path
Recording PID, PGID, SID, and foreground PGID in one table makes daemonization and shell job-control problems much easier to distinguish.
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.
SOURCE COORDINATES
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.
| File | Function / structure | What 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 PROGRAM
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.
cc -std=c17 -Wall -Wextra -O2 job_ids.c -o job_ids01#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
Code notes
pid_t pgid = getpgrpGets the process-group ID of the calling process. A pipeline commonly uses the PID of its first child as the PGID.
getsid(0)0 means the calling process itself. A session ID is equal to the PID of its session leader.
tcgetpgrpReads the foreground PGID of the terminal connected to stdin. ENOTTY is normal when stdin is a plain redirection or pipe.
foreground == pgidIf the current job is in the foreground, it can read the terminal and receive terminal-generated signals normally.
errno == ENOTTYFor a daemon or pipeline, classify the absence of a controlling terminal as a distinct execution environment rather than only an error message.
DETAILS
Detailed behavior
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.
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.
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
Objects and lifetimes
| Object | Creation and release | Values to inspect |
|---|---|---|
process group | Created with setpgid and removed when its last member leaves | PGID and orphaned status |
session | Created with setsid and ends when all contained process groups disappear | SID, leader, tty |
controlling tty | Attached by the session leader and disconnected on hangup/revoke | foreground pgrp, termios |
FAILURE PATH
Failure conditions and common misconceptions
| Observed symptom | Likely causes | How to verify |
|---|---|---|
| A background read stops | SIGTTIN job-control stop | ps state, TPGID, signal disposition |
| setsid returns EPERM | The caller is a process-group leader | Compare PID and PGID |
| Ctrl-C reaches only some children | The pipeline PGID was not configured | ps -o pid,pgid,sid,tpgid |
LAB
Verify it yourself
- Run the program in a terminal, through a pipe, and in the background, then compare the foreground PGID and ENOTTY results.
- Run sleep 100 | cat in a shell and inspect the pipeline group with ps -o pid,ppid,pgid,sid,tpgid,stat.
- Write a small parent that places two children in the same PGID and terminates the whole group with kill(-pgid, SIGTERM).
./job_idsstrace -e trace=getpid,getpgid,getsid,ioctl ./job_idsPRIMARY REFERENCES