Saturday, 10 November 2012

System V IPC: Shared Memory

In this post, I will give you a clear idea of using shared memory between two processes.

Why shared memory?

Let us know why to use shared memory. Processes actually need to communicate with each other and one method of accomplishing this is using Semaphores as discussed previously. But this doesn't provide a way to send and receive data between the processes. Here comes the shared memory into the picture. Let us delve into it.


The data structure:

Each shared memory segment has a structure which stores all the necessary information and is defined as follows:

struct shmid_ds {
   struct ipc_perm shm_perm;        /* operation perms */
   int     shm_segsz;               /* size of segment (bytes) */
   time_t  shm_atime;               /* last attach time */
   time_t  shm_dtime;               /* last detach time */
   time_t  shm_ctime;               /* last change time */
   unsigned short  shm_cpid;        /* pid of creator */
   unsigned short  shm_lpid;        /* pid of last operator */
   short   shm_nattch;              /* no. of current attaches */
                                    /* the following are private */
   unsigned short   shm_npages;     /* size of segment (pages) */
   unsigned long   *shm_pages;     /* array of ptrs to frames -> SHMMAX */ 
   struct vm_area_struct *attaches; /* descriptors for attaches */
        };

I think the comments are self-explainable about the structure except the first and last ones which we can ignore.


System calls:

There are only 4 System calls associated with shared memory.

shmget()
shmat()
shmdt()
shmctl()

Let us see one-by-one.

shmget():

The prototype of this is as follows:

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

The first argument is a unique key for which the kernel generates the identifier accordingly. If we have already created a shared memory segment and again try to create it with the same key value, then it returns the same identifier provided IPC_EXCL is not set ( discussed shortly).

For more information about the key generation you can see here

The second argument is the size(in bytes) of the memory you want to create.

The third argument refers to the flags. These can be bit-wise ORed accordingly.

IPC_CREAT:

This flag says to create the shared memory segment if it doesn't exist already.

IPC_EXCL:

If this weren't specified, the function would normally return the memory ID, even if it already exists. But if this is specified, it returns -1 and this is used to ensure that we are creating a brand new shared memory segment.

If everything goes well, it returns an ID of a shared memory segment or else you will end up with a -1 if you commit any mistake.


shmat():

After successfully getting the memory ID, you have to attach it to the process, meaning you have to map into the process's address space. It is done with shmat. The prototype is as follows:

int shmat ( int shmid, char *shmaddr, int shmflg);

The first argument is the shm ID that you got from the previous method. The second argument is the address where you want to map the memory segment. If it is specified as NULL, then the kernel automatically maps into some address. It is recommended to pass it as NULL, because no one wants to mess up a process's address space without knowing the exact address contents.

The third argument can be SHM_RDONLY which makes the segment read only for the calling process.(Of course, it should have appropriate permissions to do that) or else it can be NULL.

Upon success, it returns the address of the segment or if it fails it returns -1. The returned address can be used just like any other array in order to read or modify the contents.


shmdt():

When you are done using the memory segment, you need to detach it from the address space and it can be done using shmdt() whose prototype is as follows:

int shmdt ( char *shmaddr );

You just need to specify the address that was previously returned by shmat(). It detaches the memory and makes necessary changes to the corresponding shmid_ds structure.


shmctl():

We have already learnt all the necessary system calls required for shms. Then why this one? This is used to retrieve the information stored in the shmid_ds structure and to change the permissions and lastly to delete the memory segment. It has the following prototype:

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

The first one is the shm ID, while the second one can be one of these:

IPC_STAT:

This can be used to get all the information about the memory segment and this is filled in the structure pointed to by the bug.

IPC_SET:

This can be used to change the permissions of the memory segment. You need to store the necessary permissions in the shm_perm of the buf structure and it sets the necessary permissions. You need to have the necessary permissions to do this.

IPC_RMID:

This marks the memory segment as deleted and this is actually done when all other processes detach it from their address space. Be cautious that if you won't delete this, the memory segment remains until the next reboot of your system. This can be checked by using ipcs command.


errno:

I used to think if there was an exception class in C like JAVA does. Through which we can know the exact error that has occured during run-time. But voila its there in C! I mean not an explicit exeception class, but a special variable called errno which is a global one and most of the system calls set this value to appropriate number which indicates an error code. It is in the library erron.h.

There are about 133 such different error codes and this can be viewed by the following command:

errno -l

So, whenever a system call returns -1, you can directly check the errno value and get the error message corresponding to the code. Sounds cool isn't it?


example:

Lastly, let us see an example program in which two processes communicate with each other by passing strings of characters. Informally, this is like a chat between the two processes.

I have also used semaphores in this program, to signal when a process is ready with message and other issues. This can be extended to more than two processes just by increasing the number of semaphores and making some other changes.

The code of the server( not really a server, but is apt for current context) process is as follows and this one should run first:


#include <stdio.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

union semun {
 int val;
 struct semid_ds *buf;
 unsigned short int *array;
 };
int main()
{
 char *shared;
 int sem_id=semget(3456, 2, IPC_CREAT | 666);
 if(sem_id==-1)
 {
  perror("semget error!\n");
  exit(1);
 }
 int shm_id=shmget(4567, 30, IPC_CREAT | 666);
 if(shm_id==-1)
 {
  perror("shmget error!\n");
  exit(1);
 }
 union semun T;
 T.val=0;
 for(int i=0; i<2; i++)
 {
  if(semctl(sem_id, i,SETVAL, T)==-1)
  {
   printf("semctl error!\n");
   exit(1);
  }
 }
 if((shared=shmat(shm_id, NULL,0))==(char *)-1)
 {
  perror("shmat error!\n");
  exit(1);
 }
 int ID=fork();
 if(ID==0)
 {
  char buf[30];
   int n;
   char *tmp;
   struct sembuf tk;
  //this reads the input from console...
  while(1)
  {
    n=read(0, buf, 30);
   if(n==0)
   {
    printf("writing terminated..\n");
    break;
   }
   tmp=shared;
   strncpy(shared, buf, strlen(buf));
   tk.sem_num=0;//0-->used by this program
   tk.sem_flg=0;
   tk.sem_op=1;
   if(semop(sem_id, &tk, 1)==-1)
   {
    printf("semop error for writing!\n");
    exit(0);
   }
  }
 }
 else if(ID>0)
 {
  //takes from shared memory and writes to the console... 
  char shr[30];
  struct sembuf gh;
  while(1)
  {
   gh.sem_num=1;
   gh.sem_flg=0;
   gh.sem_op=-1;
   if(semop(sem_id, &gh, 1)==-1)
   {
    printf("semop error for reading..!\n");
    exit(1);
   }
   strncpy(shr, shared, strlen(shared));
   write(1, shr, strlen(shr));
  }
 }
 else
 {
  printf("fork error!\n");
 }
 exit(0);
}

The client program is as follows:

#include <stdio.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

union semun {
 int val;
 struct semid_ds *buf;
 unsigned short int *array;
 };
int main()
{
 char *shared;
 int sem_id=semget(3456, 2, IPC_CREAT | 666);
 if(sem_id==-1)
 {
  printf("semget error!\n");
  exit(1);
 }
 int shm_id=shmget(4567, 30, IPC_CREAT | 666);
 if(shm_id==-1)
 {
  printf("shmget error!\n");
  exit(1);
 }
 if((shared=shmat(shm_id, NULL,0))==(char *)-1)
 {
  printf("shmat error!\n");
  exit(1);
 }
 int ID=fork();
 if(ID==0)
 {
  char buf[30];
   int n;
   char *tmp;
   struct sembuf tk;
  //this reads the input from console...
  while(1)
  {
    n=read(0, buf, 30);
    if(n==0)
   {
    printf("writing terminated..\n");
    break;
   }
   tmp=shared;
   strncpy(shared, buf,strlen(buf));
   tk.sem_num=1;//1-->used by this program
   tk.sem_flg=0;
   tk.sem_op=1;
   if(semop(sem_id, &tk, 1)==-1)
   {
    printf("semop error for writing!\n");
    exit(0);
   }
  }
 }
 else if(ID>0)
 {
  //takes from shared memory and writes to the console... 
  char shr[30];
  struct sembuf gh;
  while(1)
  {
   gh.sem_num=0;
   gh.sem_flg=0;
   gh.sem_op=-1;
   if(semop(sem_id, &gh, 1)==-1)
   {
    printf("semop error for reading..!\n");
    exit(1);
   }
   strncpy(shr, shared,strlen(shared));
   write(1, shr, strlen(shr));
  }
 }
 else
 {
  printf("fork error!\n");
 }
 exit(0);
} 

When you run both the programs, you can see that when you enter a string in one process, it appears in the other process's console and vice versa. The understanding of the above code is left as an exercise to the reader. There may be few bugs in this code, but i assure that it does what it is intended to do.



No comments:

Post a comment