Module Quiz
Complete this quiz after finishing all concept and practice pages.
Current Module Questions
Question 1: Process vs Program
State the difference between a program and a process in one sentence each.
Answer: A program is the executable artifact on disk (machine code + metadata). A process is a running instance of that program with a pid, its own virtual address space, an fd table, and a place in the scheduler. Many processes can share one program; a process is not a program.
Question 2: fork Return Values
A C program calls pid_t p = fork();. In which process is p == 0? What does a non-zero positive value mean, and what does a negative value mean?
Answer: p == 0 in the child. A positive p is the child's pid, and the process is the parent. p < 0 means the fork failed (no process was created); check errno.
Question 3: Syscall Sequence Tracing
Given this strace excerpt from a program invoked as ./prog input.txt:
openat(AT_FDCWD, "input.txt", O_RDONLY) = 3
read(3, "hello\nworld\n", 4096) = 12
write(1, "hello\nworld\n", 12) = 12
close(3) = 0
Write a C program whose source would produce exactly this sequence. What assumption did you have to make about the size of input.txt?
Answer: A program that opens the file (getting fd 3 because 0, 1, 2 are already taken), calls read once with a 4 KB buffer, writes the result to stdout, and closes. Example skeleton: int fd = open(argv[1], O_RDONLY); char buf[4096]; ssize_t n = read(fd, buf, sizeof buf); write(1, buf, n); close(fd);. The assumption is that the file fits in one read -- in reality you must loop read because a single call is not guaranteed to return everything.
Question 4: Shell Redirection Mechanics
When a shell runs ./prog > out.txt, what exactly does the shell do to the child's fd table before exec?
Answer: fork, then in the child: open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644) to get some fd (say, 3), dup2(3, 1) to make fd 1 point at out.txt, then close(3), then execvp. After exec, the program's printf/write(1, ...) writes to out.txt without any changes to the program itself.
Question 5: Race Condition Diagnosis from Output
Two threads each increment a shared long counter one million times. Final value observed across 10 runs: 1384122, 1602910, 1441098, 1712045, 1500388, .... None reaches 2000000. Explain the specific race.
Answer: counter++ compiles to three instructions: load counter into a register, add 1, store back. Two threads can both load the same old value, both add 1, both store back -- one update is lost. The more interleavings, the lower the final value. Fix with atomic_fetch_add(&counter, 1) or guard the increment with a pthread_mutex_t.
Question 6: mmap vs read
A program repeatedly accesses random 8-byte records in a 10 GB file. Would you reach for mmap or read/lseek? Defend your choice.
Answer: mmap. Random access means each read/lseek is a syscall + memory copy; with mmap, the kernel page cache holds the working set, and a random access is a user-mode pointer dereference (free if the page is cached). For streaming sequential reads, the choice is more even and read with a large buffer is often simpler.
Question 7: Producer-Consumer Correctness
In the canonical producer-consumer, why is while (empty) cond_wait(¬_empty, &m); used instead of if (empty) cond_wait(¬_empty, &m);? Give two independent reasons.
Answer: (1) Spurious wakeups. POSIX allows cond_wait to return with no signal sent. (2) Race between signaller and waiter. Another consumer, waking first, may drain the last item before our thread re-acquires the mutex; on return from wait we must re-check the predicate. if proceeds unconditionally and can dequeue from an empty buffer; while re-checks.
Question 8: FD Table Through pipe + fork + dup2
Before: parent has pipe returning p[0]=3, p[1]=4. Parent forks. Each child wants to be one side of A | B. Write out the fd table of each child right before exec, assuming A is the writer.
Answer:
| Process | fd 0 | fd 1 | fd 3 | fd 4 |
|---|---|---|---|---|
| child running A | stdin (terminal) | pipe write end (dup'd from 4) | closed | closed |
| child running B | pipe read end (dup'd from 3) | stdout (terminal) | closed | closed |
| parent after closes | terminal | terminal | closed | closed |
The critical closes: each child closes both p[0] and p[1] (the duplicates still point at the pipe via fd 0 or fd 1). The parent closes both so that when child A exits, child B sees EOF on its read end.
Question 9: Signal Safety
Why is calling printf from inside a signal handler dangerous, and what should you do instead in a long-running program?
Answer: printf is not async-signal-safe. It takes an internal stdio lock; if the main program was already inside printf when the signal arrived, the handler's printf will deadlock or corrupt the buffer. Inside a handler, use only async-signal-safe functions (write, _exit, kill, ...). The standard pattern is to set a volatile sig_atomic_t flag in the handler and let the main loop notice it on its next iteration.
Question 10: Debugging Tool Selection
A server suddenly spikes to 100% CPU and responses time out. Pick the first diagnostic tool to reach for and justify the choice. Then pick the first tool if the same server is instead hung at 0% CPU and justify that choice.
Answer: 100% CPU -> perf top -p <pid>. The process is burning CPU in user code; sampling profiling will show which function. strace would show few or no syscalls and waste time. 0% CPU -> strace -p <pid>. The process is blocked in a syscall; strace will print the exact syscall (usually read, futex, or accept) it is waiting in. perf would show no CPU samples because there are none.
Interleaved Review Questions (from previous modules)
Question 11: (from S4M1, C Fundamentals)
Why does the C declaration char *p = "hello"; produce a string you cannot modify, while char p[] = "hello"; does?
Answer: The first makes p point into a read-only data segment (string literal). Writing through p is undefined behavior and usually crashes with SIGSEGV. The second allocates a writable array on the stack and copies the literal into it; modifications work.
Question 12: (from S4M2, Memory and Pointers)
In a C program, where does malloc-returned memory physically come from, assuming a glibc userspace?
Answer: glibc malloc satisfies small allocations from a per-thread arena it maintains, backed by large chunks obtained from the kernel via sbrk (the program break) or mmap (for large allocations). It is not one syscall per malloc; it is many mallocs per syscall.
Question 13: (from S4M3, Computer Organization)
Explain why accessing a pointer in a tight inner loop can be dramatically faster the second time than the first.
Answer: The first access is likely a cache miss -- the data is not in L1/L2/L3 cache and must be fetched from DRAM (hundreds of cycles). Subsequent accesses hit the cache (a few cycles). The same reasoning explains why mmap'd files appear "fast on warm runs": the kernel page cache holds the pages.
Question 14: (from S3M1, Software Design)
A programmer wraps every syscall in a "safety" class with 20 methods. Discuss in terms of cohesion and SRP.
Answer: Cohesion is low because 20 disparate syscall wrappers do not share data or purpose. SRP is violated because open, socket, mmap, and pthread_create answer to different stakeholders (file I/O, networking, memory, threading). Break into focused utilities per domain; the "safety" concern (error checking) is a cross-cutting concern best solved by a small check(rc, msg) helper, not a god-class.
Question 15: (from S2M2, Algorithms)
What is the time complexity of a correct wc -c implementation over a file of size N bytes, in terms of syscalls and in terms of user-mode work?
Answer: User-mode work is O(N) -- you must touch every byte to distinguish newlines. Syscalls are O(N / B) where B is the read-buffer size; with B=4096, a 4 GB file costs about 10^6 syscalls, so the buffer size is a meaningful constant factor. Alternatively, mmap makes syscalls O(1) amortized and turns the work into O(N) user-mode loads (with page faults).