DæmonNews: News and views for the BSD community

March 2000 Search Submit Article Contact Us Join Us Merchandise

InterProcess Communications for the Scared and Confused

By Johnathan Meehan jmeehan@easynet.co.uk

The good news is that this is the first in a series of articles providing a basic tour of IPC under UNIX. The bad news is that this is the first in a series of articles. IPC is a broad subject, and worthy of the many doorstops already written about it - don't let this put you off! To understand IPC is to understand your UNIX machine, and a major step in entering the dark world of the systems programmer.

Reading these articles will not make you an IPC guru. Far from it. What they will do, however, is provide a thorough grounding in the basics and a solid base from which to continue your own work. As with everything, you get out what you put in - I will pose questions for you, and give some code. Nothing can take the place of your own efforts, though. Play with the code, run around the kernel, and above all _read the man pages_.

The Basics

The heart of your operating system is the kernel. This magical piece of software acts as a layer between you and the hardware, and provides a common environment for both users and other software. This concept of "layer" is an important one. By acting as a layer, the kernel can help ensure that no action is taken upon the hardware by any one-rogue application that would threaten system stability for all other users or programs. Similarly, it can prevent other users from viewing files in your home directory. It is the step between action and result.

As stated, the kernel also provides a common environment under which to run our programs. When we enter the name of a program to be run at the prompt, the file is sought along our path variable and, when found, the executable file is loaded into memory and executed. During the lifetime of its operation it is referred to as a process, and given a unique identifier. The processes currently running on your machine can be seen using the program ps. Here is the output currently on my machine, using ps without my favourite options:

% ps
    PID  TT STAT    TIME COMMAND
    300  v0 Ss   0:00.04 -csh (csh)
    371  v0 R+       0:00.00 ps
    284  v1 Ss+  0:00.04 -csh (csh)
    285  v2 Is+     0:00.03 -csh (csh)
%

From this we can see that I have three terminal sessions open, from TT, and each is using a copy of csh as the shell. We can also see that ps listed itself as a running process under the PID of 371. PID is the unique identifier we mentioned, and is short for Process ID. Note that each of the three csh shells running is operating under a different PID, despite being called from the same executable.

The system memory within which these processes reside can be divided into two distinct areas - user space and kernel space. The example program that we will look at shortly is a user process, and will run in user space. It will be running in user mode. The kernel, on the other hand, executes in kernel space and is said to be running in kernel mode. A user process can only access kernel space by making a system call - in other words, by asking the kernel to perform some task. In doing so, the user process makes a context switch and will, temporarily, be running in kernel mode. System calls are more common than you may at first think - the C command printf() makes a system call to write.

Whilst in system memory, the user process is divided into three segments:

Text Segment

This is a read only area of memory, containing the executable program code and constant data. If we run the same application more than once, then all processes will share this segment unless this option is disabled with the compiler. In the program below, the function main resides in this area.

Data Segment

This can be thought of as continuing from the text segment, and is divided into two areas: initialised and uninitialised data. In the example below, the char pointer init will be found in the initialised area, the char buf in the uninitialised. During the lifetime of a process a call to, for example, malloc may be made to request more memory. System calls brk and sbrk handle this by extending the size of the data segment toward the stack segment, adding memory from the heap to the end of the uninitialised data area.

Stack Segment

The location of the stack segment is system dependent, and holds register variables, automatic identifiers and function call information for the process. The integer i in the function main in the example below is stored in this area. It grows toward the uninitialised data area as required.

The area beyond the stack segment holds the command line arguments and environment variables for the process.

We can use the external variables etext, edata, and end to find the position of the text, initialised data, and uninitialised data segments:

#include <stdio.h>
extern int etext, edata, end;
char *init = "FreeBSD";
char buf[50];

void main(void)
{
    int i=0;
    printf("etext: %8X\n", &etext;);
    printf("edata: %8X\n", &edata;);
    printf("end  : %8X\n", &end;);
    printf("main : %8X\n", &main;);
    printf("init : %8X\n", &init;);
    printf("buf  : %8X\n", &buf;);
    printf("i    : %8X\n", &i;);
    exit(0);
}

Studying the output of this program should also help you understand the divisions of memory more clearly, with reference to where things are kept.

It is worth briefly mentioning another area of memory - the u-area. This area of memory contains information such as open files, that is specific to a process alongside a system stack segment. Were a process to make a system call, the information needed would be stored in the system stack segment - an area not normally accessible by the process. System calls are needed to access this information, which is paged in and out by the kernel.

More about Processes

We know that any process under UNIX is assigned a PID. Here is a little program that will run and show us the PID assigned:

#include <stdio.h>
#include <unistd.h> // getpid()
#include <sys/types.h> // pid_t

void main(void)
{
   printf("My PID is %d. Bye!\n", getpid());
   exit(0);
}

The function getpid() does the work here, accepting no arguments and returning the value of the PID. If you run this program several times, you will see the value for the assigned PID increment. Under FreeBSD, this will happen until we reach a number defined by PID_MAX, whereupon number allocation will wrap around and continue from the lowest, unused number.

Where is PID_MAX defined, and what is the value? Which possible PID allocations are reserved by FreeBSD? What the hell is pid_t, anyway?

To explore processes more fully, we firstly need to examine the most famous of all UNIX system calls - the mighty fork(). Here is a quick example:

#include <stdio.h>
#include <unistd.h> // fork()
#include <sys/types.h>

int main(void)
{
    printf("You will see this once!\n");
    fork();
    printf("You will see this twice!\n");
    exit(0);
}

In these examples, the currently running process (the parent), makes a call to fork() which makes a copy of it (a child process). The child process execution starts after the fork() call, the same place as the parent continues from. Note that fork() makes a copy of the process. The man pages will reveal features that are unique to the child, but it is still a copy of the parent - data is not shared between the two processes.

When fork() is called, it accepts no arguments but will return twice. The child process knows that it is the child because a 0 is returned. The PID 0 by the way is reserved for swapper under FreeBSD. The parent receives a copy of the child's PID. If you fork multiple processes, and your parent needs to know where the children area you must store this information as there is no call to retrieve it later. A child can however find the PID of it's parent using the call getppid(). The brief program below demonstrates both of these concepts:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(void)
{
    pid_t i;
    i = fork();
    if(i==0) // The child
        printf("I am the child. My parent is %d\n", getppid());
    else
        printf("I am the parent. My PID is %d\n", getpid());
    exit(0);
}

It doesn't work! What went wrong? The parent returns the PID for itself, but the child returns a PID of 1. What is happening?

The parent is terminating before the child, and so the child has been orphaned. In this case it is taken under the loving wing of init, for which FreeBSD reserves the PID 1. init takes control of any orphaned children, and upon completion of their work, issues the wait() on behalf of the parent.

When a child process dies, it stops running but leaves a small remnant of itself lying around containing all sorts of goodies for the parent to read. I'll leave you find out what is there for yourself.

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void) {     if(!fork()) {         printf("I am the child. My parent is %d\n", getppid());     } else {         printf("I am the parent. My PID is %d\n", getpid());         wait(NULL); // The magic line     }     exit(0); }

Now things work. The command wait() tells the parent to suspend activity until it receives word of the child's passing away. Passing the value NULL to wait, says that we are not interested in any information other than this death. When the child dies, we exit. The use of wait() is rather limited. Were we to create two children, then wait() would exit upon the first to complete. If we needed to wait for a particular child, we could use waitpid(), passing the pid of the child as a parameter. The man(2) wait page yields more information, including information on wait3() and wait4(). In this example, the child commits suicide, by sending signal 9 to itself (we look at signals next article), and the parent informs us with which signal the child died:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void)
{
    int xstat;
    if(!fork()) {
        kill(getpid(), 9) // Suicidal Tendencies...
    } else {
        wait(&xstat;);
        printf("Child killed by terminal signal %d\n", WTERMSIG(xstat));
    }
    exit(0);
}

We know that a child process is related to the parent, but more than this each child is, as you may have thought, related to all of the others. This family type relationship is termed a process group, with the parent being the process leader and the children being members. We can retrieve the process information with the call getpgid(). This example creates a number of child processes, and attempts to display this relationship:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.>>
#include <sys/wait.h>

int main(void)
{
    int i;
    printf("\tPID\tPPID\tGID\n");
    if(!fork()) {
        fork();
        printf("Forked:\t%d\t%d\t%d\n", getpid(), getppid(), getpgid(0));
    } else {
        printf("Parent:\t%d\t%d\t%d\n", getpid(), getppid(), getpgid(0));
        wait(NULL);
        exit(0);
    }
}

Note here also, that the child forks off a process of it's own. When I ran the program, I saw the following output:

        PID  PPID PGID
Parent: 520  286  520
Forked: 521  520  520
Forked: 522  1    520

The first thing to note here is the PGID, or Process Group ID. As we thought, all processes belong to the one group, 520. Note also the PPID of the initial process - 286. What is going on here, then? Remember ps?

% ps -aux | grep 286
jonno 286 0.0 0.1 456 340 v3 Ds 5:40PM 0:00.06 -csh (csh)

The parent of my parent is the login shell. This makes sense, when you think about it. Note also, that process 522 has been fostered by init.

We have passed a 0 to getpgid(). Why? Is there any difference between this and using getpgrp()?

Free Beer!

Here's a question with a prize. If you're getting the hang of this it is a doddle. Who was the parent of the orphaned process? The first one to mail me the correct smallest possible modification to the code to support the correct answer gets a bottle of beer, sent all the way from here in sunny Germany.

getpgid() has an opposing call, setpgid(). We'll take a look at this in the next article, along with permissions, process limits and signals. Until then!



Author maintains all copyrights on this article.
Images and layout Copyright © 1998-2000 Dæmon News. All Rights Reserved.