Review of the fork system call
The only way to create a new process in Unix is for a process to call fork() This system call creates a new process which is an exact duplicate of the calling process. The process which called fork() is referred to as the parent and the new process which fork creates is called the child. Both processes, the parent and the child, are runnable and when run, start immediately after the fork system call.
Here is the function prototype
#include <sys/types.h> #include <unistd.h> pid_t fork(void);
Here is a simple program which demonstrates this.
#include <unistd.h> #include <sys/types.h> #include <stdio.h> extern int errno; int main() { pid_t pid; pid = fork(); if (pid == 0) printf("I'm the child\n"); else if (pid > 0) { printf("I'm the parent, "); printf("child pid is %d\n",pid); } else { /* pid < 0 */ perror("Error forking"); fprintf(stderr,"errno is %d\n",errno); } return 0; }Note that although the values of all variables are the same, all of the various data segments, including the run time stack are copied, so that there are two instances of each variable, allowing each process to update data independently.
The one exception to this is the return value from the call to fork()
The wait system call
Whether the parent or the child will be run first is undetermined; the term for this is a race condition.
It is possible for the parent to control this by executing a wait() system call. This call causes the parent process to be blocked until a child dies. If the process has no children, a call to wait returns immediately.
Here is the function prototype
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status);
The return value from wait is the process id of the child that died.
If a process dies before all of its children have terminated, the children become orphans. Since all processes except init have a parent, orphans are adopted by the init process.
Zombies
If a child dies before its parent calls wait(), it is possible that the parent might call wait at some later time, and would want information about the status of the dead child. In this case, the process is not really terminated, but some information is retained. A process which has terminated but whose parent has not called wait() is called a zombie. Zombies occupy a slot in the process table of the operating system although they do not consume other resources. When you examine the processes on the computer with the ps command, the status of zombies is defunct. A zombie is terminated either when the parent calls wait and gets the information about that child or when the parent dies, because zombies whose parent is init will be killed.
The exec family of system calls
It should have occurred to you that since fork can only create a copy of itself, it is of limited use. The fork call is usually used with another system call, exec, which overwrites the entire process space with a completely new process image. Execution of the new image starts at the beginning. Exec is actually a family of six system calls, the simplest of which is execl Here is the function prototype
#include <unistd.h> /* standard unix header file */ int execl(const char *path, const char *arg0, ..., const char *argn, NULL);
The first argument, path should be the pathname of an executable program. The remaining arguments are the arguments to be passed to this program as argv. The argument list is terminated by a NULL. Here is a short sample program.
#include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <wait.h> extern int errno; int main() { pid_t p; p=fork(); if (p == 0) { /* child */ execl("/bin/ls", "ls", "-l", NULL); perror("Exec failed"); } else if (p > 0) { wait(NULL); printf("Child is done\n"); } else { perror("Could not fork"); } return 0; }This program forks, creating a new process. The image of the child process is overwritten by the image for the command /bin/ls, and this is called with two arguments, ls and -l (recall that by convention,
argv[0]
is the
name of the command). The child then runs ls. When it
terminates, the parent is awakened, displays its message, and
also terminates.
Any of the exec calls can fail. The obvious cause of failure is that path is not the pathname of an executable file. If any of the exec calls succeed, there is no return because all of the code for the calling process is overwritten by the new image. If it fails, it returns a negative value like any other system call, but there is no need to check for this because it can only return if it failed. This is why there is no if before the perror call; the only way that the program can get to that line is if the call to execl failed.
There are five other system calls in the exec family. They all overwrite the current process with a new image; they differ only in the arguments that they accept and in other minor ways.
int execv(const char *path, char *const argv[])
This call is the same as execl except that it takes only two arguments, the second
being an argument vector.
int execle(const char *path, const char *arg0, ..., const
char *argn,
char * /*NULL*/, char *const envp[])
Like execl, this call
takes a variable number of arguments, but its final argument is a
vector which represents the new environment.
By default, the environment of the process which is exec'ed is the same as that of the parent, but this allows the user to change the environment.
int execve(const char *path, char *const argv[], char *const
envp[])
this is the same as execv except that it passes the
environment vector as a third argument.
int execlp(const char *file, const char *arg0, ..., const
char *argn,
char * /*NULL*/)
This differs from the above calls
in that its first argument is just a filename rather than a path, and
the call searches the PATH environment variable for the executable.
int execvp(const char *file, char *const argv[])
this is
the same as execlp except that the arguments are passed as a single
argument.
The shell
The Unix command processor, or shell, is just another process. It gets commands entered by the user, forks off a process, the child calls exec to execute the command and and the parent waits for the child process to finish.
Here is some very simplistic pseudocode for a shell.
pid_t pid; while (1) { GetNextCommand(); pid = fork(); if (pid == 0) { ExecCommand(); PrintErrorMsg() exit(0); } else if (pid > 0) wait(); else HandleForkFailure(); }
Unix allows the user to run a command in the background by appending an ampersand (&) to the command line. When a program runs in background, it cannot receive user input from the terminal and the shell prompt is displayed immediately, allowing the user to enter a new command before the background process is completed. What change would you make to the above pseudocode to allow a command to run in the background?
Creating a New Process in Win32
The Win32API to create a new process is
BOOL CreateProcess( LPCTSTR lpApplicationName, /* executable program */ LPTSTR lpCommandLine, /* command line args */ LPSECURITY_ATTRIBUTES lpProcessAttributes, /* use NULL */ LPSECURITY_ATTRIBUTES lpThreadAttributes, /* use NULL */ BOOL bInheritHandles, /* does proc inherit parents open handles */ DWORD dwCreationFlags, LPVOID lpEnvironment, /* if NULL, use parent environment */ LPCTSTR lpCurrentDirectory, /* if NULL, use parent curr dir */ LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation ); typedef struct PROCESS_INFORMATION { HANDLE hProcess; HANDLE hThread; DWORD dwProcessId; DWORD dwThreadId; } PROCESS_INFORMATION;
This is more or less equivalent to both fork() and exec() on Unix.
Win32 APIs almost always take a great many more arguments than do the equivalent Unix system calls.
LPCTSTR lpApplicationName This is the path name of the process to execute; this is more or less equivalent to the first argument of the Unix execl command. This can be either a relative or absolute pathname.
LPTSTR lpCommandLine This should normally be NULL, but it provides an opportunity to pass arguments to the process.
LPSECURITY_ATTRIBUTES lpProcessAttributes We will discuss security attributes later in the course. For the moment, this should be NULL
LPSECURITY_ATTRIBUTES lpThreadAttributes See above
BOOL bInheritHandles If TRUE, each open file handle (or other handle) is also open in the child process
DWORD dwCreationFlags There are lots of flags that can be set. For now, this can be set to zero.
LPVOID lpEnvironment You can change the environment if you wish. If this is NULL, the child process inherits the environment of the parent.
LPCTSTR lpCurrentDirectory You can change the current working directory of the child process. If NULL, the child has the same current working directory as the parent.
LPSTARTUPINFO lpStartupInfoThis is a pointer to information about how to render the new process. Since in a windows environment, a new process is typically a new window, this tells the OS where to put the window, the height and width of the window, and other information.
LPPROCESS_INFORMATION lpProcessInformation This is a structure that returns information to the parent about the child, such as the process id and the handle.
This call returns TRUE on success and FALSE on failure.
Here is a short piece of sample code that starts a new instance of Notepad.
#include <windows.h> #include <stdio.h> #include <string.h> char *GetErrorMessage() { char *ErrMsg; FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language (LPTSTR) &ErrMsg, 0, NULL ); return ErrMsg; } int main() { char commandline[255]; PROCESS_INFORMATION ProcessInfo; STARTUPINFO StartupInfo; strcpy(commandline,"notepad"); GetStartupInfo(&StartupInfo); if (CreateProcess ( "c:\\winnt\\notepad.exe", commandline, NULL, NULL, FALSE, 0, NULL, NULL, &StartupInfo, &ProcessInfo) == TRUE) { printf("Create was successful\n"); printf("Proc id is %d\n", ProcessInfo.dwProcessId); } else { printf("error, CreateProcess failed, error number %d\n",GetLastError()); printf("%s\n",GetErrorMessage()); } return 0; }
Pay particular attention to the first argument of CreateProcess. Note that since the backslash is the escape character in a string, the backslash character in a string has to be represented by a double backslash. This doesn't come up much with Unix, but it is a constant issue with Windows, because the backslash is the directory delimiter.
You can just copy my ugly function char *GetErrorMessage() which displays an error message if the process fails.
Here is a link to the full on-line help page for CreateProcess().
On both Unix and Windows, when a new process is created, three I/O streams are automatically created. These are called Standard Input, Standard Output, and Standard Error. Standard Input defaults to the keyboard of the controlling terminal; Standard Output and Standard Error default to the monitor of the controlling terminal. The reason why there are two separate output streams which default to the monitor is that it is sometimes the case that the user would like to redirect the normal output to a file, but would like to have error messages displayed on the terminal (or sent to a different file).
Users can redirect the output of a process from the terminal to a
file with the > operator at the shell prompt. For example:
ls -l > outfile
will send the output of the ls -l command to a file
called outfile instead of the terminal.
The shell can also redirect standard input from the keyboard to
a file with the <
character.
Every Unix process has an array of file descriptors associated with it. The open system call returned a file descriptor. A file descriptor is a low positive integer. By convention, standard input is file descriptor zero, standard output is file descriptor 1, and standard error is file descriptor 2. When your program opens a file, it is usually assigned the lowest unused file descriptor.
The read and write system calls take a file descriptor as their first argument.
The various C library functions that write to the terminal (e.g. printf) or read from the keyboard (e.g. scanf, getchar) have code in them which calls the write and read system calls with the file descriptors set to standard output and standard input.
To redirect standard input or standard output to a file from within the program, use the dup or dup2 system calls. These two calls duplicate a file descriptor. The call to dup2 takes two arguments, fd1 and fd2, both file descriptors. The first argument, fd1, must be an already open file descriptor. The call makes fd2 refer to the same file as fd1. If fd2 is open, it is closed first. An example will clarify this.
/* dup2.c - a demo of the dup2 call */ #include <stdio.h> #include <fcntl.h> #include <errno.h> #include <unistd.h> int main() { int fd, retval; fd = open("temp",O_WRONLY | O_CREAT | O_TRUNC, 0200 | 0400); /* fd is probably 3, the lowest unused file descriptor */ if (fd < 0) { perror("Error on opening temp"); exit(0); } printf("This line written on the terminal\n"); retval = dup2(fd,1); if (retval != 1) { perror("Error on dup2"); exit(0); } printf("This line is written to the file temp\n"); return 0; }This program opens a file called temp for writing. It then calls printf, which writes a line to the terminal. The next line is the crucial statement of the program
retval = dup2(fd,
1);
In the printf function, there is a line of code that looks
like this
n = write(1,.....
Ordinarily, the
file descriptor 1 refers to standard output, but our program has
redefined it so that it refers to the file temp. This call
to write will thus write to the file instead of to the
terminal.
If successful, dup2 returns the value of its second argument. Otherwise it returns a negative value.
dup is an earlier version of dup2 which took only one argument, the file descriptor to be duplicated, and it set the lowest unused file descriptor to be equivalent. To use this, close file descriptor zero or one as appropriat before calling dup.
Here is the man page for dup2
A call to fork creates a new process which is identical (except for the return value from fork) to the old process. The file descriptor table of the parent is also copied to the child, so if the parent has an open file, the child will inherit this open file. Also, if the parent has duplicated a file descriptor, the child will inherit this. The file descriptor table is also copied across a call to a member of the exec family of system calls.
This is important for the implementation of a shell. Recall that a shell forks off a new process to execute the command. If the user specifies the output to go to a file rather than to standard output, the shell can use dup2 to redirect standard output to a file, then call exec to execute the command.
There is a lot of overhead associated with creating a new process. A new process structure has to allocated for the process table and the appropriate values inserted, memory has to be allocated for the new process, and so on. One solution to this is for one process to have multiple threads of execution within the same process structure.
When a program executes, the cpu uses the process program counter to determine which instruction to execute next. The resulting stream of instructions is called the program's thread of execution.
A traditional process has a single thread of execution. Newer operating systems allow for the creation of multiple threads of execution running concurrently within the same process. These are sometimes called lightweight processes, because they can do many of the same things that can be accomplished by forking off multiple processes. This can best be demonstrated with a diagram.
When a traditional, single threaded program calls a function, execution stops in the calling function and starts in the called function. When the called function terminates, control returns to the calling function.
In a threaded program, it is possible to write code so that the calling function calls a function, but the called function is in a new thread and so both the called function and the calling function are executing concurrently.
A threaded program differs from spawning off a new process because when a new thread is created, the operating system does not need to go through all of the overhead of making a new entry in the process table, allocating memory, copying the parent process, and so on.
There are a number of advantages to threads over a single threaded process.
Threads are implemented differently on different operating systems. At one extreme is a pure user thread, in which the process scheduler in the kernel does not know about the existence of multiple threads in the same process. When a process is running, it has its own thread scheduler as a part of its code which decides which of several possible threads is executing at any given instant.
An alternative is a kernel thread. In this case, the kernel knows about the different threads within a single process and is responsible for thread scheduling and thread management.
When a process supports multiple threads, each thread needs its own program counter and its own run time stack for automatic variables, but the code segments and typically the global variable segments are not copied; they are common to all threads in the process.
It is far more efficient for a process to create a new thread than it is to create a new process. On a typical Solaris system, it takes 350 microseconds to create a new thread, while a fork() takes 1700 microseconds.