Day 4 of 5
⏱ ~60 minutes
C Programming in 5 Days — Day 4

Systems Programming: Processes & Threads

C is the language of operating systems. Today you write programs that create processes, communicate between them, and use threads for concurrency — the same primitives that power every Unix-derived OS.

fork, exec, and wait

fork() creates an exact copy of the current process. The parent gets the child's PID; the child gets 0. exec() replaces the current process image with a new program. waitpid() reaps a child process, collecting its exit status and preventing zombie processes. The fork+exec pattern is how every shell launches programs. Pipes (pipe()) connect process stdout to another's stdin.

POSIX Threads (pthreads)

pthread_create() starts a new thread executing a function. pthread_join() waits for it to finish. Threads share the process's address space — which enables fast communication but requires synchronization. Mutex (pthread_mutex_t) ensures only one thread accesses shared data at a time. Condition variables (pthread_cond_t) let threads wait for events efficiently without busy-waiting.

Signals

Signals are asynchronous notifications: SIGINT (Ctrl+C), SIGSEGV (segfault), SIGTERM (kill), SIGCHLD (child exited). sigaction() installs handlers. Signal-safe functions are limited — you cannot safely call malloc or printf inside a signal handler. The self-pipe trick: write a byte to a pipe in the handler, read it in the main event loop. Signal handling is where many subtle C bugs live.

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

#define NTHREADS 4
#define ARRAY_SIZE 1000000

long data[ARRAY_SIZE];
long partial[NTHREADS];

typedef struct { int id; int start; int end; } Args;

void *sum_range(void *arg) {
    Args *a = arg;
    long s = 0;
    for (int i = a->start; i < a->end; i++) s += data[i];
    partial[a->id] = s;
    return NULL;
}

int main(void) {
    for (int i = 0; i < ARRAY_SIZE; i++) data[i] = i + 1;

    pthread_t threads[NTHREADS];
    Args args[NTHREADS];
    int chunk = ARRAY_SIZE / NTHREADS;

    for (int i = 0; i < NTHREADS; i++) {
        args[i] = (Args){i, i*chunk, (i+1)*chunk};
        pthread_create(&threads[i], NULL, sum_range, &args[i]);
    }
    for (int i = 0; i < NTHREADS; i++) pthread_join(threads[i], NULL);

    long total = 0;
    for (int i = 0; i < NTHREADS; i++) total += partial[i];
    printf("Sum: %ld\n", total);  // 500000500000
    return 0;
}
// gcc -O2 -pthread threads.c -o threads
💡
Never share data between threads without synchronization. Even reading a variable that another thread writes is undefined behavior in C11. Use a mutex, atomic operation, or design your data to be immutable after thread creation.
📝 Day 4 Exercise
Build a Process Pipeline
  1. Write a C program that creates a pipe and forks two processes
  2. Parent writes 100 lines of text to the pipe's write end
  3. Child reads from the pipe's read end and counts lines
  4. Parent calls waitpid() to collect the child's exit status
  5. Extend: chain three processes with two pipes (like ls | grep | wc)

Day 4 Summary

  • fork() copies the process; exec() replaces it; waitpid() reaps children
  • Threads share memory — use mutexes for any shared mutable data
  • pthread_create/join are the POSIX thread lifecycle functions
  • Signals are asynchronous — only async-signal-safe functions are safe in handlers
  • Compile with -pthread for POSIX threads; -lm for math
Challenge

Implement a thread pool in C: a fixed number of worker threads that pull tasks from a thread-safe queue. Submit 100 tasks, each sleeping 10ms, and measure total runtime vs. a single-threaded approach.

Finished this lesson?