Skip to main content

     
  TPF : Library : TPF Newsletters
  Products   >   Software   >   Transaction Systems   >   TPF   >   Library   >   TPF System Newsletters   >  
Plumbing for Programmers

Edwin W. Van de Grift, IBM TPF Services

The functionality that has become available on TPF during the past few years has opened up a whole new spectrum of programming options. One of the interesting aspects hereof is that of passing information between processes, which in POSIX terminology is called Interprocess Communications (IPC). This is the first in a series of articles covering these mechanisms. What will be covered is not so much all theoretical backgrounds, but practical coding samples. The coding samples are in C++ to additionally show how the IPC mechanisms can be implemented effectively without disturbing application logic. Note that the sample code shown is far from complete: only relevant statements are shown and error handling (to name one aspect) has been completely ignored.

Unnamed pipes

One of the oldest mechanisms for passing information between processes is the pipe. A pipe---or more correctly, an unnamed pipe---provides an I/O channel through which a process can communicate with another process (or with itself). The processes between which the communication is to take place need to be related (for example parent-child, parent-grandchild, or any two processes that share some common ancestor). A pipe provides a one-way channel of communication (in other words, a pipe is unidirectional), so in order to pass information back and forth between processes, two pipes are needed. Pipe support on TPF was introduced through APAR PJ26188 on PUT 10.

#include <unistd.h>

int pipe(int fd[2]);



To create a pipe, the pipe function needs to be called. Its argument is an array of two integers in which pipe returns two file descriptors: one for the read end of the pipe (stored in fd[0]), and one for the write end of the pipe (stored in fd[1]). When a process creates a child process (for example, using the tpf_cresc or tpf_fork function), all open file descriptors1 of the parent process are inherited by the child process. A parent process can thus pass information to its child by writing to the write end of the pipe. The child process can then receive the information by reading it from the read end of the pipe.

The following sample shows the parent code. Because the child code will reside in a separate program, we need to make sure the child process knows what inherited file descriptor to read from. This is established by closing file descriptor zero (that is mapped to the standard input stream) immediately before creating the pipe. The file descriptors mapped to the read and write ends of the pipe will be the lowest file descriptors that are currently not in use. In other words, by closing file descriptor zero before creating the pipe, the read end of the pipe will be mapped to file descriptor zero. If you are in a situation where you cannot get away with simply closing file descriptor zero, you can still close it after duplicating it to another file descriptor by using the dup or the dup2 function. Directly after creating the child process, the parent process can close the read end of the pipe. The parent then associates a stream with the write end of the pipe to be able to conveniently use a stream function to pass its information to the child. It could, of course, also be done with the (lower-level) write function, but this would need us to explicitly take care of the data to be written (and its length).

#include <unistd.h>

#define _POSIX_SOURCE

#include <stdio.h>

 

int main(int, char**) {

fclose(stdin);

int fd[2];

pipe(fd);

tpf_fork(...);

close(fd[0]);

FILE* toChild = fdopen(fd[1],"w");

fprintf(toChild,"How's life, kiddo?");

}



The next piece of code shows the child and the way it receives the information from its parent. The important thing to note is that the child process is not aware of the mechanism used to establish the communication path to it; it does not know about pipes at all!

#include <stdio.h>

 

int main(int, char**) {

char* message[80];

scanf("%s", message);

}



FIFOs

For two independent processes to communicate with each other, named pipes can be used. Named pipes are typically referred to as FIFOs, or FIFO special files, because data is passed in a first-in-first-out format. FIFOs have been introduced to TPF by APAR PJ27214 on PUT 13.

#include <sys/types.h>

#include <sys/stat.h>

int mkfifo(const char* path, mode_t mode);



 

A FIFO is created by calling the mkfifo function. This function creates a new file (called path). The file permission bits in the mode parameter are modified by the file creation mask of the process and then used to set the file permission bits of the pipe being created. The file mode creation mask of a process can be queried and modified using the umask function. In the case where the FIFO already exists, the mkfifo function returns -1 and errno contains the value EEXIST.

After creating a FIFO, the FIFO can be opened for sending (writing), or retrieving (reading) information. As with pipes, writing to a FIFO appends the data, and reading from it returns the data that is at the beginning of the FIFO. One thing to be aware of is that the open of a FIFO is by default blocking; that is, a process opening a FIFO for reading is suspended until another process has opened the FIFO for writing.

The sample code writing into the FIFO starts with creating the FIFO, allowing every process in the system both read and write access to the FIFO. In this case the sample code uses the low-level write and read functions referring to file descriptors.

#include <sys/stat.h>

#include <fcntl.h>

#include <string.h>

#include <unistd.h>

 

int main(int, char**) {

mkfifo("afile",S_IRWXU|S_IRWXG|S_IRWXO);

int w = open("afile",O_WRONLY|O_NONBLOCK);

char out[10] = "fifo test";

write(w,out,strlen(out)+1);

close(w);

}



The program reading from the FIFO opens the FIFO for reading, reads the data, and finishes by deleting the FIFO (using the unlink function), which is optional. You may decide to leave FIFOs in existence after creating them.

#include <fcntl.h>

#include <unistd.h>

 

int main(int, char**) {

int r = open("afile",O_RDONLY|O_NONBLOCK); char in[10];

read(r,in,sizeof(in));

close(r);

unlink("afile");

}



To verify whether an existing file is a FIFO, the S_ISFIFO function is available. S_ISFIFO queries the mode member of a file's stat structure, which is retrieved through the stat (or fstat, or lstat) function.

#include <sys/stat.h>

 

int main(int, char**) {

struct stat st

stat("afile", &st);

if(S_ISFIFO(st.st_mode)) {

// "afile" is a fifo

}

}



Is it a pipe?

One of the powerful concepts of C++ is that it allows the developer to completely hide implementation details from application logic, commonly called encapsulation. Consider the following two pieces of sample code. The first process streams data into some object of type Sample.

#include "Sample.h"

 

int main(int, char**) {

Sample s("some_name");

s << 2 << "Some text.";

}



The second process receives data from an object of type Sample.

#include "Sample.h"

#include <string>

 

int main(int, char**) {

Sample s("some_name");

int i;

string text;

s >> i >> text;

}



Of course, Sample may use many different mechanisms of transporting the data from the first process to the second (unrelated) process, but one of the ways to implement Sample is to use FIFOs. A part of the Sample implementation is shown in the following code fragments, starting with Sample's header file (Sample.h). The Sample class shown is capable of transporting integers and strings. Sample may support transportation of any type of data. To add more types, simply declare and implement additional input and output stream operators.

#ifndef SAMPLE_H

#define SAMPLE_H

 

#include <string>

 

class Sample {

public:

Sample(const char* path);

~Sample();

friend Sample& operator<<(Sample& s,int val);

friend Sample& operator<<(Sample& s,string& val);

friend Sample& operator<<(Sample& s,const char* val);

friend Sample& operator>>(Sample& s,int& val);

friend Sample& operator>>(Sample& s,string& val);

private:

Sample(const Sample& s);

int _rfd;

int _wfd;

};

 

#endif



A noteworthy aspect of the sample class declaration is the way that the operator>> and operator<< functions are declared (and implemented in code shown below). Overloaded input and output operators are never declared as member functions. Declaring them as member functions would require them to be called in the manner shown in the following figure, which is counter intuitive, and would cause a lot of confusion.

Sample s;

2 >> s;



The reason the input and output operators are declared as friends of class Sample is to allow them access to nonpublic members of class Sample: to the integers _rfd and _wfd. Finally, Sample's copy constructor is declared as a private function to prevent the copying of Sample objects.

The final piece of code shows the first part of the implementation of class Sample (Sample.cpp). A FIFO is opened in the Sample constructor, and created if not existing yet. The Sample destructor closes the FIFO. The FIFO could also have been deleted in the destructor, but it was chosen not to do this. An implementation for Sample's copy constructor has not been coded because it was declared private, and thus cannot be called by any user of the Sample class.

#include "Sample.h"

#include <sys/types.h>

#include <sys/stat.h>

#include <errno.h>

#include <fcntl.h>

#include <unistd.h>

 

Sample::Sample(const char* path) {

if(mkfifo(path) == -1 && errno != EEXIST) {

// Error...

}

_rfd = open(path,O_RDONLY|O_NONBLOCK);

_wfd = open(path,O_WRONLY|O_NONBLOCK);

}

 

Sample::~Sample() {

close(_rfd);

close(_wfd);

}



The second part of Sample.cpp, shown below, contains the implementation of Sample's input and output operators. In the input and output operators of class Sample for the character string types, I chose to implement these by prefixing every string by its length in order to improve performance. Not doing this would force the recipient to read a character at a time when receiving data because every character might be the final one.

Sample& operator<<(Sample& s, int val) {

write(s._wfd,(char*)&val,sizeof val);

return s;

}

 

Sample& operator<<(Sample& s, string& val) {

int length = val.size()+1;

write(s._wfd,(char*)&length,sizeof length);

write(s._wfd,val.c_str(),length);

return s;

}

 

Sample& operator<<(Sample& s, const char* val) {

string str(val);

int length = str.size()+1;

write(s._wfd,(char*)&length,sizeof length);

write(s._wfd,str.c_str(),length);

return s;

}

 

Sample& operator>>(Sample& s, int& val) {

read(s._rfd,(char*)&val,sizeof val);

return s;

}

 

Sample& operator>>(Sample& s, string& val) {

int length;

read(s._rfd,&length,sizeof length);

char* buffer = new char[length];

char* pos = buffer;

int soFar(0);

do {

int i = read(s._rfd,pos,length-soFar);

soFar += i;

pos += i;

} while(soFar < length);

val = buffer;

delete [] buffer;

return s;

}



As I have hopefully been able to show, pipes (named and unnamed) make life in Interprocess Communications a lot easier. Applied in, for example, system or application maintenance processing, where processing is distributed across many processes but still needs to be coordinated, the use of pipes has proven to be of great value.

Edwin W. Van de Grift is a consultant in IBM's TPF Services group with 15 years TPF experience. His primary focus areas include C and C++, TPFDF, TPFCS, application development tools, and application performance optimization. Edwin is also the instructor for IBM's "TPF4.1 C/C++ Architecture and Internals" and "TPF C++ Application Development" classes.


1 Actually, only file descriptors that do not have the FD_CL0EXEC file descriptor flag set to 1 are inherited.