Process Groups, Sessions, and Daemons
Unix organizes processes into groups and sessions. This hierarchy controls which processes receive signals from the terminal, how job control works, and how daemons detach from everything. If you write a server, a driver helper, or anything that outlives a login session, you need this.
The Process Hierarchy
Session (SID)
|
+-- Process Group (PGID) -- foreground job
| +-- Process (PID)
| +-- Process (PID)
|
+-- Process Group (PGID) -- background job
| +-- Process (PID)
|
+-- Process Group (PGID) -- background job
+-- Process (PID)
+-- Process (PID)
A session is a collection of process groups, typically one per login. A
process group is a collection of processes, typically one per pipeline. The
session leader is the process that called setsid() -- usually the shell.
Process Groups
Every process belongs to a process group. The group is identified by the PID of its leader.
/* pgid_demo.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void)
{
printf("Parent: PID=%d PGID=%d SID=%d\n",
getpid(), getpgrp(), getsid(0));
pid_t pid = fork();
if (pid == 0) {
printf("Child before setpgid: PID=%d PGID=%d\n",
getpid(), getpgrp());
/* Put child in its own process group */
setpgid(0, 0);
printf("Child after setpgid: PID=%d PGID=%d\n",
getpid(), getpgrp());
_exit(0);
}
waitpid(pid, NULL, 0);
return 0;
}
$ gcc -o pgid_demo pgid_demo.c && ./pgid_demo
Parent: PID=5000 PGID=5000 SID=4900
Child before setpgid: PID=5001 PGID=5000
Child after setpgid: PID=5001 PGID=5001
setpgid(0, 0) means "set my PGID to my own PID" -- making the calling process
a new group leader.
Why Process Groups Matter
When you press Ctrl+C in a terminal, the kernel sends SIGINT to the entire
foreground process group, not just one process. A pipeline like
cat file | grep pattern | wc -l runs as three processes in one group, so
Ctrl+C kills them all.
Terminal (controlling terminal)
|
| SIGINT (Ctrl+C)
v
Foreground Process Group
+-- cat (receives SIGINT)
+-- grep (receives SIGINT)
+-- wc (receives SIGINT)
Sessions and Controlling Terminals
A session is created by setsid(). The calling process becomes the session
leader and is disconnected from any controlling terminal.
/* session_demo.c */
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void)
{
printf("Original: PID=%d PGID=%d SID=%d\n",
getpid(), getpgrp(), getsid(0));
pid_t pid = fork();
if (pid == 0) {
/* setsid() fails if caller is already a group leader,
so we fork first to guarantee we are not */
pid_t new_sid = setsid();
if (new_sid < 0) {
perror("setsid");
_exit(1);
}
printf("Child: PID=%d PGID=%d SID=%d\n",
getpid(), getpgrp(), getsid(0));
/* Now PID == PGID == SID -- session leader */
_exit(0);
}
waitpid(pid, NULL, 0);
return 0;
}
Caution:
setsid()fails if the calling process is already a process group leader (PID == PGID). The standard trick: fork first, then callsetsid()in the child.
Job Control Basics
The shell manages foreground and background jobs by manipulating process groups and the terminal's foreground group.
| Action | Shell command | What happens |
|---|---|---|
| Run foreground | ./prog | Shell sets prog's PGID as terminal foreground group |
| Run background | ./prog & | Shell keeps its own PGID as foreground group |
| Suspend | Ctrl+Z | Kernel sends SIGTSTP to foreground group |
| Resume foreground | fg | Shell calls tcsetpgrp() + sends SIGCONT |
| Resume background | bg | Shell sends SIGCONT without changing foreground group |
/* fg_group.c -- show foreground process group */
#include <stdio.h>
#include <unistd.h>
int main(void)
{
pid_t fg = tcgetpgrp(STDIN_FILENO);
printf("Foreground PGID: %d\n", fg);
printf("My PID: %d\n", getpid());
printf("My PGID: %d\n", getpgrp());
return 0;
}
Try It: Run
fg_groupnormally, then run it with./fg_group &. Compare the foreground PGID to your PGID in each case.
The Classic Daemon Recipe
A daemon is a process that runs in the background, detached from any terminal. The traditional recipe:
/* daemon_classic.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
static void daemonize(void)
{
/* Step 1: Fork and let parent exit */
pid_t pid = fork();
if (pid < 0) { perror("fork"); exit(1); }
if (pid > 0) _exit(0); /* Parent exits */
/* Step 2: Create new session */
if (setsid() < 0) { perror("setsid"); exit(1); }
/* Step 3: Ignore SIGHUP, fork again to prevent
acquiring a controlling terminal */
signal(SIGHUP, SIG_IGN);
pid = fork();
if (pid < 0) { perror("fork"); exit(1); }
if (pid > 0) _exit(0); /* First child exits */
/* Step 4: Change working directory */
chdir("/");
/* Step 5: Reset umask */
umask(0);
/* Step 6: Close all open file descriptors */
for (int fd = sysconf(_SC_OPEN_MAX); fd >= 0; fd--)
close(fd);
/* Step 7: Redirect stdin/stdout/stderr to /dev/null */
open("/dev/null", O_RDWR); /* stdin = fd 0 */
dup(0); /* stdout = fd 1 */
dup(0); /* stderr = fd 2 */
}
int main(void)
{
daemonize();
/* Daemon work loop */
FILE *log = fopen("/tmp/daemon_demo.log", "a");
if (!log) _exit(1);
for (int i = 0; i < 10; i++) {
fprintf(log, "Daemon tick %d, PID=%d\n", i, getpid());
fflush(log);
sleep(2);
}
fclose(log);
return 0;
}
The double-fork pattern:
Shell
|
+-- fork() --> Parent exits (shell gets exit status)
|
+-- setsid() --> New session, no controlling terminal
|
+-- fork() --> First child exits
|
+-- Daemon (not session leader,
cannot acquire controlling terminal)
Caution: The double fork is critical. A session leader that opens a terminal device can acquire it as a controlling terminal. The second fork ensures the daemon is not a session leader.
The Modern Way: systemd
On modern Linux systems, systemd manages daemons. You do not need to daemonize manually. Instead, write a simple foreground program and let systemd handle it.
A systemd service file:
# /etc/systemd/system/myservice.service
[Unit]
Description=My Demo Service
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/myservice
Restart=on-failure
User=nobody
[Install]
WantedBy=multi-user.target
Your program just runs in the foreground:
/* myservice.c -- systemd-friendly daemon */
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
static volatile sig_atomic_t running = 1;
static void handle_term(int sig)
{
(void)sig;
running = 0;
}
int main(void)
{
signal(SIGTERM, handle_term);
while (running) {
printf("Service tick, PID=%d\n", getpid());
fflush(stdout);
sleep(5);
}
printf("Service shutting down\n");
return 0;
}
systemd captures stdout to the journal. No syslog gymnastics needed.
Driver Prep: Kernel drivers do not daemonize -- they are loaded into kernel space. But user-space driver companions (firmware loaders, device managers, monitoring daemons) absolutely do. The
udevddaemon is a perfect example: it manages device nodes and runs in the background from boot.
Rust: Daemon Patterns with nix
// daemon_nix.rs // Cargo.toml: nix = { version = "0.29", features = ["process", "signal", "fs"] } use nix::unistd::{fork, ForkResult, setsid, chdir, close, dup2}; use nix::sys::stat::umask; use nix::sys::stat::Mode; use std::fs::OpenOptions; use std::os::unix::io::AsRawFd; use std::process::exit; use std::io::Write; use std::thread; use std::time::Duration; fn daemonize() { // First fork match unsafe { fork() }.expect("first fork failed") { ForkResult::Parent { .. } => exit(0), ForkResult::Child => {} } // New session setsid().expect("setsid failed"); // Second fork match unsafe { fork() }.expect("second fork failed") { ForkResult::Parent { .. } => exit(0), ForkResult::Child => {} } // Change directory chdir("/").expect("chdir failed"); // Reset umask umask(Mode::empty()); // Redirect std fds to /dev/null let devnull = OpenOptions::new() .read(true) .write(true) .open("/dev/null") .expect("open /dev/null"); let fd = devnull.as_raw_fd(); dup2(fd, 0).ok(); dup2(fd, 1).ok(); dup2(fd, 2).ok(); if fd > 2 { close(fd).ok(); } } fn main() { daemonize(); let mut log = OpenOptions::new() .create(true) .append(true) .open("/tmp/rust_daemon.log") .expect("open log"); for i in 0..10 { writeln!(log, "Rust daemon tick {}, PID={}", i, std::process::id()) .expect("write log"); log.flush().expect("flush"); thread::sleep(Duration::from_secs(2)); } }
Rust Note: In practice, most Rust services run as simple foreground processes under systemd. The
daemonizecrate exists for cases where you truly need the classic pattern, but it is increasingly rare.
Rust: Process Groups with nix
// pgid_nix.rs // Cargo.toml: nix = { version = "0.29", features = ["process"] } use nix::unistd::{fork, ForkResult, getpid, getpgrp, setpgid, Pid}; use nix::sys::wait::waitpid; use std::process::exit; fn main() { println!("Parent: PID={} PGID={}", getpid(), getpgrp()); match unsafe { fork() }.expect("fork failed") { ForkResult::Parent { child } => { waitpid(child, None).expect("waitpid"); } ForkResult::Child => { println!("Child before: PID={} PGID={}", getpid(), getpgrp()); setpgid(Pid::from_raw(0), Pid::from_raw(0)) .expect("setpgid"); println!("Child after: PID={} PGID={}", getpid(), getpgrp()); exit(0); } } }
A Process Hierarchy Inspector
This utility prints the session, process group, and parent for the current process:
/* proc_info.c */
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("PID: %d\n", getpid());
printf("Parent PID: %d\n", getppid());
printf("Process Group ID: %d\n", getpgrp());
printf("Session ID: %d\n", getsid(0));
printf("Foreground PGID: %d\n", tcgetpgrp(STDIN_FILENO));
printf("Is session leader: %s\n",
getpid() == getsid(0) ? "yes" : "no");
printf("Is group leader: %s\n",
getpid() == getpgrp() ? "yes" : "no");
return 0;
}
Try It: Run
proc_infofrom a shell. Then runcat | ./proc_info(pipe into it). Compare the Session ID and Foreground PGID. Why does the PGID change in the piped case?
Sending Signals to Process Groups
The kill() system call with a negative PID sends a signal to an entire
process group:
/* kill_group.c */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(void)
{
/* Create 3 children in the same new process group */
pid_t first_child = 0;
for (int i = 0; i < 3; i++) {
pid_t pid = fork();
if (pid == 0) {
if (i == 0) setpgid(0, 0);
else setpgid(0, first_child);
printf("Child %d: PID=%d PGID=%d\n", i, getpid(), getpgrp());
pause(); /* Wait for signal */
_exit(0);
}
if (i == 0) first_child = pid;
setpgid(pid, first_child);
}
sleep(1);
printf("Parent: sending SIGTERM to group %d\n", first_child);
kill(-first_child, SIGTERM); /* Negative PID = process group */
for (int i = 0; i < 3; i++) {
int status;
pid_t w = wait(&status);
if (WIFSIGNALED(status))
printf("Reaped %d, killed by signal %d\n", w, WTERMSIG(status));
}
return 0;
}
kill(-PGID, SIGTERM)
|
+----------+---------+
| | |
Child 0 Child 1 Child 2
(all share the same PGID)
Knowledge Check
-
What does
setsid()do, and why must the caller not be a process group leader? -
Why does the classic daemon recipe fork twice?
-
When you press Ctrl+C in a terminal, which processes receive
SIGINT?
Common Pitfalls
-
Calling setsid() as group leader: It fails with
EPERM. Fork first. -
Single fork for daemons: The process remains a session leader and can accidentally acquire a controlling terminal.
-
Forgetting to close file descriptors: Inherited fds can hold locks, keep files open, or leak information. Close them all.
-
Not redirecting stdio: A daemon with no terminal that writes to stdout gets
SIGPIPEand dies. -
Manual daemonization under systemd: If systemd starts your service, do not daemonize. systemd expects
Type=simpleservices to stay in the foreground. -
Ignoring SIGHUP in daemons: When the session leader exits,
SIGHUPis sent to the session. Daemons must handle or ignore it.