Chapter 4, Writing IPC Programs with RCOS

To do good and to communicate forget not.

-- Hebrews, ch. 13, v. 16.

Introduction

Interprocess communication (IPC) is one of the major abilities that is being implemented in the new generation of operating systems. UNIX, Windows NT and OS/2 all implement IPC in a somewhat similar manner. RCOS follows this example by supplying primitives by which application programmers can write programs that use shared memory and semaphores.

The aim of this chapter is to demonstrate how to write RCOS programs that use shared memory and semaphores. It assumes you understand the concepts of semaphores and shared memory.

The Programmer's Interface

To implement semaphores and shared memory an operating system must contain specific data structures and algorithms to implement and manage their use. In RCOS these algorithms and data structures are defined in the files MSHARE.CPP (shared memory) and IPC.CPP (semaphores).

For users or their programs to make use shared memory or semaphores the operating system provides a set of system calls. These system calls define the valid operations that a program can perform on semaphores and shared memory. Under RCOS a system call is generated by using the CSP p-code instruction. The different system calls supported by RCOS are defined in the PCI.HPP and PCI.CPP files.

In turn the programming language that application programs are written in must contain a mechanism by which user programs can execute system calls. The PLL/2 source language that is used to write application programs for RCOS implements a special function that allows access some of the system calls provided by RCOS. The function name is SYSTEM and it takes a variable number of parameters depending on the system call being called.

Diagram 4.1.

How IPC Primitives are Accessed.

Tables 4.1 and 4.2. lists the PLL/2 system functions used by PLL/2 programs to access the IPC facilities provided by RCOS. Refer to the following section on identifying IPC primitives for an explanation of handle and id.

Function Prototype                           Purpose                               
SYSTEM( SEM_CLOSE, handle )                  Disconnect from a semaphore.  If      
                                             the process is the last using the     
                                             semaphore it is deleted.              
SYSTEM( SEM_CREATE, 'id'[,init_value])       Create a new semaphore identified     
                                             by id and initialised to an           
                                             optional init_value.  If left out     
                                             initial value is 1 and semaphore is   
                                             a binary semaphore.                   
SYSTEM( SEM_OPEN, 'id')                      Connect to an existing semaphore.     
SYSTEM( SEM_SIGNAL, handle)                  signal the specified semaphore        
SYSTEM( SEM_WAIT, handle)                    wait on the specified semaphore       

Table 4.1.

Semaphore SYSTEM Functions.

For example:

VAR
  sem, dummy: INTEGER;

BEGIN {main}
  sem := SYSTEM(SEM_CREATE, 'my_sem', 0);
  Writeln('I am waiting for other process to signal.... ' );
  dummy := SYSTEM(SEM_WAIT, sem)
  Writeln('The other program did a signal on my semaphore. ' );
  sem := SYSTEM( SEM_CLOSE, sem );
END.
Function Prototype                       Purpose                               
SYSTEM(SHR_CLOSE, handle)                Disconnect from a shared memory       
                                         segment.  If no more processes are    
                                         connected then delete the shared      
                                         memory segment.                       
SYSTEM( SHR_CREATE, 'id', size)          Create a new shared memory segment    
                                         of a specified size                   
SYSTEM( SHR_OPEN, 'id' )                 Connect to an existing shared         
                                         memory segment                        
SYSTEM( SHR_READ, handle,                return the contents of a shared       
offset )                                 memory segment at offset              
SYSTEM( SHR_WRITE,handle, offset,        Write to the shared memory segment    
variable )                               at offset the contents of variable.   
SYSTEM( SHR_CLOSE,handle)                return the size of a shared memory    
                                         segment                               

Table 4.2.

Shared Memory SYSTEM Functions.

For example:

VAR
  myshm, value, result, temp : INTEGER;

BEGIN
  myshm := SYSTEM(SHR_OPEN, 'myshm' );

  IF myshm > 0 THEN
  BEGIN
    { Get Value from shared memory }
    value := SYSTEM(SHR_READ, myshm, 0 );
    
    Write( 'I read ', value# );
    WriteLn( ' from shared memory, squaring it..' );
    result := value * value;

    temp := SYSTEM(SHR_WRITE, myshm, 0, result );
    temp := SYSTEM(SHR_CLOSE, myshm)
  END
END.

Steps for Writing Programs that use IPC

There are three basic steps involved in writing a program to use either IPC primitives, semaphores or shared memory. They are

  • initialise or connect to the primitive,
  • manipulate the primitive, and
  • disconnect from the primitive.

    All programs that use either shared memory or semaphores must complete these three steps. (actually programs don't need to manipulate the primitive but then why do the other 2 steps if it doesn't?)

    Identifying an IPC Primitive

    Before you can do any of the three steps mentioned above you need to be able to identify the semaphore or shared memory segment you are manipulating.

    There are two types of identifiers used for IPC primitives

    Connecting to an IPC Primitive

    There are two different methods for connecting to an IPC primitive

    In the example programs below, the program SEM1 is the first program that must be executed and so it uses SEM_CREATE. The program SEM2 then uses SEM_OPEN to connect to the semaphore that already exists because SEM1 has created it.

    Manipulating a Primitive

    Both shared memory and semaphores have a small set of valid operations that a program can use. The following examines just the manipulation operations and not the creation, connecting or closing operations.

    RCOS supports the standard semaphore operations

    The initialisation of a semaphore is carried out as part of the SEM_CREATE operation. If the semid used in either a SEM_WAIT or SEM_SIGNAL operation is invalid an error will be generated.

    The three valid operations on a shared memory segment are

  • writing a new value to the memory,
  • reading the value of the shared memory segment, and
  • obtaining the size of the shared memory segment.

    Shared memory segments can be almost any size (within memory limits). The function call SYSTEM( SEM_CREATE, 'mysem', 1 ) creates a shared memory segment that is one PLL/2 integer variable in size. The function call SYSTEM( SEM_CREATE, 'mysem', 10 ) creates one that is ten PLL/2 integer variables in size.

    If you examine Table 4.2. you will see that the read and write functions require an offset. The function call SYSTEM( SEM_READ, handle, 0 ) will return the contents of the first location in the shared memory segment. The function call SYSTEM( SEM_WRITE, handle, 4, myvar ) will write the contents of the variable myvar into the fifth location in the shared memory segment, remember start counting at 0 (we're all C programmers after all).

    Disconnecting from a Primitive

    Both shared memory and semaphores are a restricted resource under RCOS (like most operating systems). RCOS can only support a certain number of both semaphores and shared memory. Therefore it is important that every program that makes use of an IPC primitive disconnects from the primitive. Both primitives provide a ???_CLOSE function, where ??? is replaced with SEM or SHM.

    This function will disconnect the current process from the IPC primitive. If it is the last process connected to the primitive it will also delete the primitive from the system. Thereby allowing some other process to create a new primitive.

    Example IPC Programs

    This section provides four example programs that make use of RCOS's IPC primitives. You should find these programs in the directory into which you installed RCOS.

    Filenames         Purpose                                              
    sem1.pll          Provide a simple example of using semaphores         
    sem2.pll                                                               
    shm1.pll          Provide an example of using shared memory, and of    
    shm2.pll          using semaphores to control access to shared memory  
    shmarr1.pll       A shm example using a large shm segment and that     
    shmarr2.pll       fixes some of the problems suffered by shm1 and      
                      shm2                                                 
    
    

    Table 4.3.

    Example IPC Programs.

    sem1.pll and sem2.pll

    These two programs are designed to demonstrate how to use semaphores as a simple form of process synchronisation. The program sem1 must always be executed first, it creates the semaphore (used by both programs), displays a message and then waits for the program sem2 to signal on the semaphore. sem1 will do nothing more until sem2 is executed.

    sem2 connects to the semaphore using a SEM_OPEN operation, does a SEM_SIGNAL on the semaphore and then closes.

    PROGRAM Sem1 (INPUT, OUTPUT);
    {-----------------------------------------------------------}
    { PURPOSE: Simple example use of semaphores                 }
    { AUTHOR:  David Jones                                      }
    { DATE:    15-JAN-95                                        }
    {-----------------------------------------------------------}
    
    VAR
      sem, dummy: INTEGER;
    
    BEGIN {main}
      sem := SYSTEM(SEM_CREATE, 'my_sem', 0);
      Writeln('I am waiting for other process to signal.... ' );
      dummy := SYSTEM(SEM_WAIT, sem)
      Writeln('The other program did a signal on my semaphore. ' );
      sem := SYSTEM( SEM_CLOSE, sem );
    END.
    
    PROGRAM Sem2 (INPUT, OUTPUT);
    {-----------------------------------------------------------}
    { PURPOSE: Simple example use of semaphores, number 2       }
    { AUTHOR:  David Jones                                      }
    { DATE:    15-JAN-95                                        }
    {-----------------------------------------------------------}
    
    VAR
      mysem, dummy: INTEGER;
    
    BEGIN {main}
      mysem := SYSTEM(SEM_OPEN, 'my_sem');
      Writeln('I am the second process.' );
      dummy := SYSTEM(SEM_SIGNAL, mysem);
      Writeln('I just did a signal on the semaphore.' );
      mysem := SYSTEM( SEM_CLOSE, mysem );
    END.
    

    Exercises

    4.1. Compile and execute the above two programs. Observe what happens when they block, both on the Devices display and on the CPU scheduler display.

    shm1.pll and shm2.pll

    shm1 and shm2 demonstrate the use of shared memory. The first program shm1 reads a number from the user and then writes it into the shared memory segment. Once the value has been written the second program shm2 reads the number from the shared memory segment, squares it and writes the result back to the shared memory segment. After the result has been written shm1 (the first program) then reads the result and displays it.

    These two programs provide an example of using shared memory to communicate between two different processes.

    All the waiting done by shm1 and shm2 is not achieved by magic. Instead semaphores are used to synchronise access to the shared memory segments. In some instances semaphores may be used to implement mutual exclusion on the shared memory (to make sure only one process can be modifying it at any one time).

    PROGRAM Shm1 (INPUT, OUTPUT);
    {-----------------------------------------------------------}
    { PURPOSE: Simple Shared memory example                     }
    {          Binary semaphore used to synchronise access      }
    {                                                           }
    { AUTHOR:  David Jones                                      }
    { DATE:    15-JAN-95                                        }
    {-----------------------------------------------------------}
    
    VAR
      sem, shm, value, result, temp : INTEGER;
    
    BEGIN
      { create semaphore and make sure it is set to 0 }
      sem := SYSTEM(SEM_CREATE, 'mysem', 0);
      { create a shm segment that will hold 1 variable }
      shm := SYSTEM(SHR_CREATE, 'myshm', 1);
    
      IF (sem > 0) AND (shm > 0) THEN   { create succeeds }
      BEGIN
        Write('Enter a number ==> ' );
        Read( value# );
        Writeln( 'Value was read' );
    
        temp := SYSTEM(SHR_WRITE, shm, 0, value);
        { Signal other process that it now has a value to work with }
        temp := SYSTEM( SEM_SIGNAL, sem );    
        
        { Wait for other process to read value }
        { from shared memory and square it }
        Write('Waiting.. ');
        temp := SYSTEM(SEM_WAIT, sem);
        WriteLn;  WriteLn;
    
        result := SYSTEM(SHR_READ, shm, 0);
        WriteLn( value#, ' squared is ', result# );
        
        temp := SYSTEM(SEM_CLOSE, sem);
        temp := SYSTEM(SHR_CLOSE, shm)
      END;
    END.
    
    PROGRAM shm2 (INPUT, OUTPUT);
    {-----------------------------------------------------------}
    { PURPOSE: Simple Shared memory example                     }
    {          Binary semaphore used to synchronise access      }
    {                                                           }
    { AUTHOR:  David Jones                                      }
    { DATE:    15-JAN-95                                        }
    {-----------------------------------------------------------}
    
    VAR
      mysem, myshm, value, result, temp : INTEGER;
    
    BEGIN
      mysem := SYSTEM(SEM_OPEN, 'mysem');
    
      IF mysem > 0  THEN
      BEGIN
        { wait for other process to signal it's }
        { placed a value in the shared memory }
        Writeln( 'I am now waiting for a signal from other process ' );
        temp := SYSTEM( SEM_WAIT, mysem );
    
        myshm := SYSTEM(SHR_OPEN, 'myshm' );
    
        IF myshm > 0 THEN
        BEGIN
          { Get Value from shared memory }
          value := SYSTEM(SHR_READ, myshm, 0 );
         
          Write( 'I read ', value# );
          WriteLn( ' from shared memory, squaring it..' );
          result := value * value;
    
          temp := SYSTEM(SHR_WRITE, myshm, 0, result );
        
          { tell other process I've squared the value }
          temp := SYSTEM(SEM_SIGNAL, mysem );
    
          temp := SYSTEM(SEM_CLOSE, mysem);
          temp := SYSTEM(SHR_CLOSE, myshm)
        END
      END
    END.
    

    Exercise

    4.2. Compile and execute the two shared memory programs above. Observe their behaviour.

    4.3. The synchronisation of the two shm programs is not implemented correctly. Start shm1, enter a number and press return (don't start shm2). What happens? Why?

    shmarr1.pll and shmarr2.pll

    The last two programs serve two purposes

  • provide a demonstration of using a shared memory segment that contains more than one variable, and
  • provide a solution to the problem suffered by shm1 and shm2.

    The problem above is caused by the fact that the programs use one semaphore to block on two different events

    shm1 has to block waiting for shm2 to calculate the result, and

    shm2 has to block waiting for shm1 to place the number into the shared memory segment.

    One solution to this problem is to use a different semaphore for each event. That is shm1 will block on one semaphore while shm2 will block on another one.

    These programs perform the same tasks as shm1 and shm2 but instead of squaring a single number they square ten numbers.

    PROGRAM Shm1 (INPUT, OUTPUT);
    
    {-----------------------------------------------------------}
    { PURPOSE: Shared memory example using shm segment array    }
    {          Binary semaphore used to synchronise access      }
    { AUTHOR:  David Jones                                      }
    { DATE:    17-JAN-95                                        }
    {-----------------------------------------------------------}
    
    VAR
      count, semA, semB, shm, value, result, temp : INTEGER;
    
    BEGIN
      { create two semaphores and set both to 0 }
      { semA - is used to block Shm1 waiting for Shm2 }
      { semB - is used to block Shm2 waiting for Shm1 }
      semA := SYSTEM(SEM_CREATE, 'sem1', 0);
      semB := SYSTEM(SEM_CREATE, 'sem2', 0);
      { create shared memory segment for 10 values }
      shm := SYSTEM(SHR_CREATE, 'myshm', 10);
    
      IF (semA > 0) AND (shm > 0) AND (semB > 0 ) THEN
      BEGIN
        
        FOR count := 1 TO 10 DO
        BEGIN
          Write( count#, ', ' );
          temp := SYSTEM(SHR_WRITE, shm, count-1, result )
        END;
        Writeln;  Writeln;
    
        { Signal other process that it now has a value to work with }
        temp := SYSTEM( SEM_SIGNAL, semB );    
        
        { Wait for other process to read value }
        { from shared memory and square it }
        Write('Waiting.. ');
        temp := SYSTEM(SEM_WAIT, semA);
        WriteLn;  WriteLn;
    
        FOR count := 1 TO 10 DO
        BEGIN
          result := SYSTEM(SHR_READ, shm, count-1);
          WriteLn( count#, ' squared is ', result# )
        END;    
        
        { close all the IPC primitives }
        temp := SYSTEM(SEM_CLOSE, semA);
        temp := SYSTEM(SEM_CLOSE, semB);
        temp := SYSTEM(SHR_CLOSE, shm)
      END;
    END.
    
    PROGRAM shm2 (INPUT, OUTPUT);
    
    {-----------------------------------------------------------}
    { PURPOSE: Simple Shared memory example                     }
    {          Binary semaphore used to synchronise access      }
    { AUTHOR:  David Jones                                      }
    { DATE:    18-JAN-94                                        }
    {-----------------------------------------------------------}
    
    VAR
      count, mysemA, mysemB, myshm, value, result, temp : INTEGER;
    
    BEGIN
      mysemA := SYSTEM(SEM_OPEN, 'sem1');
    
      IF mysemA > 0  THEN	{ if TRUE then sem has been created }
      BEGIN
        mysemB := SYSTEM(SEM_OPEN, 'sem2');
        Writeln( 'I am now waiting for a signal from other process ' );
        { wait for other process to signal it's }
        { placed a value in the shared memory }
        temp := SYSTEM( SEM_WAIT, mysemB );
        myshm := SYSTEM(SHR_OPEN, 'myshm' );
    
        IF myshm > 0 THEN
        BEGIN
          FOR count := 1 TO 10 DO
          BEGIN
            { Get Value from shared memory }
            value := SYSTEM(SHR_READ, myshm, count-1 );
         
            Write( 'I read ', value# );
            WriteLn( ' from shared memory, squaring it..' );
            result := value * value;
    
            { write result back to shared memory }
            temp := SYSTEM(SHR_WRITE, myshm, count-1, result )
          END;    
          
          { tell other process I've squared the value }
          temp := SYSTEM(SEM_SIGNAL, mysemA );
    
          temp := SYSTEM(SEM_CLOSE, mysemA);
          temp := SYSTEM(SEM_CLOSE, mysemB);
          temp := SYSTEM(SHR_CLOSE, myshm)
        END
        ELSE
        BEGIN
          Writeln('ERROR: You must execute shmarr1 before this program.')
        END
      END
    END.
    

    Another way to Initialise

    The above example program pairs all have one program that creates the IPC primitive and another that connects to it. If the program that connects is executed before the one that creates an error results. So the program that creates must always be executed first.

    It is possible to incorporate both actions into one program by using the return value of the create function. The program SEMTX9.PLL included with RCOS provides one demonstration of this. The basic method is summarised below.

    result := SYSTEM( SEM_CREATE, "mySEM" );
    IF result > 0 THEN  { CREATE successful }
    THEN
    BEGIN
      { do create program actions }
    END
    ELSE
    BEGIN
      { create not successful so try to open it }
      result := SYSTEM( SEM_OPEN, "mySEM" );
      { do open program actions }
    END;
    
    Exercise 4.4. Rewrite the sem1 and sem2 example programs using the above mechanism.

    Conclusions

    Most modern operating systems provide some form of inter-process communication primitives. A typical implementation usually includes semaphore and shared memory primitives. RCOS provides both of these.

    Programs make use of IPC primitives by using language level facilities that allow the program to access the operating system's system calls. The facility of RCOS's PLL/2 language that provide access to RCOS's system calls is the function SYSTEM.

    RCOS provides both binary and counting semaphores. The semaphore operations that RCOS supports are SEM_OPEN, SEM_CREATE, SEM_WAIT, SEM_SIGNAL and SEM_CLOSE.

    Shared memory under RCOS allows processes to share a single variable (PLL/2's integer type) using the operations SHR_OPEN, SHR_CREATE, SHR_READ, SHR_WRITE and SHR_CLOSE.