11.5 Object-Oriented Pipes and fifos as Low-Level Building Blocks

To arrive at a design for object-oriented pipes, we start with basic characteristics and behavior that all pipes have in common. A pipe is a channel of communication between two or more processes. In order for the processes to communicate, they will transmit some sort of information between them. The information may represent data or commands to be performed. Typically, the information is translated into a sequence of data and inserted into the pipe and retrieved by a process on the other side of the pipe. The data is reassembled into meaningful data by the retrieving process. Whatever the data represents, there must be somewhere to store the data while it is in transit from one process to another. We call the storage area for the information a data buffer. Insertion and extraction operations are needed to place data into and extract data from the buffer. Before any insertion into the data buffer or extraction from a data buffer can begin, the data buffer must first exist. Object-oriented piping facilities must support an operation that creates and initializes a data buffer. Once the communications between processes have been completed, the data buffer used to hold the information will no longer be necessary. This means that our object-oriented pipe must be able to remove the data buffer after it is no longer needed. This suggests there are at least five basic components that any object-oriented piping facility should have:

  • Buffer

  • Insertion operation

  • Extraction operation

  • Creation/initialization operation

  • Destruction operation

In addition to these five basic components, a pipe will have two ends. One end of the pipe will be for inserting data. The other end of the pipe will be for extracting data. These two ends can be accessed from different processes. To complete our notion of a pipe we must include an input port and an output port, which could be connected to separate processes. This gives seven basic components needed to describe our object-oriented pipe:

  • Input port

  • Output port

  • Buffer

  • Insertion operation

  • Extraction operation

  • Creation/initialization operation

  • Buffer destruction operation

These components represent minimal core requirements for our description of a pipe. Once we have the basic components, we can identify how existing system APIs or data structures can be used to help us design an object-oriented pipe. In the same way that we use encapsulation and operator overloading to design a pvm_stream , we use the same techniques to wrap the pipe and fifo functions.

Notice that five of the seven basic components are common to many basic I/O data structures and container types. Most UNIX/Linux file services support:

  • Buffers

  • Buffer insertion operations

  • Buffer extraction operations

  • Buffer creation operations

  • Buffer destruction operations

We use the notion of C++ interface classes to encapsulate the functionality provided by UNIX/Linux system services. We build object-oriented versions of the input/output services. Whereas we had to start from scratch with the pvm_stream class for the PVM library, here we can take advantage of the existing C++ standard library and the iostreams. Recall that the iostreams class library supports an object-oriented model of input and output streams. Furthermore, this object-oriented library has support for the data buffer notion and all the operations upon the data buffer. Figure 11-7 shows a simple class diagram of the iostream class.

Figure 11-7. Class diagram for the major components of the iostream class.

graphics/11fig07.gif

The major components of the iostream class can be described by three kinds of classes: a buffer component, a translation component, and a state component (see Hughes & Hughes, 1999). The buffer component is used as a holding area for bytes that are in transit. The translation component is responsible for translating anonymous sequences of bytes into the appropriate datatypes and data structures, and for translating data structures and data-types in anonymous sequences of bytes. The translation component is responsible for providing the programmer with a stream of bytes metaphor where all I/O regardless of source or destination is treated as a stream of bytes. The state component encapsulates the state of the object-oriented stream. The state component maintains what type of formatting is applicable to the data bytes that are in the buffer component. The state component also maintains whether a stream has been opened in the append mode, create mode, exclusive read, exclusive write, or whether numbers will be interpreted as hexadecimal, octal, or binary. The state component also can be used to determine the error state of I/O operations on the buffer component. By querying the state component the programmer can determine if the buffer component is in a good or bad state. These three components are objects and can be used together to form a complete object-oriented stream, or separately as support objects in other tasks .

Five of the basic requirements for our pipe are already implemented in the iostreams class library. All we need to add is the notion of the input port and output port. To do this, we can examine the system services that support the use of pipes. The UNIX/Linux system calls create a pipe.

Example 11.19 Using system call to create a pipe.
 int main(int argc, char *argv[]) {    //...    int Fd[2];    pipe(Fd);    //... } 

The pipe function call is used to create a pipe data structure, which can be used between parent and child processes to communicate. If the call to pipe is successful, it will return two file descriptors. File descriptors are integers used to identify successfully opened files. In this case, the descriptors are stored in the array Fd . Fd[0] will be open for reading, and Fd[1] will be open for writing. Once these two file descriptors are created, they can be used with regular read( ) and write( ) functions. The write( ) function will cause data to be inserted into the pipe via Fd[1] , and the read( ) function will cause data to be extracted from the pipe via Fd[0] . Because the pipe() function returns file descriptors, access to a pipe can be accomplished using system file services. The sysconf(_SC_OPEN_MAX) system call can be used to determine the maximum allowable file descriptors open by a process. The pathconf(_PC_PIPE_BUF) call can be used to find the size of the pipe.

These two file descriptors represent our logical input port and output port, respectively. We also use these two file descriptors to provide links to the iostream class library. Specifically, they provide a link to the buffer class. The buffer component of the iostream classes has three families of classes. Table 11-3 lists the three types of buffer classes and their descriptions.

Table 11-3. Three Types of Buffer Classes

Types of Buffer Classes

Description

basic_streambuf

Describes the behavior of various stream buffers in order to control input and output sequences of characters .

basic_stringbuf

Associates input and output sequences with a sequence of arbitrary characters that can be initialized from or made available as a string object.

basic_filebuf

Associates input and output sequences of characters with a file.

We are interested in the filebuf class. Whereas the basic_streambuf class is used as an object-oriented buffer in I/O from standard in and standard out, and the basic_stringbuf class is used for object-oriented memory buffers, the filebuf class is used as object-oriented buffers for files. By examining the interface for the filebuf class and the interface for its translator classes, ifstream , ofstream , and fstream , we can find a way to connect the file descriptors returned from the pipe() system call to the iostream objects. Figure 11-8 shows the class diagrams for the fstream family of classes.

Figure 11-8. The class diagrams for the fstream family of classes.

graphics/11fig08.gif

Notice that the ifstream , ofstream , and fstream classes all contain the filebuf class. Therefore, we can use any class from the fstream family of classes to help us in creating object-oriented pipe facilities. We can connect the file descriptors returned by the pipe() system call either through constructors, or through the attach() member function.

Synopsis

 #include <fstream> // UNIX systems ifstream(int fd) fstream(int fd) ofstream(int fd) // gnu C++ void attach(int fd); 

11.5.1 Connecting Pipes to iostream Objects Using File Descriptors

There are three iostream classes that we can use to connect to a pipe. They are ifstream , ofstream , and fstream . An ifstream object is used for input and an ofstream object is used for output. An fstream object can be used for both input and output. Although direct support for file descriptors and the iostreams is not yet part of the ISO standard, most UNIX and Linux C++ environments support iostream access to file descriptors. The GNU C++ iostreams library supports a file descriptor in one of the ifstream , of-stream , and fstream constructors and with the attach() method of the ifstream and ofstream classes. UNIX compilers such as Sun's C++ compiler supports file descriptors through one of the ifstream , ofstream , and fstream constructors. So the sequence of code:

 //... int Fd[2]; Pipe(Fd); ifstream IPipe(Fd[0]); ofstream OPipe(Fd[1]); 

will create to object-oriented pipes. IPipe will be an input stream and OPipe will be an output stream. Once these streams are created they can be used to communicate between concurrently executing processes using the stream metaphor and the inserter << and >> extractor operators. For the C++ environments that support the attach() method, the file descriptor can be attached to an ifstream , ofstream , or fstream object using the syntax:

Example 11.20 Creating a pipe and using the attach() function.
 int Fd[2]; ofstream OPipe; //... pipe(Fd); //... OPipe.attach(Fd[1]); //... OPipe << Value << endl; 

This usage of object-oriented pipes assumes the existence of some child process that can read the pipe. Program 11.1 uses the fork command to create two processes. The parent process sends a value to the child process using an iostreams-based pipe.

Program 11.1
 1 #include <unistd.h>  2 #include <iostream.h>  3 #include <fstream.h>  4 #include <math.h>  5 #include <sys/wait.h>  6  7  8  9 10 int main(int argc, char *argv[]) 11 { 12 13 int Fd[2]; 14 int Pid; 15 float Value; 16 int Status; 17 if(pipe(Fd) != 0){ 18    cerr << "Error Creating Pipe " << endl; 19    exit(1); 20 } 21 Pid = fork(); 22 if(Pid == 0){ 23    ifstream IPipe(Fd[0]); 24    IPipe >> Value; 25    cout << "Value Received From Parent " << Value << endl; 26    IPipe.close(); 27 } 28 else{ 29        ofstream OPipe(Fd[1]); 30        OPipe << M_PI << endl; 31        wait(&Status); 32        OPipe.close(); 33 34 } 35 36  } 

Recall that when a fork() call is made, the return value of belongs to the child process. In Program 11.1 the pipe is created on line 17. On line 29 the parent process opens the pipe for writing. The file descriptor Fd[1] is the write end of the pipe. This end of the pipe is attached to an ofstream object through the constructor on line 29. The read end of the pipe is attached to an ifstream object on line 23. The child process opens the pipe for reading. The child process has access to the file descriptor because along with the parent's environment, file descriptors are also inherited. Therefore, any files that are open in the parent will be opened in the child unless explicit instructions are given to the operating system using the fcntl system call. Besides open files being inherited, the position markers within the files remain where they are during the spawning of the child process so that the child process also has access to the position marker. When the position is moved in the parent, the marker in the child process is also moved. In this case, we were able to accomplish the stream metaphor without creating an interface class. Simply by attaching the pipe's file descriptors to the ofstream and ifstream objects we are able to use the << inserter and >> extractor operators. Likewise, any class that has the >> extraction or << insertion operators defined can be extracted from or inserted into the pipe without requiring any further work from the programmer. The parent inserts the value M_PI into the pipe on line 30. The child extracts the value from the pipe using the >> operator on line 24. The details for executing and compiling this program are contained in Program Profile 11.1.

Program Profile 11.1

Program Name

 program11-1.cc 

Description

Program 11.1 demonstrates the use of an object-oriented stream metaphor with anonymous system pipes. The program uses the fork() program to create two processes that will communicate using the << inserter and >> extractor operators.

Headers Required

 <wait.h>, <unistd.h>, <iostream.h>, <fstream.h>, <math.h> 

Compile and Link Instructions

 c++ -o program11-1 program11-1.cc 

Test Environment

Solaris 8, SuSE Linux 7.1

Execution Instructions

 ./program11-1 

The gnu C++ compiler also supports the attach() method. We could use this method to connect the file descriptors to the ifstream and ofstream objects. For instance:

Example 11.21 Connecting file descriptors to an ofstream object.
 int main (int argc, char *argv[]) {    int Fd[2];    ofstream Out;    pipe(Fd);    Out.attach(Fd[1]);    //...    // Interprocess Communication    //...    Out.close( ); } 

The Out.attach(Fd[1]) call attaches an ofstream object to a pipe file descriptor. Now any information that is inserted into the Out object is actually being written to a pipe. Using extractors and insertors to perform automatic format translation is a major advantage of using the fstream family of classes in conjunction with the pipe communication. The ability to use userdefined extractors and insertors removes some of the difficulty encountered with pipe programming. So instead of requiring the explicit enumeration of data sizes of everything written and read from the pipe we use the number of elements to control read/write access, which makes the entire process simpler. In addition, this cost savings makes the parallel programming efforts easier. The technique we recommend is to use architecture to support a divide-and-conquer approach to parallelization . Once the correct components are in place, the programming becomes easier. For instance, since the pipe is tied to ofstream and ifstream objects, we are able to use the information retained by the ios component to determine the state of the pipe. The translation components of the iostreams can be used to perform automatic conversions as the data is inserted into one end of the pipe and extracted out of the other end. Using the pipes with the iostreams also allows the programmer to integrate the standard containers and algorithms with pipe inter-process communication. Figure 11-9 shows the relationship between the ifstream , ofstream , extractor, insertor, pipe, and the inserter and extractor when iostreams are used for interprocess communication.

Figure 11-9. The relationships between ifstream and ofstream objects, pipe, and the inserter and extractor when the iostreams are used for interprocess communication.

graphics/11fig09.gif

The fstream family of classes can also use the read( ) and write( ) member functions to read data to a pipe, and write data from a pipe.

11.5.2 Accessing Anonymous Pipes Using the ostream_iterator

We can also use the pipe with the ostream_iterator and istream_iterator . These iterators are generic object-oriented pointers. The ostream_iterator will allow you to transfer entire containers (i.e., lists, vectors, sets, queues, etc.) across the pipe. Without the use of iostreams and ostream_iterator , transferring containers of objects would be a very tedious and error-prone process. Table 11-4 lists the set of operations that are available on the ostream_iterator and istream_iterator class.

Table 11-4. Set of Operations Available on the ostream_iterator and istream_iterator

Iterators

Operations

Description

istream_iterator

a == b

Equivalence relation

 

a != b

Nonequivalence relation

 

*a

Dereference

 

++ r

Preincrementation

 

r ++

Postincrementation

ostream_iterator

++ r

Preincrementation

 

r ++

Postincrementation

Typically, these iterators are used with the iostreams classes and the standard algorithms. The ostream_iterator is a sequential write-only iterator. Once an item has been accessed, the programmer cannot go back to it without starting the iteration over. The pipe is treated like a sequence container when used with these iterators. This means that when the pipe is connected to the iostreams through ostream_iterator and the file descriptors, we can apply standard algorithm type processing to the input from a pipe or the input to a pipe. The reason these iterators can be used in conjunction with pipes is the connection between the iterators and the iostream classes. The diagram in Figure 11-10 shows the relationship between the I/O iterators and the iostream classes.

Figure 11-10. The relationship between the I/O iterators and the iostream classes.

graphics/11fig10.gif

Figure 11-10 also shows how these classes interact with the notion of our object-oriented pipe. Let's take a close look at how the ostream_iterator is used with an ostream object. If a pointer is incremented, we expect it to point at the next location in memory. When the ostream_iterator is incremented, it moves or points to the next position in the output stream. When we assign a value to a dereferenced pointer, we are placing that value at the location that the pointer points to. When we assign a value to an ostream_iterator , we are placing that value in the output stream. If that output stream is connected to cout , then the value will be displayed on the standard out. If we declare an ostream_iterator object such as:

 ostream_iterator<int> X(cout,"\n"); 

Then X is an object of type ostream_iterator . The increment operation:

 X++; 

causes X to move to the next position in the output stream. The statement:

 *X = Y; 

causes Y to be displayed on standard out. This is because the assignment operator = has been overloaded to use an ostream object. The declaration:

 ostream_iterator<int> X(cout, "\n"); 

caused X to be constructed with a cout as the stream. The other argument in the constructor is the delimiter that will automatically be placed after every int that is inserted into the stream. The declaration for the ostream_iterator looks like this:

Example 11.22 Declaration of the ostream_iterator class.
 template <class _Tp> class ostream_iterator { protected:    ostream* _M_stream;    const char* _M_string; public:    typedef output_iterator_tag iterator_category;    typedef void                value_type;    typedef void                difference_type;    typedef void                pointer;    typedef void                reference; ostream_iterator(ostream& __s) : _M_stream(&__s), _M_string(0) {} ostream_iterator(ostream& __s, const char* __c)  : _M_stream(&__s), _M_string(__c) {}  ostream_iterator<_Tp>& operator=(const _Tp& __value) {  *_M_stream << __value; if (_M_string) *_M_stream << _M_string;    return *this; } ostream_iterator<_Tp>& operator*() { return *this; } ostream_iterator<_Tp>& operator++() { return *this; } ostream_iterator<_Tp>& operator++(int) { return *this; } }; 

The constructor for the ostream_iterator accepts a reference to an ostream object. The ostream_iterator class has an aggregate relationship with the ostream class. The istream_iterator has just the opposite use of the ostream_iterator . It is used with istream objects instead of ostream objects. If istream_iterator and ostream_iterator objects are connected to iostream objects that in turn are connected to pipe file descriptors, then every time the istream_iterator is incremented, the pipe is being read, and every time the ostream_iterator is incremented, the pipe is being written. To demonstrate how these components work together, we have two programs: Programs 11.2 and 11.2b that use anonymous pipes to communicate. Program 11.2 is the parent and Program 11.2b is the child. The parent uses the fork() and execl() system calls to create the child process. Although file descriptors are inherited by the child, their values are immediately available to Program 11.2b because an execl() call has been made.

Program 11.2
 10 int main(int argc, char *argv[]) 11 { 12 13   int Size,Pid,Status,Fd1[2],Fd2[2]; 14   pipe(Fd1); pipe(Fd2); 15   strstream Buffer; 16   char Value[50]; 17   float Data; 18   vector<float> X(5,2.1221), Y; 19   Buffer << Fd1[0] << ends; 20   Buffer >> Value; 21   setenv("Fdin",Value,1); 22   Buffer.clear(); 23   Buffer << Fd2[1] << ends; 24   Buffer >> Value; 25   setenv("Fdout",Value,1); 26   Pid = fork(); 27   if(Pid != 0){ 28      ofstream OPipe; 29      OPipe.attach(Fd1[1]); 30      ostream_iterator<float> OPtr(OPipe,"\n"); 31      OPipe << X.size() << endl; 32      copy(X.begin(),X.end(),OPtr); 33      OPipe << flush; 34      ifstream IPipe; 35      IPipe.attach(Fd2[0]); 36      IPipe >> Size; 37      for(int N = 0; N < Size;N++) 38      { 39         IPipe >> Data; 40         Y.push_back(Data); 41      } 42      wait(&Status); 43      ostream_iterator<float> OPtr2(cout,"\n"); 44      copy(Y.begin(),Y.end(),OPtr2); 45      OPipe.close(); 46      IPipe.close(); 47   } 48   else{ 49          execl("./program11-2b","program11-2b",NULL); 50   } 51 52   return(0); 53 } 

In lines 21 and 25, the setenv() system call is used to pass the values of file descriptors to the child. This is possible because the child process inherits the environment of the parent process. We can set environment variables within a program using the setenv() system calls. So, in this case, we set:

 Fdin=filedesc; Fdout=filedesc; 

The child process then uses the getenv() system call to retrieve the values of Fdin and Fdout . The value in Fdin will be the read end of the pipe for the child and the value of Fdout will be the write end. Using the setenv() and getenv() system calls provide a simple form of IPC between parent and child processes. The pipes are created on line 14. On line 29 the parent attaches to one end of the pipe for writing using the attach() method. Once the attach is performed, any data inserted into the OPipe ofstream object will be written to the pipe. An ostream_iterator is connected to the OPipe object on line 30 using:

 ostream_iterator<float> OPtr(OPipe,"\n"); 

This causes the iterator OPtr to refer to OPipe . The "\n" will be inserted as a delimiter after every insertion. Using OPtr we may insert any number of float values to the pipe. We can attach more than one iterator with different types to pipe. However, this does require that on the receiving end data is extracted using the appropriate types. In Program 11.2 the number of elements is first inserted into the pipe using:

 OPipe << X.size() << endl; 

The actual elements are sent using one of the C++ standard algorithms.

 copy(X.begin(),X.end(),OPtr); 

The copy() algorithm copies the contents of its container into the container associated with the target iterator. Here the target iterator is OPtr . OPtr is connected to the OPipe so copy() causes the entire contents of the container to be written to the pipe in one line of code. This demonstrates how the standard algorithms can be used to help with the communication in parallel programming or distributed programming environments. Here the copy is sending information from one process to another process in a different address space. These processes are executing concurrently and the copy() algorithm makes the communication between the processes considerably easier. We emphasize this approach because everything that can be done to make the logic for a parallel or distributed program simpler should be done. Interprocess communication is one of the issues that complicates parallel and distributed programming. The C++ algorithms, the iostreams, and the ostream_iterator help to reduce that complexity. The flush manipulator on line 33 ensures that the data is moved along the pipe.

Program Profile 11.2

Program Name

 program11-2.cc 

Description

Program uses the iostreams and the ostream_iterator to send the contents of a vector container over an anonymous pipe.

Headers Required

[View full width]
 
[View full width]
<algorithm>, <fstream>, <vector>, <iterator>, <stdlib.h>, <string graphics/ccc.gif .h>,<unistd.h>

Compile and Link Instructions

 c++ -o program11-2 program11-2.cc 

Test Environment

SuSE Linux 7.1, 6.2

Execution Instructions

 ./program11-2 

In Program 11.2b on line 36 the child will get the number of elements to be retrieved from the pipe first and then it uses the istream object IPipe to retrieve the objects from the pipe.

Program 11.2b
 11 class multiplier{ 12   float X; 13 public: 14   multiplier(float Value) { X = Value;} 15   float &operator()(float Y) { X = (X * Y);return(X);} 16 }; 17 18 19 int main(int argc,char *argv[]) 20 { 21   char Value[50]; 22   int Fd[2]; 23   float Data; 24   vector<float> X; 25   int NumElements; 26   multiplier N(12.2); 27   strcpy(Value,getenv("Fdin")); 28   Fd[0] = atoi(Value); 29   strcpy(Value,getenv("Fdout")); 30   Fd[1] = atoi(Value); 31   ifstream IPipe; 32   ofstream OPipe; 33   IPipe.attach(Fd[0]); 34   OPipe.attach(Fd[1]); 35   ostream_iterator<float> OPtr(OPipe,"\n"); 36   IPipe >> NumElements; 37   for(int N = 0;N < NumElements;N++) 38   { 39     IPipe >> Data; 40     X.push_back(Data); 41   } 42   OPipe << X.size() << endl; 43   transform(X.begin(),X.end(),OPtr,N); 44   OPipe << flush; 45   return(0); 46 47 } 

The child process retrieves the items from the pipe, inserts them into a vector class, and then performs a mathematical transformation on each element of the vector as it is sending it back to the parent. The mathematical transformation occurs on line 43 using the standard C++ transform algorithm and a user -defined multiplier class. The transform algorithm applies an operation to each element in a container and then inserts the results into the target container. Here, the target container is Optr , which is connected to an OPipe object. The required headers for Program 11.2b are shown in Program Profile 11.2b.

Program Profile 11.2b

Program Name

 program11-2b.cc 

Description

Child process that is launched by Program 11.2. This program uses an if-stream object to receive the contents of a container that are sent from Program 11.2. The program uses a ostream_iterator object and the standard transform algorithm to send information back through the pipe to the parent.

Headers Required

[View full width]
 
[View full width]
<iostream>, <algorithm>, <fstream>, <vector>, <iterator>, <stdlib graphics/ccc.gif .h>, <string.h>, <unistd.h>

Compile and Link Instructions

 c++ -o program11-2b program11-2b.cc 

Execution Instructions

This program is spawned by Program 11.2.


Although the iostreams classes, istream_iterator , and ostream_iterator make pipe programming easy, they do not change the behavior of the system pipe construct. The blocking issues and the issues concerning the correct order to open and close the pipes discussed in Chapter 5 still apply. The underlying mechanisms of the same object-oriented programming techniques reduce the complexity of parallel and distributed programming.

11.5.3 fifos (Named Pipes), iostreams, and the ostream_iterator Classes

The techniques we used to implement object-oriented anonymous pipes had two setbacks. First, any processes involved in interprocess communication need access to the file descriptors returned from the pipe() system call. So there is the issue of getting these file descriptors to all of the processes involved. This was straightforward because the processes that were created in Programs 11.1, 11.2a, and 11.2b had parent “child relationships, which leads us to the second problem. The processes using unnamed pipes need to be related , although this requirement could be subverted with a descriptor-passing scheme. The fifo (First In-First Out) structure is the solution to the problem. Its most important advantage is it can be accessed by unrelated processes. The processes do need to be on the same machine but otherwise don't require any relationship. The processes may be running programs implemented in different languages using different programming paradigms (e.g., generic and object-oriented). Crowd computations and other peer-to-peer configurations can take advantage of fifo (sometimes called the named pipe) because in the UNIX and Linux environment the fifo has a user-defined file-name in the system and is somewhat of a permanent structure (in contrast to anonymous pipes). The fifo is a one-way structure, that is, the user of a named pipe in the UNIX environment should open it for either reading or writing, but not both. Named pipes created in the UNIX environment remain in the file system until they are explicitly removed using unlink( ) from within a program, or some form of deletion at the command prompt such as the rm command. Named pipes are given the equivalent of a file-name when they are created. Any process that knows the name of a pipe and has the necessary access permissions can open, read, and write the pipe.

To connect the anonymous pipes to the ifstream and ofstream objects, we used the nonstandard file descriptor connection. File descriptors and the iostreams are not yet tightly coupled by the ISO C++ standard. We are a lot safer using the fifo. The special file type fifo is accessed through a filename in the file system. The connection with the C++ ifstream and ofstream classes is supported. So in the same way that we simplified IPC using iostream classes with the anonymous pipe, we make fifo access easy. So the fifo that has the same basic functionality as the anonymous pipe allows us to extend the communication between unrelated classes. However, each program involved will still have to know the names of the fifos. It seems like the same restriction as we encountered with the file descriptors. However, the fifo is a definite improvement. First, only the system determines what the available file descriptors are when the anonymous pipe is opened. This is out of the programmer's control. Second, there is a limit to the number of file descriptors the system has. Third, since fifos are user-defined names, there is no limit to the names that may be used. The file descriptors must belong to previously and successfully opened files. fifo names are just names . The fifo name is user-specified ; file descriptors are system-specified. Filenames are associated with ifstream , fstream , and ofstream objects using either the constructor or the open() method. Program 11.3a uses the constructor to associate the ofstream and ifstream objects with the fifo.

Program 11.3
 14 using namespace std; 15 16 const int FMode = S_IRUSR  S_IWUSR  S_IRGRP  S_IROTH; 17 18 int main(int argc, char *argv[]) 19 { 20 21   int Pid,Status,Size; 22   double Value; 25   mkfifo("/tmp/channel.1",FMode); 26   mkfifo("/tmp/channel.2",FMode); 28   vector<double> X(100,13.0); 29   vector<double> Y; 30   ofstream OPipe("/tmp/channel.1",ios::app); 31   ifstream IPipe("/tmp/channel.2"); 32   OPipe << X.size() << endl; 33   ostream_iterator<double> Optr(OPipe,"\n"); 34   copy(X.begin(),X.end(),Optr); 35   OPipe << flush; 36   IPipe >> Size; 37   for (int N = 0;N < Size; N++) 38   { 39      IPipe >> Value; 40      Y.push_back(Value); 41   } 42 43   IPipe.close(); 44   OPipe.close(); 45   unlink("/tmp/channel.1"); 46   unlink("/tmp/channel.2"); 47   cout << accumulate(Y.begin(),Y.end(),-13.0) << endl; 48 49   return(0); 50 } 

There are two fifos in Program 11.3a. Recall that fifos are one-way communication components. So if processes are to exchange data, at least two fifos are necessary. In Program 11.3a the fifos are called channel.1 and channel2 . Notice on line 16 the permissions flags that will be set for the fifos. These are the most generic settings for UNIX/Linux setting. These permissions indicate that the owner of the fifo has read and write access and all others have read-only access to the fifo. On line 30 channel.1 is open for output only . We could have also accomplished this by:

 OPipe.open("/tmp/channel.1", ios::app); 

This says that the fifo will be opened in append mode. Program 11.3a uses the copy() algorithm to insert the objects into the OPipe fstream object and indirectly into the fifo. We could also use a fstream object here if we declare it as:

 fstream OPipe("/tmp/channel.1", ios::out  ios::app); 

This restricts the communication to output only in append mode. If we don't use the ios::app flag, the ofstream object on line 30 will make a failed attempt at creating the fifo. Unfortunately, this will not work. Creation of fifos is the province of the mkfifo() routine. Lines 40 and 41 Program 11.3a deletes the fifos from the file system. At this point in the processing any processes that happen to still have the fifo open will continue to be able to access it. However, the name will be removed. So those processes will not be able to call open() or construct any new ofstream or ifstream objects based on the filename that has been unlinked . On lines 32-34, ostream_iterator and ofstream objects are used to insert items into the fifo. Notice that Program 11.3a does not do any forking and does not have any child processes to communicate with. Program 11.3 depends on some other program to read from channel.1 or at least to write to channel.2 . If there is no program executing at the time to access the fifo, then Program 11.3a will block. The implementation specifics are contained in Program Profile 11.3a.

Program Profile 11.3a

Program Name

 program11-3.cc 

Description

Uses an ostream_iterator object and an ofstream object to send a container object through a fifo. Extracts information from a fifo using an ifstream object.

Headers Required

 <unistd.h>, <iomanip>, <algorithm>, <fstream.h>, <vector>, <iterator> <strstream.h>, <stdlib.h>, <sys/wait.h>, <sys/types.h>, <sys/stat.h> <fcntl.h>, <numeric> 

Compile and Link Instructions

 c++ -o program11-3 program11-3.cc 

Test Environment

SuSE Linux 7.1, gcc 2.95.2, Solaris 8, Sun Workshop 6

Execution Instructions

 ./program11-3 & program11-3b 

Notes

Start Program 11.3a first. Program 11.3b has a sleep statement to account for the lack of real synchronization.


Program 11.3b reads from channel.1 and writes to channel.2 .

Program 11.3b Reads from channel1 and write to channel2 .
 10 using namespace std; 11 12 class multiplier{ 13   double X; 14 public: 15   multiplier(double Value) { X = Value;} 16   double &operator()(double Y) { X = (X * Y);return(X);} 17 }; 18 19 20 int main(int argc,char *argv[]) 21 { 22 23   double Size; 24   double Data; 25   vector<double> X; 26   multiplier R(1.5); 27   sleep(15); 28   fstream IPipe("/tmp/channel.1"); 29   ofstream OPipe("/tmp/channel.2",ios::app); 30   if(IPipe.is_open()){ 31   IPipe >> Size; 32   } 33   else{ 34          exit(1); 35   } 36   cout << "Number of Elements " << Size << endl; 37   for(int N = 0;N < Size;N++) 38   { 39      IPipe >> Data; 40      X.push_back(Data); 41   } 42   OPipe << X.size() << endl; 43   ostream_iterator<double> Optr(OPipe,"\n"); 44   transform(X.begin(),X.end(),Optr,R); 45   OPipe << flush; 46   OPipe.close(); 47   IPipe.close(); 48   return(0); 49 50 } 

Notice that Program 11.3a opens channel.1 for output and Program 11.3b opens channel.1 for input. Keep in mind that fifos are one-way communication mechanisms. Don't try to send data both ways! Another advantage of using iostreams in conjunction with fifos is that you have access to the iostream methods as they would be applied to the fifo. For instance, on line 30 we use the basic_filebuf () method is_open() to determine whether the fifo is open. If it isn't Program 11.3b doesn't continue any further. The implementation specifics for Programs 11.3a and 11.3b are provided in Program Profiles 11.3a and 11.3b.

Program Profile 11.3b

Program Name

 program11-3b.cc 

Description

This program reads objects from the fifo using a ifstream object. It uses the ostream_iterator and the standard transform algorithm to send information through the fifo.

Headers Required

 <unistd.h>, <iomanip>, <algorithm>, <fstream.h>, <vector> <iterator>, <strstream.h>,<stdlib.h>, <sys/wait.h>, <sys/types.h>, <sys/stat.h>, <fcntl.h>, <numeric> 

Compile and Link Instructions

 c++ -o program11-3b program11-3b.cc 

Test Environment

SuSE Linux 7.1, gcc 2.95.2, Solaris 8, Sun Workshop 6.0

Execution Instructions

 program11.3 & program11-3b 

Notes

Start Program 11.3a first. Program 11.3b has a sleep statement to account for the lack of real synchronization.


11.5.3.1 fifo Interface Classes

In addition to simplifying the IPC using iostreams , istream_iterator , and ostream_iterator , we can also simplify matters by encapsulating the fifo into a fifo class.

Example 11.23 Declaration of the fifo class.
 class fifo{    mutex Mutex;    //... protected:    string Name; public:    fifo &operator<<(fifo &In, int X);    fifo &operator<<(fifo &In, char X);    fifo &operator>>(fifo &Out, float X);    //... }; 

Using this technique, we can easily create fif os in the constructor. We can pass them easily as parameters and return values. We can use them in conjunction with the standard container classes. The construction of such a component greatly reduces the amount of code needed to use fifos. It provides opportunities for type safety and generally allows the programmer to work at a higher level.



Parallel and Distributed Programming Using C++
Parallel and Distributed Programming Using C++
ISBN: 0131013769
EAN: 2147483647
Year: 2002
Pages: 133

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net