Loading content...
Behind every spooling system operates one or more daemon processes—background services that run continuously, waiting for work and processing jobs without direct user interaction. These daemons are the silent engines that transform spooled data into physical output.
The term "daemon" comes from Greek mythology, referring to benevolent spirits that work behind the scenes. In computing, daemons are processes that run in the background, detached from any controlling terminal, providing services to users and system components alike.
This page covers daemon architecture, the classic UNIX daemonization process, modern systemd integration, signal handling, privilege management, and how spooler daemons specifically operate. You'll understand how to design, implement, and manage robust background services.
Daemons have specific characteristics that distinguish them from regular processes:
Core Characteristics:
Spooler Daemon Requirements:
| Daemon | Purpose | Protocol | Config File |
|---|---|---|---|
| cupsd | Print spooling (CUPS) | IPP/HTTP | /etc/cups/cupsd.conf |
| lpd | Legacy print spooling | LPD (RFC 1179) | /etc/printcap |
| sendmail | Mail transfer agent | SMTP | /etc/mail/sendmail.cf |
| postfix master | Mail transfer agent | SMTP | /etc/postfix/main.cf |
| crond | Scheduled job execution | N/A | /etc/crontab |
| atd | One-time job execution | N/A | /var/spool/at |
Traditional UNIX daemons follow a specific startup ritual to properly detach from the terminal and become background processes. Understanding this process is fundamental to systems programming.
The Daemonization Steps:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
/* Classic UNIX Daemonization Pattern */#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <signal.h>#include <sys/stat.h>#include <fcntl.h>#include <syslog.h> void daemonize(const char *name) { pid_t pid; /* Step 1: Fork and let parent exit */ pid = fork(); if (pid < 0) exit(EXIT_FAILURE); if (pid > 0) exit(EXIT_SUCCESS); /* Parent exits */ /* Step 2: Create new session */ if (setsid() < 0) exit(EXIT_FAILURE); /* Ignore terminal signals */ signal(SIGHUP, SIG_IGN); /* Step 3: Fork again to prevent terminal acquisition */ pid = fork(); if (pid < 0) exit(EXIT_FAILURE); if (pid > 0) exit(EXIT_SUCCESS); /* Step 4: Set working directory */ chdir("/"); /* Step 5: Reset umask */ umask(0); /* Step 6: Close standard file descriptors */ close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); /* Step 7: Redirect to /dev/null */ open("/dev/null", O_RDONLY); /* stdin = fd 0 */ open("/dev/null", O_WRONLY); /* stdout = fd 1 */ open("/dev/null", O_WRONLY); /* stderr = fd 2 */ /* Open syslog for logging */ openlog(name, LOG_PID, LOG_DAEMON); syslog(LOG_INFO, "Daemon started successfully");} /* Main daemon loop */int main(int argc, char *argv[]) { daemonize("myspooler"); /* Create PID file */ write_pid_file("/var/run/myspooler.pid"); /* Main service loop */ while (1) { /* Check for new jobs */ process_spool_queue(); /* Sleep or wait for signals */ sleep(1); } return 0;}The double-fork ensures the daemon cannot accidentally acquire a controlling terminal. After setsid(), the process is a session leader and could get a terminal if it opens one. The second fork creates a non-session-leader that can never acquire a controlling terminal.
Modern Linux systems use systemd for service management. Daemons designed for systemd are simpler—they don't need to daemonize themselves because systemd handles process supervision.
Systemd Advantages:
123456789101112131415161718192021222324
# /lib/systemd/system/cups.service[Unit]Description=CUPS SchedulerDocumentation=man:cupsd(8)After=network.target nss-user-lookup.targetWants=cups.socket cups.path [Service]Type=notifyExecStart=/usr/sbin/cupsd -lExecReload=/bin/kill -HUP $MAINPIDRestart=on-failure # Security hardeningPrivateTmp=trueProtectSystem=fullProtectHome=trueNoNewPrivileges=true # File descriptor limitLimitNOFILE=8192 [Install]WantedBy=multi-user.target12345678910111213141516171819202122
#!/bin/bash# Managing daemons with systemctl # Start/stop/restartsystemctl start cupssystemctl stop cupssystemctl restart cupssystemctl reload cups # HUP signal for config reload # Enable/disable at bootsystemctl enable cupssystemctl disable cups # Status and logssystemctl status cupsjournalctl -u cups -f # Follow logs # Check if runningsystemctl is-active cups # List all spool-related servicessystemctl list-units --type=service | grep -E 'cups|postfix|cron'| Type | Description | Use Case |
|---|---|---|
| simple | Main process is the service | Most daemons |
| forking | Service forks and parent exits | Legacy daemons |
| notify | Service sends ready notification | Modern daemons |
| oneshot | Single execution, then done | Startup scripts |
| dbus | Ready when on D-Bus | Desktop services |
Daemons must handle UNIX signals properly for clean operation, configuration reload, and graceful shutdown.
Essential Signals:
| Signal | Default Action | Daemon Should |
|---|---|---|
| SIGHUP | Terminate | Reload configuration |
| SIGTERM | Terminate | Graceful shutdown |
| SIGINT | Terminate | Graceful shutdown |
| SIGCHLD | Ignore | Reap child processes |
| SIGUSR1/2 | Terminate | Custom actions (log rotate, etc.) |
| SIGPIPE | Terminate | Ignore (handle in code) |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
/* Proper Signal Handling for Spooler Daemon */#include <signal.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>#include <syslog.h> static volatile sig_atomic_t running = 1;static volatile sig_atomic_t reload_config = 0; void handle_signal(int sig) { switch (sig) { case SIGTERM: case SIGINT: syslog(LOG_INFO, "Shutdown requested"); running = 0; break; case SIGHUP: syslog(LOG_INFO, "Config reload requested"); reload_config = 1; break; case SIGCHLD: /* Reap zombie children */ while (waitpid(-1, NULL, WNOHANG) > 0); break; }} void setup_signals(void) { struct sigaction sa; sa.sa_handler = handle_signal; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; /* Restart interrupted syscalls */ sigaction(SIGTERM, &sa, NULL); sigaction(SIGINT, &sa, NULL); sigaction(SIGHUP, &sa, NULL); sigaction(SIGCHLD, &sa, NULL); /* Ignore SIGPIPE - handle write errors in code */ signal(SIGPIPE, SIG_IGN);} void daemon_main_loop(void) { setup_signals(); while (running) { if (reload_config) { syslog(LOG_INFO, "Reloading configuration"); load_config(); reload_config = 0; } /* Process jobs, accept connections, etc. */ do_work(); } /* Graceful shutdown */ syslog(LOG_INFO, "Finishing pending jobs..."); finish_pending_jobs(); cleanup_resources(); syslog(LOG_INFO, "Daemon exiting");}Daemons often need elevated privileges at startup but should drop to minimal privileges for ongoing operation.
Privilege Dropping Pattern:
CUPS Privilege Model:
lp for job processing1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
/* Drop Privileges After Initialization */#include <pwd.h>#include <grp.h>#include <unistd.h>#include <sys/types.h> int drop_privileges(const char *username) { struct passwd *pw = getpwnam(username); if (!pw) return -1; /* Set supplementary groups */ if (initgroups(username, pw->pw_gid) != 0) { return -1; } /* Set group ID first (can't after setuid) */ if (setgid(pw->pw_gid) != 0) { return -1; } /* Set user ID - no going back after this */ if (setuid(pw->pw_uid) != 0) { return -1; } /* Verify we can't regain root */ if (setuid(0) == 0) { /* Security failure - still have root */ return -1; } return 0;} int main(void) { /* Phase 1: Root-privileged initialization */ int server_fd = bind_privileged_port(631); if (server_fd < 0) exit(1); open_device("/dev/lp0"); /* Needs device access */ /* Phase 2: Drop privileges */ if (drop_privileges("lp") != 0) { syslog(LOG_ERR, "Failed to drop privileges"); exit(1); } /* Phase 3: Run as unprivileged user */ syslog(LOG_INFO, "Running as uid %d", getuid()); daemon_main_loop(); return 0;}Running services as root longer than necessary is a major security risk. A vulnerability in a root daemon gives attackers complete system control. Always drop to the minimum required privileges as early as possible.
Daemons use different process models to handle concurrent work:
1. Single-Process (Event Loop)
2. Pre-fork (Worker Pool)
3. Fork-on-Demand
4. Thread Pool
CUPS Model: CUPS uses a hybrid: single scheduler process with forked filters and backends per job. This isolates job processing while maintaining efficient queue management.
| Model | Isolation | Overhead | Concurrency | Best For |
|---|---|---|---|---|
| Event Loop | None | Lowest | High | I/O-bound, many connections |
| Pre-fork | Per-worker | Medium | Limited by pool | CPU-bound, memory isolation |
| Fork-on-Demand | Per-request | Highest | Unlimited | Infrequent, untrusted code |
| Thread Pool | Partial | Low | High | Shared state, lower isolation OK |
The final page covers job scheduling—how spooling systems decide which jobs to process, priority algorithms, fairness policies, and scheduling optimization.