A process group is a kernel-maintained set of processes identified by a process group ID, or PGID. It provides a target for operations such as delivering a signal to a job. Parent-child relationships and group membership are separate: related processes can occupy different groups, and a group’s leader does not need to remain alive for other members to continue belonging to it.
When it breaks
Signaling a process does not automatically signal its children. Signaling its group selects current group members, not every descendant in the process tree. A child that starts another session or group can be outside that target. Conversely, multiple siblings can deliberately share a group even though neither sibling is the other’s parent.
Group delivery also does not guarantee a uniform response. One member can use the signal’s default action, another can install a handler, and another can ignore or block an eligible signal. The operation therefore does not atomically stop a job or prove that all its work completed. Per-process dispositions and the application’s shutdown protocol still determine the outcome.
The leader’s exit is not the group’s exit. A PGID can remain meaningful while another member survives, even after the leader is reaped. Supervisors that watch only the leader’s status may report completion while a member continues running. Group identity should be combined with actual lifecycle management rather than treated as a permanent identifier for all work launched together.
Check it quickly
Save this as groups.py and run python3 groups.py in a disposable Linux environment with Python 3.11 or later. The recorded run used Linux 6.12.76 and Python 3.11.2. Python’s process_group option assigns groups before execution, while start_new_session requests a new session. Every signaled process is a child created by this fixture:
import os,signal,subprocess,sys
worker=r'''
import os,signal,sys
if sys.argv[1]=='ignore-term':signal.signal(signal.SIGTERM,signal.SIG_IGN)
signal.signal(signal.SIGUSR1,lambda *_:print('USR1',flush=True))
print(os.getpid(),os.getpgrp(),os.getsid(0),flush=True)
while True:signal.pause()
'''
children=[]
def launch(role,**settings):
process=subprocess.Popen([sys.executable,'-c',worker,role],stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True,**settings)
children.append(process)
identity=tuple(map(int,process.stdout.readline().split()))
assert len(identity)==3 and identity[0]==process.pid
return process,identity
try:
leader,first=launch('default',process_group=0)
member,second=launch('ignore-term',process_group=leader.pid)
outsider,third=launch('default',start_new_session=True)
group=leader.pid
assert first[1]==second[1]==group and first[2]==second[2]==os.getsid(0)
assert third==(outsider.pid,outsider.pid,outsider.pid)
assert group!=os.getpgrp() and third[1]!=group
print('two sibling children share a group; another sibling starts a separate session and group')
os.killpg(group,signal.SIGTERM)
assert leader.wait(timeout=5)==-signal.SIGTERM
assert member.poll() is None and outsider.poll() is None
assert os.getpgid(member.pid)==group
print('group TERM terminates the default recipient, leaves the ignoring member alive, and excludes the other group')
os.killpg(group,signal.SIGUSR1)
assert member.stdout.readline().strip()=='USR1'
print('the group remains addressable after its leader has exited and been reaped')
os.killpg(group,signal.SIGKILL)
assert member.wait(timeout=5)==-signal.SIGKILL
assert outsider.poll() is None
outsider.terminate();assert outsider.wait(timeout=5)==-signal.SIGTERM
print('cleanup reaps all three children; signaling a group did not signal the whole family tree')
finally:
for process in children:
if process.poll() is None:process.kill();process.wait()
The first child starts a new process group whose PGID equals its PID. The second child joins that group within the controller’s session. The third child starts a separate session, becoming both its session leader and process-group leader. The script reads actual PID, PGID, and session ID values rather than assuming the requested arrangement took effect.
All three are siblings launched by the same controller. The first two share a signal target, while the third does not. This configuration makes the difference between parenthood and group membership explicit. The script also asserts that the target PGID differs from the controller’s own group, so the group operation cannot accidentally target its controlling process.
Sending TERM to the shared group terminates the first child, which uses the default disposition. The second deliberately ignores TERM and remains alive, as does the child outside the group. The controller waits for and reaps the first child, then confirms that the surviving member still reports the departed leader’s PID as its PGID.
A subsequent group-directed USR1 reaches that surviving member, whose installed handler prints an acknowledgment. This establishes that the group remains addressable after the leader has exited. The example uses a handler acknowledgment for this particular request instead of inferring application response solely from the signal-sending call’s successful return.
Finally, KILL terminates the remaining group member while leaving the separate-session child alive. The controller terminates that third child individually and reaps all three. Cleanup also handles an assertion failure. The result demonstrates selective group targeting and differing dispositions, without requiring a terminal or sending a signal to unrelated host processes.
Separate sessions, terminals, and supervision
setpgid changes membership subject to session and process-state restrictions. setsid creates a new session and group for an eligible caller; a process-group leader cannot use it directly. Creating a session does not change who a process’s parent is. Reparenting, terminal association, and group selection are related operational concerns, but they are not one interchangeable action.
An interactive shell with job control typically places a pipeline in its own group and assigns the terminal’s foreground PGID. The terminal driver delivers configured control-character signals to that foreground group. A noninteractive shell does not universally create one group per pipeline, so the grouping policy must be verified in the actual launch mode.
Signal permissions remain per recipient. Linux documents group-send success as delivery to at least one eligible process; it is not acknowledgment from every member. Use the appropriate service manager or an explicit child-management protocol when completion matters, and observe exits or application acknowledgments. A convenient group target helps control a job, but does not replace tracking its real members and outcomes.
Sources
- setpgid(2), process-group membership, inheritance, and session restrictions, checked 2026-09-12.
- setsid(2), creating a session and group, with process-group-leader restrictions, checked 2026-09-12.
- kill(2), group-directed delivery, permission checks, and success semantics, checked 2026-09-12.
- credentials(7), process IDs, groups, sessions, and controlling terminals, checked 2026-09-12.