Lesson 08intermediateIPC

Pipes

Anonymous pipes for parent-child IPC. Progresses from the shell pipe operator through the pipe() system call, byte-by-byte reading, dup2 for stdin/stdout redirection, bidirectional communication with two pipes, and an introduction to named pipes (mkfifo).

pipedup2IPCmkfifofile-descriptor
1

Pipe Concepts

Conceptual overview of how pipes work — unidirectional byte streams between related processes.

01-basics.c40 lines
/*

this is a pure text file, no code here
we are talking about pipes

Introduction to Pipes


What is a Pipe?

Definition: A pipe is a unidirectional communication channel that allows data to flow from one process to another.

Purpose: Facilitates IPC by enabling the output of one process to be used as the input of another.

Types:
Anonymous Pipes: Typically used between parent and child processes. (the pipe() system call)
Named Pipes (FIFOs): Persist in the filesystem and can be used between unrelated processes. (mkfifo() system call)

Pipe Characteristics

Unidirectional: Data flows in one direction. (meaning the other way around is closed while the data is flowing)

Byte Streams: Data is treated as a continuous stream of bytes

Limited Buffer Size: POSIX defines a minimum (or maximum atomic) buffer size (PIPE_BUF) - anything less and equal to PIPE_BUF happens atomically (guaranteed to be written/read in one go)

The Pipe | Operator

Purpose: Connects the standard output (stdout) of one command to the standard input (stdin) of another.

Example:
$ ls -l | grep "file"

or:
cat file.txt | sort | uniq
Displays unique sorted lines from file.txt.



*/
2

Shell Pipe Producer

A program that writes to stdout, designed to be piped on the shell command line.

02-pipe-operator-with-programs/producer.c24 lines
/*
 * producer.c — Producer — writes data to stdout (used with shell pipe)
 *
 * Key concepts: printf to stdout, paired with consumer via pipe operator
 * Compile: gcc -o producer producer.c
 * Run:     ./prog
 */


/// the producer program will write data to stdout

// we need to run both compiled programs (consumer and producer) : ./producer | ./consumer

#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
    fprintf(stderr, "[Producer PID %d] Writing data to stdout...\n", getpid());
    fprintf(stderr, "[Producer] Usage: ./producer | ./consumer\n");
    printf("Yaniv\n");
    fprintf(stderr, "[Producer PID %d] Wrote \"Yaniv\" to stdout. Exiting.\n", getpid());
    return 0;
}
3

Shell Pipe Consumer

A program that reads from stdin, completing the shell pipe: ./producer | ./consumer.

02-pipe-operator-with-programs/consumer.c24 lines
/*
 * consumer.c — Consumer — reads from stdin (used with shell pipe)
 *
 * Key concepts: scanf from stdin, paired with producer: ./producer | ./consumer
 * Compile: gcc -o consumer consumer.c
 * Run:     ./prog
 */


/// the consumer program will read the output of the producer program using stdin

// we need to run both compiled programs (consumer and producer) : ./producer | ./consumer

#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
    fprintf(stderr, "[Consumer PID %d] Reading from stdin...\n", getpid());
    char buffer[100];
    scanf("%s", buffer);
    printf("[Consumer PID %d] Received via pipe: \"%s\"\n", getpid(), buffer);
    return 0;
}
4

pipe() System Call

Using pipe() to create a pipe in code, then fork() so the parent writes and the child reads.

03-pipe-system-call.c133 lines
Expected Outputclick Run to execute live
========================================
  The pipe() system call: parent write
========================================

[Parent PID 378386] Writing to pipe: "Hello, Pipe!"
[Parent PID 378386] Wrote 13 bytes. Closing write end.

========================================
  The pipe() system call: parent write
========================================

[Child  PID 378387] Received from pipe: "Hello, Pipe!" (13 bytes)
[Child  PID 378387] String length: 12
5

Byte-by-Byte Reading

Reading from a pipe one byte at a time — demonstrating the blocking behavior when the pipe is empty.

03.5-pipe-reading-1-byte.c73 lines
/*
 * 03.5-pipe-reading-1-byte.c — Reading from pipe one byte at a time
 *
 * Key concepts: read() with size 1, byte-by-byte pipe reading, EOF detection
 * Compile: gcc -o byte_pipe 03.5-pipe-reading-1-byte.c
 * Run:     ./prog
 */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
int main() {
    printf("\n");
    printf("══════════════════════════════════════\n");
    printf("  Byte-by-Byte Pipe Reading\n");
    printf("  read(fd, &c, 1) in a loop\n");
    printf("══════════════════════════════════════\n\n");
    int my_pipe[2];
    pid_t pid;
    char c;

    // Create the pipe
    if (pipe(my_pipe) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    // Fork the process
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) { // Child Process: Reader
        close(my_pipe[1]); // Close unused write end

        ssize_t bytesRead;
        while ((bytesRead = read(my_pipe[0], &c, 1)) > 0) {
            // Example processing: Print each character
            printf("[Child  PID %d] Read byte: '%c' (0x%02x)\n", getpid(), c, (unsigned char)c);
        }

        if (bytesRead == -1) {
            perror("read");
            exit(EXIT_FAILURE);
        }

        close(my_pipe[0]); // Close read end
        exit(EXIT_SUCCESS);
    } else { // Parent Process: Writer
        close(my_pipe[0]); // Close unused read end

        const char *message = "Hello, Pipe!";
        printf("[Parent PID %d] Writing \"%s\" one byte at a time...\n", getpid(), message);
        ssize_t len = 0;
        sleep(2);
        while (message[len] != '\0') {
            if (write(my_pipe[1], &message[len], 1) != 1) {
                perror("write");
                exit(EXIT_FAILURE);
            }
            len++;
        }

        printf("[Parent PID %d] Wrote %ld bytes. Closing write end (signals EOF).\n", getpid(), len);
        close(my_pipe[1]); // Close write end to signal EOF
        wait(NULL); // Wait for child to finish
    }

    return 0;
}
Expected Outputclick Run to execute live
========================================
  Reading from pipe one byte at a time
========================================

Child received: H
Child received: e
Child received: l
Child received: l
Child received: o
Child received: ,
Child received:  
Child received: P
Child received: i
Child received: p
Child received: e
Child received: !

========================================
  Reading from pipe one byte at a time
========================================
6

More Pipe Examples

Parent writes character-by-character into a pipe, child reads it all. Demonstrates pipe buffering, closing the write end to signal EOF, and how read() returns 0 at end-of-stream.

04-more-pipe.c74 lines
/*
 * 04-more-pipe.c — More pipe examples — parent/child communication
 *
 * Key concepts: pipe(), fork(), read/write, closing unused ends
 * Compile: gcc -o more_pipe 04-more-pipe.c
 * Run:     ./prog
 */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#define BUFFER_SIZE 1024


/*

Parent Writing All Data Before Child Reads:

The parent writes the message character by character into the pipe. Since write() is non-blocking (buffer has enough space), the parent continues writing without waiting for the child to read each character.
The child only begins to process data when it executes its read() loop after the parent's write() calls have already started.
Closing the Write End:

When the parent closes my_pipe[1], it signals EOF to the child. This EOF is not immediately "received" by the child but is interpreted as the end of the data stream when the child has read all the buffered data.
After closing the write end, the child's read() detects EOF (when read() returns 0) and exits the loop.

*/

int main() {
    printf("\n");
    printf("══════════════════════════════════════\n");
    printf("  Pipe: Char-by-Char Write, Bulk Read\n");
    printf("  Parent writes loop → Child reads all\n");
    printf("══════════════════════════════════════\n\n");
    int pipefd[2];
    pid_t pid;
    ssize_t bytes;
    char buffer[BUFFER_SIZE];

    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    pid = fork();
    if (pid == 0) { // Child: Reader
        close(pipefd[1]); // Close write end
        while ((bytes = read(pipefd[0], buffer, sizeof(buffer)-1)) > 0) {
            buffer[bytes] = '\0';
            printf("[Child  PID %d] Read %ld bytes: \"%s\"", getpid(), (long)bytes, buffer);
        }
        close(pipefd[0]);
        exit(EXIT_SUCCESS);
    } else { // Parent: Writer
        close(pipefd[0]); // Close read end
        const char *msg = "Hello, this is a message from the parent.\n";
        printf("[Parent PID %d] Writing %ld bytes to pipe...\n", getpid(), (long)strlen(msg));
        size_t len = strlen(msg);
        ssize_t total_written = 0;

        while (total_written < len) {
            bytes = write(pipefd[1], msg + total_written, len - total_written);
            if (bytes == -1) {
                perror("write");
                break;
            }
            total_written += bytes;
        }
        close(pipefd[1]);
    }

    return 0;
}
Expected Outputclick Run to execute live
========================================
  More pipe examples — parent/child 
========================================


========================================
  More pipe examples — parent/child 
========================================

Child read: Hello, this is a message from the parent.
7

Pipe with File Descriptors

Working directly with pipe file descriptors — read end (fd[0]) and write end (fd[1]).

05-pipe-with-fd.c55 lines
/*
 * 05-pipe-with-fd.c — Using pipes with file descriptor operations
 *
 * Key concepts: pipe(), file descriptors, read/write
 * Compile: gcc -o pipe_fd 05-pipe-with-fd.c
 * Run:     ./prog
 */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>

int main() {
    printf("\n");
    printf("══════════════════════════════════════\n");
    printf("  Pipe with FILE* (fdopen)\n");
    printf("  Use fprintf/fscanf instead of raw read/write\n");
    printf("══════════════════════════════════════\n\n");
    int pipefd[2];
    pid_t pid;
    char buffer[100];
    FILE *read_fp, *write_fp;

    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    pid = fork();
    if (pid == 0) { // Child: Reader
        close(pipefd[1]); // Close write end
        read_fp = fdopen(pipefd[0], "r");
        if (read_fp == NULL) {
            perror("fdopen");
            exit(EXIT_FAILURE);
        }
        fscanf(read_fp, "%99s", buffer);
        printf("[Child  PID %d] Read via FILE*/fscanf: \"%s\"\n", getpid(), buffer);
        fclose(read_fp);
        exit(EXIT_SUCCESS);
    } else { // Parent: Writer
        close(pipefd[0]); // Close read end
        printf("[Parent PID %d] Writing via FILE*/fprintf...\n", getpid());
        write_fp = fdopen(pipefd[1], "w");
        if (write_fp == NULL) {
            perror("fdopen");
            exit(EXIT_FAILURE);
        }
        fprintf(write_fp, "Hello_File_Stream\n");
        fclose(write_fp);
    }

    return 0;
}
Expected Outputclick Run to execute live
========================================
  Using pipes with file descriptor ope
========================================

Child received via FILE*: Hello_File_Stream

========================================
  Using pipes with file descriptor ope
========================================
8

dup2 + exec Pattern

The classic pattern: dup2 to redirect stdin/stdout to pipe ends, then exec a child program that reads/writes normally.

06-pipe-exec-dup.c146 lines
Expected Outputclick Run to execute live
========================================
  Connecting two programs with pipe + 
========================================


========================================
  Connecting two programs with pipe + 
========================================

execlp producer: No such file or directory
execlp consumer: No such file or directory
9

Bidirectional Communication

Using two pipes for two-way parent-child communication — one pipe per direction.

07-two-ways-communication.c117 lines
Expected Outputclick Run to execute live
========================================
  Bidirectional IPC using two pipes
========================================

[Child  PID 378509] Received from parent via pipe1: "Hello from Parent!"
[Child  PID 378509] Sending reply via pipe2...

========================================
  Bidirectional IPC using two pipes
========================================

[Parent PID 378508] Sending message to child via pipe1...
[Parent PID 378508] Received from child via pipe2: "Hello from Child!"
10

Bidirectional Parent (dup2)

Parent side of a two-way pipe setup using dup2 for clean fd management.

08-two-ways-with-dupes/parent-program.c133 lines
11

Bidirectional Child (dup2)

Child side that communicates with the parent through redirected stdin/stdout.

08-two-ways-with-dupes/child.c26 lines
/*
 * child.c — Child — reads stdin, writes stdout (duped from pipes)
 *
 * Key concepts: Reads from redirected stdin, writes to redirected stdout
 * Compile: gcc -o child child.c
 * Run:     ./prog
 */
// child_program.c
#include <stdio.h>
#include <string.h>

int main() {
    char buffer[100];

    // Read message from parent (via redirected stdin ← pipe)
    if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
        buffer[strcspn(buffer, "\n")] = '\0';
        // stdout goes back to parent via the other pipe
        printf("[Child] Received from parent: \"%s\"\n", buffer);
    }

    printf("[Child] Sending reply back to parent.\n");

    return 0;
}
12

Named Pipe Reader

Introduction to mkfifo — creating a named pipe on the filesystem and reading from it.

09-mkfifo/reader.c53 lines
/*
 * reader.c — Named pipe reader — intro to mkfifo (FIFO)
 *
 * Key concepts: mkfifo, open, read from named pipe
 * Compile: gcc -o reader reader.c
 * Run:     ./prog
 */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define FIFO_PATH "/tmp/my_fifo"
// FIRST create the fifo like:
// terminal -> mkfifo /tmp/my_fifo
// than we can open 2 terminals
// terminal 1: start the reader: (reading blocks remebebr!?!?!) ./reader
// terminal 2: start the writer ./writer


int main() {
    printf("\n");
    printf("══════════════════════════════════════\n");
    printf("  Named Pipe (FIFO) Reader\n");
    printf("  First: mkfifo %s\n", FIFO_PATH);
    printf("══════════════════════════════════════\n\n");
    char buffer[100];

    printf("[Reader PID %d] Opening FIFO %s for reading...\n", getpid(), FIFO_PATH);
    printf("[Reader] (blocks until a writer opens the other end)\n");
    fflush(stdout);
    int fifo_fd = open(FIFO_PATH, O_RDONLY);
    if (fifo_fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // Read the message from the FIFO
    ssize_t bytesRead = read(fifo_fd, buffer, sizeof(buffer) - 1);
    if (bytesRead == -1) {
        perror("read");
        close(fifo_fd);
        exit(EXIT_FAILURE);
    }

    buffer[bytesRead] = '\0'; // Null-terminate the string
    printf("[Reader PID %d] Received %ld bytes: \"%s\"\n", getpid(), (long)bytesRead, buffer);

    close(fifo_fd);
    return 0;
}
13

Named Pipe Writer

Writing to a named pipe — run alongside the reader to see data flow between unrelated processes.

09-mkfifo/writer.c54 lines
/*
 * writer.c — Named pipe writer — writing to a FIFO
 *
 * Key concepts: open, write to named pipe, FIFO blocks until reader connects
 * Compile: gcc -o writer writer.c
 * Run:     ./prog
 */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

// FIRST create the fifo like:
// terminal -> mkfifo /tmp/my_fifo
// than we can open 2 terminals
// terminal 1: start the reader: (reading blocks remebebr!?!?!) ./reader
// terminal 2: start the writer ./writer



#define FIFO_PATH "/tmp/my_fifo"

int main() {
    printf("\n");
    printf("══════════════════════════════════════\n");
    printf("  Named Pipe (FIFO) Writer\n");
    printf("  First: mkfifo %s\n", FIFO_PATH);
    printf("══════════════════════════════════════\n\n");
    const char *message = "Hello from the Writer!\n";

    printf("[Writer PID %d] Opening FIFO %s for writing...\n", getpid(), FIFO_PATH);
    printf("[Writer] (blocks until a reader opens the other end)\n");
    fflush(stdout);
    int fifo_fd = open(FIFO_PATH, O_WRONLY);
    // you can also you File* fifo_fd = fopen(FIFO_PATH, "w");
    if (fifo_fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // Write the message to the FIFO
    if (write(fifo_fd, message, sizeof(char) * strlen(message)) == -1) {
        perror("write");
        close(fifo_fd);
        exit(EXIT_FAILURE);
    }

    printf("[Writer PID %d] Wrote %ld bytes to FIFO: \"%s\"\n", getpid(), (long)strlen(message), "Hello from the Writer!");

    close(fifo_fd);
    return 0;
}

Open Full Terminal

Get a Linux terminal with all 13 files from this lesson pre-loaded. Compile, run, and experiment freely.