Practice Exercises

Concept

Exercises by level. Solve them on Linux (or WSL) and compare with the course examples. Three complete solutions follow in the Example section.

Convert numbers between bases by hand and in C · Build a 4-bit subtractor from gates · Extend the TinyCPU with MUL and compute 5! · Print the sizes of all C types · Write a program with 3 sources and link it manually · Use strace to find which config files a program opens

Write a mini shell with fork/exec/wait and > redirection · Implement ls | wc -l with pipes · Build a ps clone from /proc · Solve producer–consumer with condition variables · Simulate FCFS/SJF/RR for your own workload · Implement a thread pool · Measure system-call cost vs function-call cost

Cache simulator with configurable size, associativity and LRU · Page-table walker and TLB simulator · FIFO/LRU/Clock page replacement comparison · Buddy allocator in C · Detect deadlocks in a wait-for graph · Disk-usage tool with nftw · Measure false sharing and fix it

Lock-free single-producer/single-consumer ring buffer with acquire/release atomics · Pipeline simulator with forwarding and branch prediction · A tiny container with namespaces (unshare, clone flags) · A memory allocator with size classes · Profile and speed up a real program 2× with evidence · Port a small kernel-style scheduler to an RTOS on a microcontroller

Why It Matters

OS and architecture knowledge only sticks when you build and measure things yourself.

Syntax

Compile C exercises with gcc -O2 -Wall -Wextra -pthread file.c -o prog; add -fsanitize=address or -fsanitize=thread while developing.

Example

32.1 A ps clone built from /proc 32.2 A thread pool with a bounded work queue

32.3 Disk usage of a directory tree with nftw

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <dirent.h>

/* A tiny `ps`: every numeric directory in /proc is a process */
int main(void)
{
    DIR *d = opendir("/proc");
    struct dirent *e;
    int count = 0;
    printf("  PID  PPID S THREADS  RSS(KB) COMMAND\n");
    while ((e = readdir(d))) {
        if (!isdigit((unsigned char)e->d_name[0])) continue;
        char path[300], line[256], name[64] = "?", state = '?';
        int ppid = 0, threads = 0; long rss = 0;
        snprintf(path, sizeof path, "/proc/%s/status", e->d_name);
        FILE *f = fopen(path, "r");
        if (!f) continue;
        while (fgets(line, sizeof line, f)) {
            sscanf(line, "Name: %63s", name);
            sscanf(line, "State: %c", &state);
            sscanf(line, "PPid: %d", &ppid);
            sscanf(line, "Threads: %d", &threads);
            sscanf(line, "VmRSS: %ld", &rss);
        }
        fclose(f);
        printf("%5s %5d %c %7d %8ld %s\n", e->d_name, ppid, state, threads, rss, name);
        count++;
    }
    closedir(d);
    printf("%d processes\n", count);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

/* A fixed-size thread pool with a mutex + condition-variable work queue */
enum { WORKERS = 4, QUEUE = 64 };
typedef struct { void (*fn)(int); int arg; } Task;

static Task queue[QUEUE];
static int head, tail, count, shutting_down;
static pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER, not_full = PTHREAD_COND_INITIALIZER;
static long results[32];
static int done_by[WORKERS];

static void submit(void (*fn)(int), int arg)
{
    pthread_mutex_lock(&m);
    while (count == QUEUE) pthread_cond_wait(&not_full, &m);
    queue[tail] = (Task){fn, arg}; tail = (tail + 1) % QUEUE; count++;
    pthread_cond_signal(&not_empty);
    pthread_mutex_unlock(&m);
}

static void *worker(void *arg)
{
    int id = (int)(long)arg;
    for (;;) {
        pthread_mutex_lock(&m);
        while (count == 0 && !shutting_down) pthread_cond_wait(&not_empty, &m);
        if (count == 0 && shutting_down) { pthread_mutex_unlock(&m); return NULL; }
        Task t = queue[head]; head = (head + 1) % QUEUE; count--;
        pthread_cond_signal(&not_full);
        pthread_mutex_unlock(&m);
        t.
…
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <ftw.h>
#include <sys/stat.h>

/* Disk usage of a directory tree with nftw(): files, directories, total bytes */
static long files, dirs, bytes;

static int visit(const char *path, const struct stat *st, int type, struct FTW *ftw)
{
    (void)path; (void)ftw;
    if (type == FTW_F) { files++; bytes += st->st_size; }
    else if (type == FTW_D) dirs++;
    return 0;
}

int main(void)
{
    nftw("/usr/include/linux", visit, 16, FTW_PHYS);
    printf("/usr/include/linux: %ld files in %ld directories, %.1f MB\n", files, dirs, bytes / 1e6);
    return 0;
}

Expected Result

The process list is small because this WSL distribution runs only a few processes (the WSL init processes, the session, bash, and the program itself — in state R, running).

PID  PPID S THREADS  RSS(KB) COMMAND
    1     0 S       2     2252 init(Ubuntu-22.
    6     1 S       2     2044 init
   21     1 S       1     1108 SessionLeader
   22    21 S       1     1184 Relay(23)
   23    22 S       1     3616 bash
   24    23 R       1     1812 m32_ps
6 processes
fib(10) = 55, fib(31) = 1346269
32 tasks submitted, 32 completed by 4 workers
/usr/include/linux: 759 files in 29 directories, 4.4 MB

Common Mistakes

Skipping error checks in system-level code (every system call can fail).

Testing concurrency code only once; races need many runs and sanitizers.

Benchmarking without warm-up or with tiny inputs.

Best Practices

Start with the simplest correct version, then measure, then optimize.

Keep a journal of experiments and results.

Read real implementations after finishing an exercise (xv6, musl libc, the Linux kernel).

Interview Relevance

Very high: interviews for systems roles often ask for exactly these programs (mini shell, thread pool, LRU cache, producer–consumer, ring buffer).

Practice Questions

Add CPU-time columns to the ps clone using /proc/<pid>/stat.

Make the thread pool's submit return a handle the caller can wait on.

Extend the tree walker to list the 5 largest files.

Build the SPSC ring buffer and measure its throughput against a mutex-protected queue.

Quick Quiz

Q1. (Multiple choice) Where does the ps clone read a process's resident memory? A) /proc/<pid>/maps B) /proc/<pid>/status (VmRSS) C) /etc/passwd D) /sys/kernel/mm Answer: B.

Q2. (Predict the output) In the thread pool, are all 32 tasks completed before the workers exit?

Answer: Yes: workers exit only when the queue is empty *and* shutdown was requested (verified: 32 completed).

Q3. (True/False) The thread pool runs each task while holding the queue mutex.

Answer: False. It removes the task under the lock and runs it after unlocking, so other workers aren't blocked.

Q4. (Find the bug) A worker loop uses if (count == 0) pthread_cond_wait(...) and occasionally dequeues from an empty queue.

Answer: Use while: spurious wake-ups or another worker taking the task leave the queue empty after waking.

Coding Challenge

Write a single-producer/single-consumer ring buffer (capacity 8) with C11 atomics (release on publish, acquire on consume) and pass the numbers 1..1000 through it between two threads, verifying the sum.

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

enum { CAP = 8, COUNT = 1000 };
static int slots[CAP];
static atomic_uint head, tail;                 /* head: next read, tail: next write */

static void *producer(void *arg)
{
    (void)arg;
    for (int v = 1; v <= COUNT; v++) {
        unsigned t = atomic_load_explicit(&tail, memory_order_relaxed);
        while (t - atomic_load_explicit(&head, memory_order_acquire) == CAP) ;   /* full: wait */
        slots[t % CAP] = v;
        atomic_store_explicit(&tail, t + 1, memory_order_release);                 /* publish */
    }
    return NULL;
}

static void *consumer(void *arg)
{
    long sum = 0;
    for (int i = 0; i < COUNT; i++) {
        unsigned h = atomic_load_explicit(&head, memory_order_relaxed);
        while (atomic_load_explicit(&tail, memory_order_acquire) == h) ;          /* empty: wait */
        sum += slots[h % CAP];
        atomic_store_explicit(&head, h + 1, memory_order_release);
    }
    *(long *)arg = sum;
    return NULL;
}

int main(void)
{
    pthread_t p, c;
    long sum = 0;
    pthread_create(&c, NULL, consumer, &sum);
    pthread_create(&p, NULL, producer, NULL);
    pthread_join(p, NULL)
…