[Next] [Previous ] | Chapter 5 |
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.
|
D_CLIENT.C
D_CLIENT.MAK
D_CLIENT.DEF
DCOMMON.H
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.
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.
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.CFirst, 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.
QCLIENT.DEF
QCOMMON.H
Q_CS.MAK
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.
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.
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.CExample of usage:
FTHREAD.DEF
STHREAD.C
STHREAD.DEF
SFTHREAD.MAK
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 |