Thursday, December 2, 2010

Linux process management

This is a fairly pedestrian linux c problem. But one that I've been revisiting at least a half dozen times or so this past year when working on the Vyatta router, API, or webgui backend. Seems like each time with a new tweak or requirement. But posting this here is a good reference, particularly since something very close to this is shipping in several forms in the Vyatta router. And I always seem to be coming back to this reference implementation.

The goal for this snippet is to run a process and capture STDOUT and STDERR from this process to be used for later processing. The key ingredients here are pipes and a process exec method.



First I'll start off with the listing in it's full glory. And then let's pick this apart.

int cp[2]; // Child to parent pipe
if( pipe(cp) < 0) {                                                                                                
    return -1;
}

pid_t pid = fork();                                                                                 
if (pid == 0) {                                                                                          
    //child
    close(STDIN_FILENO); // Close current stdin.
    close(cp[0]);
    dup2(cp[1],STDOUT_FILENO); // Make stdout go to write end of pipe.
    dup2(cp[1],STDERR_FILENO); // Make stderr go to write end of pipe.
    close(cp[1]);
    int ret = 0;  
    if (execl("/bin/sh","sh","-c",cmd,NULL) == -1) {                                   
      ret = errno;
    }
    exit(ret);
}
else {
    //parent         
    char buf[8192];
    char last_char = '\0';
    memset(buf,'\0',8192);
    close(cp[1]);
    fd_set rfds;
    struct timeval tv;

    //set file descriptor to non-blocking
    int flags = fcntl(cp[0], F_GETFL, 0);
    fcntl(cp[0], F_SETFL, flags | O_NONBLOCK);

    FD_ZERO(&rfds);
    FD_SET(cp[0], &rfds);
    tv.tv_sec = 1;
    tv.tv_usec = 0;

    while (select(FD_SETSIZE, &rfds, NULL, NULL, &tv) != -1) {
      int err = 0;
      if ((err = read(cp[0], &buf, 8191)) > 0) { 
        //do some other stuff here...
        fprintf(out_stream,"%s",buf);
        memset(buf,'\0',8192);
      }
      else if (err == -1 && errno == EAGAIN) {
         //try again
      }
      else {
 break;
      }
      FD_ZERO(&rfds);
      FD_SET(cp[0], &rfds);
      tv.tv_sec = 1;
      tv.tv_usec = 0;

      fflush(NULL);
    }

    //now wait on child to kick the bucket                  
    int status;
    wait(&status);
    close(cp[0]);
    if (WIFEXITED(status)) {
      return WEXITSTATUS(status);
    }
    return 1;
}


There are two ends to the pipe (a read end cp[0] and a write end cp[1])--that's what the file descriptor allocation of 2 is for. On linux it's a unidirection pipe (meaning data travels in one direction).

Now that a pipe has been created the process is forked, which means a child process is created. The child process is the process that returns from fork with a PID of 0. In this example, the child is responsible for running the process, while the parent process will read from STDOUT/STDERR and "manage" the process.

dup2(cp[1],STDOUT_FILENO); // Make stdout go to write end of pipe.
dup2(cp[1],STDERR_FILENO); // Make stderr go to write end of pipe.


In the child, the STDOUT and STDERR file descriptors are redirected to the write end of the pipe. dup2() points the STDOUT file descriptor to the inode of the write end of the pipe. The same is also applied to STDERR.

We're ready to run the command at this point via execl() with the "-c" option or as a non-interactive process. At this point we are done with our work in the child. Normal processing from execl() at this point dictates that the process execl() only returns if an error occurs. Otherwise the parent process will reap the child process via the wait() method.


execl("/bin/sh","sh","-c",cmd,NULL)


The child simply reads from the read end of the shared pipe, copies the data into buf until there is no more data to read, and waits (i.e. blocks) until the child process has completed. The code below uses a non-blocking read with a select statement (that blocks for 1 second or until data is available on the read end of the pipe).

Without the wait() the child process will turn into a zombie process that remains in the system process table after the child has finished execution (a bad thing).

FD_ZERO(&rfds);
FD_SET(cp[0], &rfds);
tv.tv_sec = 1;
tv.tv_usec = 0;

while (select(FD_SETSIZE, &rfds, NULL, NULL, &tv) != -1) {
      int err = 0;
      if ((err = read(cp[0], &buf, 8191)) > 0) { 


And the following check is required since the read is non-blocking. The read will return an error of EAGAIN if there's no data and the pipe is non-blocking. In this case we need to hit the select statement again and wait...

else if (err == -1 && errno == EAGAIN) {
 //try again
      }



The very last thing that remains is capturing the child process exit code, which is

WEXITSTATUS(status)

The final value in this snippet is status which is a 2 byte value that stores the exit code of the process (higher byte),and how the process was terminated (lower byte). This macro just ensures that you see the process return code. To get the process termination code you would need to use the WTERMSIG macro.

Note that the call to WEXITSTATUS can only be made if the process terminated normally. Therefore a check to WIFEXITED needs to be made first.


That's it, nothing too fancy, but it is nice to see it all work together.

No comments:

Post a Comment