Spawning in Unix

I write a lot of low-level C code dealing with Unix processes and the like. I have seen a few folks roll their own spawn function, and do it wrong, so this page is meant to serve as an overview of your options when writing programs using only core Unix functions.

Naïve spawn

The essence of the problem is simple: we must create a child process running a particular program, and return successfully if doing so was a success. One might be tempted to write something like this:

int
spawnv(const char *file, char *const argv[])
{
    switch(fork()) {
    case 0:
        execvp(file, argv);
        exit(EXIT_FAILURE);
    case -1:
        return -1;
    }
    return 0;
}

However, this implementation has two problems,

  1. The child process becomes a zombie, since its status code has not been collected by the parent calling wait. This wastes memory and space in the process table.
  2. We don't know whether the exec has in fact succeeded, which means we're only really congratulating ourselves on a successful fork.

Signal handler

The immediate response to the first problem may be to install a signal handler to listen out for SIGCHLD. Ignoring sanity checks for the moment,

act.sa_handler = sigchld;
sigaction(SIGCHLD, &act, NULL);
if(fork() == 0) {
    execvp(file, argv);
    exit(EXIT_FAILURE);
}
void
sigchld(int signum) {
    while(waitpid(-1, NULL, WNOHANG) > 0)
        ;
}

But this doesn't work, since if the action for SIGCHLD has already been changed we'll clobber that, and likewise another call to sigaction would clobber our handler, too. Signals are not a sensible approach to this problem.

Double fork

This trick lets you spawn processes whilst avoiding zombies, without installing any signal handler. The first process forks and waits for its child; the second process forks and immediately exits and is reaped; the third process is adopted by init, and executes the desired program. All zombies accounted for, since init is always waiting.

if(fork() == 0) {
    if(fork() == 0) {
        execvp(file, argv);
        exit(EXIT_FAILURE);
    }
    exit(EXIT_SUCCESS);
}
wait(NULL);

Piped errors

This next trick solves the second problem by letting you spawn a process and obtain the error code, if any, from the exec. You create a pipe and fork; the writing end of the pipe is set to close on exec; if the exec fails you write errno to the pipe, from which the parent reads it. If all goes well the pipe will EOF, and the parent returns success.

pipe(fd);
if(fork() == 0) {
    close(fd[0]);
    fcntl(fd[1], F_SETFD, FD_CLOEXEC);
    execvp(file, argv);
    write(fd[1], &errno, sizeof errno);
    exit(EXIT_FAILURE);
}
close(fd[1]);
if(read(fd[0], &errno, sizeof errno))
    return -1;
close(fd[0]);
return 0;

Smarter spawn

We can of course combine these two techniques into a very cool spawn function! The first process creates a pipe, forks, and waits; the second process forks and immediately exits and is reaped; the third process is adopted by init, attempts to execute the program, and if this fails writes to the pipe; the first process reads the error, and returns failure (or otherwise the pipe will EOF and the process returns success).

int
spawnv(const char *file, char *const argv[])
{
    int fd[2];

    if(pipe(fd) == -1)
        return -1;
    switch(fork()) {
    case 0:
        switch(fork()) {
        case 0:
            close(fd[0]);
            if(fcntl(fd[1], F_SETFD, FD_CLOEXEC) == 0)
                execvp(file, argv);
            /* fallthrough */
        case -1:
            write(fd[1], &errno, sizeof errno);
            exit(EXIT_FAILURE);
        }
        exit(EXIT_SUCCESS);
    case -1:
        close(fd[0]);
        close(fd[1]);
        return -1;
    }
    wait(NULL);
    close(fd[1]);
    if(read(fd[0], &errno, sizeof errno)) {
        close(fd[0]);
        return -1;
    }
    close(fd[0]);
    return 0;
}

Variadic spawn

For convenience we'd like a version of spawn which takes a variable number of arguments, rather than the array of strings used in spawnv. This just involves a little shuffling around to convert the va_list into an array of strings.

int
spawnl(const char *file, ...)
{
    va_list ap;
    int i, n;

    va_start(ap, file);
    for(n = 1; va_arg(ap, char *); n++)
        ;
    va_end(ap);

    char *argv[n];

    va_start(ap, file);
    for(i = 0; i < n; i++)
        argv[i] = va_arg(ap, char *);
    va_end(ap);
    return spawnv(file, argv);
}

And finally,

Now we're zombie free, and no matter how spawn fails errno is set appropriately.

int
main(int argc, char *argv[])
{
    int i;
    
    for(i = 1; i < argc; i++)
        if(spawnl(argv[i], argv[i], NULL) == -1)
            perror(argv[i]);
    return 0;
}
$ ./spawn uname example
example: No such file or directory
Linux

Perfect.