Shared Memory Operations

There are two shared memory operation system calls. The first, shmat , is used to attach (map) the referenced shared memory segment into the calling process's data segment. See Table 8.6.

Table 8.6. Summary of the shmat System Call.

Include File(s)

 

Manual Section

2

Summary

void *shmat(int shmid, const void *shmaddr, int shmflg);

Return

Success

Failure

Sets errno

Reference to the data segment

-1

Yes

The first argument to shmat , shmid , is a valid shared memory identifier. The second argument, shmaddr , allows the calling process some flexibility in assigning the location of the shared memory segment. If a nonzero value is given, shmat uses this as the attachment address for the shared memory segment. If shmaddr is 0, the system picks the attachment address. In most situations ( especially if portability is of concern), it is advisable to use a value of 0 and have the system pick the address. The third argument, shmflg , is used to specify the access permissions for the shared memory segment and to request special attachment conditions, such as an aligned address or a read-only segment. The values of shmaddr and shmflg are used by the system to determine the attachment address, using the algorithm shown in Figure 8.3.

Figure 8.3. Determining the attachment address.

By default, attached segments are accessible for reading and writing. If needed, the SHM_RDONLY flag can be bitwise OR ed with the shmflg value to indicate a read-only segment. There is no flag to specify a write-only memory segment. The SHM_RND flag is used to specify whether or not the attachment address should be aligned on a page boundary. The value in the defined constant SHMLBA (found in ) is used by the system as the page size . For reference, a page is a unit of virtual address space. When a page is mapped to physical memory it is called a page frame.

When shmat is successful, it returns the address of the actual attachment. It also sets shm_atime to the current time, shm_lpid to the ID of the calling process, and increments shm_nattch by one. If shmat fails, it returns a value of -1 and sets errno to indicate the source of the error. Table 8.7 lists the error codes generated and their interpretation when the shmat system call fails. Remember that after a fork , the child inherits the attached shared memory segment(s). However, after an exec or an exit attached, shared memory segment(s) are detached but are not destroyed .

Table 8.7. shmat Error Messages.

#

Constant

perror Message

Explanation

12

ENOMEM

Cannot allocate memory

There is insufficient memory available to accommodate the shared memory segment.

13

EACCES

Permission denied

The requested operation is not allowed by current access permissions.

22

EINVAL

Invalid argument

  • The shared memory identifier is invalid.
  • Illegal address.

24

EMFILE

Too many open files

Number of attached memory segments has exceeded system limits.

EXERCISE

Create three 1-byte shared memory segments. Specify a shmaddr of 0 when attaching the segments. Does the system place the segments at contiguous locations? Why? Will the system allow reference to or modification of an address just "outside" the segment size (say, segment size +1 [or 2]) without generating an error? Why? Does the system respond the same way if the segment size is 4096? Why?

The second shared memory operation, shmdt , is used to detach the calling process's data segment from the shared memory segment. See Table 8.8.

Table 8.8. Summary of the shmdt System Call

Include File(s)

 

Manual Section

2

Summary

int shmdt ( const void *shmaddr);

Return

Success

Failure

Sets errno

-1

Yes

The shmdt system call has one argument, shmaddr , which is a reference to an attached memory segment. If shmdt is successful in detaching the memory segment, it returns a value of 0. It also sets shm_atime to the current time, shm_lpid to the ID of the calling process, and decrements shm_nattch by one. If shm_nattch becomes 0 and the memory segment is marked for deletion by the operating system, it is removed. If the shmdt call fails, it returns a value of -1 and sets errno . Table 8.9 gives the error code that is generated when shmdt fails.

Table 8.9. shmdt Error Message

#

Constant

perror Message

Explanation

22

EINVAL

Invalid argument

The value in shmaddr does not reference a valid shared memory segment.

In Program 8.2, a private shared memory segment, 30 bytes in length, is created at line 18. The shared memory segment is mapped to the process's data space (line 22) using the first available address (as picked by the system). The actual attachment address along with the addresses for etext, edata , and end are displayed for reference. A character pointer is set to reference the shared memory segment, and then a sequence of uppercase alphabetic characters is written to the referenced location (lines 3133). A fork system call is used to generate a child process. The child process redisplays the contents of the shared memory segment. The child process then modifies the contents of the shared memory by converting the uppercase alphabetics to lowercase (line 49). After it converts the alphabetics, the child process detaches the shared memory segment and exits. The parent process, after waiting for the child to exit, redisplays the contents of shared memory (which now is in lowercase), detaches the shared memory segment, and removes it.

Program 8.2 Creating, attaching, and manipulating shared memory.

File : p8.2.cxx /* Using shared memory */ #include + #include #include #include #include #include 10 #include #define SHM_SIZE 30 using namespace std; extern int etext, edata, end; int + main( ) { int shmid; char c, *shm, *s; if ((shmid=shmget(IPC_PRIVATE,SHM_SIZE,IPC_CREAT0660))< 0) { perror("shmget fail"); 20 return 1; } if ((shm = (char *)shmat(shmid, 0, 0)) == (char *) -1) { perror("shmat : parent"); return 2; + } cout << "Addresses in parent" << endl; cout << "shared mem: " << hex << int(shm) << " etext: " << &etext << " edata: " << &edata << " end: " << &end << endl << endl; 30 s = shm; // s now references shared mem for (c='A'; c <= 'Z'; ++c) // put some info there *s++ = c; *s='

File : p8.2.cxx /* Using shared memory */ #include < iostream > + #include #include #include #include #include 10 #include #define SHM_SIZE 30 using namespace std; extern int etext, edata, end; int + main( ) { int shmid; char c, *shm, *s; if ((shmid=shmget(IPC_PRIVATE,SHM_SIZE,IPC_CREAT0660))< 0) { perror(" shmget fail"); 20 return 1; } if ((shm = (char *)shmat(shmid, 0, 0)) == (char *) -1) { perror("shmat : parent"); return 2; + } cout << "Addresses in parent" << endl; cout << "shared mem: " << hex << int(shm) << " etext: " << &etext << " edata: " << &edata << " end: " << &end << endl << endl; 30 s = shm; // s now references shared mem for (c='A'; c <= 'Z'; ++c) // put some info there *s++ = c; *s=''; // terminate the sequence cout << "In parent before fork, memory is: " << shm << endl; + switch (fork( )) { case -1: perror("fork"); return 3; default: 40 wait(0); // let the child finish cout << "In parent after fork, memory is : " << shm << endl; cout << " Parent removing shared memory" << endl; shmdt(shm); shmctl(shmid, IPC_RMID, (struct shmid_ds *) 0); + break; case 0: cout << "In child after fork, memory is : " << shm << endl; for ( ; *shm; ++shm) // modify shared memory *shm += 32; 50 shmdt(shm); break; } return 0; }

'; // terminate the sequence cout << "In parent before fork, memory is: " << shm << endl; + switch (fork( )) { case -1: perror("fork"); return 3; default: 40 wait(0); // let the child finish cout << "In parent after fork, memory is : " << shm << endl; cout << " Parent removing shared memory" << endl; shmdt(shm); shmctl(shmid, IPC_RMID, (struct shmid_ds *) 0); + break; case 0: cout << "In child after fork, memory is : " << shm << endl; for ( ; *shm; ++shm) // modify shared memory *shm += 32; 50 shmdt(shm); break; } return 0; }

When Program 8.2 is run (output shown in Figure 8.4), we find that the address the system picks for the shared memory segment is not in the text or data segment address space for the process. In addition, the child process, via the fork system call, obtains access to the shared memory segment without having to make its own calls to shmget and shmat . As shown, the modifications to the shared memory segment made by the child process are seen by the parent even after the child process has detached its reference to the shared memory segment and terminated .

Figure 8.4 Output of Program 8.2.

linux$ p8.2 Addresses in parent shared mem: 40018000 etext: 0x8048c6e edata: 0x8049f6c end: 0x8049fb4 In parent before fork, memory is: ABCDEFGHIJKLMNOPQRSTUVWXYZ In child after fork, memory is : ABCDEFGHIJKLMNOPQRSTUVWXYZ In parent after fork, memory is : abcdefghijklmnopqrstuvwxyz Parent removing shared memory

EXERCISE

Run Program 8.2 on your system and record the addresses it displays. Modify the program by adding a variety of static and automatic variable declarations. Does the first free address the system picks for the shared memory segment remain constant? If not, is there a consistent set distance the system uses as an offset from the etext, edata , or end values? Why might this be? If the shared memory segment is not in the text or data segment of the process, is it actually found in the stack segment of the process? How did you determine this?

Using our previous producer/consumer example from Program 7.4 as a base, we can implement a producer/consumer relationship that uses shared memory in place of a file to convey information from one process to another. In our example, the producing process generates a series of random messages that are stored in a shared memory segment for the consumer process to read. To facilitate communication between the two processes, which may operate at differing rates, an array with six message buffers (slots) is used. The message buffer array is treated as a queue, whereby new messages are added to the tail of the list and messages to be processed are removed from the head of the list. The two integer indices, referencing the head and tail of the list respectively, are also stored in the shared memory segment. The basic configuration of the shared memory segment is shown in Figure 8.5.

Figure 8.5. Conceptual configuration of memory.

We will use two semaphores to coordinate access to the shared memory segment. The first semaphore, treated as a counting semaphore, will contain the number of available slots that can be written to. As long as this semaphore is nonzero, the producing process can continue to write its messages to the shared memory segment. Initially, this semaphore is set to indicate that six slots are available. The second semaphore, also treated as a counting semaphore, indicates the number of slots available for consumption (reading). Both the producer and consumer processes execute concurrently and reference the same shared memory segment. The activities of the processes are shown in Figure 8.6 with the areas within the boxes indicating access to the shared memory segment.

Figure 8.6. Producer and consumer activities.

To reduce the amount of coding and to provide programming consistency, a common local header file, called local.h , is generated. The local.h file contains the include statements and variable declarations needed by each of the programs that make up this example. Each program references this file in its first lines of program code via the preprocessor statement #include " local.h ". The contents of the local.h file are shown in Figure 8.7. Lines 35 through 38 define the makeup of the shared memory segment.

Figure 8.7 The common header file.

File : local.h /* Common header file: parent, producer and consumer */ #ifndef LOCAL_H + #define LOCAL_H #define _GNU_SOURCE #include #include #include 10 #include #include #include #include #include + #include #include #include #define ROWS 5 // Establish some common values #define COLS 3 20 #define SLOT_LEN 50 #define N_SLOTS 6 using namespace std; #if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED) // definition in + #else union semun { // We define: int val; // value for SETVAL struct semid_ds *buf; // buffer for IPC_STAT, IPC_SET unsigned short int *array; // array for GETALL, SETALL 30 struct seminfo *__buf; // buffer for IPC_INFO }; #endif enum {AVAIL_SLOTS, TO_CONSUME}; // Layout for shared memory + struct MEMORY { char buffer[N_SLOTS][SLOT_LEN]; int head, tail; }; // Actions for semaphores 40 struct sembuf acquire = { 0, -1, SEM_UNDO}, release = { 0, 1, SEM_UNDO}; #endif

In this example, a parent process is responsible for creating and initializing the shared memory segment and the two semaphores that control access to it. Once this has been done, the parent process will fork two child processes. The first process will be the producing process and the second, the consuming process. The code for the parent process is shown in Program 8.3.

Program 8.3 The parent process.

File : parent.cxx /* The PARENT */ #include "local.h" + int main(int argc, char *argv[ ]) { static struct MEMORY memory; static unsigned int short start_val[2] = {N_SLOTS, 0}; int semid, shmid, croaker; 10 char *shmptr; pid_t p_id, c_id, pid = getpid( ); union semun arg; memory.head = memory.tail = 0; // Check command line arguments + if ( argc != 3 ) { cerr << argv[0] << " producer_time consumer_time" << endl; return 1; } // Create, attach, clear segment 20 if ((shmid=shmget((int)pid, sizeof(memory), IPC_CREAT 0600 )) != -1){ if ((shmptr=(char *)shmat(shmid, 0, 0)) == (char *) -1){ perror("shmptr -- parent -- attach "); return 2; + } memcpy(shmptr, (char *)&memory, sizeof(memory)); } else { perror("shmid -- parent -- creation "); return 3; 30 } // Create & initialize semaphores if ((semid=semget((int)pid, 2, IPC_CREAT 0666)) != -1) { arg.array = start_val; if (semctl(semid, 0, SETALL, arg) == -1) { + perror("semctl -- parent -- initialization"); return 4; } } else { perror("semget -- parent -- creation "); 40 return 5; } // Fork PRODUCER process if ( (p_id=fork( )) == -1) { perror("fork -- producer"); + return 6; } else if ( p_id == 0 ) { execl( "producer", "./producer", argv[1], (char *) 0); perror("execl -- producer"); return 7; 50 } // Fork CONSUMER process if ( (c_id =fork( )) == -1) { perror("fork -- consumer"); return 8; + } else if ( c_id == 0 ) { execl( "consumer", "./consumer", argv[2], (char *) 0); perror("execl -- consumer"); return 9; } // Wait for 1 to die - 60 croaker = (int) wait( (int *) 0 ); // kill remaining process kill( ((croaker == p_id ) ? c_id : p_id), SIGKILL); shmdt( shmptr ); shmctl(shmid,IPC_RMID,(struct shmid_ds *)0); semctl( semid, 0, IPC_RMID, 0); + return 0; }

The parent process expects two integer values to be passed via the command line (program lines 1518). These values indicate a maximum time, in seconds, for a process to sleep during its execution cycle. The first value is passed to the producing process and the second to the consuming process. By specifying differing values on the command line, we can easily simulate producer/consumer relationships that operate at different speeds. In lines 20 through 25, we create and attach the shared memory segment. Once this is done, we copy the contents of our memory structure (which has been set to its initial values) to the shared memory segment using the library function memcpy . The memcpy function is one of a group of functions that work with sequences of bytes bounded by a byte count value rather than by a terminating NULL character. See Table 8.10.

The memcpy function copies n number of bytes from the location referenced by src to the location referenced by dest . Upon completion, a pointer to the dest location is returned. Be careful: The memcpy function does not check for overflow.

In lines 32 through 41 of the parent program, the two semaphores that control access to the shared memory segment are created and set to their initial values. The AVAIL_SLOTS semaphore is set to 6 to reflect the six available slots, and the TO_CONSUME semaphore is set to 0. A child process is then forked and overlaid with the producer process code (line 47). The producing process is passed a single integer argument to be used as its sleep time. Following this, the parent process forks a second child process, which it then overlays with the consumer process code (line 56). The consumer process is also passed an integer sleep value as its first argument. Once this is done, the parent process waits for one of its child processes (either the producer or consumer) to terminate. When this occurs, the PID is returned and stored in the program variable croaker . The parent process then checks the contents of this variable to determine which child process remains. The remaining process is removed with a call to kill , and the shared memory segment is detached and removed. The code for the producer process is shown in Program 8.4.

Table 8.10. Summary of the memcpy Library Function.

Include File(s)

Manual Section

3

Summary

void *memcpy(void *dest, const void *src,size_t n);

Return

Success

Failure

Sets errno

A pointer to dest

   

Program 8.4 The producer process.

File : producer.cxx /* The PRODUCER ... */ #include "local.h" + int main(int argc, char *argv[]) { static char *source[ROWS][COLS] = { {"A", "The", "One"}, {" red", " polka-dot", " yellow"}, 10 {" spider", " dump truck", " tree"}, {" broke", " ran", " fell"}, {" down", " away", " out"} }; static char local_buffer[SLOT_LEN]; + int i, r, c, sleep_limit, semid, shmid; pid_t ppid = getppid( ); char *shmptr; struct MEMORY *memptr; // Check command line 20 if ( argc != 2 ) { cerr << argv[0] << " sleep_time" << endl; return 20; } // Access, attach & ref mem + if ((shmid=shmget((int) ppid, 0, 0)) != -1 ){ if ((shmptr=(char *)shmat(shmid,(char *)0,0))==(char *)-1){ perror("shmat -- producer -- attach "); return 21; } 30 memptr = (struct MEMORY *) shmptr; } else { perror("shmget -- producer -- access "); return 22; } + // Access semaphore set if ( (semid=semget((int) ppid, 2, 0)) == -1 ) { perror("semget -- producer -- access "); return 23; } 40 sleep_limit = atoi(argv[1]) % 20; i = 20 - sleep_limit; srand((unsigned)getpid()); while( i-- ) { memset(local_buffer, '

File : producer.cxx /* The PRODUCER ... */ #include "local.h" + int main(int argc, char *argv[]) { static char *source[ROWS][COLS] = { {"A", "The", "One"}, {" red", " polka-dot", " yellow"}, 10 {" spider", " dump truck", " tree"}, {" broke", " ran", " fell"}, {" down", " away", " out"} }; static char local_buffer[SLOT_LEN]; + int i, r, c, sleep_limit, semid, shmid; pid_t ppid = getppid( ); char *shmptr; struct MEMORY *memptr; // Check command line 20 if ( argc != 2 ) { cerr << argv[0] << " sleep_time" << endl; return 20; } // Access, attach & ref mem + if ((shmid=shmget((int) ppid, 0, 0)) != -1 ){ if ((shmptr=(char *)shmat(shmid,(char *)0,0))==(char *)-1){ perror("shmat -- producer -- attach "); return 21; } 30 memptr = (struct MEMORY *) shmptr; } else { perror("shmget -- producer -- access "); return 22; } + // Access semaphore set if ( (semid=semget((int) ppid, 2, 0)) == -1 ) { perror("semget -- producer -- access "); return 23; } 40 sleep_limit = atoi(argv[1]) % 20; i = 20 - sleep_limit; srand ((unsigned)getpid()); while( i-- ) { memset (local_buffer, '', sizeof(local_buffer)); + for (r = 0; r < ROWS; ++r) { // Make a random string c = rand() % COLS; strcat(local_buffer, source[r][c]); } acquire.sem_num = AVAIL_SLOTS; 50 if (semop(semid, &acquire, 1 ) == -1 ){ <-- 1 perror("semop -- producer -- acquire "); <-- 1 return 24; <-- 1 } <-- 1 strcpy (memptr->buffer[memptr->tail], local_buffer); <-- 1 + cout << "P: [" << memptr->tail << "] " <-- 1 << memptr->buffer[memptr->tail] << endl; <-- 1 memptr->tail = (memptr->tail +1) % N_SLOTS; <-- 1 release.sem_num = TO_CONSUME; <-- 1 if (semop( semid, &release, 1 ) == -1 ) { 60 perror("semop -- producer -- release "); return 25; } sleep( rand( ) % sleep_limit + 1 ); } + return 0; }

', sizeof(local_buffer)); + for (r = 0; r < ROWS; ++r) { // Make a random string c = rand() % COLS; strcat(local_buffer, source[r][c]); } acquire.sem_num = AVAIL_SLOTS; 50 if (semop(semid, &acquire, 1 ) == -1 ){ <-- 1 perror("semop -- producer -- acquire "); <-- 1 return 24; <-- 1 } <-- 1 strcpy(memptr->buffer[memptr->tail], local_buffer); <-- 1 + cout << "P: [" << memptr->tail << "] " <-- 1 << memptr->buffer[memptr->tail] << endl; <-- 1 memptr->tail = (memptr->tail +1) % N_SLOTS; <-- 1 release.sem_num = TO_CONSUME; <-- 1 if (semop( semid, &release, 1 ) == -1 ) { 60 perror("semop -- producer -- release "); return 25; } sleep( rand( ) % sleep_limit + 1 ); } + return 0; }

(1) Once the random string is generated, acquire the AVAIL_SLOTS semaphore, store the string, update the tail index, and increment the TO_CONSUME semaphore.

The producer process allocates a two-dimensional array, source , that contains a series of strings used to generate random messages to store in the shared memory segment. A storage location, local_buffer , is created that temporarily holds the message. Next , the PID of the parent is obtained via the getppid system call. The parent PID is used as the key value for the shmget system call. This enables the producer process to reference the shared memory segment that was created by the parent process. Another approach would be to pass the shared memory identifier from the parent process to the producer via the command line. If this were done, the parent process would convert the integer shared memory identifier to a character string before passing it, and the producing process would convert the string back to its original integer format.

In program lines 25 through 29, the producer process gains access to the shared memory segment and attaches it. The producer uses a local pointer, memptr , to assign the shared memory address at program line 30 in order to reference the shared memory location. The producer process then gains access to the semaphore set (again using the parent PID as the semget key value). After this is done, the limit for the time to sleep during its processing cycle is obtained (line 40), and the maximum number of messages to be generated is calculated.

The program then loops through the following steps. It clears the local_buffer by filling it with null characters. A short random message is produced and stored in the local_buffer . The producer then evaluates the AVAIL_SLOTS semaphore. Once the producer can acquire the semaphore (which by definition will occur only if the semaphore is nonzero), [2] the message in local_buffer is copied to the shared memory location using the value in the memory->tail location as an offset index. The message that is stored is displayed to the screen for reference. The memory->tail value is then incremented in a modular fashion so as to reference the next valid storage location. The TO_CONSUME semaphore is incremented next to indicate the addition of another message. The producer then sleeps a maximum of sleep_limit seconds and continues its processing loop. The producer exits when all messages have been produced and written to the shared memory segment or when it receives a termination signal (such as from its parent process). The code for the consumer process is shown in Program 8.5.

[2] The contents of the AVAIL_SLOTS semaphore is decremented when it is acquired .

Program 8.5 The consumer process.

File : consumer.cxx /* The CONSUMER */ #include "local.h" + int main(int argc, char *argv[]) { static char local_buffer[SLOT_LEN]; int i, sleep_limit, semid, shmid; pid_t ppid = getppid( ); 10 char *shmptr; struct MEMORY *memptr; // Check command line if ( argc != 2 ) { cerr << argv[0] << " sleep_time" << endl; + return 30; } // Access, attach & ref memory if ((shmid=shmget((int) ppid, 0, 0)) != -1 ){ if ( (shmptr=(char *)shmat(shmid,(char *)0,0)) == (char *) -1){ 20 perror("shmat -- consumer -- attach"); return 31; } memptr = (struct MEMORY *) shmptr; } else { + perror("shmget -- consumer -- access"); return 32; } // Access semaphore set if ( (semid=semget((int) ppid, 2, 0)) == -1 ) { 30 perror("semget -- consumer -- access "); return 33; } sleep_limit = atoi(argv[1]) % 20; i = 20 - sleep_limit; + srand((unsigned)getpid()); while( i ) { acquire.sem_num = TO_CONSUME; if (semop(semid, &acquire, 1 ) == -1 ){ perror("semop -- consumer -- acquire "); 40 return 34; } memset(local_buffer, '

File : consumer.cxx /* The CONSUMER */ #include "local.h" + int main(int argc, char *argv[]) { static char local_buffer[SLOT_LEN]; int i, sleep_limit, semid, shmid; pid_t ppid = getppid( ); 10 char *shmptr; struct MEMORY *memptr; // Check command line if ( argc != 2 ) { cerr << argv[0] << " sleep_time" << endl; + return 30; } // Access, attach & ref memory if ((shmid=shmget((int) ppid, 0, 0)) != -1 ){ if ( (shmptr=(char *)shmat(shmid,(char *)0,0)) == (char *) -1){ 20 perror("shmat -- consumer -- attach"); return 31; } memptr = (struct MEMORY *) shmptr; } else { + perror("shmget -- consumer -- access"); return 32; } // Access semaphore set if ( (semid=semget((int) ppid, 2, 0)) == -1 ) { 30 perror("semget -- consumer -- access "); return 33; } sleep_limit = atoi(argv[1]) % 20; i = 20 - sleep_limit; + srand((unsigned)getpid()); while( i ) { acquire.sem_num = TO_CONSUME; if (semop(semid, &acquire, 1 ) == -1 ){ perror("semop -- consumer -- acquire "); 40 return 34; } memset(local_buffer, '', sizeof(local_buffer)); strcpy(local_buffer, memptr->buffer[memptr->head]); cout << "C: [" << memptr->head << "] " + << local_buffer << endl; memptr->head = (memptr->head +1) % N_SLOTS; release.sem_num = AVAIL_SLOTS; if (semop( semid, &release, 1 ) == -1 ) { perror("semop -- consumer -- release "); 50 return 35; } sleep( rand( ) % sleep_limit + 1 ); } return 0; + }

', sizeof(local_buffer)); strcpy(local_buffer, memptr->buffer[memptr->head]); cout << "C: [" << memptr->head << "] " + << local_buffer << endl; memptr->head = (memptr->head +1) % N_SLOTS; release.sem_num = AVAIL_SLOTS; if (semop( semid, &release, 1 ) == -1 ) { perror("semop -- consumer -- release "); 50 return 35; } sleep( rand( ) % sleep_limit + 1 ); } return 0; + }

In most aspects, the logic for the consumer process is similar to that of the producer process. However, the consumer will be allowed access to the shared memory segment via the TO_CONSUME semaphore. If this semaphore is nonzero, it indicates there are messages available for the consumer to read. When a message is available, the consumer copies the message to its local_buffer array from the shared memory location using the value in memory->head as an offset index. The local_buffer contents are then displayed on the screen for reference. As in the producer process, the value referenced by memory->head is incremented in a modular fashion to reference the next valid location. The AVAIL_SLOTS semaphore is incremented, and the consumer continues its processing.

When viewing the output of a run of the program, note that if the parent process is passed a set of values that allow the producer process to be faster than the consumer process, the shared memory location will eventually become full. When this occurs, the producer must block and wait [3] for the consumer to read a message. Only after a message has been read by the consumer is a slot released and a new message stored by the producer. See Figure 8.8.

[3] The default action when attempting to acquire a zero value semaphore.

Figure 8.8 Output when the producer process works faster than the consumer process.

linux$ parent 1 3 P: [0] The yellow tree broke out C: [0] The yellow tree broke out P: [1] One yellow spider broke away C: [1] One yellow spider broke away P: [2] One red dump truck fell away <-- 1 P: [3] The polka-dot dump truck broke away <-- 1 P: [4] One red spider broke away <-- 1 C: [2] One red dump truck fell away <-- 1 P: [5] The yellow dump truck ran out P: [0] A red dump truck broke away . . .

(1) The producer is working faster than the consumer.

If values are passed to the producer/consumer that permit them to work at similar rates, we should find the six-element message array sufficient to allow both processes to continue their work without each having an inordinate amount of waiting for the other process to finish its task. However, the consumer process will still wait should no new messages be available. See Figure 8.9.

Figure 8.9 Output when the consumer process works at the same rate the producer process.

linux$ parent 3 3 P: [0] One yellow spider fell away C: [0] One yellow spider fell away P: [1] One yellow spider fell away C: [1] One yellow spider fell away P: [2] One yellow tree broke out P: [3] A yellow dump truck ran away C: [2] One yellow tree broke out C: [3] A yellow dump truck ran away P: [4] The polka-dot dump truck broke out C: [4] The polka-dot dump truck broke out . . .

EXERCISE

In this producer/consumer example, the code to display the message to the screen and the adjusting of the head/tail indices was done within the critical regions bounded by the two semaphores. Is this actually necessary? Why, why not? If both the producer and consumer know there are six buffer slots, are two semaphores actually needed? Why?

EXERCISE

Modify the producer/consumer example to support multiple consumers. Can this be done without adding another semaphore? Support your findings with output.

EXERCISE

Modify the producer/consumer example to support multiple producers . Is yet another semaphore needed to coordinate process activity? Supply output to support your findings.

Категории