In part one, I laid out the four different definitions of “terminal”, and pointed out that from here on in, I would be speaking mostly of terminal devices such as /dev/tty1
.
First, a short discussion on how Linux devices work in general.
User space: It is a well-known fact that, in the Unix philosophy, everything is a file1. Thus, in order to read data from, or write data to, a hardware device, you would read or write a file descriptor to that hardware device. Some hardware devices might additionally support seeking (for example, a disk such as /dev/sda1
). If it’s not a read, write, or a seek, it’s usually implemented as an ioctl
(2). In each case, a process would act on a file descriptor, rather than directly communicating with the device through I/O ports or some shared region of memory (which, in general, only the kernel is allowed to do).
In general, in order to obtain a file descriptor to some device, a process would open a special kind of file called a device node, which, in general, lives in /dev
. Each such device is either a block device (buffered) or character device (unbuffered), and if a single device can act as either block or character, then there will be two device nodes for that file: one block, one character. Each device node also has an associated major and minor device type number. If you rename a device node, it’ll still refer to the same device, because the major and minor numbers determine which device it refers to. You can see the major and minor numbers in the output of stat
(1):
root@foxen:~# stat /dev/ptmx
File: `/dev/ptmx'
Size: 0 Blocks: 0 IO Block: 4096 character special file
Device: 5h/5d Inode: 1802 Links: 1 Device type: 5,2
Access: (0666/crw-rw-rw-) Uid: ( 0/ root) Gid: ( 5/ tty)
Access: 2012-12-16 10:57:55.658484091 +0000
Modify: 2012-12-16 10:57:55.768481474 +0000
Change: 2012-10-22 01:46:05.078400254 +0000
(Emphasis is mine; output of GNU stat
(1) shows device types in hexadecimal. Note that the “c” in “crw-rw-rw-” means that the device is character special, as opposed to regular files which have “-” in that position, directories, which are “d”, pipes, which are “p”, block special files, which are “b”, or Unix domain sockets, which are “s”.)
There is one notable exception: on most systems, you cannot obtain a file descriptor to an internet socket by open
(2)ing a file in /dev
. Instead, such file descriptors are obtained through the Berkeley sockets API functions socket
(2), socketpair
(2), accept
(2), and connect
(2) (which are sometimes true kernel system calls, and sometimes layers over a common kernel entry point called socketcall
(2)). But never mind that—I won’t be talking about sockets.
The root user, or generally, a user with the CAP_MKNOD
capability, can create new device nodes using the mknod
(1) utility or the mknod
(2) system call. Usually, this is done automatically on system startup. However, if, for example, I accidentally delete the device node /dev/tty
, I can create it again if I know that it’s a character device with major number 4 and minor number 1: mknod /dev/tty1 c 4 1
. After the root user has created a device node, it can grant access to the device to other users by modifying the permissions on the device node appropriately.
Kernel space: Device drivers, which are part of the kernel (either statically compiled in, or dynamically linked in as kernel modules) are allowed to communicate directly with devices. In order to make a device available to user space, a device driver must obtain a major and minor device number for that device. Often, the device driver will also create a device node with that major and minor device number on initialization. So the major and minor device numbers are determined entirely on the kernel side; in user space, you can create a device node with whatever major and minor device numbers you want, but unless those numbers have been registered by a device driver, you’ll simply get ENXIO when you try to open that file.
Usually, a driver will have a single major device number associated with it. If it is one of the basic drivers necessary in any Linux system, such as a terminal driver, the major device number is fixed and defined in the kernel headers; see for example include/linux/major.h from Linux 2.6.38. (You can see here, among others, hardcoded values for PTY_MASTER_MAJOR
, PTY_SLAVE_MAJOR
, TTY_MAJOR
, and TTYAUX_MAJOR
: 2, 3, 4, and 5—the major numbers you saw in part one.) A loadable driver can either declare a fixed major number for itself or obtain an unused major number dynamically. It can then divvy up the minor numbers for that major number however it chooses. In either case it uses the kernel functions register_blkdev()
or register_chrdev()
. When a driver registers a device in this way, it must pass a struct file_operations *
to the function, containing function pointers that include (among others) open
, release
, read
, write
, and llseek
. When a process read
(2)s a device file descriptor, for example, the kernel calls whatever read
function the corresponding device driver specified at the time of device registration.
The above discussion was about Unix devices in general. Now I will say a brief word about terminal devices, and leave more detailed discussion to later parts. When a process reads from, say, /dev/tty1
, it will receive input that was typed on the first virtual terminal. Likewise, when a process writes to /dev/tty1
, characters will appear on the first virtual terminal’s virtual screen. This is the meaning of the Unix device abstraction applied to terminals. So, when the terminal driver registers the terminal devices, it will pass to the kernel a read
pointer to a function that will return keyboard input, and a write
pointer to a function that will display something on the virtual screen. It doesn’t make any sense to seek on a terminal (although you can move the cursor, which I’ll talk about later), so if you try, you’ll get ESPIPE.
If you start up bash on the first virtual terminal, and type: echo $$
, you’ll get the PID of the shell, say, 1496. You can use procfs to find out which files the shell has open:
# ls -l /proc/1496/fd
total 0
lrwx------ 1 root root 64 2012-12-16 18:30 0 -> /dev/tty1
lrwx------ 1 root root 64 2012-12-16 18:30 1 -> /dev/tty1
lrwx------ 1 root root 64 2012-12-16 18:30 2 -> /dev/tty1
lrwx------ 1 root root 64 2012-12-16 18:30 255 -> /dev/tty1
File descriptors 0, 1, and 2 have a fixed meaning of standard input, standard output, and standard error, respectively, in the sense that the C variable stdin
always refers to the file with descriptor 0 on a Unix-like system, and so on. The shell is fundamentally a command processor, which can read commands either from a terminal or from a regular file on disk somewhere. In the first case, as shown here, its standard input will refer to a terminal device, from which it can read commands as they are typed in by the user. In the second case, its standard input will refer to a regular file, from which it can read commands all the same. Similar statements can be made about standard output and standard error. This is the beauty of the Unix device system: the abstraction that it provides. (In practice, GNU bash probably treats the two differently; GNU code tends to be hacked for maximum performance rather than beautiful abstractions.)
If you’re wondering how the shell came to have open file descriptors to /dev/tty1
, you’re getting ahead of yourself. I’ll get to that soon, I promise.
1 One variation is: everything is a file; if it’s not a file, then it’s a process. Some point out that, with the advent of procfs, even processes have become files. I would be inclined to say that processes are not really files, since you can’t write to procfs, so there is no way to (for example) kill a process through the filesystem.