Processes

References:

  1. Abraham Silberschatz, Greg Gagne, and Peter Baer Galvin, "Operating System Concepts, Ninth Edition ", Chapter 3

3.1 Process Concept

3.1.1 The Process


Figure 3.1 - A process in memory

3.1.2 Process State


Figure 3.2 - Diagram of process state

3.1.3 Process Control Block

For each process there is a Process Control Block, PCB, which stores the following ( types of ) process-specific information, as illustrated in Figure 3.1. ( Specific details may vary from system to system. )


Figure 3.3 - Process control block ( PCB )


Figure 3.4 - Diagram showing CPU switch from process to process


Unnumbered side bar

Digging Deeper: The Linux task_struct definition in sched.h ( See also the top of that file. )

3.1.4 Threads

3.2 Process Scheduling

3.2.1 Scheduling Queues


Figure 3.5 - The ready queue and various I/O device queues

3.2.2 Schedulers


Figure 3.6 - Queueing-diagram representation of process scheduling


Figure 3.7 - Addition of a medium-term scheduling to the queueing diagram

3.2.3 Context Switch

3.3 Operations on Processes

3.3.1 Process Creation


Figure 3.8 - A tree of processes on a typical Linux system


Figure 3.11

3.3.2 Process Termination

3.4 Interprocess Communication


Figure 3.12 - Communications models: (a) Message passing. (b) Shared memory.

3.4.1 Shared-Memory Systems

Producer-Consumer Example Using Shared Memory

3.4.2 Message-Passing Systems

3.4.2.1 Naming
3.4.2.2 Synchronization
3.4.2.3 Buffering

3.5 Examples of IPC Systems

3.5.1 An Example: POSIX Shared Memory ( Eighth Edition Version )

  1. The first step in using shared memory is for one of the processes involved to allocate some shared memory, using shmget:

    int segment_id = shmget( IPC_PRIVATE, size, S_IRUSR | S_IWUSR );

    • The first parameter specifies the key ( identifier ) of the segment. IPC_PRIVATE creates a new shared memory segment.
    • The second parameter indicates how big the shared memory segment is to be, in bytes.
    • The third parameter is a set of bitwise ORed flags. In this case the segment is being created for reading and writing.
    • The return value of shmget is an integer identifier
  2. Any process which wishes to use the shared memory must attach the shared memory to their address space, using shmat:

    char * shared_memory = ( char * ) shmat( segment_id, NULL, 0 );

    • The first parameter specifies the key ( identifier ) of the segment that the process wishes to attach to its address space
    • The second parameter indicates where the process wishes to have the segment attached. NULL indicates that the system should decide.
    • The third parameter is a flag for read-only operation. Zero indicates read-write; One indicates readonly.
    • The return value of shmat is a void *, which the process can use ( type cast ) as appropriate. In this example it is being used as a character pointer.
  3. Then processes may access the memory using the pointer returned by shmat, for example using sprintf:
  4. sprintf( shared_memory, "Writing to shared memory\n" );

  5. When a process no longer needs a piece of shared memory, it can be detached using shmdt:
  6. shmdt( shared_memory );

  7. And finally the process that originally allocated the shared memory can remove it from the system suing shmctl.

    shmctl( segment_id, IPC_RMID );

  8. Figure 3.16 from the eighth edition illustrates a complete program implementing shared memory on a POSIX system:

3.5.1 An Example: POSIX Shared Memory ( Ninth Edition Version )

  1. The ninth edition shows an alternate approach to shared memory in POSIX systems. Under this approach, the first step in using shared memory is to create a shared-memory object using shm_open( ),in a fashion similar to other file opening commands. The name provided will be the name of the memory-mapped file.
    shm_fd = shm_open( name,O_CREAT | O_RDRW,0666 );
  2. The next step is to set the size of the file using ftruncate:
    ftruncate( shm_fd, 4096 );
  3. Finally the mmap system call maps the file to a memory address in the user program space.and makes it shared. In this example the process that created the shared memory will be writing to it:
    ptr = mmap( 0, SIZE,PROT_WRITE, MAP_SHARED, shm_fd, 0 );
  4. The "borrower" of the shared memory, ( not the one who created it ), calls shm_open( ) and mmap( ) with different arguments, skips the ftruncate( ) step and unlinks ( removes ) the file name when it is done with it. Note that the "borrower" must use the same file name as the "lender" who created it. ( This information could have been passed using messages. )
    shm_unlink( name );
  5. Note that writing to and reading from the shared memory is done with pointers and memory addresses ( sprintf ) in both the 9th and 8th edition versions, even though the 9th edition is illustrating memory mapping of a file.
  6. Figures 3.17 and 3.18 from the ninth edition illustrate a complete program implementing shared memory on a POSIX system:

3.5.2 An Example: Mach

3.5.3 An Example: Windows XP


Figure 3.19 - Advanced local procedure calls in Windows

3.6 Communication in Client-Server Systems

3.6.1 Sockets


Figure 3.20 - Communication using sockets


Figure 3.21 and Figure 3.22

3.6.2 Remote Procedure Calls, RPC


Figure 3.23 - Execution of a remote procedure call ( RPC ).

3.6.3 Pipes

3.6.3.1 Ordinary Pipes


Figure 3.24


Figure 3.25 and Figure 3.26


Figure 3.27 and Figure 3.28


Figure 3.29

3.6.3.2 Named Pipes

Race Conditions ( Not from the book )

Any time there are two or more processes or threads operating concurrently, there is potential for a particularly difficult class of problems known as race conditions. The identifying characteristic of race conditions is that the performance varies depending on which process or thread executes their instructions before the other one, and this becomes a problem when the program runs correctly in some instances and incorrectly in others. Race conditions are notoriously difficult to debug, because they are unpredictable, unrepeatable, and may not exhibit themselves for years.

Here is an example involving a server and a client communicating via sockets:

1. First the server writes a greeting message to the client via the socket:

     const int BUFFLENGTH = 100;
     char buffer[ BUFFLENGTH ];
     sprintf( buffer, "Hello Client %d!", i );
     write( clientSockets[ i ], buffer, strlen( buffer ) + 1 );

2. The client then reads the greeting into its own buffer. The client does not know for sure how long the message is, so it allocates a buffer bigger than it needs to be. The following will read all available characters in the socket, up to a maximum of BUFFLENGTH characters:

     const int BUFFLENGTH = 100;
     char buffer[ BUFFLENGTH ];
     read( mysocket, buffer, BUFFLENGTH );
     cout << "Client received: " << buffer << "\n"; 

3. Now the server prepares a packet of work and writes that to the socket:

     write( clientSockets[ i ], & wPacket, sizeof( wPacket ) );

4. And finally the client reads in the work packet and processes it:

     read( mysocket, & wPacket, sizeof( wPacket ) );

The Problem: The problem arises if the server executes step 3 before the client has had a chance to execute step 2, which can easily happen depending on process scheduling. In this case, when the client finally gets around to executing step 2, it will read in not only the original greeting, but also the first part of the work packet. And just to make things harder to figure out, the cout << statement in step 2 will only print out the greeting message, since there is a null byte at the end of the greeting. This actually isn't even a problem at this point, but then later when the client executes step 4, it does not accurately read in the work packet because part of it has already been read into the buffer in step 2.

Solution I: The easiest solution is to have the server write the entire buffer in step 1, rather than just the part filled with the greeting, as:

     write( clientSockets[ i ], buffer, BUFFLENGTH );

Unfortunately this solution has two problems: (1) It wastes bandwidth and time by writing more than is needed, and more importantly, (2) It leaves the code open to future problems if the BUFFLENGTH is not the same in the client and in the server.

Solution II: A better approach for handling variable-length strings is to first write the length of the string, followed by the string itself as a separate write. Under this solution the server code changes to:

     sprintf( buffer, "Hello Client %d!", i );
     int length = strlen( buffer ) + 1;
     write( clientSockets[ i ], &length, sizeof( int ) );
     write( clientSockets[ i ], buffer, length );

and the client code changes to:
     int length;
     if( read( mysocket, &length, sizeof( int ) ) != sizeof( int ) ) {
          perror( "client read error: " );
          exit( -1 );
     }

     if( length < 1 || length > BUFFLENGTH ) {
          cerr << "Client read invalid length = " << length << endl;
          exit( -1 );
     }

     if( read( mysocket, buffer, length ) != length ) {
     perror( "client read error: " );
     exit( -1 );
     }

     cout << "Client received: " << buffer << "\n";

Note that the above solution also checks the return value from the read system call, to verify that the number of characters read is equal to the number expected. ( Some of those checks were actually in the original code, but were omitted from the notes for clarity. The real code also uses select( ) before reading, to verify that there are characters present to read and to delay if not. )

Note also that this problem could not be ( easily ) solved using the synchronization tools covered in chapter 6, because the problem is not really one of two processes accessing the same data at the same time.

3.7 Summary


OLD 3.6.3 Remote Method Invocation, RMI ( Optional, Removed from 8th edition )


Figure omitted in 8th edition


Figure omitted in 8th edition