As we have seen, many methods were created in order to let processes communicate. All this communications is done in order to share data. The problem is that all these methods are sequential in nature. What can we do in order to allow processes to share data in a random-access manner? On a Unix system, each process has its own virtual address space, and the system makes sure no process would access the memory area of another process. This means that if one process corrupts its memory's contents, this does not directly affect any other process in the system.
With shared memory, we declare a given section in the memory as one that will be used simultaneously by several processes. This means that the data found in this memory section (or memory segment) will be seen by several processes. This also means that several processes might try to alter this memory area at the same time, and thus some method should be used to synchronize their access to this memory area ( i.e. mutual exclusion using a semaphore).
In order to understand the concept of shared memory, we should first check how virtual memory is managed on the system.
In order to achieve virtual memory, the system divides memory into small pages each of the same size. For each process, a table mapping virtual memory pages into physical memory pages is kept. When the process is scheduled for running, its memory table is loaded by the operating system, and each memory access causes a mapping (by the CPU) to a physical memory page. If the virtual memory page is not found in memory, it is looked up in swap space, and loaded from there (this operation is also called 'page in').
When the process is started, it is allocated a memory segment to hold the runtime stack, a memory segment to hold the programs code (the code segment), and a memory area for data (the data segment). Each such segment might be composed of many memory pages. When ever the process needs to allocate more memory, new pages are being allocated for it, to enlarge its data segment.
When a process is being forked off from another process, the memory page
table of the parent process is being copied to the child process, but not
the pages themselves. If the child process will try to update any of these
pages, then this page specifically will be copied, and then only the copy
of the child process will be modified. This behavior is very efficient
for processes that call fork()
and immediately use the
exec()
system call to replace the program it runs.
So, in order to support shared memory, memory pages must shared between processes, and a way to identify them will be required. This way, one process will create a shared memory segment, other processes will attach to them (by placing their physical address in the process's memory pages table). From now all these processes will access the same physical memory when accessing these pages, thus sharing this memory area.
A shared memory segment first needs to be allocated (created), using the
shmget()
system call. This call gets a key for the segment
(like the keys used in msgget()
and semget()
),
the desired segment size, and flags to denote access permissions and whether
to create this page if it does not exist yet. shmget()
returns
an identifier that can be later used to access the memory segment. Here
is how to use this call:
/* this variable is used to hold the returned segment identifier. */
int shm_id;
/* allocate a shared memory segment with size of 2048 bytes, */
/* accessible only to the current user. */
shm_id = shmget(100, 2048, IPC_CREAT | IPC_EXCL | 0600);
if (shm_id == -1) {
perror("shmget: ");
exit(1);
}
IPC_EXCL
in the flags to shmget()
. In that case,
the call will succeed only if the page did not exist before.
After we allocated a memory page, we need to add it to the memory page table
of the process. This is done using the
shmat()
(shared-memory
attach) system call. Assuming 'shm_id' contains an identifier returned
by a call to shmget()
, here is how to do this:
/* these variables are used to specify where the page is attached. */
char* shm_addr;
char* shm_addr_ro;
/* attach the given shared memory segment, at some free position */
/* that will be allocated by the system. */
shm_addr = shmat(shm_id, NULL, 0);
if (!shm_addr) { /* operation failed. */
perror("shmat: ");
exit(1);
}
/* attach the same shared memory segment again, this time in */
/* read-only mode. Any write operation to this page using this */
/* address will cause a segmentation violation (SIGSEGV) signal. */
shm_addr_ro = shmat(shm_id, NULL, SHM_RDONLY);
if (!shm_addr_ro) { /* operation failed. */
perror("shmat: ");
exit(1);
}
Placing data in a shared memory segment is done by using the pointer returned
by the shmat()
system call. Any kind of data may be placed
in a shared segment, except for pointers. The reason for this is simple:
pointers contain virtual addresses. Since the same segment might be attached
in a different virtual address in each process, a pointer referring to one
memory area in one process might refer to a different memory area in another
process. We can try to work around this problem by attaching the shared segment
in the same virtual address in all processes (by supplying an address as the
second parameter to shmat()
, and adding the SHM_RND
flag to its third parameter), but this might fail if the given virtual
address is already in use by the process.
Here is an example of placing data in a shared memory segment, and later on
reading this data. We assume that 'shm_addr' is a character pointer, containing
an address returned by a call to shmat()
.
/* define a structure to be used in the given shared memory segment. */
struct country {
char name[30];
char capital_city[30];
char currency[30];
int population;
};
/* define a countries array variable. */
int* countries_num;
struct country* countries;
/* create a countries index on the shared memory segment. */
countries_num = (int*) shm_addr;
*countries_num = 0;
countries = (struct country*) ((void*)shm_addr+sizeof(int));
strcpy(countries[0].capital_city, "U.S.A");
strcpy(countries[0].capital_city, "Washington");
strcpy(countries[0].currency, "U.S. Dollar");
countries[0].population = 250000000;
(*countries_num)++;
strcpy(countries[1].capital_city, "Israel");
strcpy(countries[1].capital_city, "Jerusalem");
strcpy(countries[1].currency, "New Israeli Shekel");
countries[1].population = 6000000;
(*countries_num)++;
strcpy(countries[1].capital_city, "France");
strcpy(countries[1].capital_city, "Paris");
strcpy(countries[1].currency, "Frank");
countries[1].population = 60000000;
(*countries_num)++;
/* now, print out the countries data. */
for (i=0; i < (*countries_num); i++) {
printf("Country %d:\n", i+1);
printf(" name: %s:\n", countries[i].name);
printf(" capital city: %s:\n", countries[i].capital_city);
printf(" currency: %s:\n", countries[i].currency);
printf(" population: %d:\n", countries[i].population);
}
malloc()
.
shmget()
, there is no need to use malloc()
when
placing data in that segment. Instead, we do all memory management
ourselves, by simple pointer arithmetic operations. We also need to make
sure the shared segment was allocated enough memory to accommodate future
growth of our data - there are no means for enlarging the size of the
segment once allocated (unlike when using normal memory management - we can
always move data to a new memory location using the realloc()
function).
After we finished using a shared memory segment, we should destroy it using
shmctl
.
It is safe to destroy it even if it is still in use (i.e. attached by some
process). In such a case, the segment will be destroyed only after all processes
detach it. Here is how to destroy a segment:
/* this structure is used by the shmctl()
system call. */
struct shmid_ds shm_desc;
/* destroy the shared memory segment. */
if (shmctl(shm_id, IPC_RMID, &shm_desc) == -1) {
perror("main: shmctl: ");
}
As a naive example of using shared memory, we collected the source code from the above sections into a file named shared-mem.c. It shows how a single process uses shared memory. Naturally, when two processes (or more) use a single shared memory segment, there may be race conditions, if one process tries to update this segment, while another is reading from it. To avoid this, we need to use some locking mechanism - SysV semaphores (used as mutexes) come to mind here. An example of two processes that access the same shared memory segment using a semaphore to synchronize their access, is found in the file shared-mem-with-semaphore.c.
ftok()
One of the problems with SysV IPC methods is the need to choose a unique identifier for our processes. How can we make sure that the identifier of a semaphore in our project won't collide with the identifier of a semaphore in some other program installed on the system?
To help with that, the ftok()
system call was introduced.
This system call accepts two parameters, a path to a file and a character,
and generates a more-or-less unique identifier. It does that by finding
the "i-node" number of the file (more or less the number of the disk sector
containing this file's information), combines it with the second parameter,
and thus generates an identifier, that can be later fed to semget
,
shmget()
or msgget()
. Here is how to use
ftok()
:
/* identifier returned by ftok()
*/
key_t set_key;
/* generate a "unique" key for our set, using the */
/* directory "/usr/local/lib/ourprojectdir". */
set_key = ftok("/usr/local/lib/ourprojectdir", 'a');
if (set_key == -1) {
perror("ftok: ");
exit(1);
}
/* now we can use 'set_key' to generate a set id, for example. */
sem_set_id = semget(set_key, 1, IPC_CREAT | 0600);
.
.
ftok
call with this file will generate
a different key. Thus, the file used should be a steady file, and not one
that is likely to be moved to a different disk or erased and re-created.