Monday, December 10, 2007

Programs, Processes and Threads

How a Program Becomes a Process

A program is a prepared sequence of instructions to accomplish a defined task. To write a C source program, a programmer creates disk files containing C statements that are organized into functions. An individual C source file may also contain variable and function declarations, type and macro definitions (e.g., typedef) and preprocessor commands (e.g., #ifdef, #include, #define). The source program contains exactly one main function.

Traditionally, C source filenames have a .c extension, and header filenames have a .h extension. Header files usually only contain macro and type definitions, defined constants and function declarations. Use the #include preprocessor command to insert the contents of a header file into the source.

The C compiler translates each source file into an object file. The compiler then links the individual object files with the necessary libraries to produce an executable module. When a program is run or executed, the operating system copies the executable module into a program image in main memory.

A process is an instance of a program that is executing. Each instance has its own address space and execution state. When does a program become a process? The operating system reads the program into memory. The allocation of memory for the program image is not enough to make the program a process. The process must have an ID (the process ID) so that the operating system can distinguish among individual processes. The process state indicates the execution status of an individual process. The operating system keeps track of the process IDs and corresponding process states and uses the information to allocate and manage resources for the system. The operating system also manages the memory occupied by the processes and the memory available for allocation.

When the operating system has added the appropriate information in the kernel data structures and has allocated the necessary resources to run the program code, the program has become a process. A process has an address space (memory it can access) and at least one flow of control called a thread. The variables of a process can either remain in existence for the life of the process (static storage) or be automatically allocated when execution enters a block and deallocated when execution leaves the block (automatic storage). Appendix A.5 discusses C storage classes in detail.

A process starts with a single flow of control that executes a sequence of instructions. The processor program counter keeps track of the next instruction to be executed by that processor (CPU). The CPU increments the program counter after fetching an instruction and may further modify it during the execution of the instruction, for example, when a branch occurs. Multiple processes may reside in memory and execute concurrently, almost independently of each other. For processes to communicate or cooperate, they must explicitly interact through operating system constructs such as the filesystem, pipes shared memory or a network .

Threads and Thread of Execution

When a program executes, the value of the process program counter determines which process instruction is executed next. The resulting stream of instructions, called a thread of execution, can be represented by the sequence of instruction addresses assigned to the program counter during the execution of the program's code.

Example 2.1

Process 1 executes statements 245, 246 and 247 in a loop. Its thread of execution can be represented as 2451, 2461, 2471, 2451, 2461, 2471, 2451, 2461, 2471 . . . , where the subscripts identify the thread of execution as belonging to process 1.

The sequence of instructions in a thread of execution appears to the process as an uninterrupted stream of addresses. From the point of view of the processor, however, the threads of execution from different processes are intermixed. The point at which execution switches from one process to another is called a context switch.

Example 2.2

Process 1 executes its statements 245, 246 and 247 in a loop as in Example 2.1, and process 2 executes its statements 10, 11, 12 . . . . The CPU executes instructions in the order 2451, 2461, 2471, 2451, 2461, [context-switch instructions], 102, 112, 122, 132, [context-switch instructions], 2471, 2451, 2461, 2471 . . . . Context switches occur between 2461 and 102 and between 132 and 2471. The processor sees the threads of execution interleaved, whereas the individual processes see uninterrupted sequences.

A natural extension of the process model allows multiple threads to execute within the same process. Multiple threads avoid context switches and allow sharing of code and data. The approach may improve program performance on machines with multiple processors. Programs with natural parallelism in the form of independent tasks operating on shared data can take advantage of added execution power on these multiple-processor machines. Operating systems have significant natural parallelism and perform better by having multiple, simultaneous threads of execution. Vendors advertise symmetric multiprocessing support in which the operating system and applications have multiple undistinguished threads of execution that take advantage of parallel hardware.

A thread is an abstract data type that represents a thread of execution within a process. A thread has its own execution stack, program counter value, register set and state. By declaring many threads within the confines of a single process, a programmer can write programs that achieve parallelism with low overhead. While these threads provide low-overhead parallelism, they may require additional synchronization because they reside in the same process address space and therefore share process resources. Some people call processes heavyweight because of the work needed to start them. In contrast, threads are sometimes called lightweight processes.

Layout of a Program Image

After loading, the program executable appears to occupy a contiguous block of memory called a program image. shows a sample layout of a program image in its logical address space [112]. The program image has several distinct sections. The program text or code is shown in low-order memory. The initialized and uninitialized static variables have their own sections in the image. Other sections include the heap, stack and environment.


An activation record is a block of memory allocated on the top of the process stack to hold the execution context of a function during a call. Each function call creates a new activation record on the stack. The activation record is removed from the stack when the function returns, providing the last-called-first-returned order for nested function calls.

The activation record contains the return address, the parameters (whose values are copied from the corresponding arguments), status information and a copy of some of the CPU register values at the time of the call. The process restores the register values on return from the call represented by the record. The activation record also contains automatic variables that are allocated within the function while it is executing. The particular format for an activation record depends on the hardware and on the programming language.

In addition to the static and automatic variables, the program image contains space for argc and argv and for allocations by malloc. The malloc family of functions allocates storage from a free memory pool called the heap. Storage allocated on the heap persists until it is freed or until the program exits. If a function calls malloc, the storage remains allocated after the function returns. The program cannot access the storage after the return unless it has a pointer to the storage that is accessible after the function returns.

Static variables that are not explicitly initialized in their declarations are initialized to 0 at run time. Notice that the initialized static variables and the uninitialized static variables occupy different sections in the program image. Typically, the initialized static variables are part of the executable module on disk, but the uninitialized static variables are not. Of course, the automatic variables are not part of the executable module because they are only allocated when their defining block is called. The initial values of automatic variables are undetermined unless the program explicitly initializes them.

Exercise 2.3

Use ls -l to compare the sizes of the executable modules for the following two C programs. Explain the results.

Version 1: largearrayinit.c

int myarray[50000] = {1, 2, 3, 4};

int main(void) {
myarray[0] = 3;
return 0;
}

Version 2: largearray.c

int myarray[50000];

int main(void) {
myarray[0] = 3;
return 0;
}

Answer:

The executable module for Version 1 should be about 200,000 bytes larger than that of Version 2 because the myarray of Version 1 is initialized static data and is therefore part of the executable module. The myarray of Version 2 is not allocated until the program is loaded in memory, and the array elements are initialized to 0 at that time.

Static variables can make a program unsafe for threaded execution. For example, the C library function readdir and its relatives described in Section 5.2 use static variables to hold return values. The function strtok discussed in Section 2.6 uses a static variable to keep track of its progress between calls. Neither of these functions can be safely called by multiple threads within a program. In other words, they are not thread-safe. External static variables also make code more difficult to debug because successive invocations of a function that references a static variable may behave in unexpected ways. For these reasons, avoid using static variables except under controlled circumstances. Section 2.9 presents an example of when to use variables with static storage class.

Although the program image appears to occupy a contiguous block of memory, in practice, the operating system maps the program image into noncontiguous blocks of physical memory. A common mapping divides the program image into equal-sized pieces, called pages. The operating system loads the individual pages into memory and looks up the location of the page in a table when the processor references memory on that page. This mapping allows a large logical address space for the stack and heap without actually using physical memory unless it is needed. The operating system hides the existence of such an underlying mapping, so the programmer can view the program image as logically contiguous even when some of the pages do not actually reside in memory.

Library Function Calls

We introduce most library functions by a condensed version of its specification, and you should always refer to the man pages for more complete information.

The summary starts with a brief description of the function and its parameters, followed by a SYNOPSIS box giving the required header files and the function prototype. (Unfortunately, some compilers do not give warning messages if the header files are missing, so be sure to use lint as described in Appendix A to detect these problems.) The SYNOPSIS box also names the POSIX standard that specifies the function. A description of the function return values and a discussion of how the function reports errors follows the SYNOPSIS box. Here is a typical summary.

The close function deallocates the file descriptor specified by fildes.

SYNOPSIS

#include

int close(int fildes);
POSIX

If successful, close returns 0. If unsuccessful, close returns –1 and sets errno. The following table lists the mandatory errors for close.

errno

cause

EBADF

fildes is not valid

EINTR

close was interrupted by a signal

This book's summary descriptions generally include the mandatory errors. These are the errors that the standard requires that every implementation detect. We include these particular errors because they are a good indication of the major points of failure. You must handle all errors, not just the mandatory ones. POSIX often defines many other types of optional errors. If an implementation chooses to treat the specified condition as an error, then it should use the specified error value. Implementations are free to define other errors as well. When there is only one mandatory error, we describe it in a sentence. When the function has more than one mandatory error, we use a table like the one for close.

Traditional UNIX functions usually return –1 (or sometimes NULL) and set errno to indicate the error. The POSIX standards committee decided that all new functions would not use errno and would instead directly return an error number as a function return value. We illustrate both ways of handling errors in examples throughout the text.

Example 2.4

The following code segment demonstrates how to call the close function.

int fildes;

if (close(fildes) == -1)
perror("Failed to close the file");

The code assumes that the unistd.h header file has been included in the source. In general, we do not show the header files for code segments.

The perror function outputs to standard error a message corresponding to the current value of errno. If s is not NULL, perror outputs the string (an array of characters terminated by a null character) pointed to by s and followed by a colon and a space. Then, perror outputs an error message corresponding to the current value of errno followed by a newline.

SYNOPSIS

#include

void perror(const char *s);
POSIX:CX

No return values and no errors are defined for perror.

Example 2.5

The output produced by Example 2.4 might be as follows.

Failed to close the file: invalid file descriptor

The strerror function returns a pointer to the system error message corresponding to the error code errnum.

SYNOPSIS

#include

char *strerror(int errnum);
POSIX:CX

If successful, strerror returns a pointer to the error string. No values are reserved for failure.

Use strerror to produce informative messages, or use it with functions that return error codes directly without setting errno.

Example 2.6

The following code segment uses strerror to output a more informative error message when close fails.

int fildes;

if (close(fildes) == -1)
fprintf(stderr, "Failed to close file descriptor %d: %s\n",
fildes, strerror(errno));

The strerror function may change errno. You should save and restore errno if you need to use it again.

Example 2.7

The following code segment illustrates how to use strerror and still preserve the value of errno.

int error;
int fildes;

if (close(fildes) == -1) {
error = errno; /* temporarily save errno */
fprintf(stderr, "Failed to close file descriptor %d: %s\n",
fildes, strerror(errno));
errno = error; /* restore errno after writing the error message */
}

Correctly handing errno is a tricky business. Because its implementation may call other functions that set errno, a library function may change errno, even though the man page doesn't explicitly state that it does. Also, applications cannot change the string returned from strerror, but subsequent calls to either strerror or perror may overwrite this string.

Another common problem is that many library calls abort if the process is interrupted by a signal. Functions generally report this type of return with an error code of EINTR. For example, the close function may be interrupted by a signal. In this case, the error was not due to a problem with its execution but was a result of some external factor. Usually the program should not treat this interruption as an error but should restart the call.

Example 2.8

The following code segment restarts the close function if a signal occurs.

int error;
int fildes;

while (((error = close(fildes)) == -1) && (errno == EINTR)) ;
if (error == -1)
perror("Failed to close the file"); /* a real close error occurred */

The while loop of Example 2.8 has an empty statement clause. It simply calls close until it either executes successfully or encounters a real error. The problem of restarting library calls is so common that we provide a library of restarted calls with prototypes defined in restart.h. The functions are designated by a leading r_ prepended to the regular library name. For example, the restart library designates a restarted version of close by the name r_close.

Example 2.9

The following code segment illustrates how to use a version of close from the restart library.

#include "restart.h"     /* user-defined library not part of standard */
int fildes;

if (r_close(fildes) == -1)
perror("Failed to close the file"); /* a true close error occurred

No comments: