libjobserver – v0.2.0 Handle parallel jobs in a build automation application compatible with GNU Make code.malloc.fr

NAME

Jobserver - A set of functions to handle parallel jobs in a build automation application compatible with GNU Make

DESCRIPTION

The purpose of the jobserver is to limit the number of concurrent processes spawned by a program and its children, and more specifically by a build automation software.

The first process initiates the jobserver with a maximum number of parallel job tokens. To spawn a new child, the jobserver locks an unused token if any is available. Otherwise, it has to wait for one of its children or sibling to exit in order to reuse the associated token. A child process will search the environment to access the jobserver and try to lock a token whenever it has to spawn a child, effectively sharing the ability to do so with its siblings and parents.

Header

#include <jobserver.h>

struct jobserver
{


bool dry_run;
bool debug;
bool keep_going;

pid_t stopped;
int status;

...

};

typedef int (*jobserver_callback_t)(void * data);
typedef void (*jobserver_callback_return_t) (void * data, int status);

void jobserver_close_(struct jobserver * js, bool inherit);

int jobserver_getenv_(int * read_fd, int * write_fd, bool * dry_run, bool * debug, bool * keep_going);
int jobserver_setenv_(int read_fd, int write_fd, bool dry_run, bool debug, bool keep_going);

Configuration

A jobserver, either created or joined, has 3 properties related to the actions that it performs. Depending on the status of each flag, a program using the jobserver:

* shall stop immediately when marked dry_run;

* print debug information when marked debug;

* ignore errors (usually of children) as much as possible when marked keep_going.

Handling forks

Creating or connecting to a jobserver installs a signal handler for SIGCHLD in order, when a child exits, to execute the jobserver_callback_return_t callback and release the token associated with the job.

Suppose that the application using the jobserver spawned children without calling jobserver_launch_job(3):

* If SIGCHLD is received while in a potentially blocking function such as jobserver_launch_job(3) or jobserver_wait(3) the terminated process might not be a job. In this case, -1 is returned, errno is set to ECHILD, and the stopped and status fields of the jobserver structure receive the pid and the exit status of the stopped process (as per waitpid(2)).

* If the application waited for a child to exit (see wait(2)), the waited process could be a job. In this case, jobserver_terminate_job(3) must be called. Failure to do so will cause the jobserver_wait(3) or jobserver_collect(3) functions to never return 0.

It is important to note that when a fork happens outside of jobserver_launch_job(3), the jobserver would be shared with the new process, including the signal handler. You might want to call jobserver_close(3) or jobserver_close_().

Internal functions

void jobserver_close_(struct jobserver * js, bool inherit);

The function jobserver_close_() can be called in a child when forking manually in order to close jobserver js in the child. In particular, the function cleans up the js data structure and resets the signal handler for SIGCHLD to its default value. If inherit is true, the jobserver remains available in the child but a new local instance has to be created with jobserver_connect(3).

int jobserver_getenv_(int * read_fd, int * write_fd, bool * dry_run, bool * debug, bool * keep_going);

See jobserver_getenv_(3).

int jobserver_setenv_(int read_fd, int write_fd, bool dry_run, bool debug, bool keep_going);

See jobserver_setenv_(3).

ENVIRONMENT

According to the GNU Make documentation the jobserver token pipe is passed from one process to the other with the MAKEFLAGS environment variable.

Letters 'n', 'd', and 'k', corresponding respectively to the dry_run, debug, and keep_going options, are looked up or inserted in the first word of this variable.

The string --jobserver-fds= (for GNU Make versions lower than 4.1) or --jobserver-auth= (for GNU Make versions greater than 4.2) is followed by a pair, separated by a comma (','), of file descriptors corresponding to the read and write sides of the token pipe.

CONFORMING TO

Section 13.1 of GNU Make.

EXAMPLE

The following program runs execv(3) on each of its argument using a jobserver which size is given as its first argument:

a.out 2 '/usr/bin/date --iso-8601' '/usr/bin/hostname -i' /usr/bin/whoami
    a.out ! '/usr/bin/make --version' '/usr/bin/sleep 2'

When '!' is given, it expects the jobserver to be preexisting (usually created by make(1)).

#include <errno.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>

    #include "jobserver.h"

    int run(void * data_)// Not empty
    {
      fprintf(stderr, "Launching job '%s'.\n", (char *)data_);

      int size = 1;

      char * data = data_;
      while((data = strchr(data, ' ')) != NULL)
        {
          ++size;
          ++data;
        }

      char ** args = alloca(size * sizeof(char *));

      args[0] = strtok(data_, " ");
      args[size] = NULL;

      size = 1;
      while((data = strtok(NULL, " ")) != NULL)
        args[size++] = data;

      int status = execv(args[0], args);

      if(status != 0) fprintf(stderr, "Execv failed: %m.\n");

      return status;
    }

    void end(void * data, int status)
    {
      fprintf(stderr, "Job '%s' collected with status: %d.\n", (char *)data, status);
    }

    void connect_to(struct jobserver * js, char * tokens)
    {
      fprintf(stderr, "Connecting to jobserver ...");

      if(jobserver_connect(js) == -1)
        {
          fprintf(stderr, " no jobserver found");

          if(*tokens == '!')
    	{
    	  if(errno == EACCES)
    	    fprintf(stderr, " recursive make invocation without '+'");

    	  fprintf(stderr, " and '!' was specified.\n");
    	  exit(EXIT_FAILURE);
    	}
          else if(errno == ENODEV)
    	{
    	  fprintf(stderr, ".\nCreating jobserver ...");

    	  if(jobserver_create_n(js, atoi(tokens), 't') == -1)
    	    exit(EXIT_FAILURE);

    	  fprintf(stderr, " done.\n");
    	}
          else
    	{
    	  fprintf(stderr, ", error (%m).\n");
    	  exit(EXIT_FAILURE);
    	}
        }
    }

    //Usage: tokens [cmds ...]
    int main(int argc, char ** argv)
    {
      const int shift = 2;

      if(argc < shift)
        return EXIT_FAILURE;

      struct jobserver js;
      connect_to(&js, argv[1]);

      for(int i = shift; i < argc; ++i)
        if(strlen(argv[i]) > 0)
          if(jobserver_launch_job(&js, -1, true, argv[i], run, end) == -1)
    	return EXIT_FAILURE;

      int status;
      while((status = jobserver_collect(&js, -1)) != 0)
        if(status == -1 && errno != EINTR)
          return EXIT_FAILURE;

      if(jobserver_close(&js) != 0)
        return EXIT_FAILURE;

      return EXIT_SUCCESS;
    }

SEE ALSO

jobserver_clear(3), jobserver_collect(3), jobserver_connect(3), jobserver_close(3), jobserver_create(3), jobserver_create_n(3), jobserver_launch_job(3), jobserver_print(3), jobserver_getenv(3), jobserver_setenv(3), jobserver_terminate_job(3), jobserver_unsetenv(3), jobserver_wait(3)