Jex’s Note

Linux TTY/PTY

What is TTY/PTY?

TTY (teletype) is a terminal used to transfer the input and output from devices.

PTY (pseudo-teletype) is a pseudoterminal that provides a terminal emulator like tty does.

How does TTY work?

Diagram:

┏━━━━━━━━━━━━┓  ┏━━━━━━━━━━━━━━━━━━━┓  ┏━━━━━━━━━━━━━━┓  ┏━━━━━━━━━━━━━━┓  ┏━━━━━━━━━━━━━━┓  ┏━━━━━━━━━━━━━━━┓
┃  monitor   ┣━━┫     VGA driver    ┣━━┫              ┃  ┃              ┃  ┃              ┣━━┫  User process ┃
┗━━━━━━━━━━━━┛  ┗━━━━━━━━━━━━━━━━━━━┛  ┃   Terminal   ┣━━┫     Line     ┣━━┫     TTY      ┃  ┗━━━━━━━━━━━━━━━┛
┏━━━━━━━━━━━━┓  ┏━━━━━━━━━━━━━━━━━━━┓  ┃   emulator   ┃  ┃  Discipline  ┃  ┃    driver    ┃  ┏━━━━━━━━━━━━━━━┓
┃  keyboard  ┣━━┫  Keyboard driver  ┣━━┫              ┃  ┃              ┃  ┃              ┣━━┫  User process ┃
┗━━━━━━━━━━━━┛  ┗━━━━━━━━━━━━━━━━━━━┛  ┗━━━━━━━━━━━━━━┛  ┗━━━━━━━━━━━━━━┛  ┗━━━━━━━━━━━━━━┛  ┗━━━━━━━━━━━━━━━┛

0,1,2 fds are allocated to shell

$ tty
/dev/ttys028
$ lsof /dev/ttys028
COMMAND   PID USER   FD   TYPE DEVICE  SIZE/OFF NODE NAME
bash     1776 root    0u   CHR  16,28 0t4584881  689 /dev/ttys028        # stdin
bash     1776 root    1u   CHR  16,28 0t4584881  689 /dev/ttys028        # stdout
bash     1776 root    2u   CHR  16,28 0t4584881  689 /dev/ttys028        # stderr
bash     1776 root  255u   CHR  16,28 0t4584881  689 /dev/ttys028

How does PTY work?

  • PTY consists of ptmx (pseudo-terminal master) and pts (pseudo-terminal slave).
  • Each pty master and slave is a separate pair and is joined together in the kernel.

How does pts being created?

  1. Call posix_openpt() to open /dev/ptmx (pty master) and get a unique file descriptor and a /dev/pts/* (pty slave).
  2. Fork a login shell with pts.

The process that allocates ptmx and pts is given an fd for each side. Then it will typically passes the slave on to a new process and close its file descriptor, something like:

get ptmfd,ptsfd (from /dev/ptmx on Linux)
if (fork() == 0)
{
    /* child */
    close(ptmfd);
    dup2(ptsfd, 0);
    dup2(ptsfd, 1);
    dup2(ptsfd, 2);
    execv($SHELL)
}
/* parent */
close(ptsfd);
proxy data from /dev/tty to ptmfd

How does sshd work with tty?

When ever user connects to server using SSH, sshd will be created and gets a pts and fds, and forks a login shell.

sshd,29496
  └─bash,29497

$ sudo ls -al /proc/29496/fd
dr-x------ 2 root    root     0 Jan 23 17:53 .
dr-xr-xr-x 9 root    root     0 Jan 23 11:51 ..
lrwx------ 1 root    root    64 Jan 23 17:53 10 -> /dev/ptmx
lrwx------ 1 root    root    64 Jan 23 17:53 12 -> /dev/ptmx
lrwx------ 1 root    root    64 Jan 23 17:53 13 -> /dev/ptmx

$ ls -al /proc/29497/fd
total 0
dr-x------ 2 root root  0 Jan 24 16:04 .
dr-xr-xr-x 9 root root  0 Jan 24 16:04 ..
lrwx------ 1 root root 64 Jan 24 16:04 0 -> /dev/pts/0
lrwx------ 1 root root 64 Jan 24 16:04 1 -> /dev/pts/0
lrwx------ 1 root root 64 Jan 24 16:04 2 -> /dev/pts/0
lrwx------ 1 root root 64 Jan 24 16:05 255 -> /dev/pts/0

SSH/TMUX only communicates with ptmx, which internally maintains every session’s fds and pts, transfers input/output to corresponding pts. Conversely, processes that user runs in login session only wire to pts through which output goes to ptmx. You can take ptmx and pts as two-way pipe to transfer input/output.

Diagram:

┏━━━━━━━━━━━━━━━━━━━━━━━┓
┃   keyboard / monitor  ┃
┗━━━━━━━━━━━┳━━━━━━━━━━━┛
┏━━━━━━━━━━━┻━━━━━━━━━━━┓
┃        terminal       ┃
┗━━━━━━━━━━━┳━━━━━━━━━━━┛
┏━━━━━━━━━━━┻━━━━━━━━━━━┓      ┏━━━━━━━━┓
┃ SSH Server            ┃      ┃        ┃
┃  ┃                    ┃ ┏━━┓ ┃        ┃ ┏━━━━━━━━━┓ ┏━━┓ ┏━━━━━━━━━┓
┃  ┣━ sshd (session 1)  ┣━┫fd┣━┫        ┣━┫  pts/0  ┣━┫fd┣━┫  shell  ┃
┃  ┃                    ┃ ┗━━┛ ┃  PTMX  ┃ ┗━━━━━━━━━┛ ┗━━┛ ┗━━━━━━━━━┛
┃  ┃                    ┃ ┏━━┓ ┃        ┃ ┏━━━━━━━━━┓ ┏━━┓ ┏━━━━━━━━━┓
┃  ┗━ sshd (session 2)  ┣━┫fd┣━┫        ┣━┫  pts/1  ┣━┫fd┣━┫  shell  ┃
┃                       ┃ ┗━━┛ ┃        ┃ ┗━━━━━━━━━┛ ┗━━┛ ┗━━━━━━━━━┛
┗━━━━━━━━━━━━━━━━━━━━━━━┛      ┗━━━━━━━━┛

How does tmux work with tty over ssh?

This situation gets a bit tricky. As follows, we can see there is a tmux client that I currently used and a tmux server with 3 bash, which comes from 3 windows in a tmux session that I created. No matter what I create a window or a pane, tmux server creates a unique pts for each of them.

sshd,29496
  └─bash,29497                              # /dev/pts/10
      └─tmux: client,10673 a

tmux: server,28411
  ├─bash,12923                              # /dev/pts/6
  ├─bash,13087                              # /dev/pts/7
  └─bash,28428                              # /dev/pts/5

Of course, every bash corresponds to its fd which links to /dev/ptmx.

$ ls -al /proc/28411/fd
total 0
dr-x------ 2 root root  0 Jan 21 15:42 .
dr-xr-xr-x 9 root root  0 Jan 21 15:42 ..
lrwx------ 1 root root 64 Jan 21 15:42 10 -> /dev/ptmx
lrwx------ 1 root root 64 Jan 21 15:42 7 -> /dev/pts/10
lrwx------ 1 root root 64 Jan 21 15:42 8 -> /dev/ptmx
lrwx------ 1 root root 64 Jan 21 15:42 9 -> /dev/ptmx

When I use tmux, I’m not actually using the bash (pid: 29497) created by sshd. Instead, I’m using the bash (one of pids: 19293/13087/28428) created by tmux server.

This is the reason why tmux is so useful. If I disconnected, it won’t affect any of bash (one of pids: 19293/13087/28428) that I worked on, because it seperate the bash (one of pids: 19293/13087/28428) I use from the bash (pid: 29497) that sshd created for me.

Tmux client has two primary roles: firstly, to transfer the environment and terminal file descriptors to the server; secondly, to occupy the terminal and prevent other processes from using it until the client exits.

Therefore, if you send messages into pts of bash (pid: 29497) or bash (one of pids: 19293/13087/28428), you can see the message with slight different way to display.

echo "Hello" > /dev/pts/10
echo "Hello" > /dev/pts/5

Diagram:

┏━━━━━━━━━━━━━━━━━━━━━━━┓
┃   keyboard / monitor  ┃
┗━━━━━━━━━━━┳━━━━━━━━━━━┛
┏━━━━━━━━━━━┻━━━━━━━━━━━┓
┃        terminal       ┃
┗━━━━━━━━━━━┳━━━━━━━━━━━┛
┏━━━━━━━━━━━┻━━━━━━━━━━━┓      ┏━━━━━━━━┓
┃ SSH Server            ┃      ┃        ┃
┃  ┃                    ┃ ┏━━┓ ┃        ┃ ┏━━━━━━━━━┓ ┏━━┓ ┏━━━━━━━━━┓
┃  ┣━ sshd (session 1)  ┣━┫fd┣━┫        ┣━┫  pts/0  ┣━┫fd┣━┫  shell  ┃
┃  ┃                    ┃ ┗━━┛ ┃        ┃ ┗━━━━━━━━━┛ ┗━━┛ ┗━━━━━━━━━┛
┃  ┃                    ┃ ┏━━┓ ┃        ┃ ┏━━━━━━━━━┓ ┏━━┓ ┏━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━┓
┃  ┗━ sshd (session 2)  ┣━┫fd┣━┫        ┣━┫  pts/1  ┣━┫fd┣━┫  shell  ┣━┫  tmux client  ┣━┓
┃                       ┃ ┗━━┛ ┃        ┃ ┗━━━━━━━━━┛ ┗━┳┛ ┗━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━┛ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━┛      ┃        ┃               ┗━━━━━━━━━━━━━━━━━━━━━━┓         ┃
┏━━━━━━━━━━━━━━━━━━━━━━━┓      ┃        ┃                                      ┃         ┃
┃ Tmux Server           ┃      ┃        ┃                                      ┃         ┃
┃  ┃                    ┃      ┃        ┃                                      ┃         ┃
┃  ┣━ session 1         ┃      ┃        ┃                                      ┃         ┃
┃  ┃   ┃                ┃ ┏━━┓ ┃        ┃ ┏━━━━━━━━━┓ ┏━━┓ ┏━━━━━━━━━┓         ┃         ┃
┃  ┃   ┣━ window 1      ┣━┫fd┣━┫  ptmx  ┣━┫  pts/2  ┣━┫fd┣━┫  shell  ┃         ┃         ┃
┃  ┃   ┃                ┃ ┗━━┛ ┃        ┃ ┗━━━━━━━━━┛ ┗━━┛ ┗━━━━━━━━━┛         ┃         ┃
┃  ┃   ┃                ┃ ┏━━┓ ┃        ┃ ┏━━━━━━━━━┓ ┏━━┓ ┏━━━━━━━━━┓         ┃         ┃
┃  ┃   ┗━ window 2      ┣━┫fd┣━┫        ┣━┫  pts/3  ┣━┫fd┣━┫  shell  ┃         ┃         ┃
┃  ┃       ┃            ┃ ┗━━┛ ┃        ┃ ┗━━━━━━━━━┛ ┗━━┛ ┗━━━━━━━━━┛         ┃         ┃
┃  ┃       ┃            ┃ ┏━━┓ ┃        ┃ ┏━━━━━━━━━┓ ┏━━┓ ┏━━━━━━━━━┓         ┃         ┃
┃  ┃       ┗━ pane 1    ┣━┫fd┣━┫        ┣━┫  pts/4  ┣━┫fd┣━┫  shell  ┃         ┃         ┃
┃  ┃                    ┃ ┗━━┛ ┃        ┃ ┗━━━━━━━━━┛ ┗━━┛ ┗━━━━━━━━━┛         ┃         ┃
┃  ┗━ session 2         ┃      ┃        ┃                                      ┃         ┃
┃      ┃                ┃ ┏━━┓ ┃        ┃ ┏━━━━━━━━━┓ ┏━━┓ ┏━━━━━━━━━┓         ┃         ┃
┃      ┣━ window 1      ┣━┫fd┣━┫        ┣━┫  pts/5  ┣━┫fd┣━┫  shell  ┃         ┃         ┃
┃      ┃                ┃ ┗━━┛ ┃        ┃ ┗━━━━━━━━━┛ ┗━━┛ ┗━━━━━━━━━┛         ┃         ┃
┃      ┃                ┃ ┏━━┓ ┃        ┃ ┏━━━━━━━━━┓ ┏━━┓ ┏━━━━━━━━━┓         ┃         ┃
┃      ┗━ window 2      ┣━┫fd┣━┫        ┣━┫  pts/6  ┣━┫fd┣━┫  shell  ┃         ┃         ┃
┃                       ┃ ┗━━┛ ┃        ┃ ┗━━━━━━━━━┛ ┗━━┛ ┗━━━━━━━━━┛         ┃         ┃
┗━━━━━━┳━━━━━━━━━┳━━━━━━┛      ┗━━━━━━━━┛                                      ┃         ┃
       ┃         ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛         ┃
       ┃                               input/output                                      ┃
       ┃                                                                                 ┃
       ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
            Unix domain socket (command line arguments, the environment, and 0,1,2 fds)

How does a process standout show on your screen?

If you show the file type of standard output (/dev/stdout), you will find that it is a symlink to /proc/self/fd/1.

$ ls -al /dev/stdout
lrwxrwxrwx 1 root root 15 Dec 12 06:34 /dev/stdout -> /proc/self/fd/1
  • see here to learn more /proc/self
  • Every process has three standard fd and ou can find these three fd under the path /proc/{PID}/fd/).

Actually, fd/1 is also a symlink to /dev/pts/*, which means if you write something into fd/1, you will get the same effect as stdout.

$ ls -al /proc/self/fd/1
lrwx------ 1 root root 64 Jan 20 09:19 /proc/self/fd/1 -> /dev/pts/1

Is /dev/pts/* another symlink? No, Its is character device file.

$ ll -al /dev/pts/1
crw--w---- 1 root tty 136, 1 Jan 20 09:21 /dev/pts/1

What is character device file? It is one of file types that provides a serial stream of input or output.

That is why process output going into stdout (/dev/stdnout) will be shown on your screen.

This flow on linux looks like:

┏━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━┓
┃  process  ┣━┫  /dev/stdout  ┣━┫  /proc/self/fd/1  ┣━┫  /proc/{PID}/fd/1  ┣━┫  /dev/pts/1  ┣━┫  screen  ┃
┗━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━┛

Standard output tricks

How to send messages to another pts?

There are two ways to do that.

The first is:

echo "Hello" > /dev/pts/10

The second is:

write user1 /dev/pts/10

How to intercept output from another pts?

There is a way to do that:

cat /dev/pts/10

But you cannot receive every input from there, you can only get discontinuous characters. It is because there are two processes waiting for the stdout, one is cat, another one is shell.

How to redirect stdout of specific process to another pts?

Run a long process for test:

#!/bin/bash
for ((i=1; i<222222222; i++)); do
  echo $i $$
  /bin/sleep 1s
done

Run this bash, and find pid of this bash.

Use gdb to redirect the output of specific process.

sudo gdb -p {pid}

(In GDB console)
(gdb) p (void) dup2(open("/dev/pts/6", 1), 1)        // or you can redirect to a file "/tmp/stdout"
(gdb) detach
(gdb) quit

You can immediately see stdout that has been redirected to pts/6 and fd/1 has been changed.

$ ll /proc/{pid}/fd
total 0
dr-x------ 2 user1 user1  0 Jan 10 13:16 .
dr-xr-xr-x 9 user1 user1  0 Jan 10 13:16 ..
lrwx------ 1 user1 user1 64 Jan 10 13:16 0 -> /dev/pts/0
l-wx------ 1 user1 user1 64 Jan 10 13:16 1 -> /dev/pts/6      <- here
lrwx------ 1 user1 user1 64 Jan 10 13:16 2 -> /dev/pts/0

Others

List current pts managed by ptmx

$ ls /dev/pt*
/dev/ptmx

/dev/pts:
0  1  2  3  4  5  8  ptmx

maximum of pty

$ cat /proc/sys/kernel/pty/max
4096

Pseudo-devices

  • /dev/tty*: PC screen and keyboard
  • /dev/ttys*: serial, e.g. /dev/ttys033 from my macbook
  • /dev/ttyUSB*: USB
  • /dev/ptmx: pty master
  • /dev/pts/*: pty slave
  • /dev/null: accepts and discards all input; produces no output (always returns an end-of-file indication on a read)
  • /dev/zero: accepts and discards all input; produces a continuous stream of NUL (zero value) bytes
  • /dev/full: produces a continuous stream of NUL (zero value) bytes when read, and returns a “disk full” message when written to
  • /dev/random and /dev/urandom: they produce a variable-length stream of pseudo-random numbers.

ref:

Comments