Pipes and Thread in C Language

In UNIX-based operating systems, pipes are a powerful feature used for inter-process communication (IPC). They allow data to flow from one process to another in a unidirectional manner, effectively making the output of one process the input of another.

This mechanism is essential for creating complex workflows and is commonly used in shell scripting and process management.

Table of Content

Table of Contents

Understanding Pipes & Threads in C Language

Key Concepts of pipe() & its Functions

What is a Pipe?

  • A pipe is a section of shared memory that processes use for communication.
  • It operates on a First-In-First-Out (FIFO) basis, ensuring that data is read in the same order it was written.
  • Pipes are unidirectional; data flows from the write end to the read end.

How Does pipe() Work?

  • The pipe() system call creates a pipe and returns two file descriptors:

    • fd[0]: The read end of the pipe.
    • fd[1]: The write end of the pipe.

Return Value:

  • Returns 0 on success.
  • Returns -1 on error.

Communication Between Processes

  • Pipes are typically used between related processes, such as a parent and its child created via fork().
  • When a process forks, the child inherits the parent’s file descriptors, including any pipes.
  • This inheritance allows the parent and child to communicate through the pipe.

Basic Pipe Usage Example

Let’s explore a simple example where a single process writes messages to a pipe and then reads them back.

				
					#include <stdio.h>
#include <unistd.h>
#include <stdlib.h> // For exit()
#define MSGSIZE 16

int main() {
    char* msg1 = "hello, world #1";
    char* msg2 = "hello, world #2";
    char* msg3 = "hello, world #3";
    char inbuf[MSGSIZE]; // Buffer for incoming data
    int p[2];            // Array to hold the two ends of the pipe

    if (pipe(p) == -1) {
        perror("pipe failed");
        exit(1);
    }

    // Write messages to the pipe
    write(p[1], msg1, MSGSIZE);
    write(p[1], msg2, MSGSIZE);
    write(p[1], msg3, MSGSIZE);

    // Read messages from the pipe
    for (int i = 0; i < 3; i++) {
        read(p[0], inbuf, MSGSIZE);
        printf("%s\n", inbuf);
    }

    return 0;
}

				
			

Explanation

  • Creating the Pipe: The pipe(p) call initializes the pipe and assigns file descriptors to p[0] and p[1].

  • Writing to the Pipe: Three messages are written to the write end p[1].

  • Reading from the Pipe: A loop reads the messages from the read end p[0] and prints them.

Parent and Child Process Communication

To demonstrate inter-process communication, let’s see how a parent and child process can share a pipe.

				
					#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>     // For exit()
#include <sys/wait.h>   // For wait()
#define MSGSIZE 16

int main() {
    char* msg1 = "hello, child #1";
    char* msg2 = "hello, child #2";
    char* msg3 = "hello, child #3";
    char inbuf[MSGSIZE];
    int p[2];
    pid_t pid;

    if (pipe(p) == -1) {
        perror("pipe failed");
        exit(1);
    }

    pid = fork();

    if (pid > 0) { // Parent process
        close(p[0]); // Close unused read end
        write(p[1], msg1, MSGSIZE);
        write(p[1], msg2, MSGSIZE);
        write(p[1], msg3, MSGSIZE);
        close(p[1]); // Close write end after writing
        wait(NULL);  // Wait for child to finish
    } else if (pid == 0) { // Child process
        close(p[1]); // Close unused write end
        while (read(p[0], inbuf, MSGSIZE) > 0) {
            printf("Child received: %s\n", inbuf);
        }
        close(p[0]); // Close read end after reading
        printf("Child finished reading\n");
        exit(0);
    } else {
        perror("fork failed");
        exit(1);
    }

    return 0;
}

				
			

Explanation

  • Pipe Creation: The parent process creates the pipe before calling fork().

  • Forking: The process splits into parent and child.

  • Parent Process:

    • Closes the read end of the pipe (p[0]) because it doesn’t need it.
    • Writes messages to the write end (p[1]).
    • Closes the write end after writing to signal EOF to the child.
    • Calls wait(NULL) to wait for the child process to complete.
  • Child Process:

    • Closes the write end of the pipe (p[1]) because it doesn’t need it.
    • Reads messages from the read end (p[0]) until EOF is reached.
    • Prints each message it receives.
    • Closes the read end and exits.

Important Points

  • Closing Unused Ends: It’s crucial to close the unused ends of the pipe in both the parent and child processes to prevent deadlocks and ensure proper behavior.
  • EOF Signaling: When the parent closes its write end, it signals EOF to the child, allowing the child’s read() call to return 0 and exit the loop.

==> Practice Exercise Question <==

Q1) Modify the previous example so that the child sends a message to the parent.

Q2) Write a C program that uses pipes for Inter-Process Communication (IPC) between a parent and child process. The parent process should prompt the user to input a number, write the number to the pipe, and then wait for the child process to complete. The child process should read the number from the pipe, calculate its factorial, and print the result. Ensure the unused ends of the pipe are closed in each process.

Code Example for Factorial Calculation using IPC (Pipe):

				
					#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int factorial(int n) {
    int fact = 1;
    for (int i = 1; i <= n; i++) {
        fact *= i;
    }
    return fact;
}

int main() {
    int fd[2];  // Array to hold the pipe descriptors
    pid_t pid;
    int number;

    if (pipe(fd) == -1) {
        printf("Pipe failed.\n");
        return 1;
    }

    printf("Enter a number: ");
    scanf("%d", &number);

    pid = fork();  // Fork a child process

    if (pid < 0) {
        printf("Fork failed.\n");
        return 1;
    }

    if (pid > 0) {  // Parent process
        close(fd[0]);  // Close reading end of the pipe
        write(fd[1], &number, sizeof(number));  // Write the number to the pipe
        close(fd[1]);  // Close writing end of the pipe
        wait(NULL);  // Wait for the child to finish
    } else {  // Child process
        close(fd[1]);  // Close writing end of the pipe
        read(fd[0], &number, sizeof(number));  // Read the number from the pipe
        close(fd[0]);  // Close reading end of the pipe

        int result = factorial(number);  // Calculate factorial
        printf("Factorial of %d is %d\n", number, result);
    }

    return 0;
}

				
			

Threads & Their Functions

Creating a Default Thread

The pthread_create() function is used to create a new thread in a process. When you don’t specify any attributes (i.e., pass NULL), a default thread is created with attributes such as:

  • Unbounded
  • Non-detached (you can later use pthread_join() to wait for this thread to terminate)
  • Default stack size and stack
  • Inherits the parent’s priority

You can also use pthread_attr_init() to initialize a thread attribute object, which also creates a default thread when passed to pthread_create().

Basic syntax:

				
					int pthread_create(pthread_t *tid, const pthread_attr_t *attr, 
                   void start_routine, void *arg);

				
			

Here:

  • tid is where the new thread ID will be stored.
  • start_routine is the function the new thread will execute.
  • arg is the argument passed to start_routine.

Creating a Thread: pthread_create()

				
					#include <pthread.h>
#include <stdio.h>

void* thread_function(void* arg) {
    printf("Thread is running...\n");
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_function, NULL);
    pthread_join(tid, NULL);
    return 0;
}

				
			

Waiting for a Thread to Terminate: pthread_join()

pthread_join() is used to wait for a thread to complete. It blocks the calling thread until the target thread specified by tid terminates.

				
					#include <pthread.h>
#include <stdio.h>

void* thread_function(void* arg) {
    printf("Thread is running...\n");
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_function, NULL);
    pthread_join(tid, NULL);
    printf("Thread has finished.\n");
    return 0;
}

				
			

This program waits for the thread to finish and then prints that it has completed.

Detaching a Thread: pthread_detach()

Instead of using pthread_join(), you can detach a thread using pthread_detach(). This means you don’t need to wait for the thread, and its resources will automatically be cleaned up when it finishes.

				
					#include <pthread.h>
#include <stdio.h>

void* thread_function(void* arg) {
    printf("Detached thread is running...\n");
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_function, NULL);
    pthread_detach(tid);  // Detach the thread
    printf("Main thread can exit without waiting for the detached thread.\n");
    return 0;
}

				
			

==> Practice Exercise Question <==

Q1) Write a C program that splits the task of summing an array of integers between multiple threads. The array will be divided into sections, and each thread will compute the sum of its assigned section. After all threads finish, the main thread should collect the partial sums and compute the total sum.

  • Use pthread_create() to create the threads.
  • Each thread should calculate the sum of a portion of the array.
  • Use pthread_join() to ensure the main thread waits for all threads to finish.
  • Print the total sum at the end.