Introduction
Inter Process Communication (IPC) is a mechanism through which different processes interact and share data between themselves. The applications, which use IPC for interaction can be divided into two categories. They are the server application and the client application. The server application provides the services to the client application, which has requested the set of services from the server application. There are the various IPC mechanism which are supported by the WIN32 SDK. Pipes is one of those mechanisms. Pipes allow the transfer of data between processes in a FIFO (first-in-first-out) manner. The application, which creates pipe, is called the pipe server and the application, which connects, to the pipe is called a pipe client. The FIFO manner means that the processes read the data in a same order in which it was written i.e. the data which was written first will be read first by the processes. Once the data is read from the pipe, the data is removed from the pipe and hence makes a space available for the process, which is writing on the pipe.
The WIN32 SDK provides two flavours of Pipes: Anonymous pipe and Named pipes.
An anonymous pipe is a one-way communication conduit, which transfers the data between the related processes i.e. between parent and child process. The anonymous pipe doesn�t support communications over the network. Anonymous pipes are always byte-stream oriented.
A named pipe provides one way or two-way communication between the pipe server and pipe client. It can be used to interact between the processes, which may be unrelated, or on different machines on a network. We will cover the implementation of named pipe in the second article of this series. This article will be covering the practical implementation of anonymous pipe along with a sample code for each implementation.
There are the following cases in which the implementation of anonymous pipes can be explored in providing the communication conduit:
- To explore how it can act as a channel between the writer thread and the reader thread where both threads belong to the same process.
- To explore how it can act as a channel between the parent and a child process. It uses the concept of handle inheritance.
- To explore how it can act as a channel between related and unrelated processes by using the concept of file mapping objects. This will be covered in PART-2 of the article.
- To explore the use of overlapped I/O operation in anonymous pipe. Windows 95/98 doesn�t supports the overlapped I/O operations on pipes, mailslots and files. This too will be covered in PART-2 of the article.
Intra-process Communication
This mode of communication is within a process; in which one thread acts as a writer thread and another thread act as a reader thread. Every process has at least a one thread and that main thread is a writer thread in our code sample. The main thread spawns a new thread and passes a handle to the read end of a pipe and a handle to the writer thread to the reader thread.The main thread creates the unnamed pipe by calling
CreatePipe
and gets the handle to the read and the write ends of the pipe. The last parameter to
CreatePipe
specifies the buffer size, which has been allocated for a pipe. There is a need to maintain the synchronization between the two threads i.e. when the reader thread is reading the data, the writer thread should sleep and as soon as the reader threads reads all the data from the pipe, it awakes the writer thread so that it can write to the empty pipe. The duration for which the writer thread will write on a pipe, the reader thread will be in sleep state. The same is for the writer thread. Once the pipe gets full, the writer thread will awakes the reader thread to read the pipe and when the reader pipe will be reading the pipe, the writer thread will go for a sleep. To maintain the toggle between the two threads, the both threads should have each other�s handle so that they resume the other thread before going into a suspended state. This has been achieved by structure
ThreadData
.
struct {
HANDLE hWrThread;
HANDLE hRdPipe;
};
ThreadData thdData;
bReturn = CreatePipe(&hReadPipe,&hWritePipe,NULL,PIPE_BUFFERLENGTH);
thdData.hRdPipe = hReadPipe;
hWrThread = GetCurrentThread();
bReturn = DuplicateHandle(GetCurrentProcess(),hWrThread,
GetCurrentProcess(),&hWrDupl,NULL,FALSE,DUPLICATE_SAME_ACCESS);
thdData.hWrThread = hWrDupl;
hRdThread = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadFunc,
(LPVOID)&thdData,CREATE_SUSPENDED,&dwThreadID);
In this code snippet, the main thread creates the unnamed pipe by calling CreatePipe
. This function returns the handle to the read and the write end of the pipe. The read end of a pipe is stored in the data member (hRdPipe
) of a ThreadData
structure. The writer thread gets it own handle by calling GetCurrentThread
and to get a compatible handle, which could be used by another threads, it calls DuplicateHandle
with the pseudo handle as an argument to it. Once we get the duplicate handle to the writer thread, it is stored in the data member of a ThreadData
structure. The ThreadData
structure is passed to the reader thread as one of the arguments to the CreateThread
function. Each thread maintains the toggle between the two threads by calling the ResumeThread
and SuspendThread
function on an appropriate thread�s handle. The reader thread reads the data from the pipe by calling the ReadFile
function. The ReadFile
call will read data from the pipe and once the data is read from the pipe, the data is removed from the pipe. If the pipe is empty, then the ReadFile
function will go into a sleep state and wait until data is written into the pipe by the writer thread. The operating system awakes the reader thread as soon as the writer thread writes data on the pipe.The writer thread writes the data on the pipe by calling WriteFile
function. The WriteFile
function will write the data into a pipe till the entire buffer size allocated for the pipe gets consumed. Once that happens, the writer thread will go to sleep and waits for the reader thread to remove the data from the pipe, so that it can write again on the pipe. The main thread (i.e. writer thread) can wait for the reader thread by calling a wait function called WaitForSingleObject
function.
Inter-Process Communication (Master-Slave Model)
This mode of communication represents a parent-child relation. A process that creates or spawns a new process is called the parent and the spawned processes are called its children. The child process, once created, doesn�t depend on the parent process and runs independently. The child process has its own virtual address space, which is independent of the parent�s virtual address space. A child process can inherit the open handles from its parent process. An inherited handle in the child process refers to the same object as an original handle in the parent process. When a child process inherits the handle, the operating system provides only access to the child process. As child process has a new virtual address space, therefore the parent process should communicate the handle value to the child process. The handle value can be passed in various ways:
- By command line argument
- By file mapping objects
- By pipes or by standard I/O channels
The handle inheritance causes the child process to have the entries for all the inheritable handles (which are in the open state) in its own object table. The inheritance gives the access on the handle, which is in the parent process, and once the handle value is passed to the child process via some IPC mechanism, the child process can also use that handle. Inheritance works fine for a related processes i.e. which has a parent-child relation.
DuplicateHandle
can be used to create a handle in a related as well as unrelated process.
An anonymous pipe provides a one-way conduit between the processes. The parent process and child process needs two pipes to exchange their respective data with each other. The parent process creates the two anonymous pipes by calling CreatePipe and
get the inheritable handle to the read end and write end to the pipes.
To have a two-way communication with the child process, the parent process provides access to the handle to the read end of pipe2 and handle to the write end of the pipe1 to the child process via handle inheritance. The parent process calls DuplicateHandle
on hWrMaster
and hRdMaster
to create non-inheritable handles and then it closes the inheritable handles. After closing these inheritable handle, the parent process spawns the child process by calling CreateProcess
and passes the value of inheritable handle to the read end to the pipe2 and value of handle to the write end to the pipe1 as a command line argument.
bReturn = CreatePipe(&hRdMaster,&hWrSlave,&secAttr,PIPE_BUFFERLENGTH);
DuplicateHandle(GetCurrentProcess(),
hRdMaster,
GetCurrentProcess(),
&hDuplRdMaster,
DUPLICATE_SAME_ACCESS,
FALSE,
DUPLICATE_SAME_ACCESS
);
CloseHandle(hRdMaster);
bReturn = CreatePipe(&hRdSlave,&hWrMaster,&secAttr,PIPE_BUFFERLENGTH);
DuplicateHandle(GetCurrentProcess(),
hWrMaster,
GetCurrentProcess(),
&hDuplWrMaster,
DUPLICATE_SAME_ACCESS,
FALSE,
DUPLICATE_SAME_ACCESS
);
CloseHandle(hWrMaster);
The Child process retrieves the value to these handles from its command line argument.
HANDLE hWrSlave = NULL;
HANDLE hRdSlave = NULL;
int laddWrSlave = 0;
int laddRdSlave = 0;
laddWrSlave = atoi(argv[2]);
laddRdSlave = atoi(argv[3]);
cout << "Value of write handle to pipe1: " << laddWrSlave << endl;
cout << "Value of read handle to pipe2 : " << laddRdSlave << endl;
hWrSlave = (void *) laddWrSlave;
hRdSlave = (void *) laddRdSlave;
After retrieving the handle values, the child process can communicate with the parent process by using the ReadFile
and WriteFile
functions. The parent process can wait for the child process to terminate by calling WaitForSingleObject
function.