Chapter 8. Shared Memory

Shared Memory

Introduction

Shared memory allows multiple processes to share virtual memory space. This is the fastest but not necessarily the easiest (synchronization-wise) way for processes to communicate with one another. In general, one process creates or allocates the shared memory segment. The size and access permissions for the segment are set when it is created. The process then attaches the shared segment, causing it to be mapped[1] into its current data space. If needed, the creating process then initializes the shared memory. Once created, and if permissions permit, other processes can gain access to the shared memory segment and map it into their data space. Each process accesses the shared memory relative to its attachment address. While the data that these processes are referencing is in common, each process uses different attachment address values. For each process involved, the mapped memory appears to be no different from any other of its memory addresses. Figure 8.1 presents a diagrammatic way to envision three processes sharing a common memory segment. As there are no intrinsic shared memory synchronization constructs, semaphores are normally used to coordinate access to a shared memory segment. When a process is finished with the shared memory segment, it can detach from it. Additionally, the creator of the segment may grant ownership of the segment to another process. When all processes are finished with the shared memory segment, the process that created the segment is usually responsible for removing it.

Envisioning three processes sharing a common memory segment.

Figure 8.1. Envisioning three processes sharing a common memory segment.

Creating a Shared Memory Segment

The shmget system call is used to create the shared memory segment and generate the associated system data structure or to gain access to an existing segment. The shared memory segment and the system data structure are identified by a unique shared memory identifier that the shmget system call returns (see Table 8.1).

Providing no system parameters are exceeded, the shmget system call creates a new shared memory segment if

  • The value for its first argument, key, is the symbolic constant IPC_PRIVATE, or

  • the value key is not associated with an existing shared memory identifier and the IPC_CREAT flag is set as part of the shmflg argument (otherwise, the existing shared memory identifier associated with the key value is returned), or

    Table 8.1. Summary of the shmget System Call.

    Include File(s)

    <sys/ipc.h>
    <sys/shm.h>
    

    Manual Section

    2

    Summary

    int shmget(key_t key, int size,int shmflg);

    Return

    Success

    Failure

    Sets errno

    Shared memory identifier.

    −1

    Yes

  • the value key is not associated with an existing shared memory identifier and the IPC_CREAT along with the IPC_EXCL flag have been set as part of the shmflg argument. With IPC_CREAT and IPC_EXCL set, the user can be assured of creating a unique shared memory segment without inadvertently gaining access to a preexisting segment.

As with previous IPC system calls for message queues and semaphores, the ftok library function can be used to generate a key value.

The argument size determines the size in bytes of the shared memory segment. If we are using shmget to access an existing shared memory segment, size can be set to 0, as the segment size is set by the creating process. Common overall default system maximums, as related to shared memory, are shown in Table 8.2.

Table 8.2. Shared Memory Limits.

Shared Memory Segment Defaults

Constant

Value

Maximum segment size

SHMMAX

4 MB

Minimum segment size

SHMMIN

1 byte

Systemwide maximum number of segments

SHMMNI

4096

Maximum number of segments per process

SHMSEG

Not specified

The last argument for shmget, shmflg, is used to indicate segment creation conditions (e.g., IPC_CREAT, IPC_EXCL) and access permissions (stored in the low order 9 bits of shmflg). At this time the system does not use the execute permission settings. To specify creation conditions along with access permissions, the individual items are bitwise ORed (e.g., 0660 | IPC_CREAT).

The shmget system call does not entitle the creating process to actually use the allocated memory; it merely reserves the requested memory. To be used by the process, the allocated memory must be attached to the process using a separate system call. The technique for accomplishing this is discussed in Section 8.4.

If shmget is successful in allocating a shared memory segment, it returns an integer shared memory identifier. At creation time, the system data structure shmid_ds, defined in the <bits/shm.h> header file, is generated and initialized. As with other System V IPC facilities, the user does not directly include <bits/shm.h> but instead includes the standard header file for shared memory <sys/shm.h>, which in turn includes the <bits/shm.h>. The standard definition for the shmid_ds data structure follows:

struct shmid_ds {
    struct ipc_perm shm_perm;        /* operation permission struct     */
    size_t shm_segsz;                /* size of segment in bytes        */
    __time_t shm_atime;              /* time of last shmat()            */
    unsigned long int __unused1;
    __time_t shm_dtime;              /* time of last shmdt()            */
    unsigned long int __unused2;
    __time_t shm_ctime;              /* time of last change by shmctl() */
    unsigned long int __unused3;
    __pid_t shm_cpid;                /* pid of creator                  */
    __pid_t shm_lpid;                /* pid of last shmop               */
    shmatt_t shm_nattch;             /* number of current attaches      */
    unsigned long int __unused4;
    unsigned long int __unused5;
  };

The source files for the kernel for System V IPC (found in /usr/src/ linux-XX.XX.XX/ipc where XX are the version numbers of the operating system) defines a similar private kernel shared memory structure called shmid_kernel.

The shmid_ds structure contains an ipc_perm permission structure called shm_perm. When created, the shm_perm.cuid and shm_perm.uid members are assigned the effective user ID of the calling process, and the shm_perm.cgid and shm_perm.gid members are set to the group ID of the calling process. The access permission bits, stored in the shm_perm.mode member, are set according to the value specified by the shmflg value. The shm_segsz member is set to the specified size from the shmget system call. The shm_lpid, shm_nattch, shm_atime, and shm_dtime members are each set to 0, while the shm_ctime member is set to the current time. The shm_cpid member stores the ID of the creating process.

If shmget fails, it returns a value of −1 and sets the value in errno to indicate the specific error condition. The values that errno may be assigned and their interpretations are shown in Table 8.3.

Table 8.3. shmget Error Messages

#

Constant

perror Message

Explanation

2

EOENT

No such file or directory

The shared memory identifier does not exist for this key, and IPC_CREAT was not set.

12

ENOMEM

Cannot allocate memory

When creating a shared memory segment, insufficient memory is available.

13

EACCES

Permission denied

The shared memory identifier exists for this key, but the requested operation is not allowed by the current access permissions.

17

EEXIST

File exists

Shared memory identifier exists for this key, but IPC_CREAT and IPC_EXCL are both set.

22

EINVAL

Invalid argument

  • The value of size is less than system minimum or greater than system maximum.

  • The shared memory identifier exists, but the requested size is too large.

28

ENOSPC

No space left on device

System-imposed limit for number of shared memory segments has been reached.

43

EIDRM

Identifier removed

Memory segment is marked as removed.

Program 8.1 attempts to create two shared memory segments of differing sizes.

Example 8.1. Creating shared memory segments.

File : p8.1.cxx
  |     /*
  |           Allocating a shared memory segment
  |      */
  |     #include <iostream>
  +     #include <cstdio>
  |     #include <sys/ipc.h>
  |     #include <sys/shm.h>
  |     using namespace std;
  |     int
 10     main( ) {
  |       key_t  key = 15;
  |       int    shmid_1, shmid_2;
  |       if ((shmid_1=shmget(key, 1000, 0640|IPC_CREAT)) == -1){
  |         perror("shmget shmid_1");
  +         return 1;
  |       }
  |       cout << "First shared memory identifier is : " << shmid_1 << endl;
  |       if ((shmid_2=shmget(IPC_PRIVATE, 20, 0640)) == -1){
  |         perror("shmget shmid_2");
 20         return 2;
  |       }
  |       cout << "Second shared memory identifier is: " <<  shmid_2 << endl;
  |       return 0;
  |     }

Figure 8.2 shows the output of Program 8.1 when invoked twice in succession.

Example 8.2. Output of Program 8.1.

linux$ p8.1        <-- 1
First shared memory identifier is : 40665091
Second shared memory identifier is: 40697860

linux$ ipcs -m        <-- 2
------ Shared Memory Segments --------
key        shmid     owner     perms     bytes     nattch    status
0x0000000f 40665091  gray      640       1000      0
0x00000000 40697860  gray      640       20        0

linux$ p8.1        <-- 3
First shared memory identifier is : 40665091
Second shared memory identifier is: 40730629

linux$ ipcs -m        <-- 4

------ Shared Memory Segments --------
key        shmid     owner     perms     bytes     nattch    status
0x0000000f 40665091  gray      640       1000      0
0x00000000 40697860  gray      640       20        0
0x00000000 40730629  gray      640       20        0
  • (1)Run the program.

  • (2)Check with ipcs.

  • (3)Run the program again.

  • (4)Recheck with ipcs.

Examination of the output shows the first invocation created two shared memory segments with the identifier values of 40665091 and 40697860. The first segment, with the shared memory identification value of 40665091, was created by the first call to shmget, as the key value (15) coded in the program was not associated with any other previously allocated memory segment. The second segment, identified by the 40697860, was created by shmget, since IPC_PRIVATE was specified. However, when the program was invoked the second time, the results were slightly different. The first call to shmget returned the shared memory identifier from the first invocation of the program, as the shared memory segment already existed for the key value of 15. The second call to shmget, since it uses IPC_PRIVATE, produced another unique shared memory segment (40730629). Notice that the output for the ipcs command shows that the key value entries for both of the unique shared memory segments generated with IPC_PRIVATE are set to zero.

Shared Memory Control

The shmctl system call permits the user to perform a number of generalized control operations on an existing shared memory segment and on the system shared memory data structure (see Table 8.4).

Table 8.4. Summary of the shmctl System Call.

Include File(s)

<sys/ipc.h>
<sys/shm.h>

Manual Section

2

Summary

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

Return

Success

Failure

Sets errno

0

−1

Yes

There are three arguments for the shmctl system call. The first, shmid, is a valid shared memory segment identifier generated by a prior shmget system call. The second argument, cmd, specifies the operation shmctl is to perform. The third argument, buf, is a reference to a structure of the type shmid_ds.

The operations that shmctl will perform, which are specified by the following defined constants, consist of

  • IPC_STAT—. Return the current values of the shmid_ds structure for the memory segment indicated by the shmid value. The returned information is stored in a user-generated structure, which is passed by reference as the third argument to shmctl. To specify IPC_STAT, the process must have read permission for the shared memory segment.

  • IPC_SET—. Modify a limited number of members in the permission structure found within the shmid_ds structure. The permission structure members that can be modified are shm_perm.uid, shm_perm.gid, and shm_perm.mode. The accessing process must have the effective ID of the superuser or have an ID that is equivalent to either the shm_perm.cuid or shm_perm.uid value. To modify structure members, the following steps are usually taken. A structure of the type shmid_ds is allocated. The structure is initialized to the current system settings by calling shmctl with the IPC_STAT flag set and passing the reference to the new shmd_ds structure. The appropriate members of the structure are then assigned their new values. Finally, with the cmd argument set to IPC_SET, the shmctl system call is invoked a second time and passed the reference to the modified structure. To carry out this modification sequence, the accessing process must have read and write permissions for the shared memory segment. When IPC_SET is specified, the shm_ctime member is automatically updated with the current time.

  • IPC_RMID—. Remove the system data structure for the referenced shared memory identifier (shmid). When specifying IPC_RMID, an address value of 0 is used for buf. The 0 address value is cast to the proper type, with (shmid_ds *). Once all references to the shared memory segment are eliminated (i.e., shm_nattch equals 0), the system will remove the actual segment. If a shmctl system call, specifying IPC_RMID, is not done, the memory segment will remain active and associated with its key value.

  • SHM_LOCK—. Lock, in memory, the shared memory segment referenced by the shmid argument. A locked shared segment is not swapped out by the system thus avoiding I/O faults when referenced. Locking can only be specified by processes that have an effective ID equal to that of the superuser.

  • SHM_UNLOCK—. Unlock the shared memory segment referenced by the shmid argument. Once unlocked the shared segment can be swapped out. Again, this can only be specified by processes that have an effective ID equal to that of the superuser.

If shmctl is successful, it returns a value of 0; otherwise, it returns a value of −1 and sets the value in errno to indicate the specific error condition. The values that errno may be assigned and their interpretation are shown in Table 8.5.

Table 8.5. shmctl Error Messages

#

Constant

perror Message

Explanation

1

EPERM

Operation not permitted

  • The value for cmd is IPC_RMID or IPC_SET, and the calling process is not the owner, creator, or superuser.

  • The value for cmd is SHM_LOCK or SHM_UNLOCK, and the calling process is not the superuser.

13

EACCES

Permission denied

The requested operation is not allowed by current access permissions.

12

ENOMEM

Cannot allocate memory

The cmd is SHM_LOCK, but there is insufficient memory available.

14

EFAULT

Bad address

The third argument to shmctl, buf, contains a reference to an illegal address.

22

EINVAL

Invalid argument

  • The shared memory identifier is invalid.

  • The value for cmd is invalid.

  • The value for cmd is IPC_SET, but the value for shm_perm.uid or shm_perm.gid is invalid.

43

EIDRM

 

Memory segment is marked as removed.

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)

<sys/types.h>
<sys/shm.h>

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.

Determining the attachment address.

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 ORed 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 <bits/shm.h>) 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.

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)

<sys/types.h>
<sys/shm.h>

Manual Section

2

Summary

int shmdt ( const void *shmaddr);

Return

Success

Failure

Sets errno

0

−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 31–33). 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.

Example 8.2. Creating, attaching, and manipulating shared memory.

File : p8.2.cxx
  |     /*
  |            Using shared memory
  |     */
  |     #include <iostream>
  +     #include <cstdio>
  |     #include <unistd.h>
  |     #include <sys/types.h>
  |     #include <sys/ipc.h>
  |     #include <sys/shm.h>
 10     #include <sys/wait.h>
  |     #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_CREAT|0660))< 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;
  |     }

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.

Example 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

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.

Conceptual configuration of memory.

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.

Producer and consumer activities.

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 #includelocal.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.

Example 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 <iostream>
  |     #include <cstdio>
  |     #include <unistd.h>
 10     #include <stdlib.h>
  |     #include <string.h>
  |     #include <sys/types.h>
  |     #include <sys/ipc.h>
  |     #include <sys/sem.h>
  +     #include <sys/shm.h>
  |     #include <signal.h>
  |     #include <wait.h>
  |     #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 <sys/sem.h>
  +     #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.

Example 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 15–18). 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)

<string.h>

Manual Section

3

Summary

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

Return

Success

Failure

Sets errno

A pointer to dest

  

Example 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, '', 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.

Example 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, '', 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.

Example 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.

Example 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
. . .

Using a File as Shared Memory

Most versions of Linux-UNIX also support the mmap system call, which can be used to map a file to a process's virtual memory address space. In many ways mmap is more flexible than its shared memory system call counterpart. Once a mapping has been established, standard system calls rather than specialized system calls can be used to manipulate the shared memory object (Table 8.11). Unlike memory, the contents of a file are nonvolatile and will remain available even after a system has been shut down (and rebooted).

Table 8.11. Summary of the mmap System Call.

Include File(s)

<unistd.h>
<sys/nman.h>

Manual Section

2

Summary

#ifdef _POSIX_MAPPED_FILES        <-- 1
void *mmap(void *start, size_t length, int prot,
      int flags, int fd, off_t offset);
#endif
  • (1)If _POSIX_MAPPED_FILES has been defined.

Return

Success

Failure

Sets errno

A pointer to the mapped area

MAP_FAILED ((void *) -1)

Yes

The mmap system call requires six arguments. The first, start, is the address for attachment. As with the shmat system call, this argument is most often set to 0, which directs the system to choose a valid attachment address. The number of bytes to be attached is indicated by the second argument, length. While the call will allow the user to specify a number of bytes for length that will extend beyond the end of the mapped file, an actual reference to these locations will generate an error (a SIGBUS signal). The third argument, prot, is used to set the type of access (protection) for the segment. The specified access should not be in conflict with the access permissions for the associated file descriptor. The prot argument uses the defined constants found in the include file <sys/mman.h>. These constants are shown in Table 8.12.

Table 8.12. Defined Protection Constants.

Defined Constant

Access

PROT_READ

Read access to specified region.

PROT_WRITE

Write access to specified region.

PROT_EXEC

Execute access to specified region.

PROT_NONE

No access.

Constants can be ORed to provide different combinations of access. The manual page for mmap notes that on some systems PROT_WRITE is implemented as PROT_READ | PROT_WRITE, and PROT_EXEC as PROT_READ | PROT_EXEC. In any case, PROT_WRITE must be set if the process is to write to the mapped segment. The fourth argument, flags, specifies the type of mapping. Mapping types are also indicated using defined constants from the <sys/mman.h> include file. These constants are shown in Table 8.13.

Table 8.13. Defined Mapping Type Constants.

Defined Constant

Mapping Type

MAP_SHARED

Share all changes.

MAP_PRIVATE

Do not share changes.

MAP_FIXED

Interpret the value for the start argument exactly.

The first two constants specify whether writes to the shared memory will be shared with other processes or be private. MAP_SHARED and MAP_PRIVATE are exclusionary. When specifying MAP_PRIVATE, a private copy is not generated until the first write to the mapped object has occurred. These specifications are retained across a fork system call but not across a call to exec. MAP_FIXED directs the system to explicitly use the address value in start. When MAP_FIXED is indicated, the values for start and length should be a multiple of the system's page size. Specifying MAP_FIXED greatly reduces the portability of a program, and its use is discouraged. When specifying the flags argument, either MAP_SHARED or MAP_PRIVATE must be indicated. Linux also supports the flags shown in Table 8.14.

Table 8.14. Linux-Specific Defined Mapping Type Constants.

Defined Constant

Mapping Type

MAP_GROWSDOWN

Treat the segment as a stack.

MAP_EXECUTABLE

Mark the segment as executable.

MAP_DENYWRITE

Do not allow writing.

MAP_NORESERVE

Do not check for reservations.

MAP_LOCKED

Lock the mapped segment.

The fifth argument, fd, is a valid open file descriptor. Once the mapping is established, the file can be closed. The sixth argument, offset, is used to set the starting position for the mapping.

If the mmap system call is successful, it returns a reference to the mapped memory object. If the call fails, it returns the defined constant MAP_FAILED (which is actually the value -1 cast to a void *). A failed call will set the value in errno to reflect the error encountered. The errors for mmap are shown in Table 8.15.

Table 8.15. mmap Error Messages.

#

Constant

perror Message

Explanation

6

ENXIO

No such device or address

The values for off or off + len are illegal for the specified device.

9

EBADF

Bad file descriptor

The file referenced by fd is invalid.

11

EAGAIN

Resource temporarily unavailable

  • Insufficient swap space for the mapping.

  • Mapping could not be locked in memory.

  • Mapped file is already locked.

12

ENOMEM

Cannot allocate memory

Insufficient address space to implement the mapping.

13

EACCES

Permission denied

  • MAP_PRIVATE indicated and file descriptor is not open for reading.

  • File descriptor is not open for writing, and PROT_WRITE was indicated with a mapping type of MAP_SHARED.

19

ENODEV

No such device

fd references an invalid device (such as a terminal).

22

EINVAL

Invalid argument

  • MAP_FIXED specified, and value for start or offset are not multiples of the system's page size.

  • Illegal flag value.

  • Argument length is less than 1.

26

ETXTBSY

Text file busy

MAP_DENYWRITE was set but fd is open for writing.

While the system will automatically unmap a region when a process terminates, the system call munmap, shown in Table 8.16, can be used to explicitly unmap pages of memory.

Table 8.16. Summary of the munmap System Call.

Include File(s)

<unistd.h>
<signal.h>

Manual Section

2

Summary

#ifdef _POSIX_MAPPED_FILES
int munmap(void *start, size_t length);
#endif

Return

Success

Failure

Sets errno

0

−1

Yes

The munmap system call is passed the starting address of the memory mapping (argument start) and the size of the mapping (argument length). If the call is successful, it returns a value of 0. Future references to unmapped addresses generate a SIGVEGV signal. If the munmap system call fails, it returns the value −1 and sets the value in errno to EINVAL. The interpretation of munmap-related error is given in Table 8.17.

Table 8.17. munmap Error Messages.

#

Constant

perror Message

Explanation

22

EINVAL

Invalid argument

  • Argument length is less than 1.

  • Argument start is not a multiple of the system page size.

  • Argument start or start + length is outside the process's address space.

The msync system call is used in conjunction with mmap to synchronize the contents of mapped memory with physical storage (Table 8.18). A call to msync will cause the system to write all modified memory locations to their as sociated physical storage locations. If MAP_SHARED is specified with mmap, the storage location is a file. If MAP_PRIVATE is specified, then the storage location is the swap area.

Table 8.18. Summary of the msync Library Function.

Include File(s)

<unistd.h>
<sys/mman.h>

Manual Section

2

Summary

#ifdef _POSIX_MAPPED_FILES
#ifdef _POSIX_SYNCHRONIZED_IO
int msync(const void *start, size_t length, 
          int flags);
#endif
#endif

Return

Success

Failure

Sets errno

0

−1

Yes

The start argument for msync specifies the address of the mapped memory; the length argument specifies the size (in bytes) of the memory. The flags argument directs the system to take the actions shown in Table 8.19.

Table 8.19. Defined Flag Constants for msync.

Defined Constant

Action

MS_ASYNC

Return immediately once all writes have been scheduled.

MS_SYNC

Return once all writes have been performed.

MS_INVALIDATE

Invalidate cached copies of memory—system reloads memory from the associated storage location.

If msync fails, it returns a −1 and sets errno (Table 8.20). If the call is successful, it returns a value of 0.

Table 8.20. mmap Error Messages.

#

Constant

perror Message

Explanation

1

EPERM

Operation not permitted

MS_INVALIDATE indicated but some of the referenced locations are locked in memory.

14

EFAULT

Bad address

Invalid address reference.

16

EBUSY

Device or resource busy

MS_SYNC and MS_INVALIDATE specified but some of the referenced addresses are currently locked.

22

EINVAL

Invalid argument

  • Argument addr is not a multiple of the page size.

  • Argument flags not a combination of MS_ASYNC | MS_INVALIDATE | MS_SYNC.

Program 8.6 demonstrates the use of the mmap system call.

Example 8.6. Using mmap.

File : p8.6.cxx
  |     /*
  |            Using the mmap system call
  |      */
  |     #define _GNU_SOURCE
  +     #include <iostream>
  |     #include <cstdio>
  |     #include <sys/types.h>
  |     #include <sys/mman.h>
  |     #include <sys/stat.h>
 10     #include <fcntl.h>
  |     #include <stdlib.h>
  |     #include <unistd.h>
  |     #include <signal.h>
  |     #include <string.h>
  +     using namespace std;
  |     int
  |     main(int argc, char *argv[]){
  |       int          fd, changes, i, random_spot, kids[2];
  |       struct stat  buf;
 20       char         *the_file, *starting_string="ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  |       if (argc != 3) {
  |         cerr << "Usage " << *argv << " file_name #_of_changes" << endl;
  |         return 1;
  |       }
  +       if ((changes = atoi(argv[2])) < 1) {
  |         cerr << "# of changes < 1" << endl;
  |         return 2;
  |       }
  |       if ((fd = open(argv[1], O_CREAT | O_RDWR, 0666)) < 0) {
 30         perror("file open");
  |         return 3;
  |       }
  |       write(fd, starting_string, strlen(starting_string));
  |                                            // Obtain size of file
  +       if (fstat(fd, &buf) < 0) {
  |         perror("fstat error");
  |         return 4;
  |       }
  |                                            // Establish the mapping
 40       if ((the_file = (char *) mmap(0, (size_t) buf.st_size,
  |                       PROT_READ | PROT_WRITE, MAP_SHARED,
  |                       fd, 0)) == (void *) - 1) {
  |         perror("mmap failure");
  |         exit(5);
  +       }
  |       for (i = 0; i < 2; ++i)
  |         if ((kids[i] = (int) fork()) == 0)
  |           while (1) {
  |             cout << "Child " << getpid() << " finds: " << the_file << endl;
 50             sleep(1);
  |           }
  |       srand((unsigned) getpid());
  |       for (i = 0; i < changes; ++i) {
  |         random_spot = (int) (rand() % buf.st_size);
  +         *(the_file + random_spot) = '*';
  |         sleep(1);
  |       }
  |       cout << "In parent, done with changes" << endl;
  |       for (i = 0; i < 2; ++i)
 60         kill(kids[i], 9);
  |       cout << "The file now contains: " << the_file << endl;
  |       return 0;
  |     }

Program 8.6 uses a parent/two-child process arrangement to demonstrate the use of mmap. The parent process modifies the contents of a memory-mapped file. Each child process repetitively displays the contents of the mapped files to allow verification of the changes. The program is passed two command-line arguments. The first argument is the name of a file that it will use for mapping. The second argument indicates the number of modifications that should be made to the file. Upon execution of the program, the validity of the command-line arguments is checked. If problems are encountered, an appropriate error message is generated and the program exits. If the command-line arguments are good, the program opens, for reading and writing, the file whose name was passed as the first command-line argument. As the O_CREAT flag is specified, if the file does not exist, it will be created. Next, the string "ABCDEFGHIJKLMNOPQRSTUVWXYZ" is written to the first part of the file. Following this, the fstat call is used to determine the size of the file.

In our example, if we start with an empty file, the size of the file is actually the length of the string that is written to the file. However, this would not be true if the file contained previous data. In many cases we will want to know the full size of the file to be mapped—fstat provides us with a handy way of determining the file's size (it is returned as part of the stat structure). The call to mmap (line 40) establishes the actual mapping. We allow the system to pick the address and indicate that we want to be able to read from and write to the mapped memory region. We also specify the region be marked as shared, be associated with the open file descriptor fd, and have an offset (starting position within the file) of 0. Two child processes are then generated. Each child process displays the contents of the memory-mapped file using the the_file reference which was returned from the initial call to mmap. It is important to note that a call to read was not needed. The child process then sleeps one second and repeats the same sequence of activities until a terminating signal is received. The parent process loops for the number of times specified by the second command-line argument. Within this loop the parent process randomly picks a memory-mapped location and changes it to an asterisk (*). Again, this is done by direct reference to the location using the the_file reference; notice no write function is used. Between changes, the parent sleeps one second to slow down the processing sequence. Once the parent process is done, it displays the final contents of the memory-mapped file, removes the child processes, and exits. A sample run of the program is shown in Figure 8.10.

Example 8.10. A sample run of Program 8.6.

linux$ p8.6 demo 7
Child 16592 finds: ABCDEFGHIJKLMNOPQRSTUVWXYZ
Child 16593 finds: ABCDEFGHIJKLMNOPQRSTUVWXYZ
Child 16592 finds: ABCDEFG*IJKLMNOPQRSTUVWXYZ
Child 16593 finds: ABCDEFG*IJKLMNOPQRSTUVWX*Z
Child 16592 finds: ABCDEFG*IJKLMNOPQRSTUVWX*Z
Child 16593 finds: ABCDEF**IJKLMNOPQRSTUVWX*Z
Child 16592 finds: ABCDEF**IJKLMNOPQRSTUVWX*Z
Child 16593 finds: ABCDEF**IJ*LMNOPQRSTUVWX*Z
Child 16592 finds: ABCDEF**IJ*LMNOPQRSTUVWX*Z
Child 16593 finds: ABCDEF**I**LMNOPQRSTUVWX*Z
Child 16592 finds: ABCDEF**I**LMNOPQRSTUVWX*Z
Child 16593 finds: ABCDEF**I**LMNOPQRS*UVWX*Z
Child 16592 finds: ABCDEF**I**LMNOPQRS*UVWX*Z
Child 16593 finds: ABCDEF**I**L*NOPQRS*UVWX*Z
Child 16592 finds: ABCDEF**I**L*NOPQRS*UVWX*Z
In parent, done with changes
The file now contains: ABCDEF**I**L*NOPQRS*UVWX*Z

In this invocation the child processes, PIDs 16592 and 16593, initially find the mapped location to contain the unmodified starting string. A second check of the mapped location shows that each child now sees the string with a single '*' replacing the letter H. Additional passes reveal further modifications. When all of the processes have terminated, we will find that the file demo will contain the fully modified string.

Shared Memory Class

A shared memory class is shown in Figure 8.11. As defined, this class can be used only with processes that have access to the same shared memory ID.

Example 8.11. Header file for a basic shared memory class.

File : Shared_mem.h
  |     /*
  |       A VERY simplified shared memory class for use in a std UNIX
  |       environment.  See the text for instructions on how to use
  |       this class.  Copyright (c) 2002 J. S. Gray
  +
  |       Exit codes for class operations:
  |
  |       1 - Unable to allocate memory      2 - Unable map memory
  |       3 - Could not remove shared memory
 10     */
  |     #pragma interface        <-- 1
  |     #ifndef Shared_mem_h
  |     #define Shared_mem_h
  |     #define _GNU_SOURCE
  +     #include <iostream>
  |     #include <cstdio>
  |     #include <sys/types.h>
  |     #include <unistd.h>
  |     #include <stdlib.h>
 20     #include <sys/ipc.h>
  |     #include <sys/shm.h>
  |     using namespace std;
  |     template <class S_type>         // Allow for different data types
  |     class Shared_mem {
  +       public:
  |         Shared_mem ( );             // Constructor
  |         ~Shared_mem( );             // Destructor - remove shared memory
  |         void   Put( const S_type ); // Assign value to shared memory
  |         S_type Get(  );             // Return value from shared memory
 30
  |       private:
  |         int    shmid;               // ID of shared memory
  |         S_type *shm_ptr;            // Reference to shared memory
  |         pid_t  my_pid;              // Hang on to originator PID
  +     };
  |     #endif
  • (1)This notifies the compiler that a template class is being declared.

The shared memory class is templated to allow the passing of a data type. The shared memory class generates a private shared memory segment of the appropriate size for the data type. There are four public methods and three private data members in the shared memory class. The public methods and their functionality are described in Table 8.21.

Table 8.21. Shared_mem Class Methods.

Method name

Explanation

Shared_mem

This is the class constructor. This method generates the shared memory segment. The size of the segment is set by the data type. Once created, the segment is attached. The creating PID is saved in the my_pid data member.

~Shared_mem

The class destructor. This method removes the shared memory segment from the system if the calling function is the process that created the segment.

Put

Put assigns a value to the shared memory segment.

Get

Get retrieves the current value stored in the memory segment.

The C++ code that implements the shared memory methods is found in Program 8.7, Shared_mem.cxx. Again, as with the previously defined System V IPC classes (Message_que and SSemaphore), this is a very rudimentary implementation.

Example 8.7. Program code for the shared memory class.

File : Shared_mem.cxx
  |     #pragma implementation        <-- 1
  |     /*
  |         Shared memory implementation -  Copyright (c) 2002 J. S. Gray
  |         Compile with: -fexternal-templates
  +      */
  |     #include "Shared_mem.h"
  |                                            // Generate private mem segment
  |     template <class S_type>                // Generalize data type
  |     Shared_mem<S_type>::Shared_mem(  ){
 10       my_pid = getpid( );           // Save PID of creating process
  |       if ((shmid = shmget(IPC_PRIVATE, sizeof(S_type), 
               IPC_CREAT | 0660)) < 0)
  |         exit(1);
  |       if ((shm_ptr = (S_type *) shmat(shmid, NULL, 0)) == NULL)
  |         exit(2);
  +     }
  |                                            // Remove memory if creator
  |     template <class S_type>
  |     Shared_mem<S_type>::~Shared_mem(  ) {
  |       if ( getpid( ) == my_pid ) {
 20         shmdt( (char *) shm_ptr );
  |         if ( shmctl(shmid, IPC_RMID, (struct shmid_ds *) 0) == -1 )
  |           exit( 3 );
  |       }
  |     }
  +                                            // Assign value to this location
  |     template <class S_type>
  |     void
  |     Shared_mem<S_type>::Put( const S_type stuff  ){
  |       *shm_ptr = stuff;
 30     }
  |                                            // Retrieve value from location
  |     template <class S_type>
  |     S_type
  |     Shared_mem<S_type>::Get(  ){
  +       static S_type stuff;
  |       stuff = *shm_ptr;
  |       return stuff;
  |     }
  |                                            // Force instantiation
 40     typedef Shared_mem<int>    Shared_int;
  |     typedef Shared_mem<char>   Shared_char;
  |     typedef Shared_mem<float>  Shared_float;
  |     typedef Shared_mem<double> Shared_double;
  • (1)This notifies the compiler that a template class is being defined.

Note that since templates are involved, a few more gymnastics are called for if we want to keep our class declaration and definition code in separate files. To accomplish this, using the g++ compiler, the directive #pragma interface must be placed at the top of the code in the header file containing the class declaration, while the directive #pragma implementation is placed in the file with the class definition (the corresponding .cxx file). At the bottom of the class definition, a typedef is used to coerce the compiler into generating object code for each specified data type. Lastly, when we compile the shared memory class into object code, the command-line compile option -fexternal-templates (generate external templates) must be specified along with the -c option. As if this were not enough, newer versions of the compiler may notify the user that the external templates option is deprecated (may not be supported in future versions). The compiler switch: -Wno-deprecated can be used to silence these warnings. The compilation of code containing templates can be somewhat daunting. The latest information on the g++ compiler can be obtained from the site http://www.gnu.org.

To use the shared memory class, the files Shared_mem.h and Shared_mem.cxx should reside locally. The Shared_mem class is compiled into object code with the command line

linux$  g++ Shared_mem.cxx -c -fexternal-templates

At the top of the source file that uses a Shared_mem object, add the statement

#include "Shared_mem.h"

to make the class definition available to the compiler. When compiling the source file, include the message queue object code file

linux$  g++  your_file_name.cxx   Shared_mem.o

Program 8.8 uses the Shared_mem class. This program is passed a small integer value (1–6) on the command line. It goes on to generate a number of child processes. The first process generated is considered to be process 0, the next 1, and so on. As each process is generated, it displays its number in the sequence. To make things a bit more interesting, the output is displayed in a tree-like format. The height of the tree being the value passed in on the command line. Common information, such as the process sequence number, the width of field for output, etc. are stored in a shared memory which is available to the parent and all child processes. The source for the program is shown as in Program 8.8.

Example 8.8. Using the shared memory class.

File : p8.8 .cxx
  |     #include <iostream>
  |     #include <iomanip>
  |     #include <sys/types.h>
  |     #include <sys/wait.h>
  +     #include "Shared_mem.h"
  |     int
  |     main(int argc, char *argv[]) {
  |       int    n;
  |       Shared_mem<int> s[4];        <-- 1
 10       if (argc < 2 || (n = atoi(argv[1])) > 6 || n < 1 ) {
  |         cerr << "Usage: " << argv[0] << " value 1-6" << endl;
  |         return 1;
  |       }
  |       setbuf(stdout, NULL);                // Standard output is unbuffered
  +                                            // Starting values
  |       s[0].Put(0);                         // Process counter
  |       s[1].Put(1);                         // Process # when @ end of line
  |       s[2].Put(64);                        // Output width
  |       s[3].Put(0);                         // Process # that starts new line
 20       cout <<  "			Tree of level " << n << endl << endl;
  |       for (int i=0; i < n; ++i) {
  |         if ( !fork() ) {                   // in the child
  |           int temp_width = s[2].Get();     // get output width
  |           if ((s[0].Get()) == s[3].Get())  // if @ start of line use 1/2
  +             temp_width /= 2;
  |           cout << setiosflags(ios::uppercase) << hex
  |                << setw(temp_width) << (s[0].Get()) % 16;
  |           s[0].Put(s[0].Get()+1);          // count the process
  |         }
 30         if ( s[0].Get() == s[1].Get() ){   // If at the end of line
  |          s[1].Put( s[1].Get() * 2 + 1 );   // update end of line process #
  |          s[2].Put( s[2].Get() / 2 );       // decrease output width
  |          s[3].Put( s[0].Get() );           // new sart of line process #
  |           cout << endl << endl;
  +         }
  |         wait(0);                           // wait for the child to finish!
  |       }
  |       return 0;
  |     }
  • (1)Instantiate an array of shared memory objects—each to hold an integer value.

In line 9 of the program, a four-element array of shared memory objects is instantiated. In line 14 the setbuf library function is used to turn off line buffering of standard out. Data streams can be block buffered, line buffered, or unbuffered. With block buffering, the operating system saves data in a temporary location until it has a block, at which time it performs the I/O operation. File I/O is normally block buffered. With line buffering, data is saved until a newline is encountered, while unbuffered data is made available immediately. The fflush library function can also be used to force the system to transfer data, as will the closing of a stream (fclose). By default, standard output (stdout, cout) is line buffered, while standard error (stderr, cerr) is not. Setting I/O to unbuffered causes the standard library I/O functions to call the underlying I/O system call for each character of data, which in turn increases the amount of CPU time the process requires. Beneath the covers, setbuf is actually an alias to the setvbuf library call. As used in the program example, the first argument of setbuf references the file stream, while second argument references the buffer where data is to be stored. If the second argument is set to NULL (as is our case), only the mode of the stream is affected.

Lines 16 through 20 establish the initial contents of the shared memory segments. The for loop is driven by the value passed on the command line. Each pass through the loop generates one level of the output display. The call to fork generates a child process. Each child process announces its presence by displaying a hexadecimal sequence value at a specific location. Back in the parent, a check is made to determine if the current shared memory values need to be adjusted (such as when at the end of a line of output). The parent process waits for the child to terminate (line 36).

Figure 8.12 shows the output generated when the program is run and passed the value 4. Note the dotted lines were not produced by the program.

A run of program p8.7 when passed the value 4.

Figure 8.12. A run of program p8.7 when passed the value 4.

Summary

Shared memory provides the user with an efficient means of communication via the sharing of data that resides in memory. Unlike pipe-based communications, this data can be accessed in a nonserial (random) manner. To prevent inconsistencies, semaphores are often used to coordinate access to shared memory segments. When using System V-based shared memory techniques, shared memory segments are generated with the shmget system call. If a shared memory segment has already been created, the shmget call provides the process with access to the segment. The shmctl system call is used to obtain the status of a memory segment, set permissions, and remove a shared memory segment. The shmat and shmdt system calls are used to attach (map the segment into the process's address space) and detach memory segments.

The mmap system call may also be used to map the virtual memory space of a process to a file. As files remain after a process has terminated, mmaped files provide a means for communicating information between processes that exist at different times. Overall, mmap-based techniques are less complex and somewhat more portable than their System V-based counterparts.

Key Terms and Concepts

#pragma implementation

#pragma interface

MAP_DENYWRITE

MAP_EXECUTABLE

MAP_FIXED

MAP_GROWSDOWN

MAP_LOCKED

MAP_NORESERVE

MAP_PRIVATE

MAP_SHARED

memcpy library function

mmap system call

msync system call

munmap system call

PROT_EXEC

PROT_NONE

PROT_READ

PROT_WRITE

setbuf library function

Shared memory

SHM_LOCK

SHM_RND

SHM_UNLOCK

shmat system call

shmctl system call

shmdt system call

shmget system call

SHMLBA

SHMMAX

SHMMIN

SHMMNI

SHMSEG



[1] The actual mapping of the segment to virtual address space is dependent upon the memory management (MMU) hardware for the system.

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

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

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

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