Chapter 3. Processes in UNIX

A process is the basic active entity in most operating-system models. This chapter covers the UNIX process model, including process creation, process destruction and daemon processes. The chapter uses process fans and process chains to illustrate concepts of parentage, inheritance and other process relationships. The chapter also looks at the implications of critical sections in concurrent processes.

Process Identification

UNIX identifies processes by a unique integral value called the process ID. Each process also has a parent process ID, which is initially the process ID of the process that created it. If this parent process terminates, the process is adopted by a system process so that the parent process ID always identifies a valid process.

The getpid and getppid functions return the process ID and the parent process ID, respectively. The pid_t is an unsigned integer type that represents a process ID.

SYNOPSIS

   #include <unistd.h>

   pid_t getpid(void);
   pid_t getppid(void) ;
                                  POSIX

Neither the getpid nor the getppid functions can return an error.

Example 3.1. outputPID.c

The following program outputs its process ID and its parent process ID. Notice that the return values are cast to long for printing since there is no guarantee that a pid_t will fit in an int.

#include <stdio.h>
#include <unistd.h>

int main (void) {
   printf("I am process %ld
", (long)getpid());
   printf("My parent is %ld
", (long)getppid());
   return 0;
}

System administrators assign a unique integral user ID and an integral group ID to each user when creating the user’s account. The system uses the user and group IDs to retrieve from the system database the privileges allowed for that user. The most privileged user, superuser or root, has a user ID of 0. The root user is usually the system administrator.

A UNIX process has several user and group IDs that convey privileges to the process. These include the real user ID, the real group ID, the effective user ID and the effective group ID. Usually, the real and effective IDs are the same, but under some circumstances the process can change them. The process uses the effective IDs for determining access permissions for files. For example, a program that runs with root privileges may want to create a file on behalf of an ordinary user. By setting the process’s effective user ID to be that of this user, the process can create the files “as if” the user created them. For the most part, we assume that the real and effective user and group IDs are the same.

The following functions return group and user IDs for a process. The gid_t and uid_t are integral types representing group and user IDs, respectively. The getgid and getuid functions return the real IDs, and getegid and geteuid return the effective IDs.

SYNOPSIS

   #include <unistd.h>

   gid_t getegid(void);
   uid_t geteuid(void);
   git_t getgid(void);
   uid_t getuid(void);
                               POSIX

None of these functions can return an error.

Example 3.2. outputIDs.c

The following program prints out various user and group IDs for a process.

#include <stdio.h>
#include <unistd.h>

int main(void) {
   printf("My real user ID is       %5ld
", (long)getuid());
   printf("My effective user ID is  %5ld
", (long)geteuid());
   printf("My real group ID is      %5ld
", (long)getgid());
   printf("My effective group ID is %5ld
", (long)getegid());
   return 0;
}

Process State

The state of a process indicates its status at a particular time. Most operating systems allow some form of the states listed in Table 3.1. A state diagram is a graphical representation of the allowed states of a process and the allowed transitions between states. Figure 3.1 shows such a diagram. The nodes of the graph in the diagram represent the possible states, and the edges represent possible transitions. A directed arc from state A to state B means that a process can go directly from state A to state B. The labels on the arcs specify the conditions that cause the transitions between states to occur.

State diagram for a simple operating system.

Figure 3.1. State diagram for a simple operating system.

While a program is undergoing the transformation into an active process, it is said to be in the new state. When the transformation completes, the operating system puts the process in a queue of processes that are ready to run. The process is then in the ready or runnable state. Eventually the component of the operating system called the process scheduler selects a process to run. The process is in the running state when it is actually executing on the CPU.

Table 3.1. Common process states.

state

meaning

new

being created

running

instructions are being executed

blocked

waiting for an event such as I/O

ready

waiting to be assigned to a processor

done

finished

A process in the blocked state is waiting for an event and is not eligible to be picked for execution. A process can voluntarily move to the blocked state by executing a call such as sleep. More commonly, a process moves to the blocked state when it performs an I/O request. As explained in Section 1.2, input and output can be thousands of times slower than ordinary instructions. A process performs I/O by requesting the service through a library function that is sometimes called a system call. During the execution of a system call, the operating system regains control of the processor and can move the process to the blocked state until the operation completes.

A context switch is the act of removing one process from the running state and replacing it with another. The process context is the information that the operating systems needs about the process and its environment to restart it after a context switch. Clearly, the executable code, stack, registers and program counter are part of the context, as is the memory used for static and dynamic variables. To be able to transparently restart a process, the operating system also keeps track of the process state, the status of program I/O, user and process identification, privileges, scheduling parameters, accounting information and memory management information. If a process is waiting for an event or has caught a signal, that information is also part of the context. The context also contains information about other resources such as locks held by the process.

The ps utility displays information about processes. By default, ps displays information about processes associated with the user. The -a option displays information for processes associated with terminals. The -A option displays information for all processes. The -o option specifies the format of the output.

SYNOPSIS

  ps [-aA] [-G grouplist] [-o format]...[-p proclist]
     [-t termlist] [-U userlist]
                                                             POSIX Shells and Utilities

Example 3.3. 

The following is sample output from the ps -a command.

>% ps -a
  PID TTY      TIME CMD
20825 pts/11   0:00 pine
20205 pts/11   0:01 bash
20258 pts/16   0:01 telnet
20829 pts/2    0:00 ps
20728 pts/4    0:00 pine
19086 pts/12   0:00 vi

The POSIX:XSI Extension provides additional arguments for the ps command. Among the most useful are the full (-f) and the long (-l) options. Table 3.2 lists the fields that are printed for each option. An (all) in the option column means that the field appears in all forms of ps.

Example 3.4. 

The execution of the ps -la command on the same system as for Example 3.3 produced the following output.

F S  UID   PID  PPID C PRI NI ADDR  SZ WCHAN TTY    TIME CMD
8 S 4228 20825 20205 0  40 20    ? 859     ? pts/11 0:00 pine
8 S 4228 20205 19974 0  40 20    ? 321     ? pts/11 0:01 bash
8 S 2852 20258 20248 0  40 20    ? 328     ? pts/16 0:01 telnet
8 O  512 20838 18178 0  50 20    ? 134       pts/2  0:00 ps
8 S 3060 20728 20719 0  40 20    ? 845     ? pts/4  0:00 pine
8 S 1614 19086 18875 0  40 20    ? 236     ? pts/12 0:00 vi

Table 3.2. Fields reported for various options of the ps command in the POSIX:XSI Extension.

header

option

meaning

F

-l

flags (octal and additive) associated with the process

S

-l

process state

UID

-f, -l

user ID of the process owner

PID

(all)

process ID

PPID

-f, -l

parent process ID

C

-f, -l

processor utilization used for scheduling

PRI

-l

process priority

NI

-l

nice value

ADDR

-l

process memory address

SZ

-l

size in blocks of the process image

WCHAN

-l

event on which the process is waiting

TTY

(all)

controlling terminal

TIME

(all)

cumulative execution time

CMD

(all)

command name (arguments with -f option)

UNIX Process Creation and fork

A process can create a new process by calling fork. The calling process becomes the parent, and the created process is called the child. The fork function copies the parent’s memory image so that the new process receives a copy of the address space of the parent. Both processes continue at the instruction after the fork statement (executing in their respective memory images).

SYNOPSIS

   #include <unistd.h>

   pid_t fork(void);
                                     POSIX

Creation of two completely identical processes would not be very useful. The fork function return value is the critical characteristic that allows the parent and the child to distinguish themselves and to execute different code. The fork function returns 0 to the child and returns the child’s process ID to the parent. When fork fails, it returns –1 and sets the errno. If the system does not have the necessary resources to create the child or if limits on the number of processes would be exceeded, fork sets errno to EAGAIN. In case of a failure, the fork does not create a child.

Example 3.5. simplefork.c

In the following program, both parent and child execute the x = 1 assignment statement after returning from fork.

#include <stdio.h>
#include <unistd.h>

int main(void) {
   int x;

   x = 0;
   fork();
   x = 1;
   printf("I am process %ld and my x is %d
", (long)getpid(), x);
   return 0;
}

Before the fork of Example 3.5, one process executes with a single x variable. After the fork, two independent processes execute, each with its own copy of the x variable. Since the parent and child processes execute independently, they do not execute the code in lock step or modify the same memory locations. Each process prints a message with its respective process ID and x value.

The parent and child processes execute the same instructions because the code of Example 3.5 did not test the return value of fork. Example 3.6 demonstrates how to test the return value of fork.

Example 3.6. twoprocs.c

After fork in the following program, the parent and child output their respective process IDs.

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

int main(void) {
   pid_t childpid;

   childpid = fork();
   if (childpid == -1) {
      perror("Failed to fork");
      return 1;
   }
   if (childpid == 0)                              /* child code */
      printf("I am child %ld
",  (long)getpid());
   else                                           /* parent code */
      printf("I am parent %ld
",  (long)getpid());
   return 0;
}

The original process in Example 3.6 has a nonzero value of the childpid variable, so it executes the second printf statement. The child process has a zero value of childpid and executes the first printf statement. The output from these processes can appear in either order, depending on whether the parent or the child executes first. If the program is run several times on the same system, the order of the output may or may not always be the same.

Example 3.7. badprocessID.c

What happens when the following program executes?

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

int main(void) {
   pid_t childpid;
   pid_t mypid;

   mypid = getpid();
   childpid = fork();
   if (childpid == -1) {
      perror("Failed to fork");
      return 1;
   }
   if (childpid == 0)                                   /* child code */
      printf("I am child %ld, ID = %ld
", (long)getpid(), (long)mypid);
   else                                                /* parent code */
      printf("I am parent %ld, ID = %ld
", (long)getpid(), (long)mypid);
   return 0;
}

Answer:

The parent sets the mypid value to its process ID before the fork. When fork executes, the child gets a copy of the process address space, including all variables. Since the child does not reset mypid, the value of mypid for the child does not agree with the value returned by getpid.

Program 3.1 creates a chain of n processes by calling fork in a loop. On each iteration of the loop, the parent process has a nonzero childpid and hence breaks out of the loop. The child process has a zero value of childpid and becomes a parent in the next loop iteration. In case of an error, fork returns –1 and the calling process breaks out of the loop. The exercises in Section 3.8 build on this program.

Figure 3.2 shows a graph representing the chain of processes generated for Program 3.1 when n is 4. Each circle represents a process labeled by its value of i when it leaves the loop. The edges represent the is-a-parent relationship. A→B means process A is the parent of process B.

Chain of processes generated by Program 3.1 when called with a command-line argument of 4.

Figure 3.2. Chain of processes generated by Program 3.1 when called with a command-line argument of 4.

Example 3.1. simplechain.c

A program that creates a chain of n processes, where n is a command-line argument.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main (int argc, char *argv[]) {
   pid_t childpid = 0;
   int i, n;

   if (argc != 2){   /* check for valid number of command-line arguments */
      fprintf(stderr, "Usage: %s processes
", argv[0]);
      return 1;
   }
   n = atoi(argv[1]);
   for (i = 1; i < n; i++)
      if (childpid = fork())
         break;

   fprintf(stderr, "i:%d  process ID:%ld  parent ID:%ld  child ID:%ld
",
           i, (long)getpid(), (long)getppid(), (long)childpid);
   return 0;
}

Example 3.8. 

Run Program 3.1 for large values of n. Will the messages always come out ordered by increasing i?

Answer:

The exact order in which the messages appear depends on the order in which the processes are selected by the process scheduler to run. If you run the program several times, you should notice some variation in the order.

Example 3.9. 

What happens if Program 3.1 writes the messages to stdout, using printf, instead of to stderr, using fprintf?

Answer:

By default, the system buffers output written to stdout, so a particular message may not appear immediately after the printf returns. Messages to stderr are not buffered, but instead written immediately. For this reason, you should always use stderr for your debugging messages.

Program 3.2 creates a fan of n processes by calling fork in a loop. On each iteration, the newly created process breaks from the loop while the original process continues. In contrast, the process that calls fork in Program 3.1 breaks from the loop while the newly created process continues for the next iteration.

Example 3.2. simplefan.c

A program that creates a fan of n processes where n is passed as a command-line argument.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main (int argc, char *argv[]) {
   pid_t childpid = 0;
   int i, n;

   if (argc != 2){   /* check for valid number of command-line arguments */
      fprintf(stderr, "Usage: %s processes
", argv[0]);
      return 1;
   }
   n = atoi(argv[1]);
   for (i = 1; i < n; i++)
      if ((childpid = fork()) <= 0)
         break;

   fprintf(stderr, "i:%d  process ID:%ld  parent ID:%ld  child ID:%ld
",
           i, (long)getpid(), (long)getppid(), (long)childpid);
   return 0;
}

Figure 3.3 shows the process fan generated by Program 3.2 when n is 4. The processes are labeled by the value of i at the time they leave the loop. The original process creates n–1 children. The exercises in Section 3.9 build on this example.

Fan of processes generated by Program 3.2 with a command-line argument of 4.

Figure 3.3. Fan of processes generated by Program 3.2 with a command-line argument of 4.

Example 3.10. 

Explain what happens when you replace the test

(childpid = fork()) <= 0

of Program 3.2 with

(childpid = fork()) == -1

Answer:

In this case, all the processes remain in the loop unless the fork fails. Each iteration of the loop doubles the number of processes, forming a tree configuration illustrated in Figure 3.4 when n is 4. The figure represents each process by a circle labeled with the i value at the time it was created. The original process has a 0 label. The lowercase letters distinguish processes that were created with the same value of i. Although this code appears to be similar to that of Program 3.1, it does not distinguish between parent and child after fork executes. Both the parent and child processes go on to create children on the next iteration of the loop, hence the population explosion.

Example 3.11. 

Run Program 3.1, Program 3.2, and a process tree program based on the modification suggested in Exercise 3.10. Carefully examine the output. Draw diagrams similar to those of Figure 3.2 through Figure 3.4, labeling the circles with the actual process IDs. Use → to designate the is-a-parent relationship. Do not use large values of the command-line argument unless you are on a dedicated system. How can you modify the programs so that you can use ps to see the processes that are created?

Answer:

In their current form, the programs complete too quickly for you to view them with ps. Insert the sleep(30); statement immediately before return in order to have each process block for 30 seconds before exiting. In another command window, continually execute ps -l. Section 3.4 explains why some of the processes may report a parent ID of 1 when sleep is omitted.

Tree of processes produced by the modification of Program 3.2 suggested in Exercise 3.10.

Figure 3.4. Tree of processes produced by the modification of Program 3.2 suggested in Exercise 3.10.

The fork function creates a new process by making a copy of the parent’s image in memory. The child inherits parent attributes such as environment and privileges. The child also inherits some of the parent’s resources such as open files and devices.

Not every parent attribute or resource is inherited by the child. For instance, the child has a new process ID and of course a different parent ID. The child’s times for CPU usage are reset to 0. The child does not get locks that the parent holds. If the parent has set an alarm, the child is not notified when the parent’s alarm expires. The child starts with no pending signals, even if the parent had signals pending at the time of the fork.

Although a child inherits its parent’s process priority and scheduling attributes, it competes for processor time with other processes as a separate entity. A user running on a crowded time-sharing system can obtain a greater share of the CPU time by creating more processes. A system manager on a crowded system might restrict process creation to prevent a user from creating processes to get a bigger share of the resources.

The wait Function

When a process creates a child, both parent and child proceed with execution from the point of the fork. The parent can execute wait or waitpid to block until the child finishes. The wait function causes the caller to suspend execution until a child’s status becomes available or until the caller receives a signal. A process status most commonly becomes available after termination, but it can also be available after the process has been stopped. The waitpid function allows a parent to wait for a particular child. This function also allows a parent to check whether a child has terminated without blocking.

The waitpid function takes three parameters: a pid, a pointer to a location for returning the status and a flag specifying options. If pid is –1, waitpid waits for any child. If pid is greater than 0, waitpid waits for the specific child whose process ID is pid. Two other possibilities are allowed for the pid parameter. If pid is 0, waitpid waits for any child in the same process group as the caller. Finally, if pid is less than –1, waitpid waits for any child in the process group specified by the absolute value of pid. Process groups are discussed in Section 11.5.

The options parameter of waitpid is the bitwise inclusive OR of one or more flags. The WNOHANG option causes waitpid to return even if the status of a child is not immediately available. The WUNTRACED option causes waitpid to report the status of unreported child processes that have been stopped. Check the man page on waitpid for a complete specification of its parameters.

SYNOPSIS

   #include <sys/wait.h>

   pid_t wait(int *stat_loc);
   pid_t waitpid(pid_t pid, int *stat_loc, int options);
                                                                   POSIX

If wait or waitpid returns because the status of a child is reported, these functions return the process ID of that child. If an error occurs, these functions return –1 and set errno. If called with the WNOHANG option, waitpid returns 0 to report that there are possible unwaited-for children but that their status is not available. The following table lists the mandatory errors for wait and waitpid.

errno

cause

ECHILD

caller has no unwaited-for children (wait), or process or process group specified by pid does not exist (waitpid), or process group specified by pid does not have a member that is a child of caller (waitpid)

EINTR

function was interrupted by a signal

EINVAL

options parameter of waitpid was invalid

Example 3.12. 

The following code segment waits for a child.

pid_t childpid;

childpid = wait(NULL);
if (childpid != -1)
   printf("Waited for child with pid %ld
", childpid);

The r_wait function shown in Program 3.3 restarts the wait function if it is interrupted by a signal. Program 3.3 is part of the restart library developed in this book and described in Appendix B. The restart library includes wrapper functions for many standard library functions that should be restarted if interrupted by a signal. Each function name starts with r_ followed by the name of the function. Include the restart.h header file when you use functions from the restart library in your programs.

Example 3.3. r_wait.c

A function that restarts wait if interrupted by a signal.

#include <errno.h>
#include <sys/wait.h>

pid_t r_wait(int *stat_loc) {
   int retval;

   while (((retval = wait(stat_loc)) == -1) && (errno == EINTR)) ;
   return retval;
}

Example 3.13. 

The following code segment waits for all children that have finished but avoids blocking if there are no children whose status is available. It restarts waitpid if that function is interrupted by a signal or if it successfully waited for a child.

pid_t childpid;

while (childpid = waitpid(-1, NULL, WNOHANG))
   if ((childpid == -1) && (errno != EINTR))
      break;

Example 3.14. 

What happens when a process terminates, but its parent does not wait for it?

Answer:

It becomes a zombie in UNIX terminology. Zombies stay in the system until they are waited for. If a parent terminates without waiting for a child, the child becomes an orphan and is adopted by a special system process. Traditionally, this process is called init and has process ID equal to 1, but POSIX does not require this designation. The init process periodically waits for children, so eventually orphaned zombies are removed.

Example 3.15. fanwait.c

The following modification of the process fan of Program 3.2 causes the original process to print out its information after all children have exited.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include "restart.h"

int main(int argc, char *argv[]) {
   pid_t childpid;
   int i, n;

   if (argc != 2) {
      fprintf(stderr, "Usage: %s n
", argv[0]);
      return 1;
   }
   n = atoi(argv[1]);
   for (i = 1; i < n; i++)
      if ((childpid = fork()) <= 0)
         break;

   while(r_wait(NULL) > 0) ; /* wait for all of your children */
   fprintf(stderr, "i:%d  process ID:%ld  parent ID:%ld  child ID:%ld
",
           i, (long)getpid(), (long)getppid(), (long)childpid);
   return 0;
}

Example 3.16. 

What happens if you interchange the while loop and fprintf statements in Example 3.15?

Answer:

The original process still exits last, but it may output its ID information before some of its children output theirs.

Example 3.17. 

What happens if you replace the while loop of Example 3.15 with the statement wait(NULL);?

Answer:

The parent waits for at most one process. If a signal happens to come in before the first child completes, the parent won’t actually wait for any children.

Example 3.18. parentwaitpid.c

Describe the possible forms of the output from the following program.

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

int main (void) {
   pid_t childpid;
                          /* set up signal handlers here ... */
   childpid = fork();
   if (childpid == -1) {
      perror("Failed to fork");
      return 1;
   }
   if (childpid == 0)
      fprintf(stderr, "I am child %ld
", (long)getpid());
   else if (wait(NULL) != childpid)
      fprintf(stderr, "A signal must have interrupted the wait!
");
   else
      fprintf(stderr, "I am parent %ld with child %ld
", (long)getpid(),
           (long)childpid);
   return 0;
}

Answer:

The output can have several forms, depending on exact timing and errors.

  1. If fork fails (unlikely unless some other program has generated a runaway tree of processes and exceeded the system limit), the "Failed to fork" message appears. Otherwise, if there are no signals, something similar to the following appears.

    I am child 3427
    I am parent 3426 with child 3427
    
  2. If the parent catches a signal after the child executes fprintf but before the child’s return, the following appears.

    I am child 3427
    A signal must have interrupted the wait!
    
  3. If the parent catches a signal after the child terminates and wait returns successfully, the following appears.

    I am child 3427
    I am parent 3426 with child 3427
    
  4. If the parent catches a signal between the time that the child terminates and wait returns, either of the previous two results is possible, depending on when the signal is caught.

  5. If the parent catches a signal before the child executes fprintf and if the parent executes its fprintf first, the following appears.

    A signal must have interrupted the wait!
    I am child 3427
    
  6. Finally, if the parent catches a signal before the child executes fprintf and the child executes its fprintf first, the following appears.

    I am child 3427
    A signal must have interrupted the wait!
    

Example 3.19. 

For the child of Exercise 3.18 to always print its message first, the parent must run wait repeatedly until the child exits before printing its own message. What is wrong with the following?

while(childpid != wait(&status)) ;

Answer:

The loop fixes the problem of interruption by signals, but wait can fail to return the childpid because it encounters a real error. You should always test errno as demonstrated in the r_wait of Program 3.3.

Example 3.20. fanwaitmsg.c

The following program creates a process fan. All the forked processes are children of the original process. How are the output messages ordered?

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main (int argc, char *argv[]) {
   pid_t childpid = 0;
   int i, n;

   if (argc != 2){      /* check number of command-line arguments */
      fprintf(stderr, "Usage: %s processes
", argv[0]);
      return 1;
   }
   n = atoi(argv[1]);
   for (i = 1; i < n; i++)
      if ((childpid = fork()) <= 0)
         break;
   for( ; ; ) {
      childpid = wait(NULL);
      if ((childpid == -1) && (errno != EINTR))
        break;
   }
   fprintf(stderr, "I am process %ld, my parent is %ld
",
                   (long)getpid(), (long)getppid());
   return 0;
}

Answer:

Because none of the forked children are parents, their wait function returns –1 and sets errno to ECHILD. They are not blocked by the second for loop. Their identification messages may appear in any order. The message from the original process comes out at the very end after it has waited for all of its children.

Example 3.21. chainwaitmsg.c

The following program creates a process chain. Only one forked process is a child of the original process. How are the output messages ordered?

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main (int argc, char *argv[]) {
   pid_t childpid;
   int i, n;
   pid_t waitreturn;

   if (argc != 2){   /* check number of command-line arguments */
      fprintf(stderr, "Usage: %s processes
", argv[0]);
      return 1;
   }
   n = atoi(argv[1]);
   for (i = 1; i < n; i++)
      if (childpid = fork())
         break;
   while(childpid != (waitreturn = wait(NULL)))
      if ((waitreturn == -1) && (errno != EINTR))
         break;
   fprintf(stderr, "I am process %ld, my parent is %ld
",
                     (long)getpid(), (long)getppid());
   return 0;
}

Answer:

Each forked child waits for its own child to complete before outputting a message. The messages appear in reverse order of creation.

Status values

The stat_loc argument of wait or waitpid is a pointer to an integer variable. If it is not NULL, these functions store the return status of the child in this location. The child returns its status by calling exit, _exit, _Exit or return from main. A zero return value indicates EXIT_SUCCESS; any other value indicates EXIT_FAILURE. The parent can only access the 8 least significant bits of the child’s return status.

POSIX specifies six macros for testing the child’s return status. Each takes the status value returned by a child to wait or waitpid as a parameter.

SYNOPSIS

   #include <sys/wait.h>

   WIFEXITED(int stat_val)
   WEXITSTATUS(int stat_val)
   WIFSIGNALED(int stat_val)
   WTERMSIG(int stat_val)
   WIFSTOPPED(int stat_val)
   WSTOPSIG(int stat_val)
                                     POSIX

The six macros are designed to be used in pairs. The WIFEXITED evaluates to a nonzero value when the child terminates normally. If WIFEXITED evaluates to a nonzero value, then WEXITSTATUS evaluates to the low-order 8 bits returned by the child through _exit(), exit() or return from main.

The WIFSIGNALED evaluates to a nonzero value when the child terminates because of an uncaught signal (see Chapter 8). If WIFSIGNALED evaluates to a nonzero value, then WTERMSIG evaluates to the number of the signal that caused the termination.

The WIFSTOPPED evaluates to a nonzero value if a child is currently stopped. If WIFSTOPPED evaluates to a nonzero value, then WSTOPSIG evaluates to the number of the signal that caused the child process to stop.

Example 3.22. showreturnstatus.c

The following function determines the exit status of a child.

#include <errno.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "restart.h"

void show_return_status(void) {
   pid_t childpid;
   int status;

   childpid = r_wait(&status);
   if (childpid == -1)
      perror("Failed to wait for child");
   else if (WIFEXITED(status) && !WEXITSTATUS(status))
      printf("Child %ld terminated normally
", (long)childpid);
   else if (WIFEXITED(status))
      printf("Child %ld terminated with return status %d
",
             (long)childpid, WEXITSTATUS(status));
   else if (WIFSIGNALED(status))
      printf("Child %ld terminated due to uncaught signal %d
",
             (long)childpid, WTERMSIG(status));
   else if (WIFSTOPPED(status))
      printf("Child %ld stopped due to signal %d
",
             (long)childpid, WSTOPSIG(status));
}

The exec Function

The fork function creates a copy of the calling process, but many applications require the child process to execute code that is different from that of the parent. The exec family of functions provides a facility for overlaying the process image of the calling process with a new image. The traditional way to use the fork–exec combination is for the child to execute (with an exec function) the new program while the parent continues to execute the original code.

SYNOPSIS

   #include <unistd.h>

   extern char **environ;
   int execl(const char *path, const char *arg0, ... /*, char *(0) */);
   int execle (const char *path, const char *arg0, ... /*, char *(0),
               char *const envp[] */);
   int execlp (const char *file, const char *arg0, ... /*, char *(0) */);
   int execv(const char *path, char *const argv[]);
   int execve (const char *path, char *const argv[], char *const envp[]);
   int execvp (const char *file, char *const argv[]);
                                                                              POSIX

All exec functions return –1 and set errno if unsuccessful. In fact, if any of these functions return at all, the call was unsuccessful. The following table lists the mandatory errors for the exec functions.

errno

cause

E2BIG

size of new process’s argument list and environment list is greater than system-imposed limit of ARG_MAX bytes

EACCES

search permission on directory in path prefix of new process is denied, new process image file execution permission is denied, or new process image file is not a regular file and cannot be executed

EINVAL

new process image file has appropriate permission and is in a recognizable executable binary format, but system cannot execute files with this format

ELOOP

a loop exists in resolution of path or file argument

ENAMETOOLONG

the length of path or file exceeds PATH_MAX, or a pathname component is longer than NAME_MAX

ENOENT

component of path or file does not name an existing file, or path or file is an empty string

ENOEXEC

image file has appropriate access permission but has an unrecognized format (does not apply to execlp or execvp)

ENOTDIR

a component of the image file path prefix is not a directory

The six variations of the exec function differ in the way command-line arguments and the environment are passed. They also differ in whether a full pathname must be given for the executable. The execl (execl, execlp and execle) functions pass the command-line arguments in an explicit list and are useful if you know the number of command-line arguments at compile time. The execv (execv, execvp and execve) functions pass the command-line arguments in an argument array such as one produced by the makeargv function of Section 2.6. The argi parameter represents a pointer to a string, and argv and envp represent NULL-terminated arrays of pointers to strings.

The path parameter to execl is the pathname of a process image file specified either as a fully qualified pathname or relative to the current directory. The individual command-line arguments are then listed, followed by a (char *)0 pointer (a NULL pointer).

Program 3.4 calls the ls shell command with a command-line argument of -l. The program assumes that ls is located in the /bin directory. The execl function uses its character-string parameters to construct an argv array for the command to be executed. Since argv[0] is the program name, it is the second argument of the execl. Notice that the first argument of execl, the pathname of the command, also includes the name of the executable.

Example 3.4. execls.c

A program that creates a child process to run ls -l.

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

int  main(void) {
   pid_t childpid;

   childpid = fork();
   if (childpid == -1)  {
       perror("Failed to fork");
       return 1;
   }
   if (childpid == 0) {                            /* child code */
       execl("/bin/ls", "ls", "-l", NULL);
       perror("Child failed to exec ls");
       return 1;
   }
   if (childpid != wait(NULL)) {                  /* parent code */
       perror("Parent failed to wait due to signal or error");
       return 1;
   }
   return 0;
}

An alternative form is execlp. If the first parameter (file) contains a slash, then execlp treats file as a pathname and behaves like execl. On the other hand, if file does not have a slash, execlp uses the PATH environment variable to search for the executable. Similarly, the shell tries to locate the executable file in one of the directories specified by the PATH variable when a user enters a command.

A third form, execle, takes an additional parameter representing the environment of the new process. For the other forms of execl, the new process inherits the environment of the calling process through the environ variable.

The execv functions use a different form of the command-line arguments. Use an execv function with an argument array constructed at run time. The execv function takes exactly two parameters, a pathname for the executable and an argument array. (The makeargv function of Program 2.2 is useful here.) The execve and execvp are variations on execv; they are similar in structure to execle and execlp, respectively.

Program 3.5 shows a simple program to execute one program from within another program. The program forks a child to execute the command. The child performs an execvp call to overwrite its process image with an image corresponding to the command. The parent, which retains the original process image, waits for the child, using the r_wait function of Program 3.3 from the restart library. The r_wait restarts its wait function if interrupted by a signal.

Example 3.23. 

The following command line to Program 3.5 causes execcmd to create a new process to execute the ls -l command.

execcmd ls -l

Program 3.5 avoids constructing the argv parameter to execvp by using a simple trick. The original argv array produced in Example 3.23 contains pointers to three tokens: myexec, ls and -l. The argument array for the execvp starts at &argv[1] and contains pointers to the two tokens ls and -l.

Example 3.24. 

How big is the argument array passed as the second argument to execvp when you execute execcmd of Program 3.5 with the following command line?

execcmd ls -l *.c

Answer:

The answer depends on the number of .c files in the current directory because the shell expands *.c before passing the command line to execcmd.

Program 3.6 creates an argument array from the first command-line argument and then calls execvp. Notice that execcmdargv calls the makeargv function only in the child process. Program 2.2 on page 37 shows an implementation of the makeargv function.

Example 3.5. execcmd.c

A program that creates a child process to execute a command. The command to be executed is passed on the command line.

#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "restart.h"

int main(int argc, char *argv[]) {
   pid_t childpid;

   if (argc < 2){      /* check for valid number of command-line arguments */
      fprintf (stderr, "Usage: %s command arg1 arg2 ...
", argv[0]);
      return 1;
   }
   childpid = fork();
   if (childpid == -1) {
      perror("Failed to fork");
      return 1;
   }
   if (childpid == 0) {                                      /* child code */
      execvp(argv[1], &argv[1]);
      perror("Child failed to execvp the command");
      return 1;
   }
   if (childpid != r_wait(NULL)) {                          /* parent code */
      perror("Parent failed to wait");
      return 1;
   }
   return 0;
}

Example 3.25. 

How would you pass a string containing multiple tokens to execcmdargv of Program 3.6?

Answer:

Place the command string in double quotes so that the command line interpreter treats the string as a single token. For example, to execute ls -l, call execcmdargv with the following command line.

execcmdargv "ls -l"

Example 3.26. 

Program 3.6 only calls the makeargv function in the child process after the fork. What happens if you move the makeargv call before the fork?

Answer:

A parent call to makeargv before the fork allocates the argument array on the heap in the parent process. The fork function creates a copy of the parent’s process image for the child. After fork executes, both parent and child have copies of the argument array. A single call to makeargv does not present a problem. However, when the parent represents a shell process, the allocation step might be repeated hundreds of times. Unless the parent explicitly frees the argument array, the program will have a memory leak.

Example 3.6. execcmdargv.c

A program that creates a child process to execute a command string passed as the first command-line argument.

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include "restart.h"

int makeargv(const char *s, const char *delimiters, char ***argvp);

int main(int argc, char *argv[]) {
   pid_t childpid;
   char delim[] = " 	";
   char **myargv;

   if (argc != 2) {
      fprintf(stderr, "Usage: %s string
", argv[0]);
      return 1;
   }
   childpid = fork();
   if (childpid == -1) {
      perror("Failed to fork");
      return 1;
   }
   if (childpid == 0) {                              /* child code */
     if (makeargv(argv[1], delim, &myargv) == -1) {
        perror("Child failed to construct argument array");
     } else {
        execvp(myargv[0], &myargv[0]);
        perror("Child failed to exec command");
     }
     return 1;
   }
   if (childpid != r_wait(NULL)) {                  /* parent code */
      perror("Parent failed to wait");
      return 1;
   }
   return 0;
}

The exec function copies a new executable into the process image. The program text, variables, stack and heap are overwritten. The new process inherits the environment (meaning the list of environment variables and their associated values) unless the original process called execle or execve. Files that are open at the time of the exec call are usually still open afterward.

Table 3.3 summarizes the attributes that are inherited by processes after exec. The second column of the table gives library functions related to the items. The IDs associated with the process are intact after exec runs. If a process sets an alarm before calling exec, the alarm still generates a signal when it expires. Pending signals are also carried over on exec in contrast to fork. The process creates files with the same permissions as before exec ran, and accounting of CPU time continues without being reinitialized.

Table 3.3. Attributes that are preserved after calls to exec. The second column lists some library functions relevant to these attributes. A * indicates an attribute inherited in the POSIX:XSI Extension.

attribute

relevant library function

process ID

getpid

parent process ID

getppid

process group ID

getpgid

session ID

getsid

real user ID

getuid

real group ID

getgid

supplementary group IDs

getgroups

time left on an alarm signal

alarm

current working directory

getcwd

root directory

 

file mode creation mask

umask

file size limit*

ulimit

process signal mask

sigprocmask

pending signals

sigpending

time used so far

times

resource limits*

getrlimit, setrlimit

controlling terminal*

open, tcgetpgrp

interval timers*

ualarm

nice value*

nice

semadj values*

semop

Background Processes and Daemons

The shell is a command interpreter that prompts for commands, reads the commands from standard input, forks children to execute the commands and waits for the children to finish. When standard input and output come from a terminal type of device, a user can terminate an executing command by entering the interrupt character. (The interrupt character is settable, but many systems assume a default value of Ctrl-C.)

Example 3.27. 

What happens when you execute the following commands?

cd /etc
ls -l

Now execute the ls -l command again, but enter a Ctrl-C as soon as the listing starts to display. Compare the results to the first case.

Answer:

In the first case, the prompt appears after the directory listing is complete because the shell waits for the child before continuing. In the second case, the Ctrl-C terminates the ls.

Most shells interpret a line ending with & as a command that should be executed by a background process. When a shell creates a background process, it does not wait for the process to complete before issuing a prompt and accepting additional commands. Furthermore, a Ctrl-C from the keyboard does not terminate a background process.

Example 3.28. 

Compare the results of Exercise 3.27 with the results of executing the following command.

ls -l &

Reenter the ls -l & command and try to terminate it by entering Ctrl-C.

Answer:

In the first case, the prompt appears before the listing completes. The Ctrl-C does not affect background processes, so the second case behaves in the same way as the first.

A daemon is a background process that normally runs indefinitely. The UNIX operating system relies on many daemon processes to perform routine (and not so routine) tasks. Under the Solaris operating environment, the pageout daemon handles paging for memory management. The in.rlogind handles remote login requests. Other daemons handle mail, file transfer, statistics and printer requests, to name a few.

The runback program in Program 3.7 executes its first command-line argument as a background process. The child calls setsid so that it does not get any signals because of a Ctrl-C from a controlling terminal. (See Section 11.5.) The runback parent does not wait for its child to complete.

Example 3.7. runback.c

The runback program creates a child process to execute a command string in the background.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include "restart.h"

int makeargv(const char *s, const char *delimiters, char ***argvp);

int main(int argc, char *argv[]) {
   pid_t childpid;
   char delim[] = " 	";
   char **myargv;

   if (argc != 2) {
      fprintf(stderr, "Usage: %s string
", argv[0]);
      return 1;
   }
   childpid = fork();
   if (childpid == -1) {
      perror("Failed to fork");
      return 1;
   }
   if (childpid == 0) {                 /* child becomes a background process */
     if (setsid() == -1)
        perror("Child failed to become a session leader");
     else if (makeargv(argv[1], delim, &myargv) == -1)
        fprintf(stderr, "Child failed to construct argument array
");
     else {
        execvp(myargv[0], &myargv[0]);
        perror("Child failed to exec command");
     }
     return 1;                                  /* child should never return */
   }
   return 0;                                                 /* parent exits */
}

Example 3.29. 

The following command is similar to entering ls -l & directly from the shell.

runback "ls -l"

Critical Sections

Imagine a scenario in which a computer system has a printer that can be directly accessed by all the processes in the system. Each time a process wants to print something, it writes to the printer device. How would the printed output look if several processes wrote to the printer simultaneously? The individual processes are allowed only a fixed quantum of processor time. If the quantum expires before a process completes writing, another process might send output to the printer. The resulting printout would have the output from the processes interspersed—an undesirable feature.

The problem with the previous scenario is that the processes are “simultaneously” attempting to access a shared resource—a resource that should be used by only one process at a time. That is, the printer requires exclusive access by the processes in the system. The portion of code in which each process accesses such a shared resource is called a critical section. Programs with critical sections must be sure not to violate the mutual exclusion requirement.

One method of providing mutual exclusion uses a locking mechanism. Each process acquires a lock that excludes all other processes before entering its critical section. When the process finishes the critical section, it releases the lock. Unfortunately, this approach relies on the cooperation and correctness of all participants. If one process fails to acquire the lock before accessing the resource, the system fails.

A common approach is to encapsulate shared resources in a manner that ensures exclusive access. Printers are usually handled by having only one process (the printer daemon) with permissions to access the actual printer. Other processes print by sending a message to the printer daemon process along with the name of the file to be printed. The printer daemon puts the request in a queue and may even make a copy of the file to print in its own disk area. The printer daemon removes request messages from its queue one at a time and prints the file corresponding to the message. The requesting process returns immediately after writing the request or after the printer daemon acknowledges receipt, not when the printing actually completes.

Operating systems manage many shared resources besides the obvious devices, files and shared variables. Tables and other information within the operating system kernel code are shared among processes managing the system. A large operating system has many diverse parts with possibly overlapping critical sections. When one of these parts is modified, you must understand the entire operating system to reliably determine whether the modification adversely affects other parts. To reduce the complexity of internal interactions, some operating systems use an object-oriented design. Shared tables and other resources are encapsulated as objects with well-defined access functions. The only way to access such a table is through these functions, which have appropriate mutual exclusion built in. In a distributed system, the object interface uses messages. Changes to modules in a properly designed object-oriented system do not have the same impact as they do for uncontrolled access.

On the surface, the object-oriented approach appears to be similar to the daemons described in Section 3.6, but structurally these approaches can be very different. There is no requirement that daemons encapsulate resources. They can fight over shared data structures in an uncontrolled way. Good object-oriented design ensures that data structures are encapsulated and accessed only through carefully controlled interfaces. Daemons can be implemented with an object-oriented design, but they do not have to be.

Exercise: Process Chains

This section expands on the process chain of Program 3.1. The chain is a vehicle for experimenting with wait and with sharing of devices. All of the processes in the chain created by Program 3.1 share standard input, standard output and standard error. The fprintf to standard error is a critical section of the program. This exercise explores some implications of critical sections. Later chapters extend this exercise to critical sections involving other devices (Chapter 6) and a token-ring simulation (Chapter 7).

Program 3.1 creates a chain of processes. It takes a single command-line argument that specifies the number of processes to create. Before exiting, each process outputs its i value, its process ID, its parent process ID and the process ID of its child. The parent does not execute wait. If the parent exits before the child, the child becomes an orphan. In this case, the child process is adopted by a special system process (which traditionally is a process, init, with process ID of 1). As a result, some of the processes may indicate a parent process ID of 1.

Do not attempt this exercise on a machine with other users because it strains the resources of the machine.

  1. Run Program 3.1 and observe the results for different numbers of processes.

  2. Fill in the actual process IDs of the processes in the diagram of Figure 3.2 for a run with command-line argument value of 4.

  3. Experiment with different values for the command-line argument to find out the largest number of processes that the program can generate. Observe the fraction that are adopted by init.

  4. Place sleep(10); directly before the final fprintf statement in Program 3.1. What is the maximum number of processes generated in this case?

  5. Put a loop around the final fprintf in Program 3.1. Have the loop execute k times. Put sleep(m); inside this loop after the fprintf. Pass k and m on the command line. Run the program for several values of n, k and m. Observe the results.

  6. Modify Program 3.1 by putting a wait function call before the final fprintf statement. How does this affect the output of the program?

  7. Modify Program 3.1 by replacing the final fprintf statement with four fprintf statements, one each for the four integers displayed. Only the last one should output a newline. What happens when you run this program? Can you tell which process generated each part of the output? Run the program several times and see if there is a difference in the output.

  8. Modify Program 3.1 by replacing the final fprintf statement with a loop that reads nchars characters from standard input, one character at a time, and puts them in an array called mybuf. The values of n and nchars should be passed as command-line arguments. After the loop, put a '' character in entry nchars of the array so that it contains a string. Output to standard error in a single fprintf the process ID followed by a colon followed by the string in mybuf. Run the program for several values of n and nchars. Observe the results. Press the Return key often and continue typing at the keyboard until all of the processes have exited.

Exercise: Process Fans

The exercises in this section expand on the fan structure of Program 3.2 through the development of a simple batch processing facility, called runsim. (Modifications in Section 14.6 lead to a license manager for an application program.) The runsim program takes exactly one command-line argument specifying the maximum number of simultaneous executions. Follow the outline below for implementing runsim. Write a test program called testsim to test the facility. Suggested library functions appear in parentheses.

  1. Write a program called runsim that takes one command-line argument.

  2. Check for the appropriate command-line argument and output a usage message if the command line is incorrect.

  3. Initialize pr_limit from the command line. The pr_limit variable specifies the maximum number of children allowed to execute at a time.

  4. Initialize the pr_count variable to 0. The pr_count variable holds the number of active children.

  5. Execute the following main loop until end-of-file is reached on standard input.

    1. If pr_count is pr_limit, wait for a child to finish (wait) and decrement pr_count.

    2. Read a line from standard input (fgets) of up to MAX_CANON characters and execute a program corresponding to that command line by forking a child (fork, makeargv, execvp).

    3. Increment pr_count to track the number of active children.

    4. Check to see if any of the children have finished (waitpid with the WNOHANG option). Decrement pr_count for each completed child.

  6. After encountering an end-of-file on standard input, wait for all the remaining children to finish (wait) and then exit.

Write a test program called testsim that takes two command-line arguments: the sleep time and the repeat factor. The repeat factor is the number of times testsim iterates a loop. In the loop, testim sleeps for the specified sleep time and then outputs a message with its process ID to standard error. Use runsim to run multiple copies of the testsim program.

Create a test file called testing.data that contains commands to run. For example, the file might contain the following lines.

testsim 5 10
testsim 8 10
testsim 4 10
testsim 13 6
testsim 1 12

Run the program by entering a command such as the following.

runsim 2 < testing.data

Additional Reading

The Design of the UNIX Operating System by Bach [9] discusses process implementation under System V. The Design and Implementation of the 4.3BSD UNIX Operating System by Leffler et al. [70] discusses process implementation for BSD UNIX. Both of these books provide detailed examinations of how real operating systems are implemented. Operating Systems: Design and Implementation, 2nd ed. by Tanenbaum and Woodhull [125] develops a full implementation of a UNIX-like operating system called MINIX. Solaris Internals: Core Kernel Architecture by Mauro and McDougall [79] is another detailed book on a UNIX implementation.

There are many books that discuss Linux implementation. For example, Linux Device Drivers, 2nd ed. by Rubini and Corbet [102] provides a detailed guide to writing device drivers for Linux. IA-64 Linux Kernel: Design and Implementation by Mossberger et al. [83] discusses the implementation of Linux on the Itanium processor.

Most general operating systems books such as Operating Systems Concepts, 6th ed. by Silberschatz et al. [107] and Modern Operating Systems by Tanenbaum [122] address the process model. Both of these references have case studies on UNIX and on Mach, a well-known microkernel operating system. Comparing these two systems would be useful at this point. P.S. to Operating Systems by Dowdy and Lowery [31] focuses on performance issues and analytical models.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset