Operating Systems


OS/2 Interprocess Communication Features

Bob Withers


Bob Withers has a BS in computer science from Oakland University in Rochester, MI and has been active in data processing for 20 years. He began programming for micros in 1985 and has focused increasingly on C and OS/2. Currently, he works for Texas Instruments as a member of its Group Technical Staff. He can be contacted at 649 Meadowbrook St., Allen, TX 75002.

OS/2 is a multitasking operating system allowing many programs to be active at the same time. OS/2 processes may create multiple threads of execution, as well as child processes, that will compete for system resources. In addition, the user is free to invoke multiple programs to be executed. Much of the usefulness of multitasking would be lost if there were not some mechanism for communication among the active processes within the system. Fortunately, OS/2 contains a very rich set of Interprocess Communication (IPC) tools that can be combined to allow information sharing.

This article will discuss the IPC options provided by the current version of the operating system, OS/2 V1.2, and how they can be used to allow information to flow to processes within the same machine.

Types Of IPC

OS/2 supports many forms of IPC that may be used alone or combined to allow programs to share information. The various IPC mechanisms allow for passing data as small as a single bit or as large as megabytes of information. Many of the IPC features of OS/2 are restricted to communication within the same physical machine, but some have been extended, via the OS/2 LAN Manager, to communicate with remote processes. Also, the array of features provided is powerful enough that the programmer is able to construct new forms of IPC by combining the existing features. These new forms allow the OS/2 developer much freedom in planning how his application will be designed and how the various subsystems will communicate with each other.

Semaphores

Semaphores are usually used as signaling mechanisms. Like their railroad namesake, semaphores are often used as a means of mutual exclusion, preventing multiple access to a serially reusable resource. They are also used as signals for one process (or thread) to advise others that some event has occurred. Semaphores are system objects that have two states, owned and not owned. The operating system guarantees that the act of setting and clearing these states is atomic. This guarantee assures a process that when it requests semaphore ownership it will be blocked until it owns the semaphore. (Or, optionally, it can receive an error code after a timeout period). Figure 1 lists the API (Application Programming Interface) functions that support semaphores. OS/2 currently supports three types of semaphores: RAM semaphores, System semaphores, and Fast-Safe RAM semaphores.

RAM semaphores are program variables that are used as semaphores. The program provides the storage for the semaphore and is responsible for initializing it. The program calls OS/2 API functions to control ownership of the semaphore. RAM semaphores are very fast and require little of system resources. They are useful for coordinating access among the multiple threads of a process. For example, multiple threads may wish to access/update variables stored in the process's data segment. Since there is only one instance of the data segment per process, a semaphore can prevent multiple threads from updating the same variable at the same time. Each thread requests ownership of the semaphore and only accesses the variable while it is the owner.

One of the disadvantages of RAM semaphores is that OS/2 provides no clean-up support for them. If a thread or process dies unexpectedly while owning a RAM semaphore, OS/2 makes no effort to release the semaphore or notify other threads that may be blocked. This lack of clean-up support could result in a deadlock in which a thread is waiting on a resource that will never be available. Restricting RAM semaphore use to coordination/signaling functions between the threads of a single process avoids this possible deadlock. If a thread ends while owning the semaphore a problem could still occur, but if the process dies (a more likely event) no harm will be done since all users of the semaphore will also die. Figure 2 shows a sample function that can be called by multiple threads to update and return an application counter. This example, although trivial, demonstrates the use of a RAM semaphore to protect the integrity of the counter.

Fast-Safe semaphores, an extension of RAM semaphores, attempt to alleviate some of the RAM semaphore shortcomings without giving up the convenience. Like RAM semaphores, Fast-Safe RAM semaphores are allocated by the process and are identified by their address. Where the RAM semaphore uses a four-byte integer (LONG/ULONG) to allocate semaphore storage, the Fast-Safe RAM semaphore uses the structure shown in Figure 3.

The major advantage of Fast-Safe RAM semaphores is a mechanism that allows an owning process to release the semaphore if it dies. Each process using a Fast-Safe RAM semaphore must register an exit list via the DosExitList API. During exit list processing, the contents of the pid field may be examined to determine if the terminating process owns the semaphore. The client field can also indicate some required process specific clean-up. If clean-up is required, the exit list code issues the DosFSRamSemRequest API call. This function, when called from a processes exit list, forces the calling process to take semaphore ownership and sets the usage count to one. The exit list code calls DosFSRamSemClear to release the semaphore and allow one of the blocked threads (presumably in another process) to acquire ownership. With the exception of the client field of the DOSFSRSEM structure, the application should not modify any fields after the semaphore is put to use by the first call to DosFSRamSemRequest.

System semaphores are named resources under OS/2 control. They reside within the operating system and are created and destroyed via API calls. Figure 1 lists the API calls that system semaphores support. Unlike both forms of RAM semaphores, any process knowing the name of a system semaphore can gain access to it. Additional API calls are provided to create, open and close a system semaphore. These functions return a handle that identifies the semaphore when using the Request/Set/Clear API functions used for RAM semaphores.

Shared Memory

Shared memory is probably the fastest way two processes can share information. Once all processes sharing information obtain access to the memory area, each process can access the data in the memory block just as it would access private memory. In general, another IPC mechanism, typically a semaphore, mediates access to the shared memory area. Since simultaneous updating of the shared memory area could wreak havoc on the reliability of the information it contains, processes usually agree to obtain ownership of a particular semaphore before accessing shared memory data.

Shared memory implementation in OS/2 V1.X lies very close to the host CPU. The architecture of the Intel 80x86 family of processors places a burden on the programmer and makes creating portable applications difficult. All memory allocation is based on a segment which may be from one to 64K bytes in size. A selector, a magic number assigned by the operating system, identities the segment. The combination of a selector and an offset within the segment accesses addressable memory.

Anonymous Shared Memory

Anonymous shared memory, as its name implies, is identified solely by its memory selector. There are two types of anonymous shared memory, normal (a single segment) and huge (multiple segments treated as one). First I'll discuss single segment shared memory.

Anonymous shared memory is allocated, freed, and reallocated exactly like private memory in OS/2. In both instances, the program issues the same API calls, both which are listed in Figure 4. Flags assigned at allocation time distinguish shared memory. The DosAllocSeg API call (and also the DosAllocHuge call) supports a parameter that the application uses to indicate how the newly allocated memory will be used. The OS/2 Programmer's Toolkit defines the manifest constants shown in Figure 5 for specifying this parameter.

The two flags that indicate shared memory are SEG_GETTABLE and SEG_GIVEABLE. A memory segment may be allocated as either GETTABLE (processes knowing the selector may gain access), GIVEABLE (processes knowing the selector may give access to others), or both. The programmer may use the SEG_DISCARDABLE flag to aid the operating system in virtual memory management. This flag states that if memory runs short, the system can discard this segment to make room for new allocations. Systems using the discardable attribute must also use the DosLockSeg and DosUnlockSeg API calls to assure that the system doesn't discard a memory segment while the segment is being accessed. If the system does discard a segment, DosLockSeg returns an error code and DosReallocSeg assigns new memory to the segment. At this point the application must regenerate the memory segment's contents. While discardable segments are a real boon to the OS/2 memory manager, they are seldom used. In most cases applications that can completely regenerate the contents of a memory segment have little use for the segment. This is particularity true of shared memory segments being used for inter-process communication.

Another new feature of v1.2 is the SEG_SIZEABLE flag. It allows a shared memory segment to be reduced. Prior to v1.2 a shared memory segment could be expanded but not reduced, because the designers thought that allowing one process to shrink a shared segment might cause another to fail. While in some cases this might be true, the definition of shared memory implies cooperating processes. Since some processes may cooperate in reducing the size of a memory block, the SEG_SIZEABLE flag now permits this.

A process relinquishes its access to a shared segment via the DosFreeSeg API call. After issuing this call the selector is no longer valid for that process. Accesses made with it will result in a General Protection Exception, terminating the offending process. Shared memory is not actually freed until all processes having access to it either issue calls to DosFreeSeg or terminate.

The concept of giveable vs. gettable memory can be confusing. A process uses the DosGiveSeg API to allow another process to access a shared segment. This API call deals with two selectors, one used by the current process and one to be used by the process obtaining access. Under certain circumstances the two selectors could be different, although I have never found this to be the case. Flagging a memory segment as both giveable and gettable guarantees that the same selector is assigned to all processes.

Another form of anonymous memory, huge shared memory, allows multiple 64K segments to be assigned to a memory object. The DosAllocHuge API call creates the huge memory object. This API's parameters include both the number of segments requested and the maximum number that may be used via the DosReallocHuge API. The selector of the first allocated segment is returned and used to identify the memory allocation. The programmer must determine when a memory access will cross a segment boundary and use the value returned by the DosGetHugeShift API to calculate the proper selector. The value returned by this API represents a power of two that should be added to one selector in a huge memory object in order to calculate the next selector in the object. For example, if DosGetHugeShift returned the value 4, the value to be added to huge selectors would be 16 (24). This value is constant for a particular version of OS/2 but may change with other versions. Huge memory objects may be freed by calling the DosFreeSeg API and passing the first selector in the huge memory block.

Named Shared Memory

Named shared memory is similar in both function and features to anonymous shared memory. The difference is that named shared memory is given a symbolic name that appears to be part of the file system. Any process which knows the name of the shared memory may request access. The names of all named shared memory begin with the directory name \SHAREMEM and may be constructed just like file system pathnames. For example, \SHAREMEM\THIS\IS\MY\MEMORY.XXX is a valid name for shared memory. The DosAllocShrSeg API creates shared memory and the DosGetShrSeg API accesses existing named shared memory. Both return a selector that may be used, in the context of the calling process, to access the memory. DosFreeSeg relinquishes access to named shared memory. The shared memory is destroyed after being freed by all using processes.

Queues

Queues are named IPC objects that may be opened and written to by any process knowing the Queue name. Figure 6 lists the OS/2 API calls used with queues. As their name implies, queues are collection points for data. Any number of processes may add records to a queue, but only the process that created the queue may read and remove records. These rules force a natural client/server situation in which multiple clients place requests for service into the queue and the server process removes and processes them as its resources permit.

OS/2 queues are supported by a dynamic link library (DLL) provided by OS/2. They rely on anonymous shared memory to provide storage for queue records. In fact, the implementation of queues in OS/2 contains no privileged functionality. It is also an application of the OS/2 kernel.

A queue is created via the DosCreateQueue API call. The parameters to this API are the name of the queue and the queue ordering method. Figure 7 shows some possible ordering methods.

The DosCreateQueue API returns a handle that may be used with the DosReadQueue, DosPeekQueue, or DosPurgeQueue APIs. These APIs can only be used with the queue owner handle returned by DosCreateQueue. Client applications may call the DosOpenQueue API to receive a handle that may be used with the DosWriteQueue API to add records to the queue. The DosOpenQueue API releases the queue handle and relinquishes access to the queue. When the queue owner calls this API, it destroys the queue even if client processes still have handles open.

As mentioned earlier, the queue IPC mechanism is built primarily on anonymous shared memory. Client processes are responsible for allocating shared memory of the appropriate size, placing the queue record in it, and giving access to the queue owner before freeing the memory. The queue owner receives a pointer to this shared memory when it reads a record from the queue. Unless the queue owner completely trusts all of the queue clients, the owner should issue the DosGetSeg API to ensure access to the passed memory segment. In addition, the DosSizeSeg API verifies that the segment size is at least as large as the client process stated. For this reason queue owners should require that shared memory passed to them be both GIVEABLE and GETTABLE. This defensive programming by the queue owner can save a lot of debugging time when an errant client process begins passing bad memory selectors. Figure 8 shows the general sequence of events to use in handling queues.

Pipes

Pipes are one of the most useful IPC features offered by OS/2. They allow large amounts of information to be transferred between processes, while requiring a minimum of collaboration. Most IPC features require advanced planning and agreement for sharing resources. For example, if a system semaphore is used and is protecting shared memory, all processes that will use the memory must agree to use the semaphore. While some agreement may be needed to use pipes, it is usually on the content of the data passed rather than how it is passed.

OS/2 pipes are based on the paradigm of serial file I/O. For the most part a program which correctly performs file I/O can easily be modified to perform I/O from a pipe. Not all features of the file system are supported however. You can only access pipes sequentially; seeking is not supported. Also, you can't rewind and re-read a pipe, once data is read it is lost.

Anonymous pipes are the simplest form of pipe. They are commonly used by the command interpreter to perform command line piping. (Anonymous pipes were discussed in detail in my article in the July 1990 CUJ so I will only briefly review them.) Anonymous pipes are created via the DosMakePipe API call, which returns a read and a write handle. These handles are valid only in the context of the creating process or one of its descendants. The handles cannot be passed to an unrelated process and used. An anonymous pipe is implemented as a circular buffer, with the write handle adding data to the tail of the buffer and the read handle extracting data from the head of the buffer. Figure 9 lists the OS/2 API calls commonly used with anonymous pipes.

Named pipes are built on the client/server model of computing. One process (the server) has a set of resources that it can export to the community at large. Other processes (the clients) make requests of the server process, allowing it to perform services for them.

Like the other named IPC features supported by OS/2, the names of pipes are modeled on the file system. All pipe names begin with the directory name \PIPE and follow the syntax of file system pathnames. This name is passed to the DosMakeNmPipe API to create a named pipe (or named pipe instance) and to the DosOpen API to obtain a client handle to the pipe. Once a pipe handle is obtained, it can be used just like a normal system file handle. Named pipes, however, have some additional API calls to help connect and disconnect the virtual circuit between asynchronous processes. Figure 10 demonstrates example code that represents typical use of named pipes from both the client and server end of the pipe. Of course production code should contain much more error checking.

Named pipes support a unique feature called Multiple instances. This feature is useful when a named pipe server wants to export multiple pipes without giving them distinct names. Using multiple instances a server process (or multiple processes) can create many pipes with the same name. Typically, a separate thread is created to monitor each pipe instance. When a client process connects to this pipe name, OS/2 treats the request like a telephone rotary and performs the connection to the next available pipe. This allows several clients to be connected to the same pipe name simultaneously.

Signals

OS/2 signals are a low-level form of IPC that simulate hardware interrupts of the receiving process. Processes may inform OS/2 of their ability to process signals via the DosSetSigHandler API call. Processes that do not set up their own signal handling are assigned default handlers that either ignore the signal or terminate the process, depending on the signal received. Figure 11 shows the signals supported.

The operating system generates SIG_BROKENPIPE whenever a pipe being used by a process is unexpectedly broken. The process ignores SIG_BROKENPIPE unless it has registered a signal handler to process it. The user generates SIG_CTRLBREAK and SIG_CTRLC by pressing the appropriate keyboard keys. By default, these signals result in process termination. The DosKillProcess API generates SIG_KILLPROCESS, which also defaults to process termination. The user-defined signals SIG_PFLG_A, SIG_PFLG_B and SIG_PFLG_C may be used for any purpose. These signals, generated by the DosFlagProcess API, can only be sent to the calling process or one of its descendants. The signal can pass one word of application-defined data. By default these signals are ignored.

In general, signals are seldom used. They are too primitive for most communication between processes.

Dynamic Data Exchange

Dynamic Data Exchange (DDE) is an event-driven protocol for interprocess communication first developed under Microsoft Windows. DDE relies heavily on the message passing ability of the Presentation Manager component of OS/2. It is only supported for use among PM applications. DDE, like named pipes, is implemented under the client/server model of communication. The basis for communication under DDE is the "conversation." A client process establishes a conversation by broadcasting a WM_DDE_INITIATE message via the WinDdeInitiate API call. This message passes two arguments — an application name and a topic name. The message is delivered to all top-level windows that exist under PM. If any window receiving the INITIATE message recognizes the passed application name and supports the requested topic, it responds to the client process by sending an acknowledgment message, WM_DDE_INITIATEACK. At this point the client can direct requests to the responding server via the WM_DDE_REQUEST message or it can establish a hot link by sending the WM_DDE_ADVISE. The REQUEST message is a one-time solicitation for information that can be provided by the server process. A hot link, on the other hand, is a request to receive the information and to be updated if the data subsequently changes. The response to both these messages is a WM_DDE_DATA message, which contains pointers to the requested data.

Conversations may continue for as long as both the client and server wish. Using the WM_DDE_TERMINATE message, either the client or the server can terminate the conversation. Once a conversation is terminated, the client must establish a new conversation via WinDdeInitiate before sending any additional requests. A client can also terminate a hot link without ending the conversation by sending the WM_DDE_UNADVISE message.

All DDE messages contain a consistent set of parameters that are generally pointers to data. The data pointed to, however, is application-dependent and must be agreed upon in advance by both parties. These pointers are always to one of two predefined structures know as DDEINIT and DDESTRUCT, which are detailed in Figure 12. The WM_DDE_INITIATE message passes the DDEINIT structure, which contains the application and topic names of the request. The DDESTRUCT structure is a general purpose structure used to send requests and receive responses. Immediately following the DDESTRUCT structure (part of the same data object) are two variable length fields, the item name and the actual item data. Two fields in the structure contain the offsets (from the base of the structure) of these fields.

Figure 13 lists DDE messages and API calls.

Conclusion

In this article I have discussed the major forms of interprocess communication under the current versions of OS/2. While space constraints prohibit an in-depth study of any of these features, I have tried to offer a well-rounded introduction. OS/2 supports a powerful set of IPC mechanisms that may be used alone or in combination to support the needs of application programmers.

I also omitted the extensions to the OS/2 IPC supported by the LAN Manager component of OS/2. LAN Manager is an added cost to the base OS/2, although I suspect it will be present on most OS/2 systems.

For readers interested in additional information on OS/2 IPC, I recommend the following books and magazines:

Duncan, Ray, Advanced OS/2 Programming, Microsoft Press, 1989.

Letwin, Gordon, Inside OS/2, Microsoft Press, 1988.

Southerton, Alan, Advanced OS/2 Presentation Manager Programming, Addison-Wesley, 1989.

OS/2 Toolkit, Microsoft Corp.

Microsoft Systems Journal, Microsoft Corp.