



(3 ratings)
One of the strong features of Unix-like operating systems, is their ability to run several processes simultaneously, and let them all share the CPU(s), memory, and other resources. Any none-trivial system developed on Unix systems will sooner or later resort to splitting its tasks into more than one process. True, many times threads will be preferred (thought these are candidates for a separate tutorial), but the methods used in both of them tend to be rather similar - how to start and stop processes, how to communicate with other processes, how to synchronize processes.
We'll try to learn here the various features that the system supplies us with in order to answer these questions. One should note that dealing with multi-process systems takes a slightly different approach than dealing with a single-process program - events happen to occur in parallel, debugging is more complicated, and there's always the risk of having a bug cause endless process creation that'll bring your system to a halt. In fact, when i took a course that tried teaching these subjects, at the week we had to deliver one of the exercises, the system hardly managed to survive the bunch of students running buggy programs that create new processes endlessly, or leaving background processes running into endless loop, and so on. OK, lets stop intimidating, and get to see how it's done.
What Is A Unix Process
Before we talk about processes, we need to understand exactly what a process is. If you know exactly what it is, and are familiar with the notion of 'Re-entrancy', you may skip to the next section...
You didn't skip? OK. Lets try to write a proper definition:
- Unix Process
- An entity that executes a given piece of code, has its own execution stack, its own set of memory pages, its own file descriptors table, and a unique process ID.
As you might understand from this definition, a process is not a program. several processes may be executing the same computer program at the same time, for the same user or for several different users. For example, there is normally one copy of the 'tcsh' shell on the system, but there may be many tcsh processes running - one for each interactive connection of a user to the system. It might be that many different processes will try to execute the same piece of code at the same time, perhaps trying to utilize the same resources, and we should be ready to accommodate such situations. This leads us to the concept of 'Re-entrancy'.
- Re-entrancy
- The ability to have the same function (or part of a code) being in some phase of execution, more than once at the same time.
This re-entrancy might mean that two or more processes try to execute this piece of code at the same time. it might also mean that a single process tries to execute the same function several times simultaneously. How this may be possible? a simple example is a recursive function. The process starts executing it, and somewhere in the middle (before exiting the function), it calls the same function again. This means that the function should only use local variables to save its state information, for example.
Of-course, with a multi-process code, we don't have conflicts of variables, because normally the data section of each process is separate from that of other processes (so process A that runs program P and process B that runs the same program P, have distinct copies of the global variable 'i' of that program), but there might be other resources that would cause a piece of code to be non-reentrant. For example, if the program opens a file and writes some data to it, and two processes try to run the program at the same time, the contents of the file might be ruined in an unpredictable way. This is why a program must protect itself by using some kind of 'locking' mechanism, that will only allow one process at a time to open the file and write data into the file. An example of such a mechanism is the usage of semaphores, which will be discussed later on.
Process Creation
As you might (hopefully) already know, it is possible for a user to run a process in the system, suspend it (Ctrl-Z), and move it to the background (using the 'bg' command). If you're not familiar with this, you would do best to read the 'Job Control' section of the 'csh' manual page (or of 'bash', if that is the shell you normally use). However, we are interested in learning how to create new processes from within a C program.
The fork() System Call
The fork() system call is the basic way to create a new process.
It is also a very unique system call, since it returns twice(!) to the caller.
Sounds confusing? good. This confusion stems from the attempt to define as
few systems calls as possible, it seems. OK, lets see:
fork()- This system call causes the current process to be split into two
processes - a parent process, and a child process. All of the memory pages
used by the original process get duplicated during the
fork()call, so both parent and child process see the exact same image. The only distinction is when the call returns. When it returns in the parent process, its return value is the process ID (PID) of the child process. When it returns inside the child process, its return value is '0'. If for some reason this call failed (not enough memory, too many processes, etc.), no new process is created, and the return value of the call is '-1'. In case the process was created successfully, both child process and parent process continue from the same place in the code where thefork()call was used.
To make things clearer, lets see an example of a code that uses this system call to create a child process that prints (you guessed it) "hello world" to the screen, and exits.
#include <unistd.h> /* defines fork(), and pid_t. */
#include <sys/wait.h> /* defines the wait() system call. */
/* storage place for the pid of the child process, and its exit status. */
pid_t child_pid;
int child_status;
/* lets fork off a child process... */
child_pid = fork();
/* check what the fork() call actually did */
switch (child_pid) {
case -1: /* fork() failed */
perror("fork"); /* print a system-defined error message */
exit(1);
case 0: /* fork() succeeded, we're inside the child process */
printf("hello world\n");
exit(0); /* here the CHILD process exits, not the parent. */
default: /* fork() succeeded, we're inside the parent process */
wait(&child_status); /* wait till the child process exits */
}
/* parent's process code may continue here... */
Notes:
- The
perror()function prints an error message based on the value of the errno variable, to stderr. - The
wait()system call waits until any child process exits, and stores its exit status in the variable supplied. There are a set of macros to check this status, that will be explained in the next section.
Note: fork() copies also a memory area known as the 'U Area'
(or User Area). This area contains, amongst other things, the file descriptor
table of the process. This means that after returning from the
fork() call, the child process inherits all files that were open
in the parent process. If one of them reads from such an open file, the
read/write pointer is advanced for both of them. On the other hand, files
opened after the call to fork() are not shared by both processes.
Further more, if one process closes a shared file, it is still kept open
in the other process.
Child Process Termination
Once we have created a child process, there are two possibilities. Either the parent process exits before the child, or the child exits before the parent. Now, Unix's semantics regarding parent-child process relations state something like this:
- When a child process exits, it is not immediately cleared off the process table. Instead, a signal is sent to its parent process, which needs to acknowledge it's child's death, and only then the child process is completely removed from the system. In the duration before the parent's acknowledgment and after the child's exit, the child process is in a state called "zombie". (for info about Unix signals, please refer to our Unix signals programming tutorial).
- When a process exits (terminates), if it had any child processes, they become orphans. An orphan process is automatically inherited by the 'init' process (process number 1 on normal Unix systems), and becomes a child of this 'init' process. This is done to ensure that when the process terminates, it does not turn into a zombie, because 'init' is written to properly acknowledge the death of its child processes.
When the parent process is not properly coded, the child remains in the zombie state forever. Such processes can be noticed by running the 'ps' command (shows the process list), and seeing processes having the string "<defunct>" as their command name.
The wait() System Call
The simple way of a process to acknowledge the death of a child process
is by using the wait() system call. As we mentioned earlier,
When wait() is called, the process is suspended until
one of its child processes exits, and then the call returns with the exit
status of the child process. If it has a zombie child process, the call
returns immediately, with the exit status of that process.
Asynchronous Child Death Notification
The problem with calling wait() directly, is that usually
you want the parent process to do other things, while its child process
executes its code. Otherwise, you're not really enjoying multi-processes,
do you? That problem has a solution by using signals. When a child
process dies, a signal, SIGCHLD (or SIGCLD) is sent to its parent process.
Thus, using a proper signal handler, the parent will get an asynchronous
notification, and then when it'll call wait(), the system
assures that the call will return immediately, since there is already
a zombie child. Here is an example of our "hello world" program, using
a signal handler this time.
#include <stdio.h> /* basic I/O routines. */
#include <unistd.h> /* define fork(), etc. */
#include <sys/types.h> /* define pid_t, etc. */
#include <sys/wait.h> /* define wait(), etc. */
#include <signal.h> /* define signal(), etc. */
/* first, here is the code for the signal handler */
void catch_child(int sig_num)
{
/* when we get here, we know there's a zombie child waiting */
int child_status;
wait(&child_status);
printf("child exited.\n");
}
.
.
/* and somewhere in the main() function ... */
.
.
/* define the signal handler for the CHLD signal */
signal(SIGCHLD, catch_child);
/* and the child process forking code... */
{
int child_pid;
int i;
child_pid = fork();
switch (child_pid) {
case -1: /* fork() failed */
perror("fork");
exit(1);
case 0: /* inside child process */
printf("hello world\n");
sleep(5); /* sleep a little, so we'll have */
/* time to see what is going on */
exit(0);
default: /* inside parent process */
break;
}
/* parent process goes on, minding its own business... */
/* for example, some output... */
for (i=0; i<10; i++) {
printf("%d\n", i);
sleep(1); /* sleep for a second, so we'll have time to see the mix */
}
}
Lets examine the flow of this program a little:
- A signal handler is defined, so whenever we receive a SIGCHLD, catch_child will be called.
- We call
fork()to spawn a child process. - The parent process continues its control flow, while the child process is doing its own chores.
- When the child calls
exit(), a CHLD signal is sent by the system to the parent. - The parent process' execution is interrupted, and its CHLD signal handler, catch_child, is invoked.
- The
wait()call in the parent causes the child to be completely removed off the system. - finally, the signal handler returns, and the parent process continues execution at the same place it was interrupted in.
Communications Via Pipes
Once we got our processes to run, we suddenly realize that they cannot communicate. After all, often when we start one process from another, they are supposed to accomplish some related tasks. One of the mechanisms that allow related-processes to communicate is the pipe, or the anonymous pipe.
What Is A Pipe?
One of the mechanisms that allow related-processes to communicate is the pipe, or the anonymous pipe. A pipe is a one-way mechanism that allows two related processes (i.e. one is an ancestor of the other) to send a byte stream from one of them to the other one. Naturally, to use such a channel properly, one needs to form some kind of protocol in which data is sent over the pipe. Also, if we want a two-way communication, we'll need two pipes, and a lot of caution...
The system assures us of one thing: The order in which data is written to the pipe, is the same order as that in which data is read from the pipe. The system also assures that data won't get lost in the middle, unless one of the processes (the sender or the receiver) exits prematurely.
The pipe() System Call
This system call is used to create a read-write pipe that may later be used to communicate with a process we'll fork off. The call takes as an argument an array of 2 integers that will be used to save the two file descriptors used to access the pipe. The first to read from the pipe, and the second to write to the pipe. Here is how to use this function:
/* first, define an array to store the two file descriptors */
int pipes[2];
/* now, create the pipe */
int rc = pipe(pipes);
if (rc == -1) { /* pipe() failed */
perror("pipe");
exit(1);
}
If the call to pipe() succeeded, a pipe will be created,
pipes[0] will contain the number of its read file descriptor, and
pipes[1] will contain the number of its write file descriptor.
Now that a pipe was created, it should be put to some real use. To do this,
we first call fork() to create a child process, and then use
the fact that the memory image of the child process is identical to the
memory image of the parent process, so the pipes[] array is still defined
the same way in both of them, and thus they both have the file descriptors
of the pipe. Further more, since the file descriptor table is also copied
during the fork, the file descriptors are still valid inside the child
process.
Lets see an example of a two-process system in which one (the parent process) reads input from the user, and sends it to the other (the child), which then prints the data to the screen. The sending of the data is done using the pipe, and the protocol simply states that every byte passed via the pipe represents a single character typed by the user.
#include <stdio.h> /* standard I/O routines. */
#include <unistd.h> /* defines pipe(), amongst other things. */
/* this routine handles the work of the child process. */
void do_child(int data_pipe[]) {
int c; /* data received from the parent. */
int rc; /* return status of read(). */
/* first, close the un-needed write-part of the pipe. */
close(data_pipe[1]);
/* now enter a loop of reading data from the pipe, and printing it */
while ((rc = read(data_pipe[0], &c, 1)) > 0) {
putchar(c);
}
/* probably pipe was broken, or got EOF via the pipe. */
exit(0);
}
/* this routine handles the work of the parent process. */
void do_parent(int data_pipe[])
{
int c; /* data received from the user. */
int rc; /* return status of getchar(). */
/* first, close the un-needed read-part of the pipe. */
close(data_pipe[0]);
/* now enter a loop of read user input, and writing it to the pipe. */
while ((c = getchar()) > 0) {
/* write the character to the pipe. */
rc = write(data_pipe[1], &c, 1);
if (rc == -1) { /* write failed - notify the user and exit */
perror("Parent: write");
close(data_pipe[1]);
exit(1);
}
}
/* probably got EOF from the user. */
close(data_pipe[1]); /* close the pipe, to let the child know we're done. */
exit(0);
}
/* and the main function. */
int main(int argc, char* argv[])
{
int data_pipe[2]; /* an array to store the file descriptors of the pipe. */
int pid; /* pid of child process, or 0, as returned via fork. */
int rc; /* stores return values of various routines. */
/* first, create a pipe. */
rc = pipe(data_pipe);
if (rc == -1) {
perror("pipe");
exit(1);
}
/* now fork off a child process, and set their handling routines. */
pid = fork();
switch (pid) {
case -1: /* fork failed. */
perror("fork");
exit(1);
case 0: /* inside child process. */
do_child(data_pipe);
/* NOT REACHED */
default: /* inside parent process. */
do_parent(data_pipe);
/* NOT REACHED */
}
return 0; /* NOT REACHED */
}
As we can see, the child process closed the write-end of the pipe (since it only needs to read from the pipe), while the parent process closed the read-end of the pipe (since it only needs to write to the pipe). This closing of the un-needed file descriptor was done to free up a file descriptor entry from the file descriptors table of the process. It isn't necessary in a small program such as this, but since the file descriptors table is limited in size, we shouldn't waste unnecessary entries.
The complete source code for this example may be found in the file one-way-pipe.c.
Two-Way Communications With Pipes
In a more complex system, we'll soon discover that this one-way communications is too limiting. Thus, we'd want to be able to communication in both directions - from parent to child, and from child to parent. The good news is that all we need to do is open two pipes - one to be used in each direction. The bad news, however, is that using two pipes might cause us to get into a situation known as 'deadlock':
- Deadlock
- A situation in which a group of two or more processes are all waiting for a set of resources that are currently taken by other processes in the same group, or waiting for events that are supposed to be sent from other processes in the group.
Such a situation might occur when two processes communicate via two pipes. Here are two scenarios that could led to such a deadlock:
- Both pipes are empty, and both processes are trying to read from their input pipes. Each one is blocked on the read (cause the pipe is empty), and thus they'll remain stuck like this forever.
- This one is more complicated. Each pipe has a buffer of limited size
associated with it. When a process writes to a pipe, the data is placed
on the buffer of that pipe, until it is read by the reading process.
If the buffer is full, the
write()system call gets blocked until the buffer has some free space. The only way to free space on the buffer, is by reading data from the pipe.
Thus, if both processes write data, each to its 'writing' pipe, until the buffers are filled up, both processes will get blocked on thewrite()system call. Since no other process is reading from any of the pipes, our two processes have just entered a deadlock.
Lets see an example of a (hopefully) deadlock-free program in which one process reads input from the user, writes it to the other process via a pipe. the second process translates each upper-case letter to a lower-case letter and sends the data back to the first process. Finally, the first process writes the data to standard output.
#include <stdio.h> /* standard I/O routines. */
#include <unistd.h> /* defines pipe(), amongst other things. */
#include <ctype.h> /* defines isascii(), toupper(), and other */
/* character manipulation routines. */
/* function executed by the user-interacting process. */
void user_handler(int input_pipe[], int output_pipe[])
{
int c; /* user input - must be 'int', to recognize EOF (= -1). */
char ch; /* the same - as a char. */
int rc; /* return values of functions. */
/* first, close unnecessary file descriptors */
close(input_pipe[1]); /* we don't need to write to this pipe. */
close(output_pipe[0]); /* we don't need to read from this pipe. */
/* loop: read input, send via one pipe, read via other */
/* pipe, and write to stdout. exit on EOF from user. */
while ((c = getchar()) > 0) {
/* note - when we 'read' and 'write', we must deal with a char, */
/* rather then an int, because an int is longer then a char, */
/* and writing only one byte from it, will lead to unexpected */
/* results, depending on how an int is stored on the system. */
ch = (char)c;
/* write to translator */
rc = write(output_pipe[1], &ch, 1);
if (rc == -1) { /* write failed - notify the user and exit. */
perror("user_handler: write");
close(input_pipe[0]);
close(output_pipe[1]);
exit(1);
}
/* read back from translator */
rc = read(input_pipe[0], &ch, 1);
c = (int)ch;
if (rc <= 0) { /* read failed - notify user and exit. */
perror("user_handler: read");
close(input_pipe[0]);
close(output_pipe[1]);
exit(1);
}
/* print translated character to stdout. */
putchar(c);
}
/* close pipes and exit. */
close(input_pipe[0]);
close(output_pipe[1]);
exit(0);
}
/* now comes the function executed by the translator process. */
void translator(int input_pipe[], int output_pipe[])
{
int c; /* user input - must be 'int', to recognize EOF (= -1). */
char ch; /* the same - as a char. */
int rc; /* return values of functions. */
/* first, close unnecessary file descriptors */
close(input_pipe[1]); /* we don't need to write to this pipe. */
close(output_pipe[0]); /* we don't need to read from this pipe. */
/* enter a loop of reading from the user_handler's pipe, translating */
/* the character, and writing back to the user handler. */
while (read(input_pipe[0], &ch, 1) > 0) {
/* translate any upper-case letter to lower-case. */
c = (int)ch;
if (isascii(c) && isupper(c))
c = tolower(c);
ch = (char)c;
/* write translated character back to user_handler. */
rc = write(output_pipe[1], &ch, 1);
if (rc == -1) { /* write failed - notify user and exit. */
perror("translator: write");
close(input_pipe[0]);
close(output_pipe[1]);
exit(1);
}
}
/* close pipes and exit. */
close(input_pipe[0]);
close(output_pipe[1]);
exit(0);
}
/* and finally, the main function: spawn off two processes, */
/* and let each of them execute its function. */
int main(int argc, char* argv[])
{
/* 2 arrays to contain file descriptors, for two pipes. */
int user_to_translator[2];
int translator_to_user[2];
int pid; /* pid of child process, or 0, as returned via fork. */
int rc; /* stores return values of various routines. */
/* first, create one pipe. */
rc = pipe(user_to_translator);
if (rc == -1) {
perror("main: pipe user_to_translator");
exit(1);
}
/* then, create another pipe. */
rc = pipe(translator_to_user);
if (rc == -1) {
perror("main: pipe translator_to_user");
exit(1);
}
/* now fork off a child process, and set their handling routines. */
pid = fork();
switch (pid) {
case -1: /* fork failed. */
perror("main: fork");
exit(1);
case 0: /* inside child process. */
translator(user_to_translator, translator_to_user); /* line 'A' */
/* NOT REACHED */
default: /* inside parent process. */
user_handler(translator_to_user, user_to_translator); /* line 'B' */
/* NOT REACHED */
}
return 0; /* NOT REACHED */
}
A few notes:
- Character handling:
isascii()is a function that checks if the given character code is a valid ASCII code.isupper()is a function that checks if a given character is an upper-case letter.tolower()is a function that translates an upper-case letter to its equivalent lower-case letter. - Note that both functions get an input_pipe and an output_pipe array. However, when calling the functions we must make sure that the array we give one as its input pipe - we give the other as its output pipe, and vice versa. Failing to do that, the user_handler function will write a character to one pipe, and then both functions will try to read from the other pipe, thus causing both of them to block, as this other pipe is still empty.
- Try to think: what will happen if we change the call in line 'A' above
to:
translator(user_to_translator, user_to_translator); /* line 'A' */
and the code of line 'B' above to:user_handler(translator_to_user, translator_to_user); /* line 'B' */ - Think harder now: what if we leave line 'A' as it was in the original program, and only modify line 'B' as in the previous question?
The complete source code for this example may be found in the file two-way-pipe.c.
Named Pipes
One limitation of anonymous pipes is that only processes 'related' to the process that created the pipe (i.e. siblings of that process.) may communicate using them. If we want two un-related processes to communicate via pipes, we need to use named pipes.
What Is A Named Pipe?
A named pipe (also called a named FIFO, or just FIFO) is a pipe whose access point is a file kept on the file system. By opening this file for reading, a process gets access to the reading end of the pipe. By opening the file for writing, the process gets access to the writing end of the pipe. If a process opens the file for reading, it is blocked until another process opens the file for writing. The same goes the other way around.
Creating A Named Pipe With The mknod Command
A named pipe may be created either via the 'mknod' (or its newer replacement,
'mkfifo'), or via the mknod() system call (or by the
POSIX-compliant mkfifo() function). To create a named pipe
with the file named 'prog_pipe', we can use the following command:
mknod prog_pipe p
We could also provide a full path to where we want the named pipe created.
If we then type 'ls -l prog_pipe', we will see something like this:
prw-rw-r-- 1 choo choo 0 Nov 7 01:59 prog_pipe
The 'p' on the first column denotes this is a named pipe. Just like any file in the system, it has access permissions, that define which users may open the named pipe, and whether for reading, writing or both.
Opening A Named Pipe For Reading Or Writing
Opening a named pipe is done just like opening any other file in the system,
using the open() system call, or using the fopen()
standard C function. If the call succeeds, we get a file descriptor (in the
case of open(), or a 'FILE' pointer (in the case of
fopen()), which we may use either for reading or for writing,
depending on the parameters passed to open() or to
fopen().
Reading/Writing From/To A Named Pipe
Reading from a named pipe is very similar to reading from a file, and the same goes for writing to a named pipe. Yet there are several differences:
- Either Read Or Write - a named pipe cannot be opened for both reading and writing. The process opening it must choose one mode, and stick to it until it closes the pipe.
- Read/Write Are Blocking - when a process reads from a named pipe that has no data in it, the reading process is blocked. It does not receive an end of file (EOF) value, like when reading from a file. When a process tries to write to a named pipe that has no reader (e.g. the reader process has just closed the named pipe), the writing process gets blocked, until a second process re-opens the named pipe.
Thus, when writing a program that uses a named pipe, we must take these limitations into account. We could also turn the file descriptor via which we access the named pipe to a non-blocking mode. This, however, is out of the scope of our tutorial. For info about how to do that, and how to handle a non-blocking pipe, please refer to the manual pages of 'open(2)', fcntl(2), read(2) and write(2).
Named Pipe - A Complete Example
As an example to an obscure usage of named pipes, we will borrow some idea
from a program that allows one to count how many times they have been
"fingered" lately. As you might know, on many Unix systems, there is a finger
daemon, that accepts requests from users running the "finger" program, with
a possible user name, and tells them when this user last logged on, as well
as some other information. Amongst other thing, the finger daemon also checks
if the user has a file named '.plan' (that is dot followed by "plan") in her
home directory. If there is such a file, the finger daemon opens it, and
prints its contents to the client. For example, on my Linux machine,
fingering my account might show something like:
[choo@simey1 ~]$ finger choo
Login: choo Name: guy keren
Directory: /home/choo Shell: /bin/tcsh
On since Fri Nov 6 15:46 (IDT) on tty6
No mail.
Plan:
- Breed a new type of dogs.
- Water the plants during all seasons.
- Finish the next tutorial on time.
As you can see, the contents of the '.plan' file has been printed out.
This feature of the finger daemon may be used to create a program that
tells the client how many times i was fingered. For that to work, we first
create a named pipe, where the '.plan' file resides:
mknod /home/choo/.plan p
If i now try to finger myself, the output will stop before showing the 'plan'
file. How so? this is because of the blocking nature of a named pipe. When
the finger daemon opens my '.plan' file, there is no write process, and thus
the finger daemon blocks. Thus, don't run this on a system where you expect
other users to finger you often.
The second part of the trick, is compiling the
named-pipe-plan.c program, and running it.
note that it contains the full path to the '.plan' file, so change that to
the appropriate value for your account, before compiling it. When you run
the program, it gets into an endless loop of opening the named pipe in
writing mode, write a message to the named pipe, close it, and sleep for
a second. Look at the program's source code for more information. A sample
of its output looks like this:
[choo@simey1 ~]$ finger choo
Login: choo Name: guy keren
Directory: /home/choo Shell: /bin/tcsh
On since Fri Nov 6 15:46 (IDT) on tty6
No mail.
Plan:
I have been fingered 8 times today
When you're done playing, stop the program, and don't forget to remove the named pipe from the file system.
Few Words About Sockets
Various sockets-based mechanisms may be used to communicate amongst processes. The underlying communications protocol may be TCP, UDP, IP, or any other protocol from the TCP/IP protocols family. There is also a socket of type 'Unix-domain', which uses some protocol internal to the operating system to communicate between processes all residing on a single machine. Unix-domain sockets are similar to named pipes in that the communicating processes use a file in the system to connect to establish a connection. For more information about programming with sockets, please refer to our tutorial about internetworking with Unix sockets.
System V IPC
Many variants of Unix these days support a set of inter-process communications methods, which are derived from Unix System V release 4, originating from AT&T Bell laboratories. These mechanisms include message queues (used for sending and receiving messages), shared memory (used to allow several processes share data in memory) and semaphores (used to co-ordinate access by several processes, to other resources). Each of these resource types is handled by the system, and unlike anonymous pipes, may out-live the process that created it. These resources also have some security support by the system, that allows one to specify which processes may access a given message queue, for example.
The fact that these resources are global to the system has two contradicting implications. On one hand, it means that if a process exits, the data it sent through a message queue, or placed in shared memory is still there, and can be collected by other processes. On the other hand, this also means that the programmer has to take care of freeing these resources, or they occupy system resources until the next reboot, or until being removed by hand.
I am going to make a statement here about these communications mechanisms,
that might annoy some readers: System V IPC mechanisms are evil regarding
their implementation, and should not be used unless there is a very good
reason. One of the problem with these mechanism, is that one cannot use
the select() (or its replacement, poll()) with
them, and thus a process waiting for a message to be placed in a message
queue, cannot be notified about messages coming via other resources (e.g.
other message queues, pipes or sockets).
In my opinion, this limitation is an oversight by the designers of these
mechanisms. Had they used file descriptors to denote IPC resources (like
they are used for pipes, sockets and files) life would be easier.
Another problem with System V IPC is their system-global nature. The total number of message queues that may live in the system, for example, is shared by all processes. Worse than that, the number of messages waiting in all messages queues is also limited globally. One process spewing many such messages will break all processes using message queues. The same goes for other such resources. There are various other limitations imposed by API (Application programming interface). For example, one may wait on a limited set of semaphores at the same time. If you want more than this, you have to split the waiting task, or re-design your application.
Having said that, there are still various applications where using system V IPC (we'll call it SysV IPC, for short) will save you a large amount of time. In these cases, you should go ahead and use these mechanism - just handle with care.
Permission Issues
Before delving into the usage of the different System V IPC mechanisms, we will describe the security model used to limit access to these resources.
Private Vs. Public
Each resource in SysV IPC may be either private or public. Private means that it may be accessed only by the process that created it, or by child processes of this process. Public means that it may be potentially accessed by any process in the system, except when access permission modes state otherwise.
Access Permission Modes - The 'ipc_perm' Structure
SysV IPC resources may be protected using access mode permissions, much like
files and directories are protected by the Unix system. Each such resource
has an owning user and an owning group. Permission modes define if and how
processes belonging to different users in the system may access this resource.
Permissions may be set separately for the owning user, for users from the owning
group, and everyone else. permissions may be set for reading the resource
(e.g. reading messages from a message queue), or writing to the resource
(e.g. sending a message on a queue, changing the value of a semaphore).
A structure of type 'ipc_perm', which is defined as follows:
struct ipc_perm
{
key_t key; /* key identifying the resource */
ushort uid; /* owner effective user ID and effective group ID */
ushort gid;
ushort cuid; /* creator effective user ID and effective group ID */
ushort cgid;
ushort mode; /* access modes */
ushort seq; /* sequence number */
};
These fields have the following meanings:
- key - the identifier of the resource this structure refers to.
- uid - effective user ID owning the resource.
- gid - effective group ID owning the resource.
- cuid - effective user ID that created the resource.
- cgid - effective group ID that created the resource.
- mode - access permission modes for the given resource. This is
a bit field, with the lowest 9 bits denoting access flags, and are
a bit-wise 'or' of the following (octal) values:
- 0400 - owning user may read from this resource.
- 0200 - owning user may write to this resource.
- 0040 - owning group may read from this resource.
- 0020 - owning group may write to this resource.
- 0004 - every other user may read from this resource.
- 0002 - every other user may write to this resource.
- seq - used to keep system-internal info about the resource. for further info, check your kernel's sources (you are working on a system with free access to its source code, right?).
Part of the SysV IPC API allows us to modify the access permissions for the resources. We will encounter them when discussing the different IPC methods.
System Utilities To Administer System-V IPC Resources
Since SysV IPC resources live outside the scope of a single process, there is a need to manage them somehow - delete resources that were left by irresponsible processes (or process crashes); check the number of existing resources of each type (especially to find if the system-global limit was reached), etc. Two utilities were created for handling these jobs: 'ipcs' - to check usage of SysV IPC resources, and 'ipcrm' - to remove such resources.
Running 'ipcs' will show us statistics separately for each of the three resource types (shared memory segments, semaphore arrays and message queues). For each resource type, the command will show us some statistics for each resource that exists in the system. It will show its identifier, owner, size of resources it occupies in the system, and permission flags. We may give 'ipcs' a flag to ask it to show only resources of one type ('-m' for shared Memory segments, -q for message Queues and '-s' for Semaphore arrays). We may also use 'ipcs' with the '-l' flag to see the system enforced limits on these resources, or the '-u' flag to show us usage summary. Refer to the manual page of 'ipcs' for more information.
The 'ipcrm' command accepts a resource type ('shm', 'msg' or 'sem') and a resource ID, and removes the given resource from the system. We need to have the proper permissions in order to delete a resource.
Using Message Queues
One of the problems with pipes is that it is up to you, as a programmer, to establish the protocol. Now, usually this protocol is based on sending separate messages. With a stream taken from a pipe, it means you have to somehow parse the bytes, and separate them to packets. Another problem is that data sent via pipes always arrives in a FIFO order. This means that before you can read any part of the stream, you have to consume all the bytes sent before the piece you're looking for, and thus you need to construct your own queuing mechanism on which you place the data you just skipped, to be read later. If that's what you're interested at, this is a good time to get acquainted with message queues.
What Are Message Queues?
A message queue is a queue onto which messages can be placed. A message is composed of a message type (which is a number), and message data. A message queue can be either private, or public. If it is private, it can be accessed only by its creating process or child processes of that creator. If it's public, it can be accessed by any process that knows the queue's key. Several processes may write messages onto a message queue, or read messages from the queue. Messages may be read by type, and thus not have to be read in a FIFO order as is the case with pipes.
Creating A Message Queue - msgget()
In order to use a message queue, it has to be created first. The
msgget() system call is used to do just that. This system call
accepts two parameters - a queue key, and flags. The key may be one of:
- IPC_PRIVATE - used to create a private message queue.
- a positive integer - used to create (or access) a publicly-accessible message queue.
open() system call,
and will be explained later, and it also contains access permission bits.
The lowest 9 bits of the flags are used to define access permission for
the queue, much like similar 9 bits are used to control access to files.
the bits are separated into 3 groups - user, group and others. In each set,
the first bit refers to read permission, the second bit - to write permission,
and the third bit is ignored (no execute permission is relevant to message
queues).Lets see an example of a code that creates a private message queue:
#include <stdio.h> /* standard I/O routines. */
#include <sys/types.h> /* standard system data types. */
#include <sys/ipc.h> /* common system V IPC structures. */
#include <sys/msg.h> /* message-queue specific functions. */
/* create a private message queue, with access only to the owner. */
int queue_id = msgget(IPC_PRIVATE, 0600); /* <-- this is an octal number. */
if (queue_id == -1) {
perror("msgget");
exit(1);
}
A few notes about this code:
- the system call returns an integer identifying the created queue. Later on we can use this key in order to access the queue for reading and writing messages.
- The queue created belongs to the user whose process created the queue. Thus, since the permission bits are '0600', only processes run on behalf of this user will have access to the queue.
The Message Structure - struct msgbuf
Before we go to writing messages to the queue or reading messages from it,
we need to see how a message looks. The system defines a structure named
'msgbuf' for this purpose. Here is how it is defined:
struct msgbuf {
long mtype; /* message type, a positive number (cannot be zero). */
char mtext[1]; /* message body array. usually larger than one byte. */
};
The message type part is rather obvious. But how do we deal with a message text that is only 1 byte long? Well, we actually may place a much larger text inside a message. For this, we allocate more memory for a msgbuf structure than
sizeof(struct msgbuf). Lets see how we create
an "hello world" message:
/* first, define the message string */
char* msg_text = "hello world";
/* allocate a message with enough space for length of string and */
/* one extra byte for the terminating null character. */
struct msgbuf* msg =
(struct msgbuf*)malloc(sizeof(struct msgbuf) + strlen(msg_text));
/* set the message type. for example - set it to '1'. */
msg->mtype = 1;
/* finally, place the "hello world" string inside the message. */
strcpy(msg->mtext, msg_text);
Few notes:
- When allocating a space for a string, one always needs to allocate
one extra byte for the null character terminating the string. In our
case, we allocated
strlen(msg_text)more than the size of "struct msgbuf", and didn't need to allocate an extra place for the null character, cause that's already contained in the msgbuf structure (the 1 byte of mtext there). - We don't need to place only text messages in a message. We may also
place binary data. In that case, we could allocate space as large as
the msgbuf struct plus the size of our binary data, minus one byte.
Of-course then to copy the data to the message, we'll use a function
such as
memset(), and notstrcpy().
Writing Messages Onto A Queue - msgsnd()
Once we created the message queue, and a message structure, we can place it
on the message queue, using the msgsnd() system call. This system
call copies our message structure and places that as the last message on the
queue. It takes the following parameters:
-
int msqid- id of message queue, as returned from themsgget()call. -
struct msgbuf* msg- a pointer to a properly initializes message structure, such as the one we prepared in the previous section. -
int msgsz- the size of the data part (mtext) of the message, in bytes.
20 Random Tutorials from the same category :
Asking questions in a discussion forum
How to use the most popular command in Unix - Grep
How to edit and understand /etc/fstab
Manipulating directories in Linux
Parallel Programming - Basic Theory For The Unwary
How to set Shell Environment Variables (bash shell)
Introduction To Unix Signals Programming
Manipulating files in Linux
Installing software from RPM packages
How to find files in Linux using 'find'
What are the SUID, SGID and the Sticky Bits?
Redirection in Linux
Getting LILO to boot Windows (by default)
How to view text files in Linux
Windows COM Ports equivalent in Linux
Execute a task 'at' the time you want..
Using Emacs
Running multiple X sessions
Scheduling tasks using Cron - Part I
Accessing CDROMs in Linux (covers mount, umount, fstab configuration file)













