Search code examples
cunixsignalssystems-programmingsigaction

SIGHUP signal handling to deamonize a command in Unix system programming


I am reading a book about Unix system programming. In the book there is a function to create a daemon process.

Part of the code is not very clear to me, particularly the following:

struct sigaction    sa;
....
/* *Become a session leader to lose controlling TTY. */
if ((pid = fork()) < 0)
{
    err_quit("%s: can’t fork", cmd);
}
else if (pid != 0) /* parent */
{
    exit(0); //the parent will exit
}
setsid();

/* *Ensure future opens won’t allocate controlling TTYs. */
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGHUP, &sa, NULL) < 0)
{
    err_quit("%s: can’t ignore SIGHUP", cmd);
}

where

SIGHUPis the signal sent to the controlling process (session leader) associated with a controlling terminal if a disconnect is detected by the terminal interface.

So basically the parent process calls fork and then exit. In this way we are guaranteed the child not a group leader. The child becomes a session leader with setsid.

I do not understand when the signal SIG_UP is generated: from the definition it seems it is generated when closing a Terminal window, but from the comment in the code

/* *Ensure future opens won’t allocate controlling TTYs. */

it seems it is generated in a different situation: when is it generated?

Secondly it wants to ignore this signal so it sets sa.sa_handler = SIG_IGN and then call sigaction. If it is ignoring the signal setting SIG_IGN as its handler, why is it setting the mask passed to sigaction as sigemptyset(&sa.sa_mask);? I mean if there is no handler, the mask set before executing the handler is not used: is it?

The complete function is the following:

void daemonize(const char *cmd)
{
    int i, fd0, fd1, fd2;
    pid_t pid;
    struct rlimit       rl;
    struct sigaction    sa;
    /* *Clear file creation mask.*/
    umask(0);
    /* *Get maximum number of file descriptors. */
    if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
    {
        err_quit("%s: can’t get file limit", cmd);
    }
    /* *Become a session leader to lose controlling TTY. */
    if ((pid = fork()) < 0)
    {
        err_quit("%s: can’t fork", cmd);
    }
    else if (pid != 0) /* parent */
    {
        exit(0); //the parent will exit
    }
    setsid();
    /* *Ensure future opens won’t allocate controlling TTYs. */
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0)
    {
        err_quit("%s: can’t ignore SIGHUP", cmd);
    }
    if ((pid = fork()) < 0)
    {
        err_quit("%s: can’t fork", cmd);
    }
    else if (pid != 0) /* parent */
    {
        exit(0);
    }
    /*
    *Change the current working directory to the root so
    * we won’t prevent file systems from being unmounted.
    */
    if (chdir("/") < 0)
    {
        err_quit("%s: can’t change directory to /", cmd);
    }
    /*
    *Close all open file descriptors.
    */
    if (rl.rlim_max == RLIM_INFINITY)
    {
        rl.rlim_max = 1024;
    }
    for (i = 0; i < rl.rlim_max; i++)
    {
        close(i);
    }
    /*
    *Attach file descriptors 0, 1, and 2 to /dev/null.
    */
    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);
    /*
    *Initialize the log file.
    */
    openlog(cmd, LOG_CONS, LOG_DAEMON);
    if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
        syslog(LOG_ERR, "unexpected file descriptors %d %d %d", fd0, fd1, fd2);
        exit(1);
    }
}

EDIT

Also I have an additional question. Why is fork called twice in the function?


Solution

  • So basically ...

    Yes, the parent process forks a child process, and that child does setsid() so that it will be the process group leader (and the only process) in the new process group, and have no controlling terminal. That last part is the key.

    (If there was a reason why the child process should run in the same process group as the parent process, one could use int fd = open("/dev/tty", O_RDWR); if (fd != -1) ioctl(fd, TIOCNOTTY); to detach from the controlling terminal. setsid() is easier, and it is usually preferable to have the child run in a new process group anyway, as it and its children can be sent a signal without affecting any other processes.)

    Now, whenever a process that has no controlling terminal opens a terminal device (a tty or a pseudo-tty), that device will become its controlling terminal (unless the O_NOCTTY flag was used when opening the device).

    Whenever the controlling terminal is disconnected, a SIGHUP signal is delivered to each process having that terminal as their controlling terminal. (That SIG_UP thing is just a typo. Signal names do not have an underscore, only the special handlers SIG_DFL, SIG_IGN, and SIG_ERR do.)

    If the daemon process opens a terminal device for any reason -- for example, because a library wants to print an error message to a console, and opens /dev/tty1 or similar to do so --, the daemon will inadvertently acquire a controlling terminal. Other than interposing open(), fopen(), opendir(), and so on, to ensure their underlying open() flags will include O_NOCTTY, there is not much a daemon can do to ensure it will not inadvertently acquire a controlling terminal. Instead, the easier option is to just assume that it might, and simply ensure that that does not cause too much trouble. To avoid the most typical issue, dying from SIGHUP when the controlling terminal is disconnected, the daemon process can simply ignore the delivery of the SIGHUP signal.

    In short, it is a belt-and-suspenders approach. The setsid() detaches the process from the controlling terminal; and SIGHUP is ignored in case the daemon inadvertently acquires a controlling terminal by opening a tty device without using the O_NOCTTY flag.