 |
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.
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.
|