[Next] [Previous ] Chapter 5
[ Contents ]  [ Chapter 4: File I/O and Extended ] [ Chapter 6: DLLs ]

Interprocess communication

OS/2 provides several different methods of interprocess communication that are all fairly easy to implement. In OS/2 1.x there were five distinct ways available for a process to communicate with another process. These communications methods used flags, semaphores, pipes, queues, and shared memory to send and receive messages and signals. Four of the most common methods were retained in 0S/2 2.0; the one that was dropped was the DosFlagProcess API. The functionality provided by DosFlagProcess  is now provided by DosRaiseException  and related APIs.
The easiest interprocess communication (IPC) method to implement is unnamed and named pipes. An unnamed pipe is a circular memory buffer that can be used to communicate between related processes. The parent process must set the inheritance flags to true in order for the child process to inherit the handles and allow the parent and the child processes to communicate. Communication is bidirectional, and the pipe remains open until both the read handle and the write handle are closed. Named pipes are also an easy way to provide remote communication. A process on the requester workstation can communicate with a process running on the server workstation as well as with a process running locally. However, the client-server remote connectivity can be achieved only with the help of some type of local area network server.

An OS/2 Named Pipe Client-Server Example

SERVER.C is, as the name suggests, the server of the Named Pipe IPC mechanism. The program allows  remote  and local communications and performs simple character redirection. The characters are highlighted in different colors to distinguish server and client modes of operation. As the user types in characters at the client, they immediately echo on the server. There is no implied limitation that the server can receive only, and the client can send only. The particular implementation is specific to this example.
The SERVER.EXE application can be started by simply typing Server followed by a carriage return from the command line. This will start the server component of the program pair. The Server must be started first, since it is the Server that creates the named pipe and allows the Client to connect to it. After the server starts successfully, the Client can be starred by typing Client [ServerName]  followed by a carriage return from the command line. Note that the [ServerName] is an optional parameter and is used only if a  remote pipe connection is being attempted. If the Server and the Client are running in the same workstation, and the workstation is capable of running the IBM OS/2 LAN Server software, the Client-Server communication can be achieved with both  local and remote connections. However, if the IBM OS/2 LAN Server is not active, or the user is not logged on to the IBM OS/2 LAN Server domain, attempting a remote connection will produce an error stating that the pipe name was not found. This is correct, and usually points to an inactive server or an unauthorised user. The best way to look at this example is to open two OS/2 window sessions and to .allow one session to run the SERVER.EXE and the other to run the CLIENT.EXE. This way it will be easier to see the Client-Server communication.

SERVER.C
SERVER.H
SERVER.DEF

First, a DosExitList call is made in order to allow the SERVER.EXE to clean up properly in an event of a Ctrl-C  / Ctrl-Brk  condition.

APIRET DosExitList(ULONG ulOrderCode, PFNEXITLIST pfn)
ulOrderCode consists of two lower-order bytes that have meaning and a high-order word must be 0. The lower-order byte can have the values lined in Table 5.1.
 
Table 5.1 Values for Lower-Order Byte of ulOrderCode
Value Description
EXLST_ADD Add an address to the termination list
EXLST_REMOVE Remove an address from the termination list
EXLST_EXIT When termination processing completes, transfer to the next address on the termination list

The high-order byte of the low-order word must be zero if EXLST_REMOVE, or EXLST_EXIT is specified. If, however, EXLST_ADD is specified, the high-order byte will indicate the invocation order.
The second parameter for DosExitList  is an address of the routine to be executed - pfn.
The CleanUp()  routine closes the named pipe handle and resets the window text color back to white black.
Next, ConnToClient()  must issue two calls: DosCreateNPipe() and DosConnectNPipe(). Issuing DosConnectNPipe call is what allows the client to perform a DosOpen()  successfully. After the first few necessary setup APIs are called, a simple handshake operation is performed by reading a known string from the pipe and writing a known string back.

/* Creates a named pipe. */

PSZ      pszName; /* The ASCIIZ name of the pipe to be opened. */
PHPIPE   pHpipe;  /* A pointer to the variable in which the system returns the handle of the pipe that is created. */
ULONG    openmode; /* A set of flags defining the mode in which to open the pipe. */
ULONG    pipemode; /* A set of flags defining the mode of the pipe. */
ULONG    cbOutbuf; /* The number of bytes to allocate for the outbound (server to client) buffer. */
ULONG    cbInbuf;  /* The number of bytes to allocate for the inbound (client to server) buffer. */
ULONG    msec;     /* The maximum time, in milliseconds, to wait for a named-pipe instance to become available. */
APIRET   ulrc;     /* Return Code. */

ulrc = DosCreateNPipe(pszName, pHpipe, openmode,
         pipemode, cbOutbuf, cbInbuf, msec);

The DosCreateNPipe()  API expects seven arguments. The first parameter, DEFAULT_PIPE_NAME, is a ASCII string that contains the name of the pipe to be created, pszName. The second is a pointer to the pipe handle that will be returned when the function returns. The next parameter is the open mode used for the pipe. The flag used in the example is NP_ACCESS_DUPLEX, which provides inbound and outbound communication. The fourth parameter is the pipe mode. This parameter is a set of bitfields that define the pipe mode. The flags used in this example are NP_WMESG | NP_RMESG | 0x01. These flags indicate the pipe can send and receive messages, and also that only one instance of the pipe can be created. The pipe can be created in either byte or message mode only. If  a byte mode pipe is created, then DosRead()  and DosWrite() must use byte stream mode when reading from or writing to the pipe. If a message mode pipe is created, then DosRead() and DosWrite() automatically  will use the first two bytes of each message, called the header, to determine the size of the message. Message mode pipes can be read from and written to using byte or message streams. Byte mode pipes, on the other hand, can be used only in byte stream mode. If a message stream is used, the operating system will encode the message header without the user having to calculate the value. Care should be taken when deciding what size buffers should be used during communications. The transaction buffer should be two bytes greater than the largest expected message

APIRET DosConnectNPipe(HPIPE hpipe);

The DosConnectNPipe() only takes one argument, the named pipe handle. At this point, the pipe is ready for a client connection

CLIENT.C
CLIENT.DEF
COMMON.H
CLNTSVR.MAK

When the Client is started, the initialization call is made to ConnToServer(). The client application must perform a DosOpen() first in order to obtain a pipe handle. Once the pipe handle is obtained, the application can freely read from the Pipe and write to the pipe. In this case, the this case write/read pair is used for primitive handshaking communication.
The most interesting set of parameters for the DosOpen() call on the client side is the ulOpenFlag, which contains the value OPEN_ACTION_OPEN_IF_EXISTS, and the ulOpenMode, which contains the
OPEN_FLAGS_WRITE_THROUGH | OPEN_FLAGS__FAIL_ON_ERROR | OPEN_FLAGS_RANDOM | OPEN_SHARE_DENYNONE | OPEN_ACCES_READWRITE
value.

Next, the while loop is entered. It can be stopped only if an API error is encountered, or if the user presses the F3 function key at the Client window. The buffer that is being transmitted from the Client to the Server represents the character received from the keyboard buffer used by the Client application. A double word is used to allow proper character translation for the F1-F12 function keys and some other extended keyboard keys. (The function key keystroke generates two characters; the first is always a 0x00 followed by the 0xYY. where YY is a unique function key identifier.)

The remote pipe connection from the Client to the Server is achieved by starting the CLIENT.EXE with the following command-line syntax:

CLIENT  [MYSERVER]
where MYSERVER is the remote Server machine name. (The NetBIOS machine name for IBM OS/2 LAN Server is found in the IBMLAN.INI file). The pipe names that are created by the Client have the following format:
local named pipe name:    \PIPE\MYPIPE
remote named pipe name:    \\MYSRVR\PIPE\MYPIPE

The functionality that this example application provides is the same in both remote  and local connectivity modes.  As a matter of fact,neither the Client nor the Server differentiates between the remote and local case; only the pipe name is significant. This is the subtle beauty of the named pipes IPC!

The main reason for choosing pipes as an IPC method is ease of implementation, but it is not the best choice  for all cases. Pipes are useful only when a process has to send a lot of information to or receive information from another process. Even though it is possible to allow pipe connections with multiple processes, connect  and disconnect algorithms must always be implemented for such situations. The remote connection advantage of named pipes sometimes outweighs the complexity of connect- disconnect algorithms. Since it is not possible under 0S/2 to communicate remotely with queues or remote shared memory,  pipes sometimes become not only the best bus the only IPC choice.
Gotcha!

Is is not unusual for an application to receive a return value of ERROR_TOO_MANY_HANDLES when attempting to open additional pipes. The system initially allows 20 file handles per process; once the limit is reached, the above error will appear. To prevent this from happening, the DosSetMaxFH(ULONG ulNumberHandlers)  call must be issued, where ulNumberHandlers is the new maximum number of handles allowed to be open. This call will be successful if system resources have not been exhausted. It is a good idea to issue this call only when needed, since additional file handles consume system resources that may be used elsewhere in the system.
 

DOS-OS/2 Client-Server Connection

To make the pipe connectivity example complete, a DOS-named pipe client must be discussed. The DOS based, D_CLIENT.EXE. is only slightly different from its big brother, the OS/2 based CLIENT.EXE. There are no logical differences between the two; the difference lies in the APIs. The DosOpen()/DosRead()/DosWrite() OS/2 calls are replaced with open()/read()/write()  DOS calls.

D_CLIENT.C
D_CLIENT.MAK
D_CLIENT.DEF
DCOMMON.H
 

An OS/2 QUEUE Client-Server Example

The next example pair is QSERVER.C and QCLIENT.C. In this example. the communication process is a little bit more complex than the one in the named pipe illustration. Here the point is to show how several different processes can communicate with one central process. The functionality is similar to the named pipe example, but with one key difference: The queue Server process does not send anything to the queue Client processes. In fact, only the queue Client process can send information to the queue Server. However, this does not mean that the queue Server cannot issue a DosWriteQueue()  call to itself;  it is just not part of this example. It is left to the reader to implement this additional functionality. By using the QSERVER.C as a prototype template, the WriteToQue   function call can enhance the QSERVER.C example program to issue DosWriteQueue  calls. The QSERVER.C-QCLIENT.C example makes use of both the OS/2 queue APIs and named shared memory segments.

The concept of an OS/2 queue is somewhat simple. It is, in fact, an ordered set of elements. The elements are 32-bit values that are passed from the Client to the Server of the queue. The Server of the queue is the process that created the queue by issuing the DosCreateQueue() API call.

APIRET  DosCreateQueue(PHQUEUE pha, ULONG ulPriority, PSZ pszName )
phq is a pointer to the queue handle of the queue that is being created. ulPriority is a set of two flags OR'ed together. The first flag can have the values listed in Table 5.2. The second flag can have the values listed in Table 5.3.
Table 5.2 Values of Low Byte of ulPriority.
Value  Description
QUE_FIFO  FIFO  queue
QUE_LIFO  LIFO  queue
 QUE_PRIORITY  Priority queue
Table 5.3.  Values of High Byte of ulPriority.
Value Description
QUE_NOCONERT_ADDRESS  Does not convert addresses  of 16-bit elements that are placed in the queue
QUE_CONVERT_ADDRESS  Convert 'addresses of 16-bit elements to 32-bit elements

The last parameter is a pointer to the ASCII name of the queue.

Only the Server of the queue can read from the queue. When the queue is read, one element is removed from it. The Server and the Client can both issue calls to write, query, and close the queue. However only the Server can issue calls to create, read, peek, and purge the queue. The Client must issue a DosOpenQueue  call prior to attempting to write elements to the queue or to query the queue elements.

APIRET DosOpenQueue ( PPID ppid, PHQUEUE phq, PSZ pszName);
ppid is a pointer to the process ID of the queue's server process. phq is a pointer to the write handle of the queue. pszName is the ASCII name of the queue to be opened.
The queue elements can be prioritized and processed in particular order. The order depends on the ulQueueFlags value used when creating the queue. This value cannot be changed once the queue has been created.

Specifying a priority will cause the DosReadQueue  API to read the queue elements in descending priority order. Priority 15 is the highest, and 0 is the lowest. FIFO order will be used for the elements with equal priority. The elements of the queue can be used to pass data to the server directly or indirectly. The indirection comes front using pointers to shared memory. When pointers are used, the shared memory can be of two types: named shared memory and unnamed shared memory. Related processes generally use named shared memory, while the rest use unnamed shared memory. In this example, the named shared memory method is implemented. OS/2 queues do not perform any data copying. They only pass pointers. They leave the rest of the work for the programmer.

 /* Reads an element from a queue. */

 HQUEUE          hque;       /* The handle of the queue from which an element is to be removed. */
 PREQUESTDATA    pRequest;   /* A pointer to a REQUESTDATA that returns a PID and an event code. */
 PULONG          pcbData;    /* A pointer to the length, in bytes, of the data that is being removed. */
 PPVOID          ppBuf;      /* A pointer to the element that is being removed from the queue. */
 ULONG           ulElement;  /* An indicator that specifies whether to remove the first element in the queue or the queue element that was previously examined by DosPeekQueue. */
 BOOL32          bWait;      /* The action to be performed when no entries are found in the queue. */
 PBYTE           pbPriority; /* The address of the element's priority value. */
 HEV             hSem;       /* The handle of an event semaphore that is to be posted when data is added to the queue and wait is set to 1. */
 APIRET          ulrc;       /* Return Code. */

 ulrc = DosReadQueue(hq, pRequest, pcbData,
          ppBuf, element, wait, pbPriority,
          hsem);

hQue is a handle of the queue to be read from. pRequest  is a pointer to a REQUESTDATA structure that returns a PID and an event code. pcbData is an output parameter that specifies the length of the data to be removed. ppBuf is an output parameter that is a pointer to the element being removed from the queue.
ulElement is an indicator that can be either 0, meaning remove the first element from the queue, or a value returned by DosPeekQueue.  Table 5.4 lists the values for bWait.
Table 5.4. Values for bWait
Value Description
DCWW_WAIT The thread will wait for an element to be added to the queue
DCWW_NOWAIT Return immediately with ERROR_QUE_EMPTY if no data is available

pbPriority is an output parameter that indicates the priority of the element being read. hSem is a handle of an event semaphore that will be posted when data is added to the queue, and DCWW_NOWAIT is specified.

The OS/2 QUEUE Client-Server example is best illustrated by starting several OS/2 window sessions from the desktop and making all of them visible to the user at the same time. The queue Server process must be started first. Once the queue is created and the queue Server is started, the queue Clients can use the queue to pass various information to the queue Server.  In this case the information that is passed is the keystrokes  the user enters from each one of the Client processes. Figure 5.1 illustrates this procedure.
 
Figure 5.1 Diagram of a queue.
Table 5.5  Queue client Text Colors
Number  Color
Client 1  Red
Client 2 Green
Client 3 Yellow
Client 4 Blue
Client 5 Magenta
Each one of the queue Clients will send keystroke characters the queue Server via FIFO queue. Once the characters are received by the queue Server. they will be displayed in color depending on the Client that sent them. Table 5.5 describes the queue client text colors.

The QSERVER.EXE allows only up to five active QCLIENT.EXE connections at any one time. Once the maximum number of clients has been reached, entering QCLIENT.EXE followed by a carriage return from the command line will produce a program error message describing the maximum number of clients.

The complete listing of QSERVER.C follows.
 

QSERVER.C
QSERVER.DEF

Now that the intended operation of the OS/2 QUEUE Client - Server has been described, the implementation itself can be discussed in greater detail.

During the initialization Server uses the InitServerQueEnv()  first to allocate the named shared memory segment, next to create the queue, and last to create the queue event semaphore.

The named shared memory segment is used as a common communications area for all of the Clients and the Server. The shared named memory segment later will contain client-specific information: the Client process ID. and the client text color ANSI escape sequence. The memory map in Figure 5.2 shows the way the shared named memory segment is used
 
  Color string  PID   
0x000 Red
<------- Client 0 Area
  Green
<------- Client 1 Area
  Yellow
<------- Client 2 Area
  Blue
<------- Client 3 Area
  Magenta
<------- Client 4 Area
0x0fff UNUSED MEMORY  
Shared memory map (\SHAREDMEM\MYQUEUE.SHR)

Figure 5.2 Shared memory map.

A client area is dedicated to each one of the queue Clients and contains the entire MYQUESTRUCT structure. After the shared memory is allocated, the queue Server creates the queue and initializes the named shared segment to nulls. The last API that is called by the initialization routine is DosCreateEventSem Even though the semaphore that is created will not be used as a semaphore during this application, its handle is required later for the DosReadQueue The reason it is required in this case because the queue is read in nonblocking mode, and the API requires a semaphore handle in that case. Choosing to read the queue in nonblocking fashion allows the queue Server main thread to perform other functions while waiting for the new queue elements.

APIRET DosCreateEventSem( PSZ pszName, PHEV phev, ULONG flAttr, BOOL32 fState )
przName is a pointer to the ASCII name of the semaphore, phev is an output parameter that is a pointer to the semaphore handle. flAttr is either DC_SEM_SHARED to indicate the semaphore is shared, or 0. All named semaphores are shared, so if pszName is not null, this argument is unused.
fState can be either TRUE, meaning the semaphore is initially "posted" or FALSE, meaning the semaphore is initially "set."

In the initialization of the queue Client environment, the InitClientQueEnv()  function call attempts to obtain the named shared memory handle. Once the handle is returned, the queue Client begins to scan the client areas, checking for the valid color string. The moment the Client finds an unused color string area, it assumes it is free and copies its color attribute there. It also saves the unique position identification number in the global sIndex variable. If the Client determines that five other Clients  are  already active, it will display an error message and exit. On the other hand, if the sIndex value is acceptable (less than maximum number of Clients), the Client will issue the DosOpenQueue()  API call, thus completing the initialization by connecting to the queue.

QCLIENT.C
QCLIENT.DEF
QCOMMON.H
Q_CS.MAK
First, the queue server attempts to read the queue; if any elements are present, they are decoded and displayed in their corresponding color; otherwise the Server loops to check for the next queue element. The ERROR_QUE_EMPTY is ignored and reset to 0. It is normal for the Server to receive this particular error since it is possible for the queue to have no messages from any of the Clients.
Readers may wonder why the queue is read continuously in nonblocking mode when it can be read in blocking mode, which will assure a returned queue element prior to completing the DosReadQueue  call. The answer is simple. If the DosReadQueue  API was implemented with the blocking flag set to true, it would be difficult for the main thread to do anything other than wait. An additional thread would have to implemented to handle any other type of work. It is also possible to implement a separate thread that waits on the queue event semaphore and displays the characters only when the semaphore was posted. Because either method would be more complex, we chose the current implementation for this sample program. The point here is to show the differences between the OS/2 queues and the OS/2 named pipes.
The Client does nothing more than read a keystroke character and write that character to the queue by issuing a WriteToQue()  function call, which in turn calls the DosWriteQueue()  API.
APIRET DosWriteQueue( HQUEUE hQue, ULONG ulRequest, ULONG cbData, PVOID pbData, ULONG ilPrority)
hQue is a handle of the queue to which data is to be written. ulRequest is a user-defined value passed with DosPeekQueue cbData is length of the data that is being written. pbData is a pointer to the data. ulPriority is a priority of the data being added to the queue. Any value between 0 and 15 is accepted. A value of 15 indicates the element is added to the top of the queue, and a value of 0 indicates the clement is the last element in the queue.
This example shows that the OS/2 queues are somewhat cumbersome to implement; however, they are very useful when several processes have to talk to a single process, even if the processes are unrelated.

Note: The InitClientQueEnv  function has a potential timing problem. If multiple clients decide to initialize concurrently, a race condition will ensue. To avoid a potential problem. a Mutes semaphore should be installed to protect the access to the shared memory. The implementation is left as an exercise for the reader.
 

An OS/2 Semaphore vs. Flag Variable Example

There are three different types of semaphores: Event, Mutes, and MuxWait. Event semaphores are used when a thread or a process needs to notify other threads or processes that some event has occurred. Mutes semaphores enable multiple threads or processes to coordinate or serialize their access to some shared resource. MuxWait semaphores, on the other hand, enable threads or processes to wait for multiple events to occur.
With this brief introduction, here is the last IPC example pair: STHREAD.C and FTHREARC. This case uses the concept of semaphores for task or event synchronization, also known as signaling. If a process is waiting for a resource to become available, such an a file or a port access right, and the resource is being used by another process, the current task must wait. In the earlier DOS operating systems the synchronization was accomplished primitively through the use of flags. The developer would set a flag, then wait for the flag to be cleared, thus signaling that the resource was free to be used. Since only one process could execute at a time under DOS, this was an acceptable form of pseudo interprocess communication. Under OS/2, however, it is not a good idea to use flags to perform the equivalent semaphore functions. An example of this bad flag synchronization processing is evident in FIILREAD.C, which employs the following construct:
while (FlagBusy); /* Wait for flag to clear */
If a task requires this type of processing, a semaphore should be used. The STHREAD.C example demonstrates the difference in the number of machine cycles that are spent waiting for a semaphore to clear as opposed to waiting for a flag to clear. The STHREAD.EXE creates several threads and then decides to wait on a semaphore. The default number of threads is 10, but that number can be changed by providing an input argument to the STHREAD.EXE program. While this wait is in process, the user is free to type characters at the keyboard, which will be echoed to the console immediately. In contrast, the FTHREAD.EXE uses the same logic but employs a flag variable to perform the wait inside the threads, which dramatically increases CPU usage, and the keystrokes will appear greatly delayed. The FTHREAD.EXE also can accept an input argument specifying the number of threads to be created to wait on the same flag variable. Even with as little as 30 threads, the difference between waiting on a flag variable and waiting on a semaphore is dramatic.
FTHREAD.C
FTHREAD.DEF
STHREAD.C
STHREAD.DEF
SFTHREAD.MAK
Example of usage:
FTHREAD [NUMTHREADS]
or
STHREAD [NUMTHREADS]
The first command-line argument,  NUMTHREADS, should be a number in the range of 11 to 255. The default number of threads created is 10; specifying a number less than 10 is unnecessary. It is not recommended to go over 100 threads with FTHREAD.EXE. Doing so even on a superfast Pentium PC will cause the system to respond to keystrokes very slowly. For example, once the C'TRL-ESC keys are pressed, it may take the system several minutes to paint the PM/WPS screen. STHREAD.EXE, on the other hand, is perfectly capable of handling 255 threads in the wait state and will still provide reasonable keyboard and display response.
[Next] [Previous ] Chapter 5
[Contents ]  [ Chapter 4: File I/O and Extended ] [ Chapter 6: DLLs ]