[ Next ] [ Previous ] | Chapter 5 |
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.
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.
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.
|
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
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.
|
|
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.
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.
![]() |
|
|
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 |
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.
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 |