Interprocess communication (IPC)
is a set of programming interfaces that allow a programmer to coordinate
activities among different program processes that can run concurrently in
an operating system. This allows a program to handle many user requests at the
same time. Since even a single user request may result in multiple processes
running in the operating system on the user's behalf, the processes need to
communicate with each other. The IPC interfaces make this possible. Each IPC
method has its own advantages and limitations so it is not unusual for a single
program to use all of the IPC methods.
Shared Memory Communication
One very commonly employed
strategy for IPC is to use shared memory. Ordinarily, processes use memory
areas within the scope of virtual memory space. However, memory management
systems ensure that every process has a well-defined and distinct data and code
area. For shared memory communication, one process would write into a certain
commonly accessed area and another process would read subsequently from that
area. One other point which we can debate is: do the processes have to be
related? We have seen that a parent may share a data area or files with a
child. Also, by using the exec() function call we may be able to populate a
process with another code segment or data. Clearly, the shared memory method
can allow access to a common data area even amongst the processes that are not
related. However, in that case an area like a process stack may not be
shareable. Also, it should be noted that it is important that the shared data
integrity may get compromised when an arbitrary sequence of reads and writes
occurs. To maintain data integrity, the access is planned carefully under a
user program control. That then is the key to shared memory protocol.
The shared memory model has the
following steps of execution.
1. First we have to set up a
shared memory mechanism in the kernel.
2. Next an identified “safe area" is
attached to each of the processes.
3. Use this attached shared data
space in a consistent manner.
4. When finished, detach the
shared data space from all processes to which it was attached.
5. Delete the information concerning the
shared memory from the kernel.
Two important .h files in this
context are: shm.h and ipc.h which are included in all the
process definitions. The first step is to set up shared memory mechanism in
kernel. The required data structure is obtained by using shmget() system call with the following syntax
int shmget( key_t key, int size, int flag );
The parameter key_t is usually a
long int. It is declared internally as key_t key. key_t is an alias defined in
sys/types.h using a typedef structure. If this key is set to IPC_PRIVATE, then
it always creates a shared memory region. The second parameter, size is the
size of the sh-mem-region in bytes. The third parameter is a combination of
usual file access permissions of r/w/e for o/g/w
struct shmid_ds
{ struct ipc_perm shm_perm;
int shm_seg_segsz /* size of segments in bytes
*/
struct region *shm_reg; /* pointer
to region struct */
char pad[4]; /* for swap compatibility */
ushort shm_lpid; /* pid of last
shmop */
ushort shm_cpid; /* pid of creator */
ushort shm_nattch; /* used for
shm_info */
ushort shm_cnattch; /* used for
shm_info */
time_t shm_atime; /* last attach
time */
time_t shm_dtime; /* last detach
time */
time_t shm_ctime; /* last change
time */
}
Once this is done we would have
created a shared memory data space. The next step requires that we attach it to
processes that would share it. This can be done using the system call shmat().
The system call shmat() has its
syntax shown below.
char *shmat( int shmid, char *shmaddr, int shmflg );
The second argument should be set
to zero as in (char *)0, if the kernel is to determine the attachment. The
system uses three possible flags which are: SHM_RND, SHM_RDONLY and the
combination SHM_RND | SHM_RDONLY. The SHM_RDONLY flag indicates the shared
region is read only. Otherwise, it is both for read and write operations. The
flag SHM_RND requires that the system enforces use of the byte address of the
shared memory region to coincide with a double word boundary by rounding.
Now that we have a well-defined
shared common area, reading and writing can be done in this shared memory
region. However, the user must write a code to ensure locking of the shared
region. For instance, we should be able to block a process attempting to write
while a reader process is reading. This can be done by using a synchronization
method such as semaphores. In most versions of Unix, semaphores are available
to enforce mutual exclusion. At some stage a process may have finished using
the shared memory region. In that case this region can be detached for that
process. This is done by using the shmdt() system call. This system call detaches
that process from future access. This information is kept within the kernel
data-space. The system call shmdt()
takes a single argument, the address of the shared memory region. The return
value from the system call is rarely used except to check if an error has
occurred (with -1 as the return value).
The last step is to clean up the
kernel's data space using the system call shmctl().
The system call shmctl() takes three parameters as input, a shared memory id, a
set of flags, and a buffer that allows copying between the user and the kernel
data space. A considerable amount of information is pointed to by the third
parameter. A call to shmctl() with the command parameter set to IPC_STAT gives
the following information.
User's id
Creator's group id
Operation permissions
Key ™ segment size
Process id of creator *
Current number of attached segments in the memory.
Last time of attachment
User's group id ™
Creator's id ™
Last time of detachment ™
Last time of change ™
Current no. of segments attached ™
Process id of the last shared memory operation
Now let us examine the shmget() system call.
int shmget( key_t key, int
region_size, int flags );
Here key is a user-defined integer, the size of the shared region to
be attached is in bytes. The flags usually turn on the bits in IPC_CREAT.
Depending upon whether there is key entry in the kernel's shared memory table,
the shmget() call takes on one of the following two actions. If there is an
entry, then shmget() returns an integer indicating the position of the entry.
If there is no entry, then an entry is made in the kernel's shared memory
table.
Also, note that the size of the
shared memory is specified by the user. It, however, should satisfy some system
constraints which may be as follows.
struct shminfo
{ int shmmax, /* Maximum shared
memory segment size 131072 for some */
shmmin, /* minimum shared
memory segment size 1 for some */
shmni, /* No. of shared memory identifiers */
shmseg, /* Maximum attached segments per process */
shmall; /* Max. total shared memory system in pages */
};
The third parameter in shmget()
corresponds to the flags which set access permissions as shown below: 400 read
by user ...... Typically in shm.h file as constant SHM_R
200 write by user .......Typically in shm.h file as constant SHM_W
040 read by group
020 write by group
004 read by others
002 read by others ......All these are octal constants. For example,
let us take a case where we have read/write permissions by the user's group and
no access by others. To be able to achieve this we use the following values. SHM_R | SHM_W | 0040 | IPC_CREAT as a
flag to a call to shmget().
Now consider the shmat()
system call.
char *shmat( int shmid, char
*address, int flags );
This system call returns a pointer to the shared memory region to be
attached. It must be preceded by a call to shmget(). The first argument is a
shmid (returned by shmget()). It is an integer. The second argument is an
address. We can let the compiler decide where to attach the shared memory data
space by giving the second argument as (char *) 0. The flags in arguments list
are to communicate the permissions only as SHM_RND and SHM_RDONLY.
The shmdt() system call
syntax is as follows: int shmdt(char *
addr ); This system call is used to detach. It must follow a call shmat()
with the same base address which is returned by shmat().
The last system call we need is shmctl().
It has the following syntax.
int shmctl( int shmid, int command, struct
shm_ds *buf_ptr );
The shmctl() call is used to change the ownership and permissions of
the shared region. The first argument is the one earlier returned by shmget()
and is an integer. The command argument has five possibilities:
• IPC_STAT : returns the status of the associated data structure for
the shared memory pointed by buffer pointer.
• IPC_RMID : used to remove the shared memory id.
• SHM_LOCK : used to lock
• SHM_UNLOCK : used to unlock
• IPC_SET : used to set permissions.
When a region is used as a shared memory data space it must be from a
list of free data space. Based on the above explanations, we can arrive at the
code given below.
#nclude <stdio.h>`
#include <string.h
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#define MAXBYTES 4096 /* Maximum bytes per shared segment */
main(int argc; char *argv[])
{ /* Inter process communication using shared memory */
char message[MAXBYTES];
int i, message_num, j, no_of_mess, nbytes;
int key = getpid();
int semid; int segid;
char *addr;
if (argc != 3) {
printf("Usage :
%s num_messages");
printf("num_of_bytes \n", argv[0]); exit(1);
}
else {
no_of_mess =
atoi(argv[1]);
nbytes = atoi(argv[2]);
if (nbytes > MAXBYTES)
nbytes = MAXBYTES;
if ( (semid=semget(
(key_t)key, 1, 0666 | IPC_CREAT ))== -1)
{ printf("semget error
\n"); exit(1); }
/* Initialise the semaphore
to 1 */
V(semid);
if ( (segid = shmget( (key_t)
key, MAXBYTES, 0666 | IPC_CREAT ) ) == -1 )
{ printf("shmget
error \n"); exit(1); } /*if (
(addr = shmat(segid, (char * )0,0)) == (char *)-1) */
if ( (addr = shmat(segid, 0, 0)) == (char *)
-1 )
{ printf("shmat
error \n"); exit(1); }
switch (fork())
{ case -1 : printf("Error in fork
\n"); exit(1);
case 0 : /* Child process,
receiving messages */
for (i=0; i < no_of_mess; i++)
if(receive(semid, message, sizeof(message)));
exit(0);
default : /*
Parent process, sends messages */
for ( i=0; i < no_of_mess; i++) { for (
j=i; j < nbytes; j++) message[j] = 'd';
if (!send(semid, message, sizeof(message)))
printf("Cannot send the message
\n");
} /* end of for loop */
} /* end of switch */
} /* end of else part */
}
/****************************************/
/* Semaphores */
#nclude <stdio.h>`
#include <string.h
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
int sid;
cleanup(int semid,int segid, char
*addr)
{ int status;
/* wait for the child process to die first */
/* removing semaphores */
wait(&status);
semctl(semid, 0, IPC_RMID, 0);
shmdt(addr);
shmctl(segid, 0, IPC_RMID, 0);
}
P(int sid;)
{ /* Note the difference in this and previous structs */
struct sembuf *sb;
sb = (struct sembuf *) malloc(sizeof(struct sembuf *));
sb -> sem_num = 0;
sb -> sem_op = -1;
sb -> sem_flg = SEM_UNDO;
if( (semop(sid, sb, 1)) == -1)
puts("semop error");
}
V( int sid)
{ struct sembuf *sb;
sb = (struct sembuf *) malloc(sizeof(struct sembuf *));
sb -> sem_num = 0;
sb -> sem_op = 1;
sb -> sem_flg = SEM_UNDO;
if( (semop(sid, sb, 1)) == -1)
puts("semop error");
}
/* send message from addr to buf */
send(int semid, char *addr, char *buf, int nbytes)
{
P(semid);
memcpy(addr, buf, nbytes);
V(semid);
}
/* receive message from addr to buf */
receive(int semid, char *addr, char *buf, int nbytes)
{ P(semid);
memcpy(buf, addr, nbytes);
V(semid);
}
From the programs above, we notice that any process is capable of
accessing the shared memory area once the key is known to that process. This is
one clear advantage over any other method. Also, within the shared area the
processes enjoy random access for the stored information. This is a major
reason why shared memory access is considered efficient. In addition, shared
memory can support many-to-many communication quite easily.
If you ever wondered who are the most sophisticated players in the IT outsourcing industry, I'd highly recommend checking out this article to learn more about outsourced product development .
ReplyDelete