Using SIGHUP to Reinitialize a Daemon

 

Using SIGHUP to Reinitialize a Daemon
Chapter 37 โ€“ Part 4: Reloading Configuration Without Restart
๐Ÿ“ก SIGHUP Signal
โš™๏ธ Config Reload
๐Ÿ”„ Zero-Downtime Update

What Will You Learn?

Most daemons read a configuration file at startup. If you change that configuration, you normally have to stop and restart the daemon โ€” which means a brief service interruption. The SIGHUP technique allows a daemon to reload its configuration while continuing to run, with zero downtime. You will understand why SIGHUP is special for daemons and how to implement it properly.

Key Concepts

SIGHUP Hangup Signal Config Reload kill -HUP sigaction() sig_atomic_t Zero Downtime Graceful Restart

1. Why Is SIGHUP Used for Reinitialization?

SIGHUP literally means “hangup” โ€” it was originally sent to a process when its controlling terminal disconnected (e.g., a modem line dropped). For regular processes, SIGHUP means the terminal is gone and the process should clean up and exit.

But daemons already have no controlling terminal. So SIGHUP can never arrive from a terminal disconnection. This makes SIGHUP available as a free signal that administrators can send to a daemon to mean “please re-read your configuration file”.

For regular processes:

SIGHUP = “your terminal disconnected” โ†’ Usually causes exit

For daemons (by convention):

SIGHUP = “administrator says: reload your config” โ†’ Reinitialize without stopping

This convention is followed by almost every major Unix/Linux daemon. Examples:

Daemon What SIGHUP Does How to Send
nginx Reloads nginx.conf with zero connection drop nginx -s reload
sshd Re-reads sshd_config without dropping connections kill -HUP $(cat /var/run/sshd.pid)
syslogd Re-reads /etc/syslog.conf kill -HUP $(cat /var/run/syslogd.pid)
cron Re-reads crontab files kill -HUP $(pidof cron)

2. How SIGHUP Reinitalization Works

The implementation follows a safe signal-handling pattern:

Admin runs:
kill -HUP PID
โ†’ Kernel delivers
SIGHUP to daemon
โ†’ Signal handler
sets
reload_flag = 1
โ†’ Main loop
sees flag, calls
load_config()
โš ๏ธ Why Not Reload Inside the Signal Handler?
Signal handlers must only call async-signal-safe functions. Reading a file, parsing config, calling malloc() โ€” none of these are async-signal-safe. Doing them inside the handler can cause deadlocks or data corruption. The safe pattern is: set a flag in the handler, do the work in the main loop.

๐Ÿ’ป Code Example 1: Daemon with SIGHUP Config Reload

A complete daemon that reads a config file at startup and reloads it when it receives SIGHUP โ€” without stopping the service.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <syslog.h>

#define CONFIG_FILE "/etc/mydaemon.conf"

/* -------- Configuration structure -------- */
struct daemon_config {
    int    interval_seconds;
    char   log_prefix[64];
    int    debug_mode;
};

static struct daemon_config cfg;  /* Current active config */

/* -------- Signal flags (must be sig_atomic_t) -------- */
static volatile sig_atomic_t reload_config = 0;
static volatile sig_atomic_t do_shutdown   = 0;

/* -------- Signal handlers (AS-safe: only set flags) -------- */
void handle_sighup(int sig)  { reload_config = 1; }
void handle_sigterm(int sig) { do_shutdown   = 1; }

/* -------- Config loading function -------- */
/*
 * load_config() - Parse config file and update cfg.
 * Called at startup AND when SIGHUP is received.
 * This function is NOT async-signal-safe, so we call it
 * from the main loop, not from the signal handler.
 */
void load_config(const char *path)
{
    FILE *f = fopen(path, "r");
    if (!f) {
        syslog(LOG_WARNING, "Cannot open config %s, using defaults: %m", path);
        /* Apply defaults */
        cfg.interval_seconds = 5;
        strncpy(cfg.log_prefix, "daemon", sizeof(cfg.log_prefix) - 1);
        cfg.debug_mode = 0;
        return;
    }

    char line[256];
    char key[64], value[192];

    /* Reset to defaults before parsing */
    cfg.interval_seconds = 5;
    strncpy(cfg.log_prefix, "daemon", sizeof(cfg.log_prefix) - 1);
    cfg.debug_mode = 0;

    while (fgets(line, sizeof(line), f)) {
        /* Skip comments and blank lines */
        if (line[0] == '#' || line[0] == '\n') continue;

        if (sscanf(line, "%63s = %191s", key, value) == 2) {
            if (strcmp(key, "interval") == 0) {
                cfg.interval_seconds = atoi(value);
            } else if (strcmp(key, "log_prefix") == 0) {
                strncpy(cfg.log_prefix, value, sizeof(cfg.log_prefix) - 1);
            } else if (strcmp(key, "debug") == 0) {
                cfg.debug_mode = (strcmp(value, "yes") == 0) ? 1 : 0;
            }
        }
    }
    fclose(f);

    syslog(LOG_INFO, "Config loaded: interval=%ds prefix=%s debug=%s",
           cfg.interval_seconds,
           cfg.log_prefix,
           cfg.debug_mode ? "yes" : "no");
}

/* -------- Main function -------- */
int main(void)
{
    struct sigaction sa;

    openlog("mydaemon", LOG_PID, LOG_DAEMON);
    syslog(LOG_INFO, "Daemon starting...");

    /* Install SIGHUP handler for config reload */
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handle_sighup;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;  /* Restart interrupted syscalls */
    if (sigaction(SIGHUP, &sa, NULL) == -1) {
        syslog(LOG_ERR, "sigaction SIGHUP: %m");
        return 1;
    }

    /* Install SIGTERM handler for graceful shutdown */
    sa.sa_handler = handle_sigterm;
    if (sigaction(SIGTERM, &sa, NULL) == -1) {
        syslog(LOG_ERR, "sigaction SIGTERM: %m");
        return 1;
    }

    /* Load initial configuration */
    load_config(CONFIG_FILE);

    syslog(LOG_INFO, "Daemon running. PID=%d", getpid());
    syslog(LOG_INFO, "Send SIGHUP to reload config: kill -HUP %d", getpid());

    /* -------- Main daemon loop -------- */
    while (!do_shutdown) {

        /* Check if a config reload was requested */
        if (reload_config) {
            reload_config = 0;   /* Clear flag first (atomic write) */
            syslog(LOG_INFO, "SIGHUP received โ€” reloading configuration...");
            load_config(CONFIG_FILE);
        }

        /* Do actual daemon work using current config */
        if (cfg.debug_mode) {
            syslog(LOG_DEBUG, "[%s] Working... next in %ds",
                   cfg.log_prefix, cfg.interval_seconds);
        } else {
            syslog(LOG_INFO, "[%s] Heartbeat", cfg.log_prefix);
        }

        sleep(cfg.interval_seconds);
    }

    syslog(LOG_INFO, "Received SIGTERM โ€” shutting down cleanly");
    closelog();
    return 0;
}

Create a sample config file and test:

# /etc/mydaemon.conf
interval = 3
log_prefix = myservice
debug = no
gcc -o mydaemon mydaemon_sighup.c
sudo ./mydaemon &
DPID=$!
echo "Daemon PID: $DPID"

# Watch syslog in another terminal:
tail -f /var/log/syslog | grep mydaemon &

# Edit the config file, then send SIGHUP to reload:
echo "interval = 10" | sudo tee /etc/mydaemon.conf
sudo kill -HUP $DPID   # Daemon reloads config โ€” no restart needed!

# Graceful shutdown:
sudo kill -TERM $DPID

๐Ÿ’ป Code Example 2: SIGHUP Log File Rotation

Another common use of SIGHUP: when a log rotation tool renames the daemon’s log file, it sends SIGHUP so the daemon closes and reopens the log file (now creating a new file at the original path).

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <time.h>
#include <syslog.h>

#define LOG_FILE "/var/log/mydaemon.log"

static volatile sig_atomic_t reopen_log = 0;
static int log_fd = -1;

void handle_sighup(int sig) { reopen_log = 1; }

/*
 * open_log_file() - Opens the daemon's log file.
 * If a file is already open, close it first.
 * This implements log rotation: the log tool renames the old file,
 * then sends SIGHUP. The daemon closes the renamed file and opens
 * a fresh new file at the original path.
 */
void open_log_file(void)
{
    if (log_fd != -1) {
        close(log_fd);
        log_fd = -1;
    }

    log_fd = open(LOG_FILE,
                  O_WRONLY | O_CREAT | O_APPEND,
                  S_IRUSR | S_IWUSR | S_IRGRP);

    if (log_fd == -1) {
        syslog(LOG_ERR, "Cannot open log file %s: %m", LOG_FILE);
    } else {
        syslog(LOG_INFO, "Log file opened: %s (fd=%d)", LOG_FILE, log_fd);
    }
}

void write_log(const char *msg)
{
    if (log_fd == -1) return;

    time_t t = time(NULL);
    struct tm *tm_info = localtime(&t);
    char timestamp[32];
    strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info);

    char line[256];
    int len = snprintf(line, sizeof(line), "[%s] %s\n", timestamp, msg);
    write(log_fd, line, len);
}

int main(void)
{
    struct sigaction sa;
    openlog("logrotate_demo", LOG_PID, LOG_DAEMON);

    /* Install SIGHUP handler */
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handle_sighup;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    sigaction(SIGHUP, &sa, NULL);

    /* Open log file initially */
    open_log_file();

    syslog(LOG_INFO, "Log rotation demo started (PID=%d)", getpid());

    int count = 0;
    while (1) {
        /* Check if log file needs to be reopened (after rotation) */
        if (reopen_log) {
            reopen_log = 0;
            syslog(LOG_INFO, "SIGHUP: reopening log file after rotation");
            open_log_file();
        }

        /* Write to log file */
        char msg[64];
        snprintf(msg, sizeof(msg), "Daemon tick #%d", ++count);
        write_log(msg);

        sleep(2);
    }

    if (log_fd != -1) close(log_fd);
    closelog();
    return 0;
}

Simulate log rotation:

gcc -o logrotate_demo logrotate_demo.c
sudo ./logrotate_demo &
DPID=$!

# Watch the log
tail -f /var/log/mydaemon.log &

# Simulate logrotate: rename the current log, send SIGHUP
sudo mv /var/log/mydaemon.log /var/log/mydaemon.log.1
sudo kill -HUP $DPID
# Daemon now creates a fresh /var/log/mydaemon.log

ls -la /var/log/mydaemon.log*
# mydaemon.log    (new, fresh file created by daemon)
# mydaemon.log.1  (old rotated file โ€” now safe to compress/archive)

sudo kill -TERM $DPID

๐ŸŽฏ Interview Questions โ€” SIGHUP Reinitialize
Q1. What does SIGHUP originally mean, and why do daemons repurpose it?
Answer: SIGHUP originally meant “hangup” โ€” sent to a process when its controlling terminal disconnected. Since daemons have no controlling terminal, SIGHUP from a terminal is impossible. By convention, system administrators repurpose SIGHUP to mean “reload your configuration” โ€” a safe channel to trigger daemon reinitialization without a restart.
Q2. Why should config file parsing NOT be done inside the SIGHUP signal handler?
Answer: POSIX requires that signal handlers only call async-signal-safe functions. File I/O functions (fopen, fread, fgets), string functions (sscanf, strtok), and memory allocation (malloc) are NOT async-signal-safe. Calling them inside a signal handler risks deadlocks (if the signal interrupts a malloc in progress) or data corruption. The safe pattern is to set a volatile sig_atomic_t flag in the handler and do the actual work in the main loop.
Q3. How does SIGHUP help with log file rotation?
Answer: When logrotate renames the current log file (e.g., syslog โ†’ syslog.1), the daemon still has the renamed file open (file descriptors refer to the inode, not the name). Logrotate sends SIGHUP, the daemon closes the old file descriptor and reopens the log at the original path โ€” creating a fresh new file. This allows rotation without losing any log messages.
Q4. What flag must be used for SIGHUP-driven variables, and why?
Answer: The flag must be declared as volatile sig_atomic_t. The volatile qualifier prevents the compiler from caching the value in a register (ensures the main loop always reads from memory). The sig_atomic_t type guarantees that reads and writes are atomic on the target architecture, preventing torn reads.
Q5. Give three real-world daemons that use SIGHUP for configuration reload.
Answer: (1) nginx โ€” reloads nginx.conf, spawns new workers with new config while old workers finish existing connections. (2) sshd โ€” re-reads sshd_config without dropping active SSH sessions. (3) syslogd/rsyslogd โ€” re-reads /etc/syslog.conf to apply new log routing rules. All use kill -HUP PID to trigger this.

Chapter Summary

SIGHUP is repurposed by daemons (which have no terminal) as a convention to trigger configuration reload. The pattern is: install a SIGHUP handler that sets a flag, then check the flag in the main loop and call the config-reading function from there (never from within the signal handler). This same technique is used for log file rotation.

โ† Daemon Guidelines Next: syslog Overview โ†’

Leave a Reply

Your email address will not be published. Required fields are marked *