How UNIX Terminal Devices Work: TTY, Pseudo-Terminals, and Line Discipline
Preamble
When you open a terminal, you are greeted with a prompt and the system expects your input. But have you ever wondered what sort of device the terminal really is? A terminal is more than just a window for typing commands — it is a character special device managed by the kernel, with a layered architecture involving device drivers, line discipline modules, and pseudo-terminal pairs. In this post, we'll explore the internals of the UNIX terminal device: how tty and pty devices work, what terminal line discipline does, and how the kernel mediates between the user and the shell process. This is the first in a three-part series. The second post covers how the shell process executes programs, and the third explores writing portable C code.
What Is a Terminal Device in UNIX?
We frequently use the terminal, yet most of us don't bother learning what sort of device it really is — specifically, a character device managed by the kernel through a dedicated tty device driver. For a program executed interactively through a shell, the terminal device associated with the shell process (and the standard streams connected with it) is provided to the program under execution. On a UNIX-like system, when you browse the /dev directory, you'll notice a bunch of tty devices. Listing 1 shows the subset of "files" that are available in my /dev directory. The first column describes two attributes of the file: the file type and the permission bits.
Inspecting TTY Devices in the /dev Directory
Script started on Sat Sep 13 16:24:21 2025
bash-5.3$ ls -la /dev | grep tty
crw-rw-rw- 1 root tty 0xf000005 Sep 13 16:24 ptmx
crw-rw-rw- 1 root wheel 0x2000000 Sep 10 20:13 tty
crw-rw-rw- 1 root wheel 0x4000000 Aug 21 20:43 ttyp0
crw-rw-rw- 1 root wheel 0x4000001 Aug 21 20:43 ttyp1
crw-rw-rw- 1 root wheel 0x4000002 Aug 21 20:43 ttyp2
crw-rw-rw- 1 root wheel 0x4000003 Aug 21 20:43 ttyp3
crw-rw-rw- 1 root wheel 0x4000004 Aug 21 20:43 ttyp4
...
bash-5.3$ ^D
exit
Script done on Sat Sep 13 16:24:41 2025
Controlling terminal device programatically is a complex task. Historical systems such as System V and 4.3BSD had their own ioctl(2) commands to control the I/O of the terminal device. Various aspects of the device can be configured along with terminal attributes, controlling terminal, line discipline, and much more. ioctl_tty(2) is for GNU/Linux-specific control commands. The term tty stands for teletype. Today, the tty subsystem underpins every terminal emulator and pseudo-terminal session on UNIX-like systems. Refer to your device's manual for implementation specific operations. For a gory detail regarding tty device, refer to: The TTY demystified.
The term driver needs to be defined first. A driver is a software component that allows the Operating System to interact with a device. A device could be anything including a hard drive, a keyboard, a mouse, a network interface, and so on.
Device Major and Minor Numbers Explained
The fifth column in Listing 1 seems cryptic. GNU/Linux provides a more readable output, but macOS does not provide such output. This information is not available for a regular file. What this column conveys is the device. A device is composed of two components: device major number and device minor number. The major number describes the device's type and the device driver used to interact with the said device. The minor number describes the instance of the device of that type. Each driver in a system is assigned a unique major number (the terminal device driver on my machine being assigned a number 0x4) while the minor number is used by the respective driver to distinguish different instances of the device.
How Pseudo-Terminals (pty) Work in UNIX
I'll use the terms master and slave side below to discuss about ptmx file and its characteristics. Some text and manual refer to master as primary and slave as replica.
Master and Slave Sides of a Pseudo-Terminal
The file ptmx is specially useful when making a ssh-like program. I'll try to briefly explain how terminal acquisition works this way. The file ptmx refers to a pseudo-terminal device (hence the different driver used). There are two components in a pseudo-terminal device: master side, and the slave side. As the name suggests, this file appears to be as a terminal device, but the device is not associated to an actual hardware. System V and 4.3BSD had their own techniques to obtain such devices, but Single Unix Specification (SUS) (and POSIX) describes the interface similar to that of System V. For example, grantpt(3) is used to establish ownership and permissions of the slave device, while unlockpt(3) is used to unlock the slave device.
I/O Flow Between pty Master and Slave Devices
After obtaining the master and slave device, we need to understand how I/O using these two devices work. I'll assume that the slave device opens the shell process and discuss the interaction. When you want to send input to this slave device, you write it on the master device and the characters are sent to the slave device. If something is written on the slave device (say, write(2)), the master device can read it and later write it to its own standard output (or error) stream for the end user to view the output. However, we aren't limited to I/O specific operations. Programs such as vi(1) draw on the screen. Such programs also take into account the current window size.
Consider that the slave device (which is running the shell process) executes the vi(1) program. It draws the screen that will be presented on the master side. The terminal emulator manages the window changes. We now change the window size. The terminal emulator will be notified of this change. The terminal emulator issues an TIOCSWINSZ ioctl(2) command for the corresponding terminal device. The kernel--keeping track of the terminal sizes--receives this command, and updates the window size for the pseudo-terminal. Consequently, any process that is associated with the slave device receives the SIGWINCH signal. Since the slave device is running vi(1), this process will receive the signal and redraw the screen appropriately.
UNIX File Types and Permission Bits
A file in UNIX-like system is an opaque term and everything is a file. The file type is described below:
The Seven UNIX File Types
| File Type | Symbolic Representation |
|---|---|
| Regular file | - |
| Directory | d |
| Symbolic link | l |
| Character special | c |
| Block special | b |
| Named pipe | p |
| Socket | s |
Understanding File Permission Bits (rwx)
The permission bits shows 9 characters. They are divided into 3 sets: user, group, and others. Each set contain 3 characters, and provides information regarding the accessibility of file to the user. Consider the example file below:
-rwxr-xr-- 1 pranavramjoshi staff 33K Sep 17 03:16 bar
From the first column, we can get the following information:
- The file is a regular file, as indicated by the
-character at the beginning. - The owner has the following permissions: read (
r), write (w), and execute (x) the file. - The group has the following permission: read (
r) and execute (x), but not write. - Others has the following permission: read (
r), but not write or execute.
The owner of the file is given in the third column, pranavramjoshi. The file belongs to the group, staff. If a user Trent is also present in the system and Trent is also in the staff group, then the user can read and execute the file but cannot make any modification. If Mallory is another user in the system and the user is not in the staff group, then the user can only read the file but cannot execute or write the file.
A standard program is provided in almost all UNIX-like system: stat(1). This provides more verbose output of the file. There are multiple flags that can be provided to this program.
To know about yourself; your User Idenification (UID), Group Identification (GID), and the groups you belong to, use the id(1) program. To list out only the groups your are in, use the groups(1) program.
Why Terminals Are Character Special Devices
From Listing 1, we are now aware that a terminal device is a character special device. The Wiki definition of a character device is as:
Character special files or character devices provide unbuffered, direct access to the hardware device. They do not necessarily allow programs to read or write single characters at a time; that is up to the device in question.
To see how the process's virtual memory maps these device files, see the memory post.
Being unbuffered means that the devices handles the data directly, without any explicit buffer being used by, say, the kernel. This also means that the system call lseek(2) can't be used on a file descriptor representing a terminal device, as like pipe(2)s. When you interact with your own or other command line program, the flow would look as seen in Listing 2. One might argue that the line is being buffered until the Enter or Return key is pressed, and they would be right. This is where the Terminal Line Discipline comes into play.
How Terminal I/O Flows from User to Shell
The following diagram illustrates the I/O flow between the user at a terminal and the shell process, passing through the terminal device driver and the terminal line discipline module within the kernel.
+-------------------+
| shell |
+-------------------+
| ^
stdout, stderr | | stdin
| |
+-----------+-----------+---------------+
| | | |
| V | |
| +-------------------+ |
| | terminal line | |
| | discipline | |
| +-------------------+ |
| | ^ |
| | | | kernel
| | | |
| V | |
| +-------------------+ |
| | terminal | |
| | device driver | |
| +-------------------+ |
| | ^ |
| | | |
| | | |
+-----------+-----------+---------------+
| |
| |
V |
+-------------------+
| user at a |
| terminal |
+-------------------+
What Is Terminal Line Discipline?
The terminal line discipline sits between the terminal device driver and the actual shell process. Consider the scenario we witness all the time. You type something into the terminal and before the Enter or Return key is pressed, two things are being done: the characters are echoed in your screen, and characters are being "prepared" to pass to the process connected to the terminal. Recall that a terminal is a duplex device; there are two channels used for input and output. Since the terminal is by default in "cooked mode" (also known as canonical mode), the input is not provided to the shell process unless the Enter or Return key is pressed. After it is pressed, the entire line is provided to the shell.
Canonical (Cooked) Mode vs Raw (Non-Canonical) Mode
The terminal device driver is ultimately responsible for handling I/O for the user. There is a notion of Canonical mode for terminal devices. In essence, it describes how input stream of the terminal device should be processed. When we say the terminal is in cooked mode, it means that the terminal will process input in unit of lines, where the delimiting character is usally the newline character along with EOF character or EOL character. Refer to Canonical Mode Input Processing section of termios(4) manual for more information.
This allows us to configure the driver to accept sequence of single characters rather than an entire line (Non-Canonical mode). We often refer to this as raw mode, which is useful when typing on a remote system. It does have its drawbacks; we can't "erase" a character that has already been transmitted. For differences between raw and cooked mode, check this StackExchange thread.
Capabilities of a Line Discipline Module
There are several functionailty provided by a line discipline module. Some of them are mentioned below:
- Echo the characters that are entered.
- Assemble the characters entered into lines, so that a process reading from the terminal receives complete lines.
- Edit the line that is input. For instance, Delete or Back Space characters are the usual Erase Character. You could also kill the entire line and start over with a new line.
- Generate signals when certain terminal keys are entered. For example,
Control-Cgenerates the SIGINT signal. - Process flow control character. For example, entering the
Control-Skey stops the output in the terminal whileControl-Qrestarts the output. - Allow you to enter an end-of-file character.
- Do character conversions. For example, every time a process writes a newline character, the line discipline can convert it to a carriage return and a line feed. Also, tab characters that are output can be converted to spaces if the terminal doesn't handle tab characters.
Viewing Terminal Settings with the stty Command
Many UNIX-like systems provide the stty(1) command that allows us to inspect and modify terminal characteristics, control characters, and line discipline settings configured via termios:
$ stty -a
speed 38400 baud; 35 rows; 143 columns;
lflags: icanon isig -iexten echo echoe -echok echoke -echonl echoctl
-echoprt -altwerase -noflsh -tostop -flusho pendin -nokerninfo
-extproc
iflags: -istrip icrnl inlcr -igncr ixon -ixoff ixany imaxbel iutf8
-ignbrk brkint -inpck -ignpar -parmrk
oflags: opost onlcr oxtabs -onocr -onlret
cflags: cread cs8 -parenb -parodd hupcl -clocal -cstopb -crtscts -dsrflow
-dtrflow -mdmbuf
cchars: discard = ^O; dsusp = <undef>; eof = <undef>; eol = <undef>;
eol2 = <undef>; erase = ^?; intr = ^C; kill = ^U;
lnext = <undef>; min = 1; quit = ^\; reprint = <undef>;
start = ^Q; status = <undef>; stop = ^S; susp = ^Z; time = 0;
werase = <undef>;
Refer to the manual of stty(1) for more information about the various terminal characteristics.
Always ensure that the terminal configuration is set to cooked or sane mode. Speaking from experience, if you manage to set terminal to non-canonical mode and then some error occurs between the task, your best choice is to simply close the application as it would be next to impossible to change the mode later on.
Terminal device is a complex topic. It deserves a dedicated blog of its own. For now, the reader might have some idea as to what a terminal device does and how we can use it alongside the shell process.
This is Part 1 of a three-part series on terminals, shells, and portability:
- How UNIX Terminal Devices Work (you are here)
- How the Shell Executes Programs: Fork, Exec, and Environment Variables
- Writing Portable C Code: Preprocessor Directives, Data Types, and GNU Autotools
