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 call setsid() in the child.

Job Control Basics

The shell manages foreground and background jobs by manipulating process groups and the terminal's foreground group.

ActionShell commandWhat happens
Run foreground./progShell sets prog's PGID as terminal foreground group
Run background./prog &Shell keeps its own PGID as foreground group
SuspendCtrl+ZKernel sends SIGTSTP to foreground group
Resume foregroundfgShell calls tcsetpgrp() + sends SIGCONT
Resume backgroundbgShell 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_group normally, 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 udevd daemon 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 daemonize crate 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_info from a shell. Then run cat | ./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

  1. What does setsid() do, and why must the caller not be a process group leader?

  2. Why does the classic daemon recipe fork twice?

  3. 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 SIGPIPE and dies.

  • Manual daemonization under systemd: If systemd starts your service, do not daemonize. systemd expects Type=simple services to stay in the foreground.

  • Ignoring SIGHUP in daemons: When the session leader exits, SIGHUP is sent to the session. Daemons must handle or ignore it.