How terminal works. Part 3: pty, stty
- Terminology
- Features configurable by stty
- Practice
- Conclusion
- Appendix: relevant stty options by category
Terminology
tty, pts, serial port, line discipline
- word tty originated as an abbreviation of teletypewriter;
- in Linux tty device originally meant a connection to a terminal;
- and later it was extended to refer to any serial-port style device [ref].
“serial-port style device” means that the device driver provides an interface to
a userland that looks like a serial-port device: a filehandle together with
certain supported ioctl
calls. Such a device doesn’t necessarily use a
physical serial port, instead, it might use the network stack or something else
[ref].
Linux kernel provides building blocks for creating new tty drivers: tty core and several existing tty line disciplines. Line discipline is a piece of logic that transforms sequence of bytes into some other structured representation. For example, it might accumulate input into PPP packets (which is useful for dial-up Internet connection). Or it might transform a sequence of ASCII characters into a sequence of lines of ASCII characters.
Here we aren’t interested in any serial-port style devices; instead, we are
discussing connections to a terminal. In case of a connection to a physical
terminal, tty driver would expose to a userland a filehandle which bash
would
use to receive user input and to send its output. For consistency with the next
diagram let’s call that filehandle a slave filehandle (note, this is a
simplification, one terminal represent several logical “seats”, see
multiseat):
Terminal Computer
+-----+ +------+
¯\_(ツ)_/¯ | | =---------=|bash |
+-----+ cable | |
xxxxxxx +------+
To support software terminal emulators similarly to hardware terminals, Linux implements pseudo terminals. In the previous diagram, the hardware terminal was receiving bash output via a physical connection. In the case of a pseudo-terminal, Linux creates one more filehandle to send data to xterm. That way xterm runs on the same machine and reads bash output via a filehandle; the entire scheme looks like that:
(1) (2) (3)
xterm <-----> tty <-----> bash
master slave
Master filehandle is referred to as ptm
, slave filehandle is pts
. The
abbreviation for pseudo-terminal is pty
(see man pty
). A mnemonic to
remember that bash uses a slave filehandle is that bash receives commands from a
user, hence it’s being controlled and it’s a slave. Xterm sends user commands,
hence it’s a master.
Back to line disciplines. Line discipline used for pseudoterminals is drivers/tty/n_tty.c. It has many settings which we are going to discuss soon. (TODO: check).
Session, background/foreground process groups
Normally, bash and all its children (the processes it spawned) are part of the
same session (associated with the same tty
). Bash organizes newly spawned
processes into process groups session. While executing commands, bash marks a
single process group as a foreground process group of the session.
session, process group, foreground process group
- are configured using system calls (so both bash and tty know about them);
- organize processes so that
- with help of tty bash can implement job control, keyboard shortcuts to interrupt/suspend a running command, and it terminates children when a terminal gets closed (with some nuances).
Here is a simple test to show how bash organizes processes into process groups.
We create a background job yes | sleep 1h
which consists of two processes
yes
and sleep
. Then we execute ps
which runs in the foreground:
yes | sleep 1h &
[1] 6741
ps -ostat,tty,sess,pgid,pid,comm -H
STAT TT SESS PGID PID COMMAND
S pts/4 6545 6545 6545 bash
S pts/4 6545 6740 6740 yes
S pts/4 6545 6740 6741 sleep
R+ pts/4 6545 6769 6769 ps
Notice that all processes run in the same session and share the same tty. yes
and sleep
share the same process group. ps
is marked with +
which means
“is in the foreground process group”.
Note that the term “job” is a synonym for a process group (see man credentials
).
We will discuss sessions and process groups in more details later. But that brief introduction should be enough to understand tty settings that we will be discussing soon.
Features configurable by stty
In Part 1, we’ve already discovered the existence of tty and observed some of its features. Now let’s recall what we’ve learned so far:
- tty passes data from xterm to bash in both directions; we can configure tty, and depending on its configuration, it will “slightly” alter data it receives from one side before passing to the other side;
- besides passing data through filehandles, tty also generates signals; again, delivering signals is configurable logic; it might work differently depending on tty’s configuration and depending on how processes are organized into logical groups.
I’ll split tty’s features into two parts depending on whether we can configure
them using the stty
tool. To explore features that cannot be configured using
stty
we will write C code.
man stty
gives us 7 sections
- Special characters
- Special settings
- Control settings
- Input settings
- Output settings
- Local settings
- Combination settings
Skipping “Combination settings” gives us 89 settings. 11 of them are synonyms, leaving us 78 settings. I’ve counted 34 settings that are “not relevant to the modern world” or seem to be not relevant to software terminal emulators. This leaves us 44 options to discuss. I’ve organized these 44 options into 7 groups based on features they control.
Outdated features
-
flow control and parity check
These features make sense only for physical serial transmission lines. Serial ports are still in use, for example, in embedded software engineering and embedded devices might implement flow control. But it’s not relevant for pseudo terminals. I didn’t check all the features, but many of them simply do nothing in the case of a pseudo-terminal.
The only relevant feature from “Control settings” is the
hup
setting (send a hangup signal when the last process closes the tty). -
delays (bsN, crN, ffN, nlN)
Delays just don’t work with pseudo terminals.
-
swtch
swtch also seems to do nothing on Linux.
-
line N - use line discipline N
pseudo terminals always (TODO: check) use
n_tty
line discipline.
More details are in this table.
Relevant features
We can further split the remaining 46 settings into several groups. It’s unnecessary to study all the options, but skimming through all the groups is useful to understand that tty has a finite amount of features.
The entire list can be found in the last section of this post. Here I’ll just discuss the groups that I’ve chosen to organize all available options.
-
input processing
tty might ignore some characters which come from the master filehandle (from xterm), or convert them into other characters;
-
output processing
tty can convert certain characters which come from the slave filehandle (from bash) into some other character sequences (
"\n" -> "\n\r"
, etc.); -
echo
tty sends data that came from xterm back to xterm so that the user can see what he/she is typing; note that echoing control characters in case of enabled line editor is a more complicated task than just sending the same characters back;
-
line editor feature
the user can enter text while using Backspace, Ctrl+w, Ctrl+u to remove the last character, word, or the whole line; to implement the feature tty buffers one line of input and sends it to bash only after receiving a return character “\r”; if echo and line editing are enabled together, tty hides most of the interaction with a user from bash but it actively interacts with xterm to implement line editing;
-
generating signals
common (but configurable) behavior is that pressing Ctrl+C will cause tty to send SIGINT, closing xterm will eventually cause certain programs to receive SIGHUP; programs receive sigWINCH signal after tty got resized;
-
size
there are several commands to get tty size and to resize it.
Combination setting
The most useful combinations are:
-
stty sane
- combines enabled echo, line editor, output processing to make text tools look nice and common key bindings; -
stty raw -echo -isig
- make pty pass characters “as is”.
Be aware that bash changes tty setting before executing commands and restores
the previous setting after the command has finished. We can easily observe that
by using watch stty ...
. Open two xterm windows and get the tty path in the
first window. Start watching settings of the first tty in another window using
watch stty -a -F <pts>
command. Execute something like cat -
in the first
terminal. Check new tty settings. Close cat
with Ctrl+d and note that the
settings have changed back.
xterm1 | xterm2
---------------------|-------------------------------
~$ tty |
/dev/pts/1 |
| ~$ watch stty -a -F /dev/pts/1
~$ cat - |
... |
Practice
Equipped with the knowledge of tty options, let’s observe:
- how tty’s line editing feature works;
- how output processing makes text files dumped into terminal look nicer;
- what signals does pty generate.
Line editor
We’ve already observed that tty can allow you to edit the current input line and
to delete a previous character, or a word, or the entire line. Let’s quickly
repeat the experiment from Part 1, but instead of strace
let’s play with bash
scripts. We’ll use one xterm window to receive input and the other to visualize
what tty is sending to our bash script. Open two xterm windows, and in the first
xterm:
mkfifo /tmp/out.pipe
dd bs=1 of=/tmp/out.pipe
Execute the following line using the second xterm. The while loop with dd
is
my attempt to make od
read characters one by one (asking it directly using od
-w1
option gives an error):
(while true; do dd count=1 bs=1 2>/dev/null | od -ax; done) < /tmp/out.pipe
Now you can enter something in the xterm1. In the other xterm window, you can observe in real-time what tty is sending to a script.
Execute stty raw
before calling dd
in the xterm1 and observe that now tty
sends data to bash character by character:
stty raw; dd bs=1 of=/tmp/out.pipe
Making plain text output look good
A few lines of text might look either like this:
stty sane; printf "\nline1\nline2\nline3\n"
line1
line2
line3
or like that:
stty raw; printf "\nline1\nline2\nline3\n"
line1
line2
line3
sh-4.4$
Using tools from the Part 1 we can easily discover that in the first test case
xterm receives \r\nline1\r\nline2\r\nline3\r\n
and in the second test case it
receives \nline1\nline2\nline3\n
. The difference is that \n
was replaced by
‘\r\n’ in the first test case. This behavior is configured by onlcr
setting
(translate newline to carriage return-newline).
Observing signals
We are going to execute two bash commands: one in the background, one in the
foreground. And then we will use strace
to observe which signals reach both
processes:
echo $$
18552
sleep 1h &
sleep 1h
Let’s find out pids of each sleep
process:
ps --ppid 18552
PID TTY TIME CMD
19055 pts/8 00:00:00 sleep
19062 pts/8 00:00:00 sleep
Then attach with strace
from other xterm windows:
strace -f -e 'trace=!all' -p 19055
and:
strace -f -e 'trace=!all' -p 19062
-
resizing xterm windows
delivers
SIGWINCH
to a foreground process; -
common key-bindings to stop the execution of a command
-
suspend/resume foreground process
Ctrl+z will cause a foreground process group receiving
SIGTSTP
.--- SIGTSTP {si_signo=SIGTSTP, si_code=SI_KERNEL} --- --- stopped by SIGTSTP ---
After that bash appends the process to a list of
jobs
.%2
resumes a stopped the background process by sending theSIGCONT
signal. -
Ctrl+c
delivers
SIGINT
to a foreground process group:--- SIGINT {si_signo=SIGINT, si_code=SI_KERNEL} --- +++ killed by SIGINT +++
-
Ctrl+\
delivers
SIGQUIT
to a foreground process group.
-
-
Tty access control
When a process from a background process group attempts to read from tty the whole process group receives
SIGTTIN
:strace -e 'trace=!all' dd bs=1 & [2] 1739 --- SIGTTIN {si_signo=SIGTTIN, si_code=SI_KERNEL} --- --- stopped by SIGTTIN ---
Similarly with configured
stty tostop
an attempt to output from a background process would cause the process group to receiveSIGTTOU
signal:stty tostop; echo 123 & stty -tostop; echo 123 &
-
Closing terminal windows
This topic has one nuance. Looking at these stty settings:
- hup - send a hangup signal when the last process closes the tty
- hupcl - same as hup
and skimming through drivers/tty/tty_jobctrl.c gives an idea that pty sends
SIGHUP
to all processes within the session when either:- bash closes master filehandle;
- bash disassociates itself from a session by starting a new session.
However, on my system, I observed that bash disables
-hupcl
tty option and hence, it doesn’t rely on this feature:stty -a ... -hupcl ...
However,
strace
shows that when I close xterm, all processes within the corresponding session receiveSIGHUP
. Tracing xterm, bash, and all background/foreground jobs reveal that:- when xterm exits it sends
SIGHUP
to bash; - when bash receives
SIGHUP
it terminates its children by using the same signal.
Hence, in practice, delivery of the
SIGHUP
signal depends on the behavior of a command-line shell such as bash. Some experimentation reveals that, for example,zsh
has a slightly different behavior and it doesn’t sendSIGHUP
to running background jobs. For example, in zshsleep 1h &
would stay alive after closing xterm.
Conclusion
In this post, we’ve explained the origins of the term tty
and relationships
between tty
, pty
, and a serial port. We’ve talked a little about
foreground/background process groups. And then we’ve experimented with tty’s
features. We also discovered that many stty’s options don’t apply to pty and
hence can be safely ignored while working with terminal emulators.
In the final Part 4 of this post series, we will continue exploring tty features
by writing C code. We will develop a simple analog of a script
utility with
the goal of understanding sessions and process groups better. Stay tuned :)
Appendix: relevant stty options by category
-
input processing
tty replaces some input characters that come from the master filehandle with some other characters:
- eof CHAR - CHAR will send an end of file (terminate the input)
- eol CHAR - CHAR will end the line
- eol2 CHAR - alternate CHAR for ending the line
- lnext CHAR - CHAR will enter the next character quoted
- icrnl - translate carriage return to newline
- ignbrk - ignore break characters
- igncr - ignore carriage return
- inlcr - translate newline to carriage return
- iuclc - translate uppercase characters to lowercase
-
output processing
first, an option to enable/disables output processing:
- opost - postprocess output
rest of the options control whether tty should replace certain characters which come from slave filehandle with some other characters:
- ocrnl - translate carriage return to newline
- olcuc - translate lowercase characters to uppercase
- onlcr - translate newline to carriage return-newline
- onlret - newline performs a carriage return
- onocr - do not print carriage returns in the first column
- crtkill - kill all line by obeying the echoprt and echoe settings
- iexten - enable non-POSIX special characters
- noflsh - disable flushing after interrupt and quit special characters
- xcase - with icanon, escape with ‘' for uppercase characters
-
echo
- echo - echo input characters
- crterase - echo erase characters as backspace-space-backspace
- ctlecho - echo control characters in hat notation (‘^c’)
- echok - echo a newline after a kill character
- echonl - echo newline even if not echoing other characters
- echoprt - echo erased characters backward, between ‘' and ‘/’
-
line editor
enable/disable a line editor:
- icanon - enable special characters: erase, kill, werase, rprnt
keybindings:
- erase CHAR - CHAR will erase the last character typed
- kill CHAR - CHAR will erase the current line
- rprnt CHAR - CHAR will redraw the current line
- werase CHAR - CHAR will erase the last word typed
and also:
- iutf8 - assume input characters are UTF-8 encoded
iutf8
instructs tty’s line editor to erase characters correctly. So that it removes several bytes from its internal buffer when needed. This logic is very simple. More complicated features such asiuclc
(translate uppercase characters to lowercase) don’t work correctly withiutf8
. -
access control
- tostop - stop background jobs that try to write to the terminal
and rudimentary flow control:
- start CHAR - CHAR will restart the output after stopping it
- stop CHAR - CHAR will stop the output
- ixany - let any character restart output, not only start character
- ixon - enable XON/XOFF flow control
-
generating signals
- hup - send a hangup signal when the last process closes the tty
- isig - enable interrupt, quit, and suspend special characters
- noflsh - disable flushing after interrupt and quit special characters
keybindings:
- intr CHAR - CHAR will send an interrupt signal
- susp CHAR - CHAR will send a terminal stop signal
- quit CHAR - CHAR will send a quit signal
-
size
- cols N - tell the kernel that the terminal has N columns
- rows N - tell the kernel that the terminal has N rows
- size - print the number of rows and columns according to the kernel