4.9. Semaphore Class

Semaphore class provides a simpler and easier interface to System V semaphore system calls.

4.9.1. DEFINITION


class Semaphore
{
public:
    Semaphore();
    ~Semaphore();

    int   create (key_t key_, int initval_ = 1);
    int   open (key_t key_);
    void  close ();
    void  remove ();

    void  wait ();
    void  signal ();

    void  op (int val_);
    key_t key () const;
    int   id () const;

    void  dump (void) const;
};
	  

4.9.2. USAGE

Semaphores are used to syncronize cooperative access of a shared resource.

A typical situation I often run into while writing test programms is when the main parent process forks a child process to simulate some network activity. Simulation activity is triggered by the parent process sending a signal to the child process.

Depending on the system load and other external factors, it can take a variable amount of time for the child process to initialize its internal structures and install an appropriate signal handler. Semaphore class can be used to synchronize the parent and child processes by letting them complete their initialization sections.

We create and use a 3-member set for the requested semaphore. The first member, [0], is the actual semaphore value, and the second member, [1], is a counter used to know when all processes have finished with the semaphore.

The counter is initialized to a large number, decremented on every Semaphore::create() or Semaphore::open(), and incremented on every Semaphore::close(). This way we can use "adjust" feature provided by System V so that any process that exits without calling Semaphore::close() is accounted for. It doesn't help us if the last process does this (as we have no way of getting control to remove the semaphore) but it will work if any process other than the last does an exit (intentional or unintentional).

The third member, [2], of the semaphore set is used as a lock variable to avoid any race conditions in the Semaphore::create() and Semaphore::close() functions.

Note

This class has been inspired by the discussion on UNIX semaphores in [Stevens92].

First, we define a test class to have Semaphore as its data member:

Example 4-2. Semaphore: Test class definition


#include "assa/GenServer.h"
#include "assa/Singleton.h"
#include "assa/Assert.h"
#include "assa/Semaphore.h"

class Test : public GenServer, public Singleton<Test>
{
public:
    // public interface
    Test ();
    ~Test ();

    void initServer ();
    void processServer ();

    key_t get_sem_key (void) const { return m_sem_key; }

private:
    void set_sem ();

private:
    Semaphore m_sem;      // Semaphore
    key_t     m_sem_key;  // Semaphore's key
};
		

The constructor initializes semaphore key that we would need to identify our semaphore from both parent and child process. C library function ftok() helps us to generate a key from file path name and a letter. We create semaphore with create().

Example 4-3. Semaphore: Test's member functions


Test::Test ()
{
    m_sem_key = ftok ("/etc/services", 'T');
}

void Test::set_sem ()
{
    Assert_exit (m_sem.create (get_sem_key (), 0) != -1);
}
		

Our semaphore is ready to be used. After we fork and exec our child process, parent waits on a semaphore to continue its control flow:

Example 4-4. Semaphore: Waiting on a semaphore


void Test::processServer ()
{
    set_sem ();
   
    Fork mychild (Fork::KILL_ON_EXIT);

    // Close debug log file here
    close_log ();

    if (mychild.isChild ()) {
        ret = execlp ("TestChild", ...);
        exit (1);
    }

    // Reopen debug log file here
    open_log ();

    // Wait on a semaphore
    m_sem.wait ();

    // Continue interaction with the child process ...

}
		

Child program attaches to the semaphore and raises it when it is through with its initialization phase:

Example 4-5. Semaphore: Child side


#include "assa/GenServer.h"
#include "assa/Singleton.h"
#include "assa/Assert.h"
#include "assa/Semaphore.h"

class TestChild 
    : public GenServer, public Singleton<TestChild>
{
public:
    // public interface
    TestChild ();
    ~TestChild ();

    void initServer ();
    void processServer ();
};

TestChild::TestChild ()
{
    close_log ();
    unlink (logfname);
    open_log (logfname + ".child");
}

TestChild::initServer ()
{
    Semaphore sem;
    
    key_k key = ftok ("/etc/services", 'T');
    Assert_exit (sem.create (get_sem_key (), 0) != -1);

    // Do all necessary initalization steps

    sem.signal ();
}

TestChild::processServer ()
{
    // TestChild is ready to communicate with the server
}
		

We, of course, could have passed semaphore's key to the child process with its command-line argument.