Project 5: Event-Driven I/O Multiplexer
Build an event loop that multiplexes pipes and sockets using poll/select or libevent.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 3: Advanced |
| Time Estimate | 1 week |
| Main Programming Language | C (Alternatives: Rust, Go) |
| Alternative Programming Languages | Rust, Go |
| Coolness Level | Level 3: Event Loop Wizardry |
| Business Potential | 2: The “Core Library” |
| Prerequisites | non-blocking I/O, file descriptors, timers |
| Key Topics | poll/select/epoll, edge cases, timers |
1. Learning Objectives
By completing this project, you will:
- Build a working implementation of event-driven i/o multiplexer and verify it with deterministic outputs.
- Explain the underlying Unix and terminal primitives involved in the project.
- Diagnose common failure modes with logs and targeted tests.
- Extend the project with performance and usability improvements.
2. All Theory Needed (Per-Concept Breakdown)
Event Loop Multiplexing with Timers
-
Fundamentals Event Loop Multiplexing with Timers is the core contract that makes the project behave like a real terminal tool. It sits at the boundary between raw bytes and structured state, so you must treat it as both a protocol and a data model. The goal of the fundamentals is to understand what assumptions the system makes about ordering, buffering, and ownership, and how those assumptions surface as user-visible behavior. Key terms include: poll, epoll, non-blocking, timerfd, readiness. In practice, the fastest way to gain intuition is to trace a single input through the pipeline and note where it can be delayed, reordered, or transformed. That exercise reveals why Event Loop Multiplexing with Timers needs explicit invariants and why even small mistakes can cascade into broken rendering or stuck input.
-
Deep Dive into the concept A deep understanding of Event Loop Multiplexing with Timers requires thinking in terms of state transitions and invariants. You are not just implementing functions; you are enforcing a contract between producers and consumers of bytes, and that contract persists across time. Most failures in this area are caused by violating ordering guarantees, dropping state updates, or misunderstanding how the operating system delivers events. This concept is built from the following pillars: poll, epoll, non-blocking, timerfd, readiness. A reliable implementation follows a deterministic flow: Mark all FDs non-blocking. -> Register FDs and interest masks. -> Call poll with a computed timeout. -> Dispatch callbacks for ready FDs. -> Trigger timer callbacks and recompute timeout.. From a systems perspective, the tricky part is coordinating concurrency without introducing races. Even in a single-threaded loop, multiple events can arrive in the same tick, so you need deterministic ordering. This is why many implementations keep a strict sequence: read, update state, compute diff, render. Another subtlety is error handling and recovery. A robust design treats errors as part of the normal control flow: EOF is expected, partial reads are expected, and transient failures must be retried or gracefully handled. The deep dive should also cover how to observe the system, because without logs and trace points, you cannot reason about correctness. When you design the project, treat each key term as a source of constraints. For example, if a term implies buffering, decide the buffer size and how overflow is handled. If a term implies state, decide how that state is initialized, updated, and reset. Finally, validate your assumptions with deterministic fixtures so you can reproduce bugs. From a systems perspective, the tricky part is coordinating concurrency without introducing races. Even in a single-threaded loop, multiple events can arrive in the same tick, so you need deterministic ordering. This is why many implementations keep a strict sequence: read, update state, compute diff, render. Another subtlety is error handling and recovery. A robust design treats errors as part of the normal control flow: EOF is expected, partial reads are expected, and transient failures must be retried or gracefully handled. The deep dive should also cover how to observe the system, because without logs and trace points, you cannot reason about correctness. From a systems perspective, the tricky part is coordinating concurrency without introducing races. Even in a single-threaded loop, multiple events can arrive in the same tick, so you need deterministic ordering. This is why many implementations keep a strict sequence: read, update state, compute diff, render. Another subtlety is error handling and recovery. A robust design treats errors as part of the normal control flow: EOF is expected, partial reads are expected, and transient failures must be retried or gracefully handled. The deep dive should also cover how to observe the system, because without logs and trace points, you cannot reason about correctness. From a systems perspective, the tricky part is coordinating concurrency without introducing races. Even in a single-threaded loop, multiple events can arrive in the same tick, so you need deterministic ordering. This is why many implementations keep a strict sequence: read, update state, compute diff, render. Another subtlety is error handling and recovery. A robust design treats errors as part of the normal control flow: EOF is expected, partial reads are expected, and transient failures must be retried or gracefully handled. The deep dive should also cover how to observe the system, because without logs and trace points, you cannot reason about correctness.
-
How this fit on projects This concept is the backbone of the project because it defines how data and control flow move through the system.
-
Definitions & key terms
- poll -> system call that waits for readiness on multiple fds
- epoll -> scalable Linux readiness API for many fds
- non-blocking -> fd mode where reads/writes return immediately
- timerfd -> Linux fd that becomes readable when a timer expires
- readiness -> signal that an fd can be read or written without blocking
-
Mental model diagram (ASCII)
[Input] -> [Event Loop Multiplexing with Timers] -> [State] -> [Output]
-
How it works (step-by-step, with invariants and failure modes)
- Mark all FDs non-blocking.
- Register FDs and interest masks.
- Call poll with a computed timeout.
- Dispatch callbacks for ready FDs.
- Trigger timer callbacks and recompute timeout.
-
Minimal concrete example
poll(fds, nfds, timeout_ms);
-
Common misconceptions
- “poll tells you how much to read” -> it only signals readiness.
- “timeout 0 is fine” -> it causes busy loops.
-
Check-your-understanding questions
- When do you prefer poll over epoll?
- How do timers interact with I/O readiness?
-
Check-your-understanding answers
- poll is simpler and fine for small fd counts.
- Timers define the max sleep time so you wake for deadlines.
-
Real-world applications
- tmux server loop
- web servers
- database reactors
-
Where you’ll apply it
- See Section 3.2 Functional Requirements and Section 5.4 Concepts You Must Understand First.
- Also used in: Project 4: Signal-Aware Process Supervisor, Project 6: Terminal UI Library (Mini-ncurses).
-
References
- TLPI Ch. 63
-
Key insights Event Loop Multiplexing with Timers works best when you treat it as a stateful contract with explicit invariants.
-
Summary You now have a concrete mental model for Event Loop Multiplexing with Timers and can explain how it affects correctness and usability.
-
Homework/Exercises to practice the concept
- Build a timer that fires every 500ms.
- Add a pipe FD and log when readable.
-
Solutions to the homework/exercises
- Use a monotonic clock and compute deadline deltas.
3. Project Specification
3.1 What You Will Build
A small server that watches multiple FDs, handles readiness events, and drives timers with deterministic ordering.
3.2 Functional Requirements
- Requirement 1: Register multiple FDs for readability/writability.
- Requirement 2: Handle timer events alongside I/O.
- Requirement 3: Avoid busy loops when no events are ready.
- Requirement 4: Provide a simple callback API.
- Requirement 5: Shut down cleanly on SIGINT.
3.3 Non-Functional Requirements
- Performance: Avoid blocking I/O; batch writes when possible.
- Reliability: Handle partial reads/writes and cleanly recover from disconnects.
- Usability: Provide clear CLI errors, deterministic output, and helpful logs.
3.4 Example Usage / Output
$ ./io_mux --listen /tmp/mux.sock --tick 500
[event] client connected fd=5
[event] timer tick
[exit code: 0]
$ ./io_mux --backend epoll --platform macos
[error] backend not supported
[exit code: 3]
3.5 Data Formats / Schemas / Protocols
Event log format: timestamp, fd, event type, action.
3.6 Edge Cases
- FD becomes invalid
- Writable event storm
- Timer drift
3.7 Real World Outcome
This section defines a deterministic, repeatable outcome. Use fixed inputs and set TZ=UTC where time appears.
3.7.1 How to Run (Copy/Paste)
make
./io_mux --listen /tmp/mux.sock --tick 500
3.7.2 Golden Path Demo (Deterministic)
The “success” demo below is a fixed scenario with a known outcome. It should always match.
3.7.3 If CLI: provide an exact terminal transcript
$ ./io_mux --listen /tmp/mux.sock --tick 500
[event] client connected fd=5
[event] timer tick
[exit code: 0]
Failure Demo (Deterministic)
$ ./io_mux --backend epoll --platform macos
[error] backend not supported
[exit code: 3]
3.7.8 If TUI
At least one ASCII layout for the UI:
+------------------------------+
| Event-Driven I/O Multiplexer |
| [content area] |
| [status / hints] |
+------------------------------+
4. Solution Architecture
4.1 High-Level Design
+-----------+ +-----------+ +-----------+
| Client | <-> | Server | <-> | PTYs |
+-----------+ +-----------+ +-----------+
4.2 Key Components
| Component | Responsibility | Key Decisions | |-----------|----------------|---------------| | Event loop | Polls FDs and dispatches callbacks. | Start with poll for portability. | | Timer wheel | Schedules time-based callbacks. | Use monotonic clock. | | Callback registry | Maps fd to handler functions. | Use arrays for small counts. |
4.4 Data Structures (No Full Code)
typedef struct {
int fd;
short events;
void (*handler)(int fd, short events, void *ctx);
void *ctx;
} Watcher;
4.4 Algorithm Overview
Key Algorithm: Poll-driven event loop
- Compute next timer deadline.
- Call poll() with timeout based on deadline.
- Dispatch handlers for ready FDs.
- Run due timers in order.
- Repeat until shutdown flag set.
Complexity Analysis:
- Time O(n) per poll; Space O(n) watchers.
5. Implementation Guide
5.1 Development Environment Setup
cc --version
make --version
pkg-config --libs libevent
5.2 Project Structure
io-mux/
|-- src/
| |-- loop.c
| |-- timers.c
| `-- main.c
|-- include/
| |-- loop.h
| `-- timers.h
`-- Makefile
5.3 The Core Question You’re Answering
“How do you handle many file descriptors without blocking?”
5.4 Concepts You Must Understand First
- poll/select/epoll
- Why it matters and how it impacts correctness.
- edge cases
- Why it matters and how it impacts correctness.
- timers
- Why it matters and how it impacts correctness.
5.5 Questions to Guide Your Design
- How do you avoid starvation between timers and I/O?
- How do you handle fd removal during callbacks?
5.6 Thinking Exercise
Draw a timeline with I/O readiness and timer events interleaving.
5.7 The Interview Questions They’ll Ask
- What causes a busy loop in an event loop?
- How do you implement timeouts with poll?
5.8 Hints in Layers
- Start with poll for simplicity.
-
Use a single vector of watchers.
5.9 Books That Will Help
| Topic | Book | Chapter | |——-|——|———| | I/O multiplexing | The Linux Programming Interface | Ch. 63 |
5.10 Implementation Phases
Phase 1: Foundation (1 week)
Goals:
- Establish the core data structures and loop.
- Prove basic I/O or rendering works.
Tasks:
- Implement the core structs and minimal main loop.
- Add logging for key events and errors.
Checkpoint: You can run the tool and see deterministic output.
Phase 2: Core Functionality (1 week)
Goals:
- Implement the main requirements and pass basic tests.
- Integrate with OS primitives.
Tasks:
- Implement remaining functional requirements.
- Add error handling and deterministic test fixtures.
Checkpoint: All functional requirements are met for the golden path.
Phase 3: Polish & Edge Cases (1 week)
Goals:
- Handle edge cases and improve UX.
- Optimize rendering or I/O.
Tasks:
- Add edge-case handling and exit codes.
- Improve logs and documentation.
Checkpoint: Failure demos behave exactly as specified.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale |
|---|---|---|---|
| I/O model | blocking vs non-blocking | non-blocking | avoids stalls in multiplexed loops |
| Logging | text vs binary | text for v1 | easier to inspect and debug |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Unit Tests | Validate components | parser, buffer, protocol |
| Integration Tests | Validate interactions | end-to-end CLI flow |
| Edge Case Tests | Handle boundary conditions | resize, invalid input |
6.2 Critical Test Cases
- Timer fires at ~500ms intervals.
- Readable pipe triggers callback.
- No CPU spike when idle.
6.3 Test Data
text
Create a pipe, write one byte, expect one readable event.
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution | |———|———|———-| | Busy loop | 100% CPU idle | Use blocking poll with timeout. | | FD leak | Stale watchers | Remove watchers on close. | | Timer drift | Timers fire late | Use monotonic timestamps and catch up. |
7.2 Debugging Strategies
- Log poll timeouts and handler durations.
- Use strace to confirm poll sleep.
7.3 Performance Traps
-
Rebuilding fd arrays every loop without reuse.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add SIGINT handling to stop loop.
- Add writable events.
8.2 Intermediate Extensions
- Add priority queues for timers.
- Support one-shot vs repeating timers.
8.3 Advanced Extensions
- Add epoll backend selectable via flag.
9. Real-World Connections
9.1 Industry Applications
- Server frameworks
- multiplexers
9.2 Related Open Source Projects
- libevent
- libuv
9.3 Interview Relevance
- Event loops, terminal I/O, and state machines are common interview topics.
10. Resources
10.1 Essential Reading
- The Linux Programming Interface by Michael Kerrisk - Ch. 63
10.2 Video Resources
- Event loop design (lecture).
10.3 Tools & Documentation
- strace: strace
- perf: perf
10.4 Related Projects in This Series
- Project 4: Signal-Aware Process Supervisor - Builds prerequisites
-
Project 6: Terminal UI Library (Mini-ncurses) - Extends these ideas
11. Self-Assessment Checklist
11.1 Understanding
- I can explain the core concept without notes
- I can explain how input becomes output in this tool
- I can explain the main failure modes
11.2 Implementation
- All functional requirements are met
- All test cases pass
- Code is clean and well-documented
- Edge cases are handled
11.3 Growth
- I can identify one thing I’d do differently next time
- I’ve documented lessons learned
- I can explain this project in a job interview
12. Submission / Completion Criteria
Minimum Viable Completion:
- Tool runs and passes the golden-path demo
- Deterministic output matches expected snapshot
- Failure demo returns the correct exit code
Full Completion:
- All minimum criteria plus:
- Edge cases handled and tested
- Documentation covers usage and troubleshooting
Excellence (Going Above & Beyond):
- Add at least one advanced extension
- Provide a performance profile and improvement notes