Module 11 Exercises — Embedded Linux Systems Programming

Back to the Course 2 syllabus. Read first: Module 11 lessons (Seacord 7–8, Rust Book 12, 16, and 21, and the nix/gpiod crate docs remain available as optional deep-dives).

Work in c/linux/src/ex-8-N/ and rust/linux/src/bin/ex-8-N.rs of the labs repo, and record everything in m11/notes.md. This is the one module whose exercises build on the board: the Jetson Orin Nano (or the Pi 5) over SSH or CLion’s remote toolchain, per Course 3’s Jetson setup essentials. The always-available Mac step is cargo check -p linux --target aarch64-unknown-linux-gnu for every Rust exercise — no board needed to know it compiles — and for C, the subset that is plain POSIX (clock_nanosleep, pthreads, sockets, termios) also builds and runs on macOS, while timerfd, epoll, signalfd, eventfd, SCHED_FIFO, and gpiod are Linux-only and are exercised on the board. Run lessons §10’s checklist before any measurement, and predict every cell before running: observed cells stay “…” until the board fills them.

Exercises

Exercise 11.1 — The 1 kHz loop, and what each knob buys. Write a periodic loop that wakes every 1 ms from a timerfd on CLOCK_MONOTONIC (absolute first deadline, 1 ms interval), timestamps each wake-up with clock_gettime, and after a fixed number of iterations reports the wake-up lateness distribution (p50, p99, max) and the overrun count taken from the timerfd read value. Write it twice: C (timerfd_create/timerfd_settime, a read loop with EINTR handling) and Rust (nix::sys::timerfd::TimerFd, Instant for timestamps). The loop body does a fixed small amount of work on a pre-allocated buffer — no allocation, no logging inside the loop; the histogram is printed at the end. Run each build under four configurations and fill the table, predicting the ordering of the rows before measuring:

Configuration p50 lateness p99 lateness max lateness overruns C or Rust differs?
Default (SCHED_OTHER, no locking)
SCHED_FIFO 80 (chrt -f 80)
SCHED_FIFO 80 + mlockall + pre-faulted stack
… + pinned to one core (taskset), jetson_clocks on
cyclictest -p 80 -i 1000 on the same board, same state reference

Then add a deliberate load on the box (a SCHED_OTHER busy loop on every core) and repeat the first and third rows. Deliverable: the table, the cyclictest reference line, and a notes.md paragraph attributing each improvement to the jitter source in lessons §4.4 it removed — and a sentence on what a PREEMPT_RT kernel would change that none of these knobs can.

Exercise 11.2 — A SCHED_FIFO pipeline and a priority inversion, reproduced then fixed. Build the three-stage acquire → process → emit pipeline of Course 3 Lab 7.2 as three threads at FIFO priorities 80/70/60, connected by bounded queues (C: ring + mutex + condvar; Rust: mpsc::sync_channel), with each stage toggling one of the marker GPIOs (pins 7/29/31) at entry and exit so the Saleae can see the schedule. Then introduce the inversion on purpose: a shared “configuration” object protected by a plain mutex, read by the priority-80 stage and held for a long update by the priority-60 stage, while a priority-70 thread that needs neither runs a busy computation. Predict the stage-80 latency distribution with the plain mutex, then measure; switch to a PTHREAD_PRIO_INHERIT mutex (C directly; Rust through a small unsafe wrapper around libc::pthread_mutex_t carrying a // SAFETY: contract, since std::sync::Mutex has no PI) and measure again:

Mutex Stage-80 p99 latency Stage-80 max latency Inversion visible on the analyzer?
plain pthread_mutex_t / std::sync::Mutex
PTHREAD_PRIO_INHERIT (both languages)

Deliverable: both listings, the Saleae capture showing the inversion and its absence, the table, and a notes.md paragraph relating this to FreeRTOS’s mutex priority inheritance and RTIC’s ceilings from Module 10 — and one on the safety argument of the Rust PI wrapper (what it must guarantee about initialization, Send/Sync, and destruction).

Exercise 11.3 — GPIO edges as events, through epoll. Wire the marker pin 7 as an input driven by the NUCLEO’s marker output from any Course 3 Module 2 lab (or a button through a pull-down). Request the line for both-edge events — C with the libgpiod version on the board (record gpiodetect --version; the 1.x and 2.x APIs differ and the notes say which one you wrote against), Rust with the gpiod crate — and add the resulting descriptor to an epoll set alongside a signalfd for SIGINT. The loop prints each edge with its kernel timestamp and the delta to the previous edge, and exits cleanly on SIGINT (releasing the line request). Before writing code, verify the line with gpioinfo and gpiomon. Drive the pin at a known frequency from the NUCLEO and fill:

Input frequency Events expected per second Events observed Max inter-event delta Any lost edges?
low (hand-toggled button)
100 Hz square from the NUCLEO
1 kHz square
10 kHz square

Deliverable: both listings, the table, and a notes.md verdict on where userspace edge events stop keeping up — and why that boundary is the argument for leaving the fast path on the microcontroller (Course 3’s implementation tracks).

Exercise 11.4 — One transport, two media, two languages. Implement the length-prefixed frame of Course 3’s host streaming harness over two media behind one interface: a serial port (/dev/ttyTHS1, 921600 baud, raw termios, VMIN/VTIME chosen and justified) and a TCP socket (TCP_NODELAY set). C: a struct transport of function pointers (open, send_frame, recv_frame, close) with two implementations; Rust: a trait Transport with SerialTransport and TcpTransport, both built on Read/Write so that the framing code exists once. The frame encoder/decoder is written from a one-paragraph layout document you write first (offsets, widths, byte order, CRC), and the C and Rust implementations are tested against each other — C sender to Rust receiver and back — over a loopback (socat pty pair for serial, localhost for TCP) before any board is involved. Fill by prediction, then measurement:

Path Frames/s at 64-byte payload Round-trip latency p50 Short writes observed? C ↔︎ Rust interop OK?
TCP, localhost, TCP_NODELAY on
TCP, localhost, TCP_NODELAY off
Serial, socat pty pair
Serial, /dev/ttyTHS1 ↔︎ NUCLEO VCP

Deliverable: the layout document, both listings, the table, and the notes.md paragraph on the one bug the cross-language test found (there is nearly always one: a width, an order, or an off-by-one in the length field).

Exercise 11.5 — The event loop, assembled. Combine the pieces into the shape a real embedded-Linux daemon has: one epoll loop waiting on (a) a timerfd at 100 Hz, (b) a signalfd carrying SIGINT/SIGTERM/SIGHUP, (c) a listening TCP socket plus its accepted connections, and (d) an eventfd that a worker thread writes to when a background job finishes. Every handler is non-blocking; the timer handler reads the expiration count and logs overruns; SIGHUP re-reads a config file; SIGTERM closes every connection, joins the worker, and exits with status 0. Write it in C and in Rust (nix for the descriptors, std::thread for the worker). Then break it, predicting first: switch the socket to EPOLLET without a drain loop and send a burst; block SIGINT in main but forget to do it before spawning the worker; let the worker write to the eventfd from inside a signal handler.

Fault injected Predicted symptom Observed symptom Which language caught it before runtime?
EPOLLET without draining
Signal mask set after the worker exists
eventfd write from a signal handler
Blocking read inside a handler

Deliverable: both loops, the fault table, and a notes.md comparison of the Rust and C shutdown paths — which resources were released by drop and which still needed an explicit call.

Exercise 11.6 — The safe-strings audit. Take one host-side C file from the Course 3 labs repo that handles text (a capture parser or a serial-command reader from course3/labs/*/host/, or if none exists yet, write a 60-line command parser in the K&R idiom first) and audit it against lessons §3: every strcpy/strcat/sprintf/strncpy/strlen/strtok/strerror/gets use, every fgets result that keeps its newline, every buffer whose size appears as a literal in two places. Rewrite it with the bounded subset and snprintf return-value checks, keep both versions, and prove the difference: feed each an over-long line, an unterminated line, and a line with embedded NULs, under -fsanitize=address,undefined. Then write the same parser in Rust on &[u8] (not &str — the input is bytes until proven UTF-8), and feed it the same three inputs.

Input Original C (sanitizer report) Rewritten C Rust
Over-long line
Unterminated line
Embedded NUL
Valid UTF-8 vs. invalid bytes

Deliverable: the three listings, the table, and a notes.md list of every function the audit removed with the replacement used — the working string subset you will hold to on this tier.

Exercise 11.7 — Thread pool shutdown, both ways. Implement Rust Book 21’s thread pool — new(n), execute(job), and a Drop that closes the channel and joins every worker — as a Rust library in rust/linux with a test that submits jobs, drops the pool, and asserts every job ran exactly once and every thread exited. Then implement the same in C: a fixed pool of pthreads, a bounded job queue with mutex and condvar, a stop atomic, pthread_cond_broadcast, and pthread_join in order — with the same test. Instrument both with a per-worker “jobs run” counter and a “shutdown latency” (from the stop request to the last join returning). Introduce the two classic C mistakes on purpose — forgetting the broadcast so one worker sleeps forever, and freeing the queue before the last worker has left it — and record what each does; then attempt each in Rust and record where the compiler or the runtime stopped you.

Scenario C behavior Rust behavior Caught by
Clean shutdown
Stop without broadcast
Queue freed before last worker exits
Job panics / longjmps out of a worker

Deliverable: both implementations with their tests, the table, and a closing notes.md note on which of Module 9’s shared-state rules this exercise re-used unchanged on the Linux tier, and which one the kernel took over.