Project 1: PTY Explorer Tool
Build a diagnostic CLI that creates PTY pairs, spawns a shell on the slave, and makes kernel TTY behavior visible with byte-level tracing and job-control probes.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 2: Intermediate (The Developer) |
| Time Estimate | 1-2 weeks |
| Main Programming Language | C (Alternatives: Rust, Go, Python) |
| Alternative Programming Languages | Rust, Go, Python |
| Coolness Level | Level 3: Genuinely Clever |
| Business Potential | Level 1: Resume Gold (Educational/Personal Brand) |
| Prerequisites | Basic C, fork/exec, file descriptors, signals |
| Key Topics | PTY/TTY, termios, sessions, job control, SIGWINCH |
1. Learning Objectives
By completing this project, you will:
- Create PTY master/slave pairs using the canonical
posix_openptpipeline. - Correctly wire a child process to a PTY slave so it becomes a controlling terminal.
- Explain and demonstrate canonical vs raw input behavior with real byte traces.
- Implement and verify window size propagation and SIGWINCH delivery.
- Build a reusable diagnostic workflow for debugging interactive terminal bugs.
- Distinguish PTY semantics from pipes and explain why
isatty()changes program behavior.
2. All Theory Needed (Per-Concept Breakdown)
Concept 1: PTY/TTY Architecture, Sessions, and Job Control
Fundamentals
A pseudo-terminal (PTY) is a kernel-provided pair of endpoints that behaves like a physical terminal device. The PTY master is owned by the terminal emulator; the PTY slave is opened by the shell or interactive application. Between them sits the kernel TTY subsystem and line discipline, which implement canonical input editing, echo, and signal generation. Sessions and process groups define job control: only the foreground process group attached to a controlling terminal can read without being stopped, and control characters like Ctrl+C are translated into signals such as SIGINT. This model explains why interactive programs behave differently on PTYs than on pipes. A PTY is therefore not just a byte stream; it is a device with enforced semantics, and the emulator must respect those semantics to behave like a real terminal.
Deep Dive into the Concept
The PTY subsystem exists to emulate physical terminals in software. On Linux, a PTY pair is created by opening /dev/ptmx (via posix_openpt), then calling grantpt and unlockpt to create a corresponding slave under /dev/pts/N. The master is a file descriptor that the emulator reads from and writes to; the slave is a device file that acts like a terminal for the child process. The kernel treats the slave side as a real terminal device, which means line discipline and job-control rules are enforced there. When the child writes to stdout, bytes appear on the master. When the emulator writes user input to the master, bytes arrive at the slave as if typed by a user.
Sessions are the key to job control. A session is a collection of process groups with a single controlling terminal. The controlling terminal is established when a session leader opens a terminal device that is not already a controlling terminal. That is why the child must call setsid() before opening the slave: it must become a session leader so the slave can become its controlling terminal. Once that is done, the kernel tracks the foreground process group (set by the shell with tcsetpgrp). When the user types Ctrl+C, the line discipline generates SIGINT and delivers it to the foreground process group. If a background process group attempts to read from the terminal, the kernel sends SIGTTIN and stops it. If it writes, it may receive SIGTTOU depending on termios. These behaviors are enforced in the kernel, not by the terminal emulator, which is why a correct PTY setup is essential.
PTYs also carry window size and terminal attributes. The emulator must set the initial window size on the slave using ioctl(TIOCSWINSZ). When the window size changes, the emulator updates the slave size and the kernel delivers SIGWINCH to the foreground process group. Interactive programs like vim and htop listen for SIGWINCH and query the new size with ioctl(TIOCGWINSZ). If you forget to propagate window size, full-screen programs draw incorrectly or corrupt your screen model.
Understanding PTY behavior also clarifies multiplexers. Tools like tmux create their own PTY masters and slaves. Each tmux pane gets a PTY slave; tmux reads output from the masters and renders a composite screen, then exposes its own PTY slave to the user’s terminal. This stacking of PTYs is why tmux can detach and reattach: tmux continues to own the PTY masters while clients come and go. For a PTY Explorer, you will make these relationships visible by logging process groups, session IDs, and terminal settings, turning otherwise invisible kernel invariants into observable state.
How this fits on projects
This concept is the backbone for this project and directly supports P04, P08, P13, and P14.
Definitions & Key Terms
- PTY master -> emulator-owned endpoint that reads program output and writes user input.
- PTY slave -> device file opened by the shell/application; behaves like a terminal.
- Controlling terminal -> terminal device attached to a session leader.
- Session leader -> process that created a new session via
setsid(). - Foreground process group -> group that receives signals and is allowed to read.
- Line discipline -> kernel layer that edits input and generates signals.
Mental Model Diagram (ASCII)
User Keys -> [Terminal UI] -> [PTY master] -> (TTY line discipline) -> [PTY slave] -> [Shell/App]
^ |
| v
read output generates signals
How It Works (Step-by-Step)
- Open a PTY master with
posix_openpt(O_RDWR | O_NOCTTY). - Call
grantpt()andunlockpt()to create and unlock the slave. - Fork a child; the child calls
setsid()to become a session leader. - Child opens the slave and uses
ioctl(TIOCSCTTY)to make it controlling terminal. - Child
dup2()s the slave to stdin/stdout/stderr andexec()s the shell. - Parent reads/writes the master and logs I/O and terminal state.
Invariants:
- The child is a session leader before opening the slave.
- The slave is the controlling terminal for the child session.
- The parent only uses the master.
Failure modes:
- Missing
setsid()-> no controlling terminal, broken job control. - Missing
TIOCSCTTY-> control characters do not map to signals. - Missing
TIOCSWINSZ-> incorrect size in full-screen apps.
Minimal Concrete Example
int master = posix_openpt(O_RDWR | O_NOCTTY);
grantpt(master);
unlockpt(master);
char *slave_name = ptsname(master);
pid_t pid = fork();
if (pid == 0) {
setsid();
int slave = open(slave_name, O_RDWR);
ioctl(slave, TIOCSCTTY, 0);
dup2(slave, STDIN_FILENO);
dup2(slave, STDOUT_FILENO);
dup2(slave, STDERR_FILENO);
execlp("/bin/bash", "bash", NULL);
}
Common Misconceptions
- “A PTY is just a pipe.” -> PTYs include line discipline and job control.
- “The emulator sends SIGINT.” -> The kernel sends signals based on termios.
- “You do not need sessions for PTYs.” -> Without sessions, job control breaks.
Check-Your-Understanding Questions
- Why does a program behave differently when stdout is a PTY vs a pipe?
- What changes when the child calls
setsid()before opening the slave? - How does the kernel decide which process group receives SIGINT?
Check-Your-Understanding Answers
- PTYs have line discipline and signals; pipes do not.
- The child becomes a session leader, enabling a controlling terminal.
- The foreground process group of the controlling terminal receives SIGINT.
Real-World Applications
- SSH sessions and remote shells
- IDE integrated terminals
- Terminal multiplexers (tmux/screen)
Where You’ll Apply It
- This project: Section 3.2 (PTY creation), Section 5.10 (phases)
- Also used in: P04-minimal-terminal-emulator-100-lines, P08-terminal-multiplexer-mini-tmux, P14-web-terminal-xterm-js-backend
References
- “The Linux Programming Interface” (Kerrisk), Chapters 34 and 64
- “Advanced Programming in the UNIX Environment” (Stevens/Rago), Chapter 9
pty(7)andtermios(3)man pages
Key Insight
A PTY is a device with kernel-enforced semantics, not just a byte pipe.
Summary
You now understand why PTYs require sessions and job control and why a correct PTY setup is the foundation of every terminal emulator.
Homework/Exercises to Practice the Concept
- Write a PTY program that prints the slave path and verifies
isatty(). - Spawn a child without
setsid()and observe how job control fails. - Send a window size update and verify
stty sizein the child.
Solutions to the Homework/Exercises
- Use
posix_openpt+ptsnameand checkisatty(slave). - Remove
setsid()and note that Ctrl+C does not signal the child. - Call
ioctl(TIOCSWINSZ, &ws)and runstty sizein the shell.
Concept 2: termios, Line Discipline, and Input Modes
Fundamentals
termios is the POSIX API that configures terminal behavior. It controls canonical vs raw input, echoing, signal generation, and input/output transformations like carriage return translation. In canonical mode, the kernel buffers input until a newline and performs line editing (erase, kill line). In raw mode, bytes are delivered immediately and the application is responsible for interpreting control characters. The line discipline interprets control characters like Ctrl+C and Ctrl+Z to generate signals, and it can also manage software flow control. Terminal emulators must observe termios state to understand why bytes appear or disappear, why input echoes, and why signals are delivered instead of raw bytes.
Deep Dive into the Concept
The termios structure is organized into input flags (c_iflag), output flags (c_oflag), control flags (c_cflag), local flags (c_lflag), and control characters (c_cc). The most common flags for terminal emulation are ICANON (canonical mode), ECHO (echo input), ISIG (generate signals), IEXTEN (implementation-defined input processing), IXON (software flow control), and OPOST (output post-processing). In canonical mode, the line discipline buffers input until it sees a line delimiter (usually newline). It also handles erase characters (backspace), kill line, and word erase without the application seeing those characters. The application receives a full line in one read call.
In raw mode, a typical cfmakeraw() configuration disables canonical processing, echo, and signal generation. The application then receives each byte as soon as it arrives. This is essential for full-screen programs like vim or less, which need to interpret escape sequences for arrow keys and other inputs. Raw mode is often configured by the application itself on the PTY slave, not by the emulator. The emulator must not overwrite these settings. For a diagnostic tool, you will toggle these modes on the slave and observe how input behavior changes from the master side, which helps explain why interactive programs behave differently from pipeline programs.
VMIN and VTIME matter for non-canonical reads. They define how read() behaves when input is unavailable. For example, VMIN=0, VTIME=1 yields a read that returns after 100ms even if no byte arrives. This matters when an app wants responsive input handling without blocking its UI. Some TUIs set VMIN/VTIME to do their own input polling rather than relying on select(). Your tool should expose these values so you can see how programs change them.
Output processing flags also matter. OPOST enables post-processing like mapping \n to \r\n. If you turn it off, some programs may display lines incorrectly because they assume the terminal expands newline into carriage return plus newline. Similarly, input flags like ISTRIP or INLCR can transform bytes before the program reads them. Debugging these transformations is often the missing piece when output appears “wrong” or input seems “eaten”.
Finally, control characters in c_cc define the actual bytes for interrupt, suspend, EOF, etc. If ISIG is set and the interrupt character is 0x03, then the line discipline generates SIGINT instead of delivering a raw byte. By toggling ISIG and observing the raw bytes on the PTY master, you can see the boundary between “bytes” and “signals” and understand how terminals are stateful devices, not just streams.
How this fits on projects
This concept is essential for this project and is revisited in P03 (termios experimenter) and P04 (minimal terminal).
Definitions & Key Terms
- Canonical mode (ICANON) -> line-buffered input with kernel editing.
- Raw mode -> unprocessed byte input; no echo, no signals.
c_cccontrol chars -> mapping of control bytes to special actions.VMIN/VTIME-> non-canonical read thresholds.OPOST-> output post-processing flag.
Mental Model Diagram (ASCII)
Keypress -> [line discipline] --(canonical)--> line buffer -> read()
|
+--(raw)--> immediate byte stream -> read()
How It Works (Step-by-Step)
- Application reads or sets termios with
tcgetattr/tcsetattr. ICANONcontrols line buffering vs immediate delivery.ECHOdetermines whether input is echoed by the kernel.ISIGdetermines whether control characters generate signals.VMIN/VTIMEtune blocking behavior for non-canonical reads.
Invariants:
- Canonical mode hides editing bytes from the application.
- Raw mode shifts responsibility to the application.
Failure modes:
- Forgetting to restore termios leaves the user’s terminal broken.
- Overwriting app-chosen termios settings causes input bugs.
Minimal Concrete Example
struct termios t;
tcgetattr(slave_fd, &t);
cfmakeraw(&t); // disables ICANON, ECHO, ISIG, etc.
t.c_cc[VMIN] = 1;
t.c_cc[VTIME] = 0;
tcsetattr(slave_fd, TCSANOW, &t);
Common Misconceptions
- “Raw mode just means no echo.” -> Raw mode disables many flags, including signals.
- “Newline is always just \n.” ->
OPOSTmay translate \n into \r\n. - “termios is global.” -> It is per-terminal-device, not per-process.
Check-Your-Understanding Questions
- Why does backspace disappear in canonical mode?
- What happens to Ctrl+C when
ISIGis cleared? - How do
VMINandVTIMEaffectread()latency?
Check-Your-Understanding Answers
- The line discipline removes editing characters before the app reads them.
- Ctrl+C is delivered as a raw byte (0x03) instead of SIGINT.
- They control the minimum byte count and timeout for non-canonical reads.
Real-World Applications
vimandhtopraw-mode input handling- Shell line editing in canonical mode
- Serial console configuration
Where You’ll Apply It
- This project: Section 3.2 (mode toggles), Section 5.4 (prereqs)
- Also used in: P03-termios-mode-experimenter, P04-minimal-terminal-emulator-100-lines
References
termios(3)andtcsetattr(3)man pages- “The Linux Programming Interface” (Kerrisk), Chapter 62
Key Insight
The line discipline is a state machine that rewrites input and output according to termios flags.
Summary
Understanding termios explains why bytes appear, vanish, or transform between your keyboard and a program.
Homework/Exercises to Practice the Concept
- Toggle
ECHOon the slave and observe the effect on output. - Set
VMIN=0, VTIME=1and measure input latency. - Disable
ISIGand show that Ctrl+C becomes a raw byte.
Solutions to the Homework/Exercises
- Use
tcgetattr/tcsetattrand type characters to see if they echo. - Use
read()in a loop and print timestamps when bytes arrive. - Clear
ISIGand log the byte stream from the master.
Concept 3: File Descriptor Wiring and Process Lifecycle
Fundamentals
A terminal emulator is a file-descriptor router. The child process must have the PTY slave connected to stdin, stdout, and stderr so it behaves like it is attached to a real terminal. This is done with dup2(). File descriptors are inherited across fork() and preserved across exec() unless marked FD_CLOEXEC. This means the emulator must carefully close unused descriptors to avoid resource leaks and broken EOF semantics. The emulator also needs to reap the child process with waitpid() to avoid zombies and to restore terminal state on exit.
Deep Dive into the Concept
File descriptor tables are per-process maps from small integers to open file descriptions in the kernel. After fork(), both parent and child reference the same open file descriptions, which means offsets, flags, and other state are shared. exec() replaces the process memory image but preserves file descriptors unless FD_CLOEXEC is set. For terminal emulation, this allows you to set up the child’s standard streams before exec() and then launch a shell. The child must close the PTY master to avoid confusing EOF semantics, and the parent should close any copy of the slave after the child is launched.
Lifecycle management matters because terminal processes are interactive and long-running. The parent should monitor SIGCHLD or use waitpid() to reap the child when it exits. If the parent does not reap, zombie processes accumulate. When the child exits, the PTY master read will return EOF; this should trigger a clean shutdown. If the emulator is closed unexpectedly, closing the master typically triggers SIGHUP to the child, which shells interpret as a logout signal. Your diagnostic tool should surface this behavior by logging child exit codes and signal causes.
Signal handling complicates the lifecycle. The emulator itself must handle SIGINT/SIGTERM and restore terminal settings before exit. If the parent is in raw mode while reading user input, it must save the original termios settings and restore them on exit to avoid leaving the user’s terminal in a broken state. This is a real-world requirement: most terminal bugs reported in the wild are actually cleanup bugs that leave the terminal in raw mode.
Finally, file descriptor wiring is how you demonstrate job-control invariants. Because the slave is the controlling terminal, tcgetpgrp() on the slave should match the foreground process group of the child shell. This means your tool can verify wiring correctness by printing session, pgrp, and tpgid values. Doing so turns an invisible property into a checkable invariant and makes job control debuggable.
How this fits on projects
This concept is central to this project and reappears in P04 and P14.
Definitions & Key Terms
- File descriptor table -> per-process map from fd numbers to open files.
dup2()-> duplicates a descriptor into a specific fd number.FD_CLOEXEC-> closes a descriptor onexec().- Zombie process -> exited process not yet reaped by parent.
- SIGHUP -> signal sent when a terminal disconnects.
Mental Model Diagram (ASCII)
Parent (emulator) Child (shell)
---------------- ----------------
master fd 3 <-----> slave fd 4 -> stdin/stdout/stderr
| |
v v
read/write exec("/bin/bash")
How It Works (Step-by-Step)
- Parent opens PTY master and forks.
- Child opens slave,
dup2()s to 0/1/2, closes master, thenexec(). - Parent closes slave and enters I/O loop on master.
- Parent waits for child exit and restores terminal state.
Invariants:
- Child has no open master fd.
- Parent has no open slave fd.
- Parent reaps child on exit.
Failure modes:
- Leaving master open in child prevents EOF detection.
- Not handling SIGCHLD creates zombies.
- Not restoring termios leaves a broken terminal.
Minimal Concrete Example
if (pid == 0) {
setsid();
int slave = open(slave_name, O_RDWR);
ioctl(slave, TIOCSCTTY, 0);
dup2(slave, STDIN_FILENO);
dup2(slave, STDOUT_FILENO);
dup2(slave, STDERR_FILENO);
close(slave);
close(master);
execlp("/bin/sh", "sh", NULL);
} else {
close(slave_fd_if_open);
}
Common Misconceptions
- “
exec()closes all fds.” -> Only fds markedFD_CLOEXEC. - “EOF arrives when the child exits.” -> EOF arrives only when all writers close.
- “Zombies are harmless.” -> They leak entries in the process table.
Check-Your-Understanding Questions
- Why must
dup2()happen beforeexec()? - What happens if both parent and child keep the master open?
- Why should the parent close the slave fd?
Check-Your-Understanding Answers
exec()preserves fds, so they must be configured first.- EOF never arrives on the master, confusing the emulator.
- It prevents unintended reads/writes and ensures clean EOF semantics.
Real-World Applications
- IDE terminals and embedded shells
- PTY-based automation tools (
expect) - Web terminals that spawn a PTY per session
Where You’ll Apply It
- This project: Section 3.2 (child spawn), Section 7.1 (pitfalls)
- Also used in: P04-minimal-terminal-emulator-100-lines, P14-web-terminal-xterm-js-backend
References
- “Advanced Programming in the UNIX Environment” (Stevens/Rago), Chapters 8-10
fork(2),execve(2),dup2(2)man pages
Key Insight
Correct PTY wiring is 80 percent about disciplined file descriptor ownership.
Summary
If you can wire a child process to a PTY slave and manage its lifecycle, you can build any terminal-based system.
Homework/Exercises to Practice the Concept
- Print the list of open fds before and after
exec(). - Demonstrate a zombie by omitting
waitpid(), then fix it. - Prove EOF behavior by keeping an extra master fd open.
Solutions to the Homework/Exercises
- Inspect
/proc/self/fdbefore and afterexec(). - Comment out
waitpid()to create zombies, then re-enable it. - Keep an extra master fd in a helper process and watch EOF never arrive.
3. Project Specification
3.1 What You Will Build
A PTY Explorer CLI that:
- Creates PTY master/slave pairs and prints identifiers.
- Spawns a child shell on the slave with correct session handling.
- Logs raw bytes read from the master in hex and decoded text.
- Toggles canonical vs raw mode and shows the difference in bytes.
- Updates window size and verifies SIGWINCH delivery.
- Reports process/session/foreground group metadata for debugging.
Intentionally excluded:
- Full terminal rendering or escape-sequence parsing.
- GUI windowing or GPU rendering.
3.2 Functional Requirements
- PTY creation: Use
posix_openpt,grantpt,unlockpt, andptsname. - Child spawn:
fork(),setsid(),TIOCSCTTY,dup2()to stdio. - Byte logger: Log raw bytes from the master with hex and escaped text.
- Mode toggles: CLI flags for canonical/raw, echo on/off, and ISIG on/off.
- Resize handling:
--resize COLSxROWSupdates slave size and logs SIGWINCH. - Process metadata: Print pid, pgid, tpgid, and session ID for child.
- Exit handling: Clean shutdown, restore termios, return meaningful exit codes.
3.3 Non-Functional Requirements
- Performance: Sustain 100k bytes/sec without dropping output.
- Reliability: Always restore the user’s terminal mode on exit.
- Usability: Clear log formatting,
--help, and deterministic demo flags.
3.4 Example Usage / Output
$ ./pty_explorer --raw --resize 120x40
[PTY] master=3 slave=/dev/pts/5
[CHILD] pid=4242 session=4242 pgid=4242 tpgid=4242
[TTY] mode=raw echo=off isig=on
[WIN] set 120x40 -> SIGWINCH delivered
RAW: 61
TEXT: a
RAW: 62
TEXT: b
RAW: 0d
TEXT: \r
3.5 Data Formats / Schemas / Protocols
- Log line format:
TIMESTAMP | CHANNEL | PAYLOAD - Hex output:
1b 5b 33 31 6d - Resize argument:
<cols>x<rows>(example:120x40)
3.6 Edge Cases
- Shell not found or fails to exec.
- Invalid resize string or zero size.
- PTY slave cannot be opened due to permissions.
ISIGdisabled so Ctrl+C becomes raw byte 0x03.
3.7 Real World Outcome
A working CLI that exposes PTY behavior with deterministic, inspectable logs.
3.7.1 How to Run (Copy/Paste)
cc -Wall -Wextra -O2 -o pty_explorer main.c
TZ=UTC LC_ALL=C ./pty_explorer --raw --resize 120x40 --demo-seed 1234
3.7.2 Golden Path Demo (Deterministic)
- Run with
--demo-seed 1234so timestamps are fixed and synthetic inputs are replayed. - Observe raw bytes for
a b <Enter>are always61 62 0d. - Verify
stty sizein the child reports120 40.
3.7.3 Failure Demo (Deterministic)
$ ./pty_explorer --resize 0x9999
error: invalid size "0x9999" (cols and rows must be > 0)
exit status: 64
3.7.4 If CLI: exact terminal transcript
$ ./pty_explorer --raw --resize 120x40
[PTY] master=3 slave=/dev/pts/5
[CHILD] pid=4242 session=4242 pgid=4242 tpgid=4242
[TTY] mode=raw echo=off isig=on
[WIN] set 120x40 -> SIGWINCH delivered
RAW: 61
TEXT: a
RAW: 62
TEXT: b
RAW: 0d
TEXT: \r
^D
[CHILD] exited with status 0
4. Solution Architecture
4.1 High-Level Design
+-----------------+ bytes +------------------+
| CLI Frontend | <----------------> | PTY Manager |
+-----------------+ +------------------+
| |
v v
+-----------------+ +------------------+
| Logger/Tracer | | Child Shell Proc |
+-----------------+ +------------------+
4.2 Key Components
| Component | Responsibility | Key Decisions |
|---|---|---|
| PTY Manager | Create PTY, spawn child, manage job control | Use manual posix_openpt path for transparency |
| TTY Config | Toggle termios, set window size | Use tcgetattr/tcsetattr and TIOCSWINSZ |
| Logger | Print raw bytes and decoded text | Hex + escaped output for visibility |
| Event Loop | Multiplex stdin/master | select() is sufficient for two FDs |
4.3 Data Structures (No Full Code)
struct PtyState {
int master_fd;
pid_t child_pid;
struct termios saved_slave;
int cols;
int rows;
};
struct LogEvent {
uint64_t ts_ns;
enum { LOG_RAW, LOG_TEXT, LOG_INFO, LOG_ERR } kind;
size_t len;
unsigned char bytes[256];
};
4.4 Algorithm Overview
Key Algorithm: PTY Setup
- Open master, grant/unlock slave.
- Fork child, create session, open slave, set controlling terminal.
dup2()slave to stdio,exec()shell.- Parent reads/writes master and logs activity.
Complexity Analysis:
- Time: O(n) for n bytes processed
- Space: O(1) plus log buffer
5. Implementation Guide
5.1 Development Environment Setup
cc --version
make --version
5.2 Project Structure
pty-explorer/
|-- src/
| |-- main.c
| |-- pty.c
| |-- tty.c
| `-- log.c
|-- include/
| |-- pty.h
| |-- tty.h
| `-- log.h
|-- tests/
| `-- test_pty.c
|-- Makefile
`-- README.md
5.3 The Core Question You’re Answering
“What is a PTY, and how does the kernel turn raw keystrokes into job-controlled terminal behavior?”
5.4 Concepts You Must Understand First
Stop and verify that you can explain:
- Why
setsid()is required before opening the slave. - How
dup2()rewires stdio and why it must happen beforeexec(). - How
ICANON,ECHO, andISIGchange observed bytes.
5.5 Questions to Guide Your Design
- How will you keep log output readable under heavy output?
- Where do you restore termios if the program is interrupted?
- How do you distinguish bytes generated by line discipline vs application?
5.6 Thinking Exercise
Trace a single keypress from keyboard to the child process and mark where the line discipline transforms it.
5.7 The Interview Questions They’ll Ask
- Why are PTYs different from pipes?
- How does Ctrl+C become SIGINT?
- What is a controlling terminal and how is it assigned?
5.8 Hints in Layers
Hint 1: Start with forkpty
Use forkpty() to validate behavior, then replace it with manual setup.
Hint 2: Save termios Always store the original termios and restore it on exit.
Hint 3: Use select() Multiplex stdin and master without blocking.
Hint 4: Print bytes Always log in hex so control characters are visible.
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| PTY mechanics | “The Linux Programming Interface” | Ch. 64 |
| Sessions and job control | “The Linux Programming Interface” | Ch. 34 |
| Process lifecycle | “Advanced Programming in the UNIX Environment” | Ch. 8-10 |
5.10 Implementation Phases
Phase 1: Foundation (1-2 days)
Goals: PTY creation and child spawn. Tasks:
- Implement
posix_openpt+grantpt+unlockpt. - Fork child and wire slave FDs to stdio. Checkpoint: Shell runs on PTY slave and accepts input.
Phase 2: Diagnostics (2-3 days)
Goals: Byte logging and mode toggles. Tasks:
- Add hex logger for master reads.
- Add termios toggles for canonical/raw. Checkpoint: You can see different byte streams per mode.
Phase 3: Polish & Edge Cases (2-3 days)
Goals: Resize, cleanup, error paths. Tasks:
- Implement
TIOCSWINSZand SIGWINCH checks. - Restore termios on signals and exit. Checkpoint: User terminal returns to normal after exit.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale |
|---|---|---|---|
| PTY creation | forkpty vs manual |
Manual | Teaches internals and debugging |
| I/O multiplexing | select / poll / epoll |
select |
Simple with few FDs |
| Logging format | Hex only vs hex + text | Hex + text | Makes transformations visible |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Unit Tests | Validate termios toggles | Verify flags with tcgetattr |
| Integration Tests | Verify PTY spawn | Spawn shell and run echo |
| Edge Case Tests | Invalid resize input | --resize 0x10 |
6.2 Critical Test Cases
- PTY creation succeeds: master/slave are valid and readable.
- Child interactive:
echo helloappears through master. - Raw mode: bytes are delivered immediately without line editing.
6.3 Test Data
Input: "abc\n"
Expected raw bytes (canonical): 61 62 63 0d
Expected raw bytes (raw): 61 62 63 0a
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution |
|---|---|---|
Missing setsid() |
Ctrl+C does nothing | Make child a session leader |
| Leaving PTY in raw mode | User terminal broken | Save and restore termios |
| Not closing master in child | EOF never arrives | Close unused fds |
7.2 Debugging Strategies
- Use
straceto confirmioctlanddup2calls. - Log
tcgetpgrpandgetsidto verify job control wiring.
7.3 Performance Traps
Excessive logging can slow I/O; provide a --quiet mode for stress tests.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add a
--timestampflag for precise log timing. - Add
--no-echoto demonstrate echo control.
8.2 Intermediate Extensions
- Add a replay mode that feeds a recorded input log.
- Integrate a curses UI with separate panes for raw/text logs.
8.3 Advanced Extensions
- Support multiple PTYs and multiplex logging.
- Add a session recording format for compatibility regression tests.
9. Real-World Connections
9.1 Industry Applications
- IDE terminals and code editors
- SSH and remote shell services
9.2 Related Open Source Projects
- tmux: PTY-based multiplexer with session management
- xterm: classic terminal emulator with extensive PTY handling
9.3 Interview Relevance
- Job control and process groups
- PTY setup for interactive programs
- Diagnosing “works in terminal but not in pipe” bugs
10. Resources
10.1 Essential Reading
- “The Linux Programming Interface” (Kerrisk) - Chapters 34, 62, 64
- “Advanced Programming in the UNIX Environment” (Stevens/Rago) - Chapters 8-10
10.2 Video Resources
- University OS lectures on terminals and job control
- Conference talks on terminal emulator internals
10.3 Tools & Documentation
man pty,man termios,man setsidstraceordtrussfor syscall tracing
10.4 Related Projects in This Series
- P02-escape-sequence-parser - decodes output bytes
- P04-minimal-terminal-emulator-100-lines - minimal screen model
11. Self-Assessment Checklist
11.1 Understanding
- I can explain why PTYs are not pipes.
- I can trace Ctrl+C to SIGINT delivery.
- I can draw the PTY/session diagram from memory.
11.2 Implementation
- PTY creation works reliably.
- Raw/canonical toggles behave as expected.
- Resize events deliver SIGWINCH.
11.3 Growth
- I documented at least three surprising behaviors.
- I can explain this project in a job interview.
12. Submission / Completion Criteria
Minimum Viable Completion:
- PTY pair creation and child shell wiring works.
- Raw vs canonical mode differences are observable.
- SIGWINCH propagation is demonstrated.
Full Completion:
- Robust cleanup and exit handling.
- Readable logging output with deterministic demo mode.
Excellence (Going Above & Beyond):
- Multi-PTY support or replay tool implemented.
- Automated test suite validates core behaviors.