|
AquilaOS documentation
How it works, and its code
|
A UNIX kernel shows devices to programs as files. A program opens /dev/hda, /dev/tty1 or /dev/null and reads and writes it like any other file; what happens behind the read is the device's business. This one idea takes care of most of the interface: cat /dev/hda1 > disk.img works without anyone having written a disk-copying program.
This document covers how a file becomes a device (device numbers, the device layer, /dev), the two kinds of devices (block and character), the disks, and the largest of the character devices: terminals, with the line discipline, virtual terminals, serial ports and pseudo-terminals. It ends with the smaller devices and PCI.
The device layer, the block cache, the line discipline and the pseudo-terminals are the kernel's own, the same on any machine. Drivers are generic code for a particular device: the ATA driver would work on any machine with an IDE controller. Drivers are told where their device is and which interrupt it raises: by the bus (PCI), or by the platform's table of the devices no bus finds, and they install their handlers with irq_install() (see "Devices: drivers and the platform" in The kernel and the machine). Some of the devices AquilaOS drives exist only on PCs (the i8042 keyboard controller, VGA text mode): sections about those say "PC" in their title.
A device is named by a device number, two small integers: the major number says which driver, the minor number which of its devices. A device node, a special file in /dev, is nothing but a type (character or block) and a device number; its name means nothing to the kernel. mknod /dev/hda1 b 3 1 makes "block device, major 3, minor 1", which is the ATA driver's first partition of the first disk.
AquilaOS packs the two into a dev_t with DEV(major, minor), eight bits each (include/dev/dev.h); DEV_MAJOR() and DEV_MINOR() take them apart. The kernel makes no nodes itself, except /dev/console: /etc/runit/1 makes them at boot with mknod, in devfs (see Filesystems).
| Major | Type | Driver | Minors |
|---|---|---|---|
| 1 | char | memory devices (dev/mem/) | 3 null, 5 zero, 8 random, 9 urandom, 11 kmsg |
| 1 | block | the ramdisk (dev/rd/) | 0 |
| 2 | char | pseudo-terminal masters (dev/tty/pty/) | one per pty |
| 3 | block | ATA disks (dev/ata/) | 64 × disk + partition (0: the whole disk) |
| 4 | char | terminals (dev/tty/) | 0 the foreground VT, 1–6 tty1–tty6, 64 and up ttyS0... |
| 5 | char | terminals' aliases | 0 tty, 1 console, 2 ptmx |
| 7 | char | virtual terminals' screens | N vcsN, 128 + N vcsaN |
| 10 | char | the PS/2 mouse (dev/mouse/) | 1 mouse |
| 29 | char | framebuffers (dev/fb/) | 0 fb0 |
| 136 | char | pseudo-terminal slaves | N /dev/pts/N |
The numbers are Linux's, so programs ported from Linux find what they expect.
The device layer, kdev (dev/kdev.c), is a table: for each major number, the struct dev of its driver, one table for character devices and one for block devices. A driver registers itself when it starts (kdev_chrdev_register(), kdev_blkdev_register()), from its MODULE_INIT() function (see Booting).
When the VFS opens or reads a device node, it calls kdev_file_open(), kdev_read() and so on with the node's struct devid (type, major, minor); kdev finds the driver and calls its function. A missing function is ENXIO, except lseek, which is ESPIPE (terminals and other streams can't seek), and close, which only drops the vnode's reference (posix_file_close()).
One major number can hold several drivers: a mux function picks the driver for a minor. Major 1 is /dev/null, /dev/zero, /dev/random and /dev/kmsg, each its own driver; major 4 is virtual terminals below 64 and serial ports from 64 on.
The two kinds differ in what they move:
A disk is a block device. Two things sit between a filesystem and the driver: partitions and the block cache.
Partitions. A PC disk starts with a master boot record (MBR): boot code, then a table of four partitions, each a first sector and a number of sectors (include/fs/mbr.h). The minor number names the partition (/dev/hda2 is minor 2); the driver reads the table the first time a partition is used, and adds the partition's first sector to every block number. A partition that isn't there is ENXIO; a write past its end, ENOSPC. Only the MBR's four primary partitions are read: no extended partitions, no GPT.
The block cache (dev/blkcache.c). Disks are slow, and filesystems read the same blocks again and again (a directory, an inode table, a bitmap). The block cache keeps what disks read and write, 8 MiB of it, in 4 KiB blocks of 8 sectors:
kdev_sync() writes the cache back, then asks each block driver to flush its disk's own write cache (sync).
The ramdisk (dev/rd/ramdisk.c) is the first boot module, the initramfs, shown as block device 1: reads and writes copy from and to the memory where the boot loader left it. Its block size is 1 byte.
A terminal is where a user types and reads: once a real terminal on a serial line, now a window, the screen of the console, or an SSH connection. Programs see the same interface for all of them, defined by POSIX: termios.
Between the device and the program sits the line discipline, the kernel code that makes a stream of key presses into what a program reads, and a program's output into what the device shows (dev/tty/generic.c). Every terminal has a struct tty (include/dev/tty.h): its termios settings, its window size, the line being edited, and its session and foreground process group. The device gives the tty two functions: master_write sends output to the device (the screen, the serial line, the pty master), and slave_write queues input for readers.
Input (tty_master_write()), each character the device received:
Output (tty_slave_write()), with OPOST: NL is sent as CR NL (ONLCR, the default), CR as NL (OCRNL), a CR in the first column is dropped (ONOCR), and a NL may return the carriage too (ONLRET). For these the tty follows the cursor's column (col), counting tabs and backspaces. If the device takes the CR of a CR NL but not the NL, the NL is owed and goes first next time (nl_owed).
ioctl()s read and change all of this: TCGETS, TCSETS (and TCSETSW, TCSETSF), TCFLSH, TIOCGWINSZ, TIOCSWINSZ, and the job control ones below (TIOCSCTTY, TIOCGPGRP, TIOCSPGRP, TIOCGSID). What libc's tcgetattr(), tcsetattr() and cfmakeraw() call is these.
Flow control (IXON, the default): ^S (VSTOP) stops the output, and ^Q (VSTART), or any character with IXANY, starts it again; neither is input. While it is stopped, programs that write to the terminal wait.
Readiness: for select() and poll(), a terminal is readable only when it holds input or a pending end of file (tty_can_read()), even with VMIN and VTIME 0, as on Linux and the BSDs.
Shortcuts: no output is ever waiting to be sent, so TCSETSW is TCSETS, tcdrain() returns at once, and TCSBRK and TCXONC do nothing; the speed in termios isn't used by any driver.
A session is a login: a shell and everything started from it. Its processes are in process groups, one per job (ls | less is one group), and the terminal has one foreground group, the one that gets the keyboard's signals and may read (see Processes and Signals).
A pseudo-terminal (pty) is a terminal with a program instead of a device at the other end: a terminal emulator, script, an SSH server. It is a pair: the program holds the master, and the programs it runs use the slave as their terminal. What the master writes is input to the slave, through the line discipline, as if typed; what the slave's programs write comes out of the master (dev/tty/pty/pty.c).
Opening /dev/ptmx (5, 2) makes a new pty: the descriptor becomes its master (major 2, which has no node), and the slave appears in devpts as /dev/pts/N (major 136, minor N); TIOCGPTN says which N. There are at most 256 ptys. Each side has a 4 KiB ring. When the master is closed, the slave's session is hung up, the slave's node goes, and slaves still open fail with ENXIO. grantpt() and unlockpt() have nothing to do: the slave is the opener's from the start, and never locked.
/dev/console (5, 1) is where init and the kernel write: on AquilaOS, the first virtual terminal (below), or, on a machine without virtual terminals (aarch64's virt), the first UART. The kernel's own messages (printk()) go to the foreground virtual terminal once there is one, and to a ring that /dev/kmsg reads. Before the terminals are up, and all along without them, they go through the early console (see Booting).
The ATA (IDE) driver (dev/ata/) drives the parallel ATA disks of an IDE controller, the kind QEMU's PC has (PIIX). The controller is found on PCI (class 1, subclass 1); it has two channels, each with a master and a slave drive, so four drives: hda, hdb on the first channel, hdc, hdd on the second (minors 0, 64, 128, 192).
Virtual terminals (VTs) make the screen and keyboard into six terminals, /dev/tty1 to /dev/tty6, switched with Alt+F1 to Alt+F6 (dev/tty/vt/vt.c). Only the one in the foreground is drawn; the others keep their screens. /dev/tty0 is whichever is in the foreground.
The VT is a device of the tty layer like any other: it gives the tty its master_write (into the emulator) and takes the line discipline's output for readers. The VT layer and the emulator are the kernel's; VGA text mode and the PS/2 scancodes are the PC's.
The PC's keyboard and mouse hang off the i8042 PS/2 controller (dev/input/i8042.c, DEV_I8042), a device in the PC's table: ports 0x60 and 0x64, the keyboard's port on IRQ 1, the mouse's on IRQ 12. The driver turns the second port on; the keyboard and mouse drivers register a function for a port (i8042_handler_register()), which gets each byte that comes, and the mouse's sends it bytes (i8042_aux_write()):
A UART is a serial port. The tty side, generic, is in dev/tty/uart/uart.c: a UART is a terminal, /dev/ttyS0 and up (4, 64 and up), with a 4 KiB ring each way. Two drivers:
Output is written a byte at a time, waiting for the transmitter.
/dev/fb0 (29, 0, dev/fb/fbdev/fbdev.c) is the screen as memory, with Linux's framebuffer ioctl()s (FBIOGET_VSCREENINFO, FBIOGET_FSCREENINFO, ...): a program can mmap() it and draw. The only framebuffer driver, vesa (dev/fb/vesa/vesa.c), is the linear framebuffer the boot loader set up: the PC's Multiboot code registers it (see Booting). AquilaOS can't change the mode itself.
PCI is how devices on a modern PC (and many other machines) are found: each one has a configuration space, 256 bytes saying who made it (vendor and device IDs), what it is (class and subclass), which memory and ports it decodes (its base address registers, BARs), its interrupt line, and a list of capabilities (dev/pci/pci.c).
The host bridge is the platform's, a struct pci_host it gives pci_host_set() in platform_init():
PC: configuration mechanism #1. Ports 0xCF8 (an address: bus, device, function, register) and 0xCFC (the data), the first 256 bytes of a function's configuration space. The BIOS assigned the BARs and wrote the interrupt lines.
virt: ECAM. PCI Express's Enhanced Configuration Access Mechanism: every function's 4 KiB in memory, bus 0's 1 MiB of it mapped (0x40_1000_0000 on QEMU's virt machine). Nothing ran before the kernel (-kernel): it assigns the BARs itself, memory from 0x1000_0000, ports from 0x1000, and INTA# to INTD# are the GIC's SPIs 3 to 6. QEMU's -device e1000 works there unchanged.
| File | Layer | What |
|---|---|---|
| kernel/include/dev/dev.h | kernel | struct dev, device numbers |
| kernel/dev/kdev.c | kernel | the device layer, blocks to bytes |
| kernel/dev/blkcache.c | kernel | the block cache |
| kernel/dev/rd/ramdisk.c | kernel | the ramdisk |
| kernel/dev/tty/generic.c | kernel | the line discipline, job control |
| kernel/dev/tty/tty.c | kernel | /dev/tty, /dev/console, major 4's minors |
| kernel/dev/tty/pty/pty.c, fs/devpts/ | kernel | pseudo-terminals |
| kernel/dev/tty/uart/uart.c | kernel | serial ports as terminals |
| kernel/dev/tty/vt/vt.c, term.c | kernel | virtual terminals, the terminal emulator |
| kernel/dev/tty/vt/fb.c | kernel | VTs on a framebuffer |
| kernel/dev/mem/ | kernel | null, zero, random, kmsg |
| kernel/dev/fb/fbdev/fbdev.c | kernel | /dev/fb0 |
| kernel/dev/platform.c | kernel | the platform's devices: platform_device_get() |
| kernel/dev/pci/pci.c | kernel | PCI: devices, BARs, MSI, the host bridge |
| kernel/dev/pci/conf1.c, ecam.c | kernel | PCI configuration space: ports, memory |
| kernel/dev/ata/ | kernel, PC | ATA disks (ISA ports and IRQs in compatibility mode) |
| kernel/dev/net/e1000.c | kernel | the e1000 network card (see Networking) |
| kernel/dev/tty/vt/vga.c | PC | VTs in VGA text mode |
| kernel/dev/tty/vt/keyboard.c | PC | PS/2 scancodes to the console's keys |
| kernel/dev/kbd/ps2kbd.c, dev/mouse/ps2mouse.c | PC | the PS/2 keyboard and mouse |
| kernel/dev/tty/uart/8250/8250.c | kernel | 8250/16550 serial ports |
| kernel/dev/tty/uart/pl011/pl011.c | kernel | the PL011 serial port |
| kernel/dev/fb/vesa/vesa.c | PC | the boot loader's framebuffer |
| kernel/dev/input/i8042.c | PC | the PS/2 controller |
| kernel/arch/i386/platform/pc/devices.c | PC | the PC's devices: serial ports, the i8042 |
| kernel/arch/i386/platform/pc/init.c | PC | PCI's host bridge: mechanism #1 |
| kernel/arch/aarch64/platform/virt/devices.c | virt | the virt machine's devices: the PL011 |
| kernel/arch/aarch64/platform/virt/init.c | virt | PCI's host bridge: ECAM, BARs, INTx |
| rootfs/etc/runit/1 | — | the device nodes, made at boot |