How system() Works

20 April, 2009
#import <stdlib.h>

Introduction

You’ve probably used or heard of the system() function. How it works is actually quite simple. Although I doubt the following code is safer than anything the Clib authors could write (they have many more years of experience than I do), it’s an interesting example of how system() works and how to use forked child processes in an application.

While you can continue to safely use system(), using the following fork/exec combination allows you to have a finer degree of control over the child process’ input and output, as well as the shell it’s run in (thanks to LordFrith for this information).

Before trying out the following code, make sure you have at least a basic handle on Linux processes.

What system() Does

This is what system() essentially does:

  1. fork the existing process into a child process
  2. override the child process with the desired command

You should already know how to do the first step (if you don’t, see the above link) – something like this:

     pid_t child_process;
     child_process = fork ();

Overriding a Process With Another

So, onto the second step. To override a process with another, you simply need to use one of the exec functions. If you use one of these in the parent process, the whole process will be stopped and overriden with the other designated process. Forking a child process before using an exec function allows the original program to continue running.

#include <unistd.h>

I find the most useful exec functions are execl and execv. You can check the header for more of these functions (execlp, execle, execvp, etc), but here are the declarations for those two:

int execl(const char *path, const char *arg0, ..., (char*)0);
int execv(const char *path, char *const argv[]);

If we wanted to use one of these functions to list the root directory, we could do it either of the following ways:

     execl("/bin/ls", "ls", "/", 0);

     char *const ls_argv[] = {"ls", "/", 0);
     execv ("/bin/ls", ls_argv);

Execv is more useful if you plan on reusing certain arguments (speaking of which, don’t forget to include a program name at index 0 of your array!)

Writing an nsystem() Function

Let’s try to replicate system() in terms of what it can do. Let’s call it nsystem (for “new system()”). Start off with including all the needed headers and writing a function declaration:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>

void nsystem (char *path, char *args[])
{

Fork a child process:

  pid_t child_pid = fork ();

Now, add a check to make sure we replace the current process only if the current process is a child:

  if ((int)child_pid == 0)
    {
        //...
    }
  else
    ;

Note how the if statement checks if the child’s PID is 0. This because when the child is forked, it is the currently running process. It does not have a child of its own (hence no PID). Replace the child process with whatever the user calls if and only if it is the child process that is being run (put this in place of the commented ellipsis with this):

     execv (path, args);

Now, let’s embed the nsystem function in a real program to make sure it works:

int main (int argc, char *argv[])
{
    char *path = "/bin/ls";
    char *args[] = {"ls", "/", 0};

    nsystem (path, args);
    return 0;
}

Here’s the output after compiling:

$ ./a.out
bin   dev  home  lost+found  mnt  proc    sbin     srv  tmp  var
boot  etc  lib     media         opt  root    selinux  sys  usr

If wanted, the program could even be further extended to allow the user to run a command, or it could be implemented within another application to have a finer degree of control over overrun child processes.

Waiting on Child Processes

A big problem with the above example is that the parent process doesn’t wait on the child process. Things can get really messy if the parent process finishes before its child. You may have noticed at times that you would have needed to press return before the program ended, sometimes the ls command printed things out oddly, and sometimes it ran just like it should. This is because the parent process didn’t wait for the child process to finish. At times they exit together, and at times one may exit before another, and one could even exit incorrectly. Let’s fix up the code a bit so it runs correctly every time.

It’s very simple. Simply replace the else statement with this:

else if ((int) getpid != 0)
{
    int status_value;
    child_pid = wait (&status_value);
}

There is one major question that needs to be answered about the above code: why did we declare a “useless” integer? The status_value integer can be used, if wanted, in extending the function to return the child process’ exit value. The macros to do that are defined in <sys/wait.h> and are WIFEXITED(status_value), WEXITSTATUS(status_value), WIFSIGNALED(status_value), WTERMSIG(status_value), WIFSTOPPED(status_value), and WSTOPSIG(status_value).

Advertisements