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.
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.
A task is a unit of work that is scheduled by the Linux scheduler. It includes both processes and threads.
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.
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!
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.
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.
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.
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;
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.
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.
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; 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)); 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.
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.
// 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;
The constructor initializes some default values.
constructor TForker.Create; begin inherited Create; FDebug := false; FWait := true; end;
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;
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;
The OnChild property provides a callback method when the child process is being executed.
This property determines if the TForker class will wait for the child or allow the parent to decide to wait or not.
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.
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;
In the constructor, properties are created and initialized.
constructor TExecuter.Create; begin inherited Create; FDebug := false; FProcName := ''; FParms := TStringList.Create; end;
The destructor releases the parameter list that was created in the constructor.
destructor TExecuter.Destroy; begin FParms.Free; inherited Destroy; end;
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)); if j = -1 then raise Exception.CreateFmt('execv failed error %d',[GetLastError]); // when properly executed, execv will never return... end;
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 := cmd; for i:= 0 to strlst.Count-1 do begin Result[i+1] := strlst.Strings[i]; end; end;
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; // 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.
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.
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.comPaper Specific