Before you start#
Read DMA, IOMMUs, and MSI and the driver model. The source baseline is Linux v6.12, a historical teaching version. The PTY lab changes only a newly allocated slave terminal and restores it before close; it does not read host keyboard events or alter your shell terminal.
Learning goals#
Trace a character-device open and its references, distinguish a terminal driver from its line discipline, explain canonical and raw input, and describe input-event lifetime separately from TTYs.
Source walkthrough#
- dev_t and cdev
chrdev_open resolves the cdev and acquires file operations.
- Open description
private_data and references govern per-open lifetime.
- TTY / n_tty
Line discipline processes terminal bytes under termios.
- Input subsystem
Typed input events have their own handlers and lifetime.
A character device node contains a dev_t device number with major and minor components. The number is a lookup key, not a pointer to hardware. A registered struct cdev associates a device-number range with struct file_operations; a successful open installs the selected methods on a struct file. The driver's open method may put per-open or device state in file->private_data, under a lifetime contract chosen by that driver. The struct device used for sysfs publication and the cdev used for dispatch have related but distinct lifetimes. See fs/char_dev.c, include/linux/cdev.h, and include/linux/fs.h.
chrdev_open, Linux v6.12, GPL-2.0, first looks for an existing inode->i_cdev while holding cdev_lock. If absent, it drops the lock to look up the number, then reacquires the lock and checks again: another opener can have installed the association in the meantime. It obtains the relevant cdev reference and drops an unused newly looked-up reference. The dispatch excerpt is:
fops = fops_get(p->ops);
if (!fops)
goto out_cdev_put;
replace_fops(filp, fops);
fops_get attempts to obtain the file-operations/module ownership needed for the call; if that fails, the cdev reference is put on the error path. replace_fops installs the acquired methods on the open file. The subsequent driver open callback can also fail, in which case out_cdev_put releases the cdev reference; the surrounding VFS open failure releases the file and its installed operations under their rules. A successful lookup of a device number is not proof that driver open or later I/O succeeds.
- ProcessVFS
open(node)
- VFSchrdev_open
find or install i_cdev
- chrdev_openDriver open
fops_get, replace_fops, open()
- Driver openchrdev_open
Return. 0 or error
- chrdev_openVFS
Return. cdev reference kept or put
- VFSProcess
Return. fd or -errno
Time runs downward. Dashed arrows show returns.
Read the text version
- Process → VFS: open(node)
- VFS → chrdev_open: find or install i_cdev
- chrdev_open → Driver open: fops_get, replace_fops, open()
- Driver open → chrdev_open (return): 0 or error
- chrdev_open → VFS (return): cdev reference kept or put
- VFS → Process (return): fd or -errno
cdev_del prevents new opens through the removed registration, but existing open file descriptions can still invoke methods. A module owner reference can keep code loaded while file operations are in use; it does not keep unplugged hardware present or guarantee private data remains valid. A hot-unplug-safe driver needs a disconnected flag, locking and references such as a kref where appropriate. It must stop new work, quiesce hardware and keep the per-open object alive until the last user releases it. An open fd after unplug should return a defined error or EOF under the driver's contract, not access unmapped registers.
In cdev_del, Linux v6.12, GPL-2.0, the kerneldoc states the guarantee and the body is shown in full:
/**
* cdev_del() - remove a cdev from the system
* @p: the cdev structure to be removed
*
* cdev_del() removes @p from the system, possibly freeing the structure
* itself.
*
* NOTE: This guarantees that cdev device will no longer be able to be
* opened, however any cdevs already open will remain and their fops will
* still be callable even after cdev_del returns.
*/
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count);
kobject_put(&p->kobj);
}
The comment is the source's own statement: no new opens through this registration, while already-open files keep callable fops. The body only removes the device-number mapping and drops a kobject reference, so it does not show how a driver protects private state used by files that are still open.
Read, poll, ioctl, and mapping contracts#
A blocking read commonly checks a predicate under the relevant lock, waits on a waitqueue if no data is available, then rechecks the predicate after wakeup. A wakeup is not a reservation of bytes: another reader can consume them, a signal can interrupt the wait, or the device can disconnect. A nonblocking read should report an appropriate EAGAIN when it would block. poll/epoll readiness similarly signals that an operation may make progress; user code must handle a race with another consumer. Never sleep while holding a spinlock or in an interrupt context that forbids sleeping.
A driver copying data to or from userspace uses the appropriate user-access helpers and validates lengths. A short copy or a signal can require a partial positive result or error under that file operation's contract. An ioctl is a userspace ABI, not an invitation to trust a pointer or structure: validate command, size, flags and input ranges; use fixed-width UAPI fields and explicitly defined padding; and account for 32-bit compat behavior on 64-bit kernels. Avoid exposing kernel pointers or uninitialized padding. If a driver supports mmap, the VMA can outlive the fd that created it, so its pages and device-private state need their own reference and removal protocol. Closing the fd is not automatically the last use of the mapping.
Imagine a device whose receive queue holds bytes ABC. Reader A requests two bytes and receives AB, leaving C. Reader B can receive C; a subsequent nonblocking read returns EAGAIN while the device remains connected and the queue is empty. If unplug occurs before another read, the driver may instead report its defined disconnected error or EOF. The exact result is driver-specific. A queued byte count, an open-file reference and a live physical device are three different pieces of state.
TTY, line discipline, and PTYs#
A TTY combines a tty_driver, a tty_struct for an endpoint and often a tty_port coordinating port-level state and lifetime. The driver handles the underlying transport; the line discipline interprets and buffers terminal input and output. The common n_tty discipline implements canonical processing, echo and related termios behavior. A terminal emulator is userspace; a physical UART/serial driver, a PTY pair, and an application terminal emulator are different components. drivers/tty/tty_io.c, tty_port.c, and n_tty.c implement relevant layers.
In canonical mode (ICANON), the line discipline normally makes input available by line, subject to special characters and settings. Echo can send typed characters back toward the terminal; IXON enables software flow-control interpretation; ISIG can cause configured terminal characters to generate signals for the foreground process group. Canonical mode is not identical to echo, and raw mode is a collection of termios changes rather than a new device type. Noncanonical VMIN and VTIME affect when reads complete. The controlling terminal and foreground process group influence job-control signals; the exact behavior depends on termios and session state.
In n_tty_read, Linux v6.12, the file's SPDX identifier is GPL-1.0+. Its read-serialization branch includes:
if (file->f_flags & O_NONBLOCK) {
if (!mutex_trylock(&ldata->atomic_read_lock))
return -EAGAIN;
The nonblocking path tries the read mutex and returns -EAGAIN if another reader holds it. The blocking path uses an interruptible mutex acquisition, after which the function takes the terminal settings read semaphore, sets up wait state and processes data according to canonical/noncanonical mode. This is one EAGAIN cause, not a complete description of all empty-input outcomes. The locks protect particular reader and termios relationships, not all driver state or userspace buffers.
A PTY has a master and a slave endpoint. Data written to the master becomes input to the slave's terminal processing; output written to the slave is read from the master. A shell in a terminal emulator commonly uses a PTY, while a physical serial port talks to hardware through a driver. The PTY lab writes only to its own master and reads its own slave. It disables echo for the canonical phase so the master does not accumulate echoed bytes, then uses raw mode for comparison. A real serial link has baud, framing, modem control and hardware error behavior not demonstrated by a PTY.
Input events are a separate subsystem#
An input_dev represents an input device and reports typed events to registered input handlers. An evdev handler can expose an event stream with timestamps, type, code and value; SYN_REPORT groups updates into an input event frame. Keyboard keys, relative mouse motion and touch events have different codes and state rules. The input core and evdev handle registration, filtering, event delivery and disconnect under their own locks and references; see drivers/input/input.c and drivers/input/evdev.c. A keyboard event may be transformed by a userspace compositor or terminal emulator before bytes reach a TTY; evdev is not the TTY line discipline.
In input_event, Linux v6.12, GPL-2.0-only, the event is checked against the device's supported types and then processed under an interrupt-safe spinlock (the kerneldoc is omitted):
void input_event(struct input_dev *dev,
unsigned int type, unsigned int code, int value)
{
unsigned long flags;
if (is_event_supported(type, dev->evbit, EV_MAX)) {
spin_lock_irqsave(&dev->event_lock, flags);
input_handle_event(dev, type, code, value);
spin_unlock_irqrestore(&dev->event_lock, flags);
}
}
The excerpt shows only the support check and the event_lock critical section. It does not show what input_handle_event does with each value or how handlers such as evdev timestamp and frame it, so it says nothing about how a key event reaches a terminal.
An input device can disappear while a userspace evdev fd exists. The handler must make the existing client observe the appropriate disconnected state and preserve its structures until references are released. Event timestamps have a selected clock context and should not be casually compared with another clock source. A SYN_DROPPED indication tells a client that its view needs resynchronization according to the evdev API; it is not one ordinary key press. This lesson inventories only sysfs names/counts, not actual input events or keystrokes.
A canonical/raw worked sequence#
Create a PTY pair with canonical input and echo disabled. Write abc to the master. Before a line delimiter, a nonblocking readiness check on the slave normally reports no complete canonical line; write a newline and the slave can read abc\n. Switch that same slave to raw mode with VMIN=1 and VTIME=0. Write Z to the master: one byte is now sufficient for a read without waiting for newline. The test uses the PTY's current settings and has no controlling-terminal or serial hardware claim. Always restore the original termios in finally before closing the fds, even if the test fails.
A different process changing the terminal's foreground group, an input signal, a hangup or a closed endpoint changes outcomes. If the master disappears, the slave may observe hangup/error according to its state. An application must not assume that every byte typed on a physical keyboard becomes one byte in a canonical read: input methods, terminal emulation, line editing and encoding can transform it. Conversely, a raw PTY test does not prove a physical UART's electrical framing or flow-control wiring.
An open description during unplug#
Suppose a character driver registers major M, minor 3 and creates a device node. A process opens that node and the VFS finds the cdev by the inode's device number. The driver takes a reference to its private object and puts that pointer in file->private_data. A second dup of the fd refers to the same open file description; it does not necessarily call the driver's open method again. When the physical device disappears, removing the cdev prevents new lookups, but the duplicated descriptors can still reach the installed operations. A disconnected flag and a private-object reference allow those operations to return a safe error while the object survives until the last release.
The driver also needs a rule for a thread already sleeping in read. Removal must change the predicate protected by the driver's lock and wake relevant waiters; otherwise a reader can remain asleep indefinitely after the device disappears. A waiter wakes, reacquires the lock and tests both data availability and disconnection. The driver must choose and document whether it returns buffered bytes first, EOF or an error in that state. The core's cdev reference and a waitqueue do not decide that policy automatically. Interrupt, worker and DMA cleanup are separate obligations before the private hardware resources are freed.
For an ioctl, a fixed-width UAPI structure still needs validation of version, flags, lengths and reserved fields. A 64-bit kernel serving a 32-bit task must account for layout, pointer representation and the driver's compat_ioctl behavior when the command is not layout-compatible. A value copied from userspace can change while the syscall runs unless the kernel uses a stable copied representation or other suitable protocol. A failed copy_from_user or copy_to_user is an error condition, not permission to use uninitialized kernel memory. These are design questions; the chapter does not present a generic ioctl implementation as production-ready.
Terminal settings and event boundaries#
Canonical line processing can interpret erase and kill characters before a read returns the resulting line. With ISIG enabled, terminal characters can generate signals for the foreground process group instead of being delivered as ordinary bytes. IXON can interpret configured start/stop characters as output flow control. Echo is independent of ICANON: a program can request canonical input with echo off, as the lab does. The exact special characters and flags are termios state, not a universal property of every terminal. Raw-mode helper functions change several flags, so applications should save the complete original settings and restore them even on errors.
A PTY's master is normally controlled by a terminal emulator or another supervisor, and its slave presents a terminal interface to the child process. Closing the master, changing foreground process group or assigning a controlling terminal can affect signal and hangup behavior; the small lab does not create an interactive shell or test job control. A real serial port can also generate framing, parity, carrier and overrun conditions, and its driver must translate hardware state into the TTY interfaces. Observing canonical buffering on a PTY says nothing about cable wiring or whether a UART is powered.
An input driver instead reports events such as a key state change through input core. A report can consist of multiple events followed by SYN_REPORT, and an evdev client's timestamps and state depend on its selected interface and clock. A userspace compositor can transform a key event into text, and a terminal emulator can then write bytes to a PTY master. Those transformations explain why a key event code is not the same thing as a line-discipline byte. The metadata inventory deliberately avoids opening event devices, which could reveal private user activity.
Readiness is a promise to retry, not a reservation#
A character driver's poll method can register waitqueue interest and then report readiness based on current state. If two readers are awakened for one available record, only one may consume it; the other must handle an empty queue or EAGAIN. The state check and waitqueue registration need a race-free protocol or an event can occur between checking and sleeping. The details belong to the particular driver's locks and wakeups. A poll bit does not automatically keep an unplugged physical device accessible, and a callback must not dereference private state after its final reference has been released.
- Register waiter
Add waitqueue interest before the final check.
- Check predicate
If data is ready, consume it under the driver lock.
- Sleep or return
Block, or return EAGAIN when nonblocking and empty.
- Wakeup
A wakeup reports possible progress, not reserved bytes.
- Recheck
Reacquire the lock and retest; another reader may have consumed the data.
- Consume or retry
Take the data, or wait again.
The PTY lab tests canonical and raw behavior with a single reader and bounded timeouts. It does not test competing readers, signal generation, VMIN/VTIME combinations, evdev overflow or hot unplug. If the canonical check reports readiness early, the correct result is to record the settings and observation rather than claim all terminals follow the expected timeline. The process restores only the PTY it created; changing the user's existing terminal would test a different object and is outside the lab.
Practice#
Source-only route:
rg -n -A65 'static int chrdev_open\(' "$KERNEL_SRC/fs/char_dev.c"
rg -n -A90 'static ssize_t n_tty_read\(' "$KERNEL_SRC/drivers/tty/n_tty.c"
rg -n -A25 '^void input_event\(' "$KERNEL_SRC/drivers/input/input.c"
Verify v6.12. Mark the cdev lookup/recheck, fops reference and failure label, then the n_tty read lock and waitqueue setup. Explain why cdev_del and final file release are separate events.
Run this own-PTY experiment:
python3 - <<'PY'
import os, pty, select, termios, tty
master, slave = pty.openpty()
old = termios.tcgetattr(slave)
try:
cfg = termios.tcgetattr(slave)
cfg[3] |= termios.ICANON
cfg[3] &= ~termios.ECHO
termios.tcsetattr(slave, termios.TCSANOW, cfg)
os.write(master, b'abc')
print('canonical before newline:', bool(select.select([slave], [], [], 0.1)[0]))
os.write(master, b'\n')
if not select.select([slave], [], [], 1)[0]: raise RuntimeError('canonical timeout')
print('canonical:', repr(os.read(slave, 32)))
tty.setraw(slave)
os.write(master, b'Z')
if not select.select([slave], [], [], 1)[0]: raise RuntimeError('raw timeout')
print('raw:', repr(os.read(slave, 1)))
finally:
termios.tcsetattr(slave, termios.TCSANOW, old)
os.close(slave); os.close(master)
PY
if [ -d /sys/class/tty ]; then ls /sys/class/tty | head; else echo 'TTY metadata unavailable'; fi
if [ -d /sys/class/input ]; then ls /sys/class/input | head; else echo 'input metadata unavailable'; fi
Expected False, b'abc\n', and b'Z'. A timeout, unavailable PTY or termios error is a valid failure to report. The finally block restores only the new slave and closes both fds. For read-only inventory, ls /sys/class/tty | head and ls /sys/class/input | head show names if exposed; no /dev/input/event* is opened. Deliver the observed mode timeline or error and the cdev ownership diagram.
Knowledge check#
- Why does
chrdev_openrecheckinode->i_cdevafter dropping the lock? - What does
cdev_delnot guarantee about already-open files? - Why can
abcwait in canonical mode whileZis readable in raw mode? - How does an evdev event differ from a TTY input byte?
- Why can an mmap outlive the descriptor used to create it?
Answers#
- Another opener can install the inode association during the unlocked lookup, so the function must reconcile references under the lock.
- Existing open descriptions can still call file operations; private data and disconnected hardware need their own lifetime handling.
- Canonical processing waits for a line delimiter under these settings, while the raw test permits a one-byte read.
- An evdev event has typed input metadata and synchronization frames; TTY bytes are processed by a terminal path and line discipline.
- A VMA has its own lifetime, so mapped pages and driver state need references independent of the original fd.
Checkpoint#
Save the PTY results and cdev open/removal timeline. Pass when you can explain the failed fops acquisition, canonical/raw state changes, EOF versus EAGAIN, and input-versus-TTY boundary. If PTYs are unavailable, complete the source route and record the limitation. This is practice, not certification.