03 - Inter Process Communication - Pipes and FIFO

Class: CSCE-313


Notes:

Outline

  1. The pipe system call (see beej.us)
    • By default pipes do not have a name
    • But there are named pipes in linux
  2. The fork system call
    • Create copies of processes
  3. fifo or named pipes
    • Exists as types in the file system
    • But ntfs does not support fifo as a named pipe

Pipes

Real-time communication between processes

Inter-process communication (IPC) is a mechanism that allows processes to communicate with each other and synchronize their actions. Processes can communicate with each other through both:

  1. Message passing
  2. Shared Memory

Pasted image 20260207202340.png300

Notes:

What is a pipe in Linux?

Pasted image 20260207202412.png400

A pipe is created via the pipe() system call.

int pipe(int fd[2])

Pasted image 20260207202600.png300

Notes:

man 2 pipe

$ man 2 pipe
NAME
     pipe – create descriptor pair for interprocess communication

SYNOPSIS
     #include <unistd.h>
     int pipe(int fildes[2]);

DESCRIPTION
     The pipe() function creates a pipe (an object that allows unidirectional data flow) and allocates
     a pair of file descriptors.  The first descriptor connects to the read end of the pipe; the second
     connects to the write end.

     Data written to fildes[1] appears on (i.e., can be read from) fildes[0].  This allows the output
     of one program to be sent to another program: the source's standard output is set up to be the
     write end of the pipe; the sink's standard input is set up to be the read end of the pipe.  The
     pipe itself persists until all of its associated descriptors are closed.

     A pipe whose read or write end has been closed is considered widowed.  Writing on such a pipe
     causes the writing process to receive a SIGPIPE signal.  Widowing a pipe is the only way to
     deliver end-of-file to a reader: after the reader consumes any buffered data, reading a widowed
     pipe returns a zero count.

     The generation of the SIGPIPE signal can be suppressed using the F_SETNOSIGPIPE fcntl command.

How to create a pipe?

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char* argv[]){
    int fd[2];
    char buf[30];
    
    //create pipe
    if(pipe(fd) == -1){
        perror("pipe");
        exit(EXIT_FAILURE);
    }
    //write to pipe
    printf("writing to file descriptor #%d\n", fd[1]);
    write(fd[1],"CSCE 313",9);
    
    //read from pipe
    printf("reading from file descriptor #%d\n", fd[0]);
    read(fd[0], buf,9);
    printf("read \"%s\"\n",buf);
    return 0;
}

Notes:

int fd[2]
pipe(fd)
write(fd[1], "CSCE-313", 9)
read(fd[0], buf, 9)

How to use a pipe?

Pasted image 20260202173312.png200

When any bytes are written to fd[1],the OS makes them available for reading from fd[0].

Linux pipe command example 1 (cmd1 | cmd2)

...

Notes:

$ cat /usr/share/dict/word
$ cat /usr/share/dict/word | less

Linux pipe command example 2 (cmd1 | cmd2)

...

Notes:

$ cat /usr/share/dict/word | grep 'zy'
$ cat /usr/share/dict/word | grep 'zy.*s'
$ cat /usr/share/dict/word | grep 'zy.*s$'

Linux command example 3 (cmd1 | cmd2)

...

Linux command example 4 (cmd1 | cmd2)

...

Notes:

cat games.txt | sort | uniq | head -3 > top3.txt

Unix philosophy

...

Notes:

Linux command example 6 (cmd1 | cmd2)

Notes:

Forks

UNIX fork()

pid_t fork(void);

Notes:

Pasted image 20260207193920.png200

UNIX fork() example 1

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
	fork();
	
	printf("Welcome to CSCE-313:\n");
	return 0;
}

Notes:

UNIX fork() example 2

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    fork();
    fork();
    printf("Welcome to CSCE 313!\n");
    return 0;
}
davidkebo@CSCE-C02F159MMD6M forks % ./a.out
Welcome to CSCE 313!
Welcome to CSCE 313!
Welcome to CSCE 313!
Welcome to CSCE 313!

Pasted image 20260130135316.png

An aside-execl

#include <unistd.h>
int main() {
	execl("/bin/echo", "echo", "Hello World", NULL);
	return 1;
}

exec is a system call that replaces a process’s user address space with data read from an executable file.

Notes:

Another example:

less /usr/share/dict/words
execl less /usr/share/dict/words

Looking ahead-exec family of functions

See online man page. Only the first two are of interest to us at this time.

int execl ( const char *path, const char *arg, ... );
int execlp( const char *file, const char *arg, ... );
int execle( const char *path, const char *arg, ..., char *const envp[] );

int execv ( const char *path, char *const argv[] );
int execvp( const char *file, char *const argv[] );
int execve( const char *path, char *const argv[], char *const envp[] );

What is a file descriptor?

Imagine you're at a library.

Key point: the descriptor is not the file itself, just a handle.

Notes:

Analogy (library):

What is a file descriptors actually?

Pasted image 20260202140246.png

fork() and pipe()

It is not useful for one process to use a pipe to talk to itself. Typically, a process creates a pipe just before it forks more child processes.

The pipe is inherited by the children, and then used for communicating either between the parent & child processes, or between two sibling processes.

In the following program, the parent writes a message to the pipe.

The child reads from the pipe 1 byte at a time until the pipe is empty.

Notes:

Pasted image 20260130142119.png500

fork() and pipe() example 1

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main(int argc, char* argv[]) {
    int pipefds[2];
    pid_t pid;
    char buf[30];
    
    //create pipe
    if(pipe(pipefds)==-1){
        perror("pipe");
        exit(EXIT_FAILURE);
    }
    
    memset(buf,0,30);
    pid = fork();
    
    if(pid>0){
        printf("PARENT: writing to the pipe\n");
        //parent close the read end
        close(pipefds[0]);
        //parent write in the pipe write end
        write(pipefds[1],"CSCI3150",9);
        //after finishing writing, parent close the write end
        close(pipefds[1]);
        //parent wait for child
        wait(NULL);
    } else {
        //child did not close the write end
        //child read from the pipe read end until the pipe is empty
        while(read(pipefds[0],buf,1)==1){
            printf("CHILD read from pipe --%s\n",buf);
        }
        close(pipefds[0]);
        printf("CHILD: EXITING!");
        exit(EXIT_SUCCESS);
    }
    
    return 0;
}
davidkebo@CSCE-C02F159MMD6M pipes \% ./a.out
PARENT: writing to the pipe
CHILD read from pipe --C
CHILD read from pipe --S
CHILD read from pipe --C
CHILD read from pipe --I
CHILD read from pipe --3
CHILD read from pipe --1
CHILD read from pipe --5
CHILD read from pipe --0
CHILD read from pipe --

Problem:
The child process doesn't exit because pipefds[1] is open. The system assumes that a write could occur while the write end is still open, and the system will not report EOF.

The child blocks and waits to read from pipefds[0], and the operating system doesn't know that no process will be writing to pipdfds[1].

fork() and pipe() example 2

int main (int argc, char *argv[]) {
    int pipefds[2];
    pid_t pid;
    char buf[30];
    
    if (pipe (pipefds) == -1) {
        perror ("pipe");
        exit (EXIT_FAILURE);
    }
    
    memset (buf, 0, 30);
    pid = fork ();
    
    if (pid > 0) {
        printf ("PARENT: writing to the pipe\n");
        close (pipefds[0]);
        write (pipefds[1], "CSCI3 150", 8);
        close (pipefds[1]);
        wait (NULL);
    } else {
        close(pipefds[1]);    // IMPORTANT!! (close right end of pipe)
        while (read (pipefds[0], buf, 1) == 1) {
            printf ("CHILD read from pipe --%s\n", buf);
        }
        close (pipefds[0]);
        printf ("CHILD: EXITING!");
        exit (EXIT_SUCCESS);
    }
    
    return 0;
}
davidkebo@CSCE-C02F159MMD6M week3 % ./a.out
PARENT: write in pipe.
CHILD read from pipe --C
CHILD read from pipe --S
CHILD read from pipe --C
CHILD read from pipe --I
CHILD read from pipe --3
CHILD read from pipe --1
CHILD read from pipe --5
CHILD read from pipe --0
CHILD read from pipe --
CHILD: EXITING!

Note:

Pasted image 20260202142351.png500

Pipes for bidirectional communication

Pasted image 20260202142628.png

Problem:

Pasted image 20260202142705.png

Solution:

Copy of a file descriptor: dup2()

The dup2() system call creates a copy of a file descriptor.

int dup2(int oldfd, int newfd)

oldfd: old file descriptor
newfd: new file descriptor which is used by dup2() to create a copy.

Pasted image 20260130142544.png600

Bash uses dup2() with pipes to link commands together.

Example: ls | sort

  1. The ls process closes its read end of the pipe and links the write end to its standard output.
  2. The sort process closes the write end of the pipe and links the read end to become its standard input.
  3. ls closes the write end of the pipe, sort closes the read end of the pipe.

Anything that ls writes to its standard output, sort would read from its standard input.

Notes:

dup2 in action

Pasted image 20260130143031.png350

Limitations of pipes

Notes:

Pasted image 20260202143145.png200

Using Files for Inter Process Communication

Two programs can communicate with one another by using an intermediate medium such as a file.

davidkebo@linux:~$ ls
Desktop Documents Downloads Music Pictures Public Templates Videos code src-cloud src-cloud.zip
davidkebo@linux:~$ ls> content.txt
davidkebo@linux:~$ more content.txt
Desktop
Documents
Downloads
Music
Pictures
Public
Templates
Videos
code
content.txt
src-cloud
src-cloud.zip

FIFO

FIFO (named PIPE)

FIFO is a first-in, first-out message-passing IPC in which bytes are sent and received as unstructured streams. It is also known as a named pipe.

The named pipe is a POSIX pipe in contrast to anonymous pipes created with the pipe() system call.

FIFOs work by associating a filename with the pipe. Once created, any process (with correct access permissions) can access the FIFO by calling open() on the associated filename.

Once the processes have opened the file, they can use the standard read() and write() functions to communicate.


In Linux, we can create a FIFO with the commands mknod (using the letter "p" to indicate the FIFO type) or mkfifo.

C library functions - <sys/stat.h>

int mkfifo (const char *pathname, mode_t mode)
Creates a new FIFO identified by the pathname

davidkebo@linux:~/pipes$ mknod pipe2 p
davidkebo@linux:~/pipes$ mkfifo pipe1
davidkebo@linux:~/pipes$ ls -l
total 0
Prw-rw-r-- 1 davidkebo davidkebo 0 Sep 11 17:33 pipe1
prw-rw-r-- 1 davidkebo davidkebo 0 Sep 11 17:33 bipe2

A common use for FIFOs is to create client/server applications on the
same machine.

E.g., An anti-virus server running in the background, scanning for infected
files.

To get a report on potentially bad files, we run a client application that
uses a FIFO to connect to the server.


FIFO (named PIPE) example

Write as client-server program:

The server should print “hello” whenever the client writes a non-zero value
to a file.

The server should shut down when the client writes a zero.

Pasted image 20260204135418.png350

FIFO (named PIPE) example – Server

int main(int argc, char const *argv[]) {
    /* Create the FIF0*/
    const char *FIFO = "/tmp/MY_FIF0";
    assert (mkfifo (FIFO, S_IRUSR | S_IWUSR) == 0);
    /* Onen the FIFO, Delete FIFO if onen() fails */
    int fifo = open (FIFO, O_RDONLY);
    if (fifo == -1)
    {
        fprintf (stderr, "Failed to open FIFO\n");
        unlink (FIFO);
        return 1;
    }
    /* Main server loop */
    while (1)
    {
        int req = 0;
        if (read (fifo, &req, sizeof (int)) != sizeof (int))
        continue;
        /* If we read a 0, quit; otherwise print hello */
        if (req == 0)
        break;
        printf ("hello\n");
    }
    /* Read a 0 from the FIFO, so close and delete the FIFO */
    close (fifo);
    printf ("Deleting FIFO\n");
    unlink (FIFO);
    return 0;
}

Explanation:

	assert (mkfifo (FIFO, S_IRUSR | S_IWUSR) == 0);
	int fifo = open (FIFO, O_RDONLY);
	...
	if (read (fifo, &req, sizeof (int)) != sizeof (int))
	break;

FIFO (named PIPE)

int main(int argc, char const *argv[]) {

const char *FIFO = "/tmp/MY_FIFO";

/* Use the file name to open the FIFO for writing */
int fifo = open (FIFO, O_WRONLY);
assert (fifo != -1);

/* Open the FIFO 6 times, writing an int each time */
for (int index = 5; index >= 0; index--)
    {
        /* Write 5, 4, 3, 2, 1, 0 into the FIFO */
        int msg = index;
        write (fifo, &msg, sizeof (int));
        
        /* Add a slight delay each time */
        sleep (1);
    }
    
/* Close the FIFO */
close (fifo);
    return 0;
}
davidkebo@CSCE-C02F159MMD6M fifo \% ./client
davidkebo@CSCE-C02F159MMD6M fifo \% ./server
hello
hello
hello
hello
hello
Deleting FIFO

Explanation:

Notes:

Multiple readers & writers on a FIFO

Pasted image 20260204140222.png700

Notes:

Limitations of FIFO

fork() - a reconsideration

A fork() in the road

Some caveats about fork

Why do people like fork?

Fork today

Notes:

Consider using posix_spawn() & variant.