Creating Fork and Exec Classes with Kylix

Rick RossPILLAR Technology Group, Inc.


Introduction

Key Concepts

Task

Processes

Creating a duplicate process using fork

Parent and Child Processes

Shared “Objects” and Properties

Executing a process

exec family API calls

Decoding the exec family of functions

Waiting on a child process

A Procedural Simple Shell Example

Creating a Forking Class

Class Definition

Constructor

Forking

Waiting

OnChild Property

WaitForChild Property

Creating an “Exec”ing Class

Class Definition

Constructor

Exec Method

Parameters Property

Using the Classes – A Simple OO Shell

Comparing the two shells

Additional Resources


Introduction

The purpose of this paper is to create Fork and Exec classes with Kylix. In this paper, you will learn what fork and exec APIs do, how to create reusable classes and how to write a simple Linux shell, one that uses the classes and one without them.

Key Concepts

Listed below are several key concepts that need to be understood before diving into the topic at hand. Since these concepts are woven into this entire paper, it is essential that these concepts are clearly understood.

Task

A task is a unit of work that is scheduled by the Linux scheduler. It includes both processes and threads.

Processes

Processes have existed from the beginning of the programming era. A process is an executable program that is currently loaded. It can either be running, stopped or blocked. Stopped processes are those are currently being debugged or have received a SIGSTOP, SIGSTP, SIGTTIN, or a SIGTTOU signal. Blocked processes are those that are waiting to be executed. Each process has it’s own instruction pointer, address space, memory space, and other various CPU registers.

The instruction pointer is used to keep track of what the current instruction the program is running. Address space (also known as the text segment) refers to the memory location of where the process’ program code is loaded. Memory space refers to the heap, where memory is dynamically allocated, the stack where local variables are located and the global process data.

Without using an IPC of some kind, a process cannot communicate with another process. The good news is a process cannot directly read or write another process’ memory. This is good news because a process cannot inadvertently corrupt another process. Imagine if a program could begin to write data in a valid working process. The results could be disastrous. At the same time, the bad news is a process cannot directly read or write another process’ memory. When processes need to communicate, they need to use an IPC like pipes or shared memory. Coordinating and using the chosen IPC mechanism requires diligent design and adds additional overhead.

New processes are created by using one of the exec functions: xeclp, execle, execv, and execvp. Theses functions are explained later in this paper.  

Creating a duplicate process using fork

In Linux and other versions of UNIX, creating duplicate processes are a normal way of life. Programmers frequently create duplicate processes to divide up tasks when it makes sense. Examples of processes that use this method include Apache (httpd), Minimal getty for consoles (mingetty) and TCP/IP IDENT protocol server (identd).

Creating a duplicate process is accomplished by using the fork API. Fork does not take any arguments and returns an integer. It’s definition from Libc.pas looks like this:

function fork: Integer; cdecl;

Fork operates is an unusual way, in that it is called once, but returns twice!

Parent and Child Processes

The process that initiates the call to fork is called the parent process. Fork then creates another process that is called the child process. This child process is identical from its parent at the moment it is created. In other words, the child gets a copy of the parent’s text and memory space. An important point to remember is that they do not share the same memory, unless they have specifically requested the memory to be shared, using the shared memory functions. Certain shared objects are also copied, these are mentioned in the section below.

After fork is called, it returns an integer to both the parent and child processes. If the value is -1 this indicates an error, with no child process created. A value of zero indicates that the child process code is being executed. Positive integers represent the child’s process identifier (PID) and the code being executed is in the parent’s process.

Once the child process has been created, a choice needs to be made regarding how to proceed. The code for the child process can be in the same executable or it can load another program in it’s place, replacing the existing child process. For server applications like Apache (on UNIX platforms), keeping the child code in the same executable is the typical approach.

Shared “Objects” and Properties

As mentioned above, when a child process is created, it shares various system “objects” with its parent process. These objects include all open file descriptors, copies of all global variables, and signal handlers of the parent. If one or more segments of shared memory have been set up, these too are shared between processes. Other objects that can be shared, if they are placed in shared memory and the process-shared attribute has been set, are POSIX Mutexes and Condition Variables, and POSIX memory based semaphores.

Executing a process

Replacing a process is accomplished by using the one of the following “exec” APIs: execl, execle, execv, execve, execlp, execvp. These functions attempt to load the program and if successful, never return, because it loads the requested executable into the child process. The table below lists the full definitions of the exec family of functions, found in Libc.pas.

Exec family of functions

function execl(__path: PChar; __arg: PChar): Integer; cdecl; varargs;

function execle(__path: PChar; __arg: PChar): Integer; cdecl; varargs;

function execv(PathName: PChar; const argv: PPChar): Integer; cdecl;

function execve(PathName: Pchar; const argv: PPChar; const envp: PPChar): Integer; cdecl;

function execlp(__file: PChar; __arg: Pchar): Integer; cdecl; varargs;

function execvp(const FileName: PChar; const argv: PPChar): Integer; cdecl;

 

Decoding the exec family of functions

Looking at the functions in the above table, there is a naming convention that helps to indicate how each function is used. First, if the function name contains a letter p, it takes a pathname, otherwise it is a filename. If the function takes a filename argument and it contains a slash, it is considered to be a pathname. Otherwise the directories in PATH environment variable are searched, looking for the specified executable file. It is possible that the file specified is not a executable file, but a shell file. Function names that contain a p, namely execlp, and execvp will attempt to run it by running the /bin/sh using the filename as the argument.

Another difference is the way the arguments are passed to the function. Second, functions that contain the letter l, parameters are passed as a list of arguments. Third, functions that contain the letter v contain a vector of arguments. Currently there is no easy way to build the parameter list needed for the execl, execle, and execlp functions, unless they are known in advance. The vector functions are much easier to deal with in Kylix.

Finally, functions that contain the letter e allow the programmer to define the list of environment variables that the new process will inherit. Those without, inherit the environment variables of the calling process. The example listed in the section below demonstrates how to use the execv function.

 

Waiting for the child process

Sometimes the parent process needs to wait for the child to finish before continuing any further. Since the fork function returns the child’s process identifier (pid), the parent can wait for the child process to terminate by using the one of the wait functions.

The table below lists the most commonly used wait functions available in Linux:

Commonly used wait functions in Linux

function wait(__stat_loc: PInteger): pid_t; cdecl;

function waitpid(__pid: pid_t; __stat_loc: Pinteger; __options: Integer): pid_t; cdecl;

The wait function blocks, waiting for any child to terminate. If the stat_loc parameter is not nil, it contains the status of the child process that terminated. When more control is needed, use the waitpid function. It allows the parent to wait for a specific process to terminate, has an option for non-blocking wait and an option for reporting on the status of stopped processes (typically used for job control). There are other wait functions available in Linux, but the functions listed above are the ones that are most commonly used.

 

A Procedural Simple Shell Example

Linux comes with various shell programs that simplify common tasks. So what exactly does a shell program do in order to execute a process? It creates another process using the fork API and loads the executable into the new, created process and runs it, waiting for it to terminate. An example that demonstrates a simple shell program is shown below.

program simpleshell;
uses Libc;
type
   TArgArray = array of AnsiString;

var
   i,j,status  : integer;
   cmdline,cmd : string;
         args  : TargArray;
  
  procedure ParseArgs(commandline : string; var cmd : string; 
                      var args : TArgArray);

  var
    tmp        : string;
    i          : integer;
    inspos     : integer;
    curLen     : integer;

  begin
    // assume there are no arguments..
    insPos := 0;
    curLen := 1;
    SetLength(args,curLen);
   
    tmp        := commandline;
    i          := Pos(' ',tmp);
    while (i > 0) do
    begin
       // found an argument
       inc(curLen);
       SetLength(args,curLen); 

       // extract the string from 1 to i-1
       args[insPos] := Copy(tmp,1,(i-1));

       tmp := Copy(tmp,i+1,length(tmp));
       i := Pos(' ',tmp);
       inc(insPos);
    end;

    if (tmp <> '') then
    begin
       // found an argument
      inc(curLen);
      SetLength(args,curLen); 
 
      // extract the string from 1 to i-1
      args[insPos] := Copy(tmp,1,length(tmp));
    end;
 
    // now get the command from the first argument
    cmd := args[0];
end;

begin
  while true do
  begin
     write('[ss]$ ');
     readln(cmdline);
     if (cmdline = '') then
       break;

     // search for arguments in the string
     ParseArgs(cmdline,cmd,args);

     i := fork;
     if (i = -1) then
     begin
       // an error occured report it and exit the loop
       perror(PChar('Error attempting to fork..'));
       break;
     end
     else if (i = 0) then
     begin
       // in the new, child process so execute the command..
       j := execv(PChar(cmd), PPChar(@args[0]));       
       if (j = -1) then
       begin
         // an error occured report it and exit the loop
         writeln('Error executing ',cmd,' cmdline ',
                cmdline,' the error was ',GetLastError);
         perror(PChar('Error: '));
         break;
       end;
     end
     else
     begin
       // in the parent process wait for my child to exit
       wait(status);
       if (status <> 0) then
       begin
          // an error occured report it and exit the loop
          perror(PChar('Error waiting for child process'));
          break;
       end;
     end;
  end;
end.

 

This simpleshell program does the following:

1) Reads a command

2) Parses the command line using the ParseArgs procedure to populate the args array.

3) Forks a new process.

4a) In the parent process, it waits for the child to exit using the wait function.

4b) In the child process, it executes the command using the execv function.

5) Once the child is finished, the parent then starts over at number one.

 A real shell program would have to handle many more things like internal commands, pipes, redirection and others. This shell shows the basics of what a shell does to execute a process.

Creating a Forking Class

Now its time to create a class that encapsulates the functionality of forking a child process. First, this class should have the ability to return which process it is in. Second, it should either wait for the child to finish or allow the caller to decide. Third, it should allow for an event process that is called when the child is being executed. Finally, it should be able to exec another process.

Class Definition

// forward definition
TExecuter = class; 

TForkerEventProc = procedure of Object;
TForkerWhichProc = (wpParent, wpChild);

TForker = class
private
  FDebug        : boolean;
  FOnChild      : TForkerEventProc;
  FWait         : boolean;
  FExec         : TExecuter;
  FStatus       : integer;

  procedure DebugMsg(msg : string);
public
  constructor Create;
  function    DoFork : TForkerWhichProc;
  procedure   DoWait;

published
  property Debug        : boolean          read FDebug   write FDebug;
  property OnChild      : TForkerEventProc read FOnChild write FOnChild;
  property WaitForChild : boolean          read FWait    write FWait;
  property Exec         : TExecuter        read FExec    write FExec;
  property WaitStatus   : integer          read FStatus;
end;

 

Constructor

The constructor initializes some default values.

constructor TForker.Create;
begin
  inherited Create;
  FDebug := false;
  FWait  := true;
end;

 

Forking

The DoFork method is the most complex function. It handles the actual forking code and determines what the class needs to do. Immeadiately, it calls the fork function. If fork returns an error, an exception is raised. When the child code is being executed, it first checks to see if the OnChild event has been assigned. If so, it calls the OnChild event. Next, it checks to see if the Exec property has been assigned. If so, it calls the Exec method. Finally, it returns notifying the caller that it is the child process. When the parent code is being executed, it checks the WaitForChild property and waits if necessary. When the waiting is over, or if there is no reason to wait, it returns, notifying the caller that it is in the parent process.

function   TForker.DoFork : TForkerWhichProc;
var
  i : integer;

begin
  i := fork;
  if i = -1 then
  begin
    raise Exception.CreateFmt('Unable to fork: Error is %d',[GetLastError]);
  end
  else if i = 0 then
  begin
    // we are in the child...
    Result := wpChild;

    // call the child
    if Assigned (FOnChild) then
      FOnChild
    else if Assigned (FExec) then
    begin
      // do the exec thing..
      FExec.Exec;
    end;

    // otherwise we fall through and let the
    // caller handle it..
  end
  else
  begin
    // we are the parent...
    Result := wpParent;

    if FWait then
    begin
      // wait for child
      wait(@FStatus);
    end;
  end;
end;

 

Waiting

The DoWait method, is using the blocking version of wait. Call this method when the parent process needs more control and does not want to have the TForker class do the waiting.

procedure TForker.DoWait;
begin  if not FWait then
    wait(@FStatus);
end;

OnChild Property

The OnChild property provides a callback method when the child process is being executed.

WaitForChild Property

This property determines if the TForker class will wait for the child or allow the parent to decide to wait or not.

Creating an “Exec”ing Class

Now it is time to write a class that wraps an exec function. This class will take a process name, a list of parameters and exec the process. In this implementation, only the execv function is being used. In order to support the other variations of the exec family functions, an environment property would need to be added and a method of choosing which exec function to use.

Class Definition

TArgArray = array of AnsiString;

TExecuter = class
private
  FDebug    : Boolean;
  FParms    : TStrings;
  FProcName : AnsiString;

  function StringListToCarray( cmd : AnsiString;
                               strlst : TStrings ) : TArgArray;
  procedure DebugMsg(msg : string);
protected
  function ListArgArray(aa : TArgArray) : string;

public
  constructor Create;
  destructor  Destroy; override;
  procedure   Exec;

published
  property Debug       : boolean      read FDebug    write FDebug;
  property Parameters  : TStrings     read FParms    write FParms;
  property ProcessName : AnsiString   read FProcName write FProcName;
end;

 

Constructor

In the constructor, properties are created and initialized.

constructor TExecuter.Create;
begin
  inherited Create;
  FDebug    := false;
  FProcName := '';
  FParms    := TStringList.Create;
end;

 

Destructor

The destructor releases the parameter list that was created in the constructor.

destructor TExecuter.Destroy;
begin
  FParms.Free;
  inherited Destroy;
end;

 

Exec Method

The Exec method takes the process name and parameters, puts them into an array and calls the execv function to overlay the current process with the one specified.

procedure TExecuter.Exec;
var
  parms : TArgArray;
  cmd   : AnsiString;
  j     : integer;

begin
  cmd   := FProcName;
  parms := StringListToCarray(cmd,FParms);
 
  j := execv(PChar(cmd), PPChar(@parms[0]));
  if j = -1 then
    raise Exception.CreateFmt('execv failed error %d',[GetLastError]);
  // when properly executed, execv will never return...
end;

 

Parameters Property

Parameters play a crucial role in executing a process and even more so, when using the execv function. In order to pass the parameters to it, the private method StringListToCarray is called to convert the string list to a structure that the execv function needs. This structure is an array of AnsiStrings. The first value is the command or process name. Subsequent positions in the array are filled with the parameters and the last position is nil, indicating the end of the array.

function TExecuter.StringListToCarray( cmd : AnsiString;
                                       strlst : TStrings ) : TArgArray;

var
  i,cnt : integer;

begin
  // set the array one bigger to account for the "NULL" end of array terminator

  cnt := strlst.Count+1;
  if cmd <> '' then
    inc(cnt);

  SetLength(Result, cnt);

  // when cmd is nothing, this will be overwritten
  Result[0] := cmd;

  for i:= 0 to strlst.Count-1 do
  begin
    Result[i+1] := strlst.Strings[i];
  end;
end;

 

 

Using the Classes – A Simple OO Shell

Now that the TForker and TExecuter classes have been created, lets re-write the simple shell example.

program oosimpleshell;

uses Classes,Process;

var
  cmdline : string;
  cmd     : string;
  f       : TForker;
  
  procedure ParseArgs(commandline : string; var cmd : string;
                      strlst : TStrings);

  var
    tmp    : string;
    i      : integer;

  begin
    // start with an empty parameter list
    strlst.Clear;

    tmp := commandline;
    i   := Pos(' ',tmp);
    while (i > 0) do
    begin
      // found an argument
      // extract the string from 1 to i-1
      strlst.Add(  Copy(tmp,1,(i-1)) );

      tmp := Copy(tmp,i+1,length(tmp));
      i   := Pos(' ',tmp);
    end;

    if (tmp <> '') then
    begin
      // found an argument
      // extract the string from 1 to len
      strlst.Add( Copy(tmp,1,length(tmp)) );
    end;

    // now get the command from the first argument
    cmd := strlst.Strings[0];

    // and delete the first parameter
    strlst.Delete(0);
  end; // ParseArgs

begin
  // create the forker object
  f := TForker.Create;
  f.Exec := TExecuter.Create;
  f.WaitForChild := true;

  while true do
  begin
    write('[ooss]$ ');
    readln(cmdline);
    if (cmdline = '') then
       break;

    // search for arguments in the string
    ParseArgs(cmdline, cmd, f.Exec.Parameters);

    f.Exec.ProcessName := cmd;
    f.DoFork;
  end;
end.

 

Comparing the two shells

Both shells have identical functionality. However, when comparing both, it is obvious that the object-oriented example is smaller, easier to understand, and hides the complexity of forking and execing processes. Using the TForker and TExecuter classes allows programmers to unleash the power of Linux and Kylix. Furthermore, by encapsulating the functionality into these easy to use classes allows expert and novice developers to write sophisticated Linux applications.

Additional Resources

Kylix 2 Development by Eric Whipple and Rick Ross, published by Wordware, ISBN # 1556227744

Advanced Programming in the UNIX Environment by W. Richard Stevens, published by Addison Wesley, ISBN 0-201-56317-7

Interprocess Communication: UNIX Network Programming Interprocess Communications Volume 2, Second Edition by W. Richard Stevens, published by Prentice Hall, ISBN 0-13-081081-9

LINUX Core Kernel Commentary by Scott Maxwell, published by Coriolis, ISBN 1-57610-469-9

Additional online resources can be found at http://rick-ross.com

Source Code

Paper Specific

Leave a Reply