Developing the first version of a queue encapsulation hierarchy

From EDM2
Jump to: navigation, search

Written by Gordon Zeglinski

Introduction

In this issue, we will get back to encapsulating bits and pieces of the OS/2 API. Instead of just dumping out the final version, I will take an iterative approach. In this issue, the first version of a queue encapsulation hierarchy, will be developed. In subsequent issues, a refined version will be presented. The focus here, will be on the API calls, and encapsulation concepts.

Loose Ends

In the last issue, the makefile distributed to build the Watcom version of POV 2.1 was actually a NMAKE file not a WMAKE file.

I recently added 4 more megs of RAM to the system here. This has greatly improved the stability of the WorkFrame. I suspect its instability was somehow related to swapping. Hopefully the OS/2 2.1 CSD will be out soon, and help improve its stability even more.

Queue Basics

What are Queues?

A queue (as implemented by the OS/2 kernel) is a linked list of elements allowing inter-process communication. Each record in the list contains a pointer to the data, the length of the data, and a ULONG parameter. The queue can be a FIFO (first-in, first-out) or LIFO (last-in, first out) and can have up to 16 priority levels. The queue is a one way communication method from the client to the server.

Visualization of a queue element

You should have noted that queues use pointers to the application defined data. This means that when the client and server are in different processes, shared memory must be used. Using shared memory efficiently will be a refinement examined in the next revision of this class.

The Queue API

OS/2 provides the following functions to manipulate queues. Our goal is to encapsulate these functions for both the client and server processes.

  • DosCloseQueue()
  • DosCreateQueue()
  • DosOpenQueue()
  • DosPeekQueue()
  • DosPurgeQueue()
  • DosQueryQueue()
  • DosReadQueue()
  • DosWriteQueue()

We will examine some of these functions in this column. The reader is referred to the Control Program Reference and Guide for an explanation of those functions not covered and for more details on those covered. As with all kernel functions, a return code of 0 indicates that no error occurred.

DosCreateQueue()

This function is used by the server process to create the queue. Its syntax follows:

APIRET DosCreateQueue(PHQUEUE phqHandle,
                      ULONG ulFlags,
                      PSZ pszName);

phqHandle
The variable pointed to by this parameter will contain the queue handle on return.
ulFlags
Used to set the queue type, one of the QUE_FIFO, QUE_LIFO, or QUE_PRIORITY flags can be or'd with one of QUE_NOCONVERT_ADDRESS or QUE_CONVERT_ADDRESS.
pszName
The name of the queue. It must have the prefix "\QUEUES\" .

DosOpenQueue()

This function is used by the client process to obtain access to the queue. Its syntax follows:

APIRET DosOpenQueue(PPID ppidOwner,
                    PHQUEUE phqHandle,
                    PSZ pszName);

The parameters of this function are the same as for the DosCreateQueue() function, except for the ppidOwner parameter. This parameter is used to return the process ID of the queue's creator to the process calling this function, which can later be used to grant access to the shared memory object.

DosWriteQueue()

This function is used by the client to place data into the queue. Its syntax follows:

APIRET DosWriteQueue(HQUEUE hqHandle,
                     ULONG ulParam,
                     ULONG ulSzBuf,
                     PVOID pvBuffer,
                     ULONG ulPriority);

hqHandle
The handle of the queue to be written to.
ulParam
The parameter in the queue record discussed previously.
ulSzBuf
Lenght of the data buffer.
pvBuffer
Pointer to the data buffer.
ulPriority
If the queue was created as a priority based queue, then this contains the elements priority. Otherwise, it is ignored.

DosReadQueue()

This function is used by the server to read data from the queue. Its syntax follows:

APIRET DosReadQueue(HQUEUE hqHandle,
                    PREQUESTDATA prdRequest,
                    PULONG pulSzData,
                    PPVOID ppvData,
                    ULONG ulCode,
                    BOOL32 bNoWait,
                    PBYTE pbPriority,
                    HEV hevSemaphore);

hqHandle
The handle of the queue to be read from.
prdRequest
Pointer to a REQUESTDATA structure.
pulSzData
Pointer to the variable which will receive the length of the data item.
ppvData
Pointer to the variable which will receive the address of the data item.
ulCode
A value of 0 is used to read the first element from the queue. A value returned by DosPeekQueue() can be used to read the element previously peeked at.
bNoWait
If 0, the thread will block until a data element is in the queue if one is not already present. If 1, the thread returns immediately.
pbPriority
Pointer to the variable which will hold the elements priority.
hevSemaphore
Handle of an event semaphore which will be posted when a element is placed into the queue and the bNoWait is 1.

The REQUESTDATA structure is defined as follows:

typedef struct _REQUESTDATA
{ 
   PID pid;       // PID of the process which placed the element in the queue
   ULONG ulData;  // The Param parameter of DosWriteQueue
} REQUESTDATA;

Note: the first time this function is called with a bNoWait of 1, the handle of the event semaphore is stored by the system; this handle must be used in subsequent reads from the queue. The event semaphore must also be created using the DC_SEM_SHARED flag.

Encapsulating the Queue API

We start the encapsulation process by listing the properties of queues that we wish to encapsulate:

  • Reading of queue elements
  • Writing of queue elements
  • Creating the queue
  • Opening the queue
  • Manipulating the shared memory

We also note that the queue has two separate sides, a client and a server. These two sides have several properties in common which will lead us to this first approximation to the queue hierarchy:

Queue Hierarchy

We will now look at the design of these three classes.

QueueObj

As the base class for this hierarchy, QueueObj provides the data members that are common to both the client and the server, as well as various support functions. These data members include the names of the queue and shared memory object, the handle of the queue, the address of the shared memory, and the size of the shared memory. For the first pass, the shared memory will be maintained by member functions of the hierarchy.

The class definition follows:

class QueueObj
{
   public:
   enum _Action { None, Create, Open };

   _exp_ QueueObj(char *ShMemNm,
                  unsigned long MemSize,
                  char *QueueNm,
                  unsigned long flag=QUE_PRIORITY,
                  _Action Mem=None,
                  _Action Queue=None);
   virtual _exp_ ~QueueObj();

   unsigned long GetError() { return Error; }

   void _exp_ SetMemName(char *N);
   void _exp_ OpenMem(unsigned long MemSize);
   void _exp_ CreateMem(unsigned long MemSize);

   void _exp_ SetQueueName(char *N);
   void _exp_ OpenQueue();
   void _exp_ CreateQueue(unsigned long flag=QUE_PRIORITY);

   PID GetOwner() { return pidQueueOwner; }

   protected:
   typedef unsigned long HQUEUE;

   HQUEUE hqueue;

   char *ShareMemName;
   char *QueueName;
   char *MemBase;
   unsigned long ShMemSize;

   PID pidQueueOwner;

   unsigned long Error;
};

This class provides support methods for both the client and server classes to use. These methods allow both the shared memory and the queue to be named, created, or opened. ClientQueueObj

class ClientQueueObj : public QueueObj
{
   public:
   _exp_ ClientQueueObj(char *ShMemNm,
                        unsigned long MemSize,
                        char *QueueNm,
                        unsigned long flag=QUE_PRIORITY,
                        int Reserv=0,
                        _Action Mem=Open,
                        _Action Queue=Open);

   int _exp_ Write(ULONG Req,ULONG Priority=15);
   int _exp_ Write(ULONG Req,char *Buff,ULONG BuffSize,ULONG Priority=15);

   void * _exp_ RequestMemBlock(unsigned long BlSize);
   void SetReserveLen(int Len) { ReservedLen=Len; }

   protected:
   char *CurrLoc;
   int CurrLenght;
   int ReservedLen;
};

By default, the constructor for the client object opens the queue and shared memory. This object is responsible for maintaining the position of the free space in the shared memory object. The variable ReservedLen is used to reserve a section of shared memory from the start. The member function Write() uses the variables CurrLoc and CurrLength as parameters in the call to DosWriteQueue().

To write to the queue, the RequestMemBlock() member function is used to request a block of shared memory. This block is then initialized by the application, and either version of the member function Write() is called. The second form of the member function requires that the location and length of the shared memory block be specific as parameters while the first assumes the last request block of shared memory is the data buffer source.

ServerQueueObj

class ServerQueueObj : public QueueObj
{
   public:
   _exp_ ServerQueueObj(char *ShMemNm,
                        unsigned long MemSize,
                        char *QueueNm,
                        unsigned long flag=QUE_PRIORITY,
                        EventSemaphore *RS=NULL,
                        _Action Mem=Create,
                        _Action Queue=Create);

   PID GetPID() { return ReqDat.pid; }
   ULONG UserData() { return ReqDat.ulData; }
   void _exp_ PostSem();
   void _exp_ ResetSem();
   void _exp_ WaitOnSem();

   void* _exp_ Read(ULONG ElCode=0, BOOL wait=0);
   void* _exp_ Peek(ULONG ElCode=0);

   ULONG GetDataLen() { return DataLen; }
   BYTE GetPriority() { return Priority; }

   protected:
   EventSemaphore *ReadSem;
   REQUESTDATA ReqDat;
   ULONG DataLen;
   void *Entry;
   BYTE Priority;
};

The constructor for the server object creates both the shared memory and queue. It contains data members and member functions to manipulate the various parameters to the DosReadQueue() function, mentioned earlier. In addition to this, it also has a pointer to an event semaphore object. As this issue focuses on queues, I will not go into details on the semaphore objects.

This object is a bit easier to use. After creating an instance of it, the read function is called to remove an element from the queue if one is present.

Using the Objects

We will now test the queue server and client objects by creating two simple applications. The client application will prompt the user to numerical input. It will place this numerical input into the queue, at which point the server will echo the input to the display. Below is the source code to the queue server with comments added.

/*  These definitions are contained in QDefs.h

#define QName         "DemoQueue"   //Name of the queue
#define SMemName      "DemoMem"     //Name of the shared memory

#define NemSize       1024*8        //size of the shared memory block
                                    //in bytes

struct QueueMessage{                //format of the queue data packet
   int Number;

};
*/

int main(){

   int loop;
   QueueMessage *Mesg;        // pointer to the data packet

   //create a shared event semphore
   EventSemaphore Sem((char*)NULL,Semaphore::SemCreate,DC_SEM_SHARED);

   //create the queue object.
   //This also creates the shared memory and the actual queue
   ServerQueueObj InQueue(SMemName,NemSize,QName,0,&Sem);


   loop=1;

   //reset the event semaphore
   Sem.Reset();

   cout<<"Queue Server Active !"<<endl;

   while(loop){

      //attempt to read an element from the queue
      Mesg=(QueueMessage *) InQueue.Read(0,1);

      //read from queue until an element occurs
      while(InQueue.GetError() ){

         //this semaphore will be posted as soon as an element is put in the queue

         Sem.Wait();
         Mesg=(QueueMessage *) InQueue.Read(0,1);
      } 

      //terminate is the number was -1, else echo the number
      if(Mesg->Number == -1){
         loop=0;
         cout<<"Terminating"<<endl;
      }
      else{
         cout<<"Number= "<<Mesg->Number<<endl;
      }
   }

return 0;
}

The queue encapsulation has made using the queues much less tedious. The code for the client is very similar. Therefore, it is left as an exercise for the curious to discover how it works.

There are limitations with the current queue implementation. They are:

  1. Only 1 client may use the shared memory pool at a time.
  2. Only 1 thread in the client process may use the shared memory pool at a time.

The following files are included with this issue:

MAKE.CMD Rexx .CMD file to make the server and client. Assumes the compiler is IBM C-Set++.
QCLIENT.CPP Main routine for the client.
QSERVE.CPP Main routine for the server.
QUEUEOBJ.CPP Queue object member function definitions.
QUEUEOBJ.H QueueObj class definition.
QDEFS.H Header used in the client a server source files.
SEMTIMOBJ.CPP Semaphore member functions.
SEMTIMOBJ.H Semaphore object definitions.
QSERVE.EXE Server executable.
QCLIENT.EXE Client executable.

Although this code is compiled using C-Set++, other compilers should be able to compile the source files.

Note: QSERVE.EXE must be run before QCLIENT.EXE.

Summary

This concludes our first attempt at encapsulating the queue objects. We have developed queue objects which encapsulate the creation and manipulation of queues and the shared memory they require. In a future issue, the next version of the queue objects will be presented. The goal for the next version is to eliminate the constraints that exist in this version.