Terminal emulators
The master side replaces the line (the pair of TX/RX wires) that goes to the terminal.
The terminal displays the characters that it receives on one of the wires (some of those are control characters and make it do things like move the cursor, change colour...) and sends on another wire the characters corresponding to the keys you type.
Terminal emulators like xterm are not different except that instead of sending and receiving characters on wires, they read and write characters on their file descriptor to the master side. Once they've spawned the slave terminal, and started your shell on that, they no longer touch that. In addition to emulating the pair of wire, xterm can also change some of the line discipline properties via that file descriptor to the master side. For instance, they can update the size attributes so a SIGWINCH be sent to the applications that interact with the slave pty to notify them of a changed size.
Other than that, there is little intelligence in the terminal/terminal emulator.
What you write to a terminal device (like the pty slave) is what you mean to be displayed there, what you read from it is what you have typed there, so it does not make sense for the terminal emulator to read or write to that. They are the ones at the other end.
The tty line discipline
A lot of the intelligence is in the tty line discipline. The line discipline is a software module (residing in the driver, in the kernel) pushed on top of a serial/pty device that sits between that device and the line/wire (the master side for a pty).
A serial line can have a terminal at the other end, but also a mouse or another computer for networking. You can attach a SLIP line discipline for instance to get a network interface on top of a serial device (or pty device), or you can have a tty line discipline. The tty line discipline is the default line discipline at least on Linux for serial and pty devices. On Linux, you can change the line discipline with ldattach
.
You can see the effect of disabling the tty line discipline by issuing stty raw -echo
(note that the bash prompt or other interactive applications like vi
set the terminal in the exact mode they need, so you want to use a dumb application like cat
to experience with that).
Then, everything that is written to the slave terminal device makes it immediately to the master side for xterm to read, and every character written by xterm to the master side is immediately available for reading from the slave device.
The line discipline is where the terminal device internal line editor is implemented. For instance with stty icanon echo
(as is the default), when you type a
, xterm writes a
to the master, then the line discipline echoes it back (makes a a
available for reading by xterm
for display), but does not make anything available for reading on the slave side. Then if you type backspace, xterm sends a ^?
or ^H
character, the line discipline (as that ^?
or ^H
corresponds to the erase
line discipline setting) sends back on the master a ^H
, space
and ^H
for xterm
to erase the a
you've just typed on its screen and still doesn't send anything to the application reading from the slave side, it just updates its internal line editor buffer to remove that a
you've typed before.
Then when you press Enter, xterm sends ^M
(CR), which the line discipline converts on input to a ^J (LF), and sends what you've entered so far for reading on the slave side (an application reading on /dev/pts/x
will receive what you've typed including the LF, but not the a
since you've deleted it), while on the master side, it sends a CR and LF to move the cursor to the next line and the start of the screen.
The line discipline is also responsible for sending the SIGINT
signal to the foreground process group of the terminal when it receives a ^C
character on the master side etc.
Many interactive terminal applications disable most of the features of that line discipline to implement it themselves. But in any case, beware that the terminal (xterm
) has little involvement in that (except displaying what it's told to display).
And there can be only one session per process and per terminal device. A session can have a controlling terminal attached to it but does not have to (all sessions start without a terminal until they open one). xterm
, in the process that it forks to execute your shell will typically create a new session (and therefore detach from the terminal where you launched xterm
from if any), open the new /dev/pts/x
it has spawned, by that attaching that terminal device to the new session. It will then execute your shell in that process, so your shell will become the session leader. Your shell or any interactive shell in that session will typically juggle with process groups and tcsetpgrp()
, to set the foreground and background jobs for that terminal.
As to what informations are stored by a terminal device with a tty discipline (serial or pty), that's typically what the stty
command displays and modifies. All the discipline configuration: terminal screen size, local, input output flags, settings for special characters (like ^C, ^Z...), input and output speed (not relevant for ptys). That corresponds to the tcgetattr()
/tcsetattr()
functions which on Linux map to the TCGETS
/TCSETS
ioctls, and TIOCGWINSZ
/TIOCSWINSZ
for the screen size. You may argue that the current foreground process group is another information stored in the terminal device (tcsetpgrp()
/tcgetpgrp()
, TIOC{G,S}PGRP
ioctls), or the current input or output buffer.
Note that that screen size information stored in the terminal device may not reflect the reality. The terminal emulator will typically set it (via the same ioctl on the master size) when its window is resized, but it can get out of sync if an application calls the ioctl on the slave side or when the resize is not transmitted (in case of an ssh connection which implies another pty spawned by sshd
if ssh
ignores the SIGWINCH
for instance). Some terminals can also be queried their size via escape sequences, so an application can query it that way, and update the line discipline with that information.
For more details, you can have a look at the termios
and tty_ioctl
man pages on Debian for instance.
To play with other line disciplines:
Emulate a mouse with a pseudo-terminal:
socat pty,link=mouse fifo:fifo
sudo inputattach -msc mouse # sets the MOUSE line discipline and specifies protocol
xinput list # see the new mouse there
exec 3<> fifo
printf '\207\12\0' >&3 # moves the cursor 10 pixels to the right
Above, the master side of the pty is terminated by socat onto a named pipe (fifo
). We connect that fifo to a process (the shell) that writes 0x87 0x0a 0x00 which in the mouse systems protocol means no button pressed, delta(x,y) = (10,0)
. Here, we (the shell) are not emulating a terminal, but a mouse, the 3 bytes we send are not to be read (potentially transformed) by an application from the terminal device (mouse
above which is a symlink made by socat
to some /dev/pts/x
device), but are to be interpreted as a mouse input event.
Create a SLIP interface:
# on hostA
socat tcp-listen:12345,reuseaddr pty,link=interface
# after connection from hostB:
sudo ldattach SLIP interface
ifconfig -a # see the new interface there
sudo ifconfig sl0 192.168.123.1/24
# on hostB
socat -v -x pty,link=interface tcp:hostA:12345
sudo ldattach SLIP interface
sudo ifconfig sl0 192.168.123.2/24
ping 192.168.123.1 # see the packets on socat output
Above, the serial wire is emulated by socat
as a TCP socket in-between hostA and hostB. The SLIP line discipline interprets those bytes exchanged over that virtual line as SLIP encapsulated IP packets for delivery on the sl0
interface.
At first I tried tracing a few xterm
s back to the xterm
pid based on info I found in /proc/locks
but it was loose. I mean, it worked, I think, but it was at best circumstancial - I don't fully understand all of the information that file provides and was only matching what seemed to correspond between its content and known terminal processes.
Then I tried watching lsof/strace
on an active write/talk
process between ptys. I had never actually used either program before, but they seem to rely on utmp
. If my targeted pty did not have a utmp
entry for whatever reason they both refused to admit that it existed. Maybe there's a way around that, but i was confused enough to abandon it.
I tried some udevadm
discovery with 136 and 128 major number device nodes as advertised for pts
and ptm
respectively in /proc/tty/drivers
, but I also lack any very useful experience with that tool and once again turned up nothing substantial. Interestingly, though, I noticed the :min
range for both device types was listed at a staggering 0-1048575
.
It wasn't until I revisited this this kernel doc that I started thinking about the problem in terms of mount
s, though. I had read that several times before but when continued research in that line led me to this this 2012 /dev/pts
patchset I had an idea:
sudo fuser -v /dev/ptmx
I thought what do I usually use to associate processes with a mount
? And sure enough:
USER PID ACCESS COMMAND
/dev/ptmx: root 410 F.... kmscon
mikeserv 710 F.... terminology
So with that information I can do, for instance from terminology
:
sudo sh -c '${cmd:=grep rchar /proc/410/io} && printf 1 >/dev/pts/0 && $cmd'
###OUTPUT###
rchar: 667991010
rchar: 667991011
As you can see, with a little explicit testing such a process could be made to pretty reliably output the master process of an arbitrary pty. Regarding the sockets, I'm fairly certain one could approach it from that direction as well using socat
as opposed to a debugger, but I've yet to straighten out how. Still, I suspect ss
might help if you're more familiar with it than I:
sudo sh -c 'ss -oep | grep "$(printf "pid=%s\n" $(fuser /dev/ptmx))"'
So I set it up with a little more explicit testing, actually:
sudo sh <<\CMD
chkio() {
read io io <$1
dd bs=1 count=$$ </dev/zero >$2 2>/dev/null
return $((($(read io io <$1; echo $io)-io)!=$$))
}
for pts in /dev/pts/[0-9]* ; do
for ptm in $(fuser /dev/ptmx 2>/dev/null)
do chkio /proc/$ptm/io $pts && break
done && set -- "$@" "$ptm owns $pts"
done
printf %s\\n "$@"
CMD
It prints $$
num \0
null bytes to each pty and checks each master process's io against a previous check. If the difference is $$
then it associates the pid with the pty. This mostly works. I mean, for me, it returns:
410 owns /dev/pts/0
410 owns /dev/pts/1
710 owns /dev/pts/2
Which is correct , but, obviously, it's a little racy. I mean, if one of those others was reading in a bunch of data at the time it would probably miss. I'm trying to figure out how to change the stty
modes on another pty in order to send the stop bit first or something like that so I can fix that.
Best Answer
That is one thing that is harder than it should be.
With newer linux kernels, the index of the slave pty paired with a master can be gathered from the
tty-index
entry from/proc/PID/fdinfo/FD
. See this commit.With older kernels, the only way you can get that is by attaching with a debugger to a process holding a master pty, and call
ptsname(3)
(or directlyioctl(TIOCGPTN)
) on the file descriptor.[but both methods will run into issues on systems using multiple devpts mounts, see below]
With that info, you can build a list of master-slave pairings, which will also allow you to look up the master starting up from the slave.
Here is a silly script that should do that; it will first try the
tty-index
way, and if that doesn't work it will fall back togdb
. For the latter case, it needs a workinggdb
(notgdb-minimal
or another half-brokengdb
most distros ship with) and because of its use ofgdb
, it will be very slow.For each pty pair, it will print something like:
the two
sshd
processes (pids 975 and 978) have open handles to the master side (one as its 9 fd and the other as its 14, 18 and 19 fds).sleep
and-bash
have open handles to the slave side as their standard (0,1 and 2) fds. The session leader (bash
) is also marked with a*
, the processes in the foreground (perl
andcut
) with a+
, and those in the background (less
and-bash
) with a-
.The
t/ct_test
processes are using the pty as their controlling terminal without having any fd open to it.tiocsti
has an open handle to it without it being its controlling terminal.Tested on Debian 9 and Fedora 28. Info about the magic numbers it's using can be found in
procfs(5)
andDocumentation/admin-guide/devices.txt
in the linux kernel source.This will fail on any system using chroots or namespace containers; that's not fixable without some changes to the kernel, since there's no reliable way to match the
tty
field from/proc/PID/stat
to a pty, and a fd opened via/dev/ptmx
to the corresponding/dev/pts
mount. See here for a rant about it.This will also not link in any fd opened via
/dev/tty
; the real tty could be worked out by attaching to the process and callingioctl(fd, TIOCGDEV, &dev)
, but that will mean another dirty heavy use of gdb, and it will run into the same issues as above with the major, minor numbers being ambiguous for pseudo-tty slaves.ptys.pl: