Unix Pipes

Şerifhan Işıklı
lTunes Tribe
Published in
5 min readOct 27, 2019

Note : This article is not theoretical and entry level

A complex programming environment often uses multiple cooperating processes to perform related operations. These processes must communicate with each other and share resources and information. The OS must provide mechanisms that make this possible. These mechanisms are collectively referred to as IPC.

The purposes of IPC are:
1. Data transfer
2. Sharing data
3.Event notification
4.Resource sharing
5. Process control.

Universal IPC Facilities

There were three IPC facilities that came with the original Unix system:
1. Signals
2. Pipes
3. Process tracing

Pipes

We can think of the pipe as a special file that can store a limited amount of data in a FIFO manner. One process writes to the pipe while another process reads from the pipe. The system keeps track of the current location of the last read/write location. Data is written to one end of the pipe and read from the other. The OS provides the synchronization between the writing and reading processes. By default,

1.If a writing process attempts to write to a full pipe, the system will automatically block the process until the pipe is able to receive data
2. If a read is attempted on an empty pipe, the process will block until data is available
3. The process will block if a specified pipe has been opened for reading, but another process has not opened the pipe for writing.

Data is written to the pipe using the unbuffered I/O write system call:

ssize_t write (int filedes, const void *buf, size_t nbyte);

Using the file descriptor specified by filedes, the write system call will attempt to write nbyte bytes from the buffer referenced by buf. If successful write returns the number of bytes actually written. The O_NONBLOCK and O_NDELAY flags control the action to be taken when write is issued and device is busy. The default is that the flags are clear and write blocks.

Data is read from the pipe using the unbuffered I/O read system call:
ssize_t read (int filedes, const void *buf, size_t nbyte);

Using the file descriptor specified by filedes, the read system call will attempt to read nbyte bytes from the buffer referenced by buf. If successful read returns the number of bytes actually read. When at the end of file, 0 is returned. The O_NONBLOCK and O_NDELAY flags control the action to be taken when read is issued and device is busy. The default is that the flags are clear and write blocks.

Unnamed Pipes

A unnamed pipe is constructed with the pipe system call:
int pipe (int filedes[2]);

If successful, the pipe system call will return two integer file descriptors, filedes[0] and filedes[1]. The file descriptors reference two data streams that are both open for reading and writing. In full duplex setting, if the process writes to filedes[0] then filedes[1] is used for reading or vice versa. In half duplex setting, filedes[1] is always used for writing and filedes[0] for reading.
Program -> Parent/child communicating via a pipe

Note that closing of the unused pipe file descriptors is a good practice.
Output of Program
% a.out this_is_a_message

Message sent by parent : [this_is_a_message]
Message received by child : [this_is_a_message]

On command line, we often use pipes to connect programs so that standard output of one program becomes the standard input of the next. E.g.:
% last | sort
will execute the last command and pipe its output to sort command. To be able to do this, we need to associate the standard input and standard output with the pipe we have created. The dup system call and dup2 library function do this.
int dup2 ( int filedes, int fildes2);

When dup2 is called, the file descriptor referenced by filedes will be a duplicate of the file descriptor filedes2. If the latter is open, it will be closed.

Program -> A last | sort pipeline

Since the sequence of generating a pipe, forking a child process, duplicating the file descriptors, and passing command execution information from one process to the other via a pipe is very common, popen and pclose I/O functions are available to do this.

FILE *popen (const char *command, const char *type);
int pclose (FILE *stream);

When successful, the popen call returns a pointer to a file. The arguments are a pointer to a shell command that will be executed and an I/O mode type. If mode is ‘w’ the parent process can write to the standard input of the shell command. In this case, the child will read from the file. If mode is ‘r’, the situation is vice versa.

pclose will close the data stream opened by popen.

Program -> Using popen and pclose

Named Pipes (FIFOs)

The main difference introduced is that the named pipes have a directory entry. This facilitates the use of pipe by other processes and file access permissions are also assigned.

At the shell level, mknod command creates a named pipe.

% mknod PIPE p

The first argument is the file name for the FIFO. The second argument notifies mknod that a FIFO file is to be created.

The default file permissions for a FIFO are assigned using the standard umask arrangement. In programming environment, mknod system call is used.

int mknod (const char *path, mode_t mode, dev_t dev);

The mknod system call will create the file referenced by path, mode should be S_FIFO and dev should be 0.

The library function mkfifo can also be used for the same purpose.

Example: Client-server

Program -> The server process

Output

The client process will accept a shell command from the user. The command will be sent to server via a public FIFO for processing. Once received, the server will execute the command using the popen- pclose sequence. The server will return the output of the command to the client over the private FIFO where the client displays it to the screen. Public FIFO is in the header file to ensure that the client uses the correct pipe.

Not easy :)

--

--

Şerifhan Işıklı
lTunes Tribe

Senior Software Engineer @Dogus Teknoloji. (Fitness & cycling)