In previous posts, I have often mentioned the term “pseudoterminal” without explaining what it means. In this final part, I will cover the function and applications of pseudoterminals in detail.
Up until this point I have been focusing on describing the system console,
/dev/console, and the numbered virtual terminals,
/dev/tty1 and so on. We have seen how these represent an abstract interface to the keyboard and monitor. You type at the keyboard and data become available for reading on the terminal; you write to the terminal and characters appear on the screen.
A pseudoterminal or pty is a device that consists of a pair of device files: the master side, and the slave side. The slave behaves almost identically to a virtual terminal; however, it does not get its input directly from a keyboard, nor does its output go directly to a display. Instead, the slave gets its input from the master, and the slave’s output goes to the master. The master takes over the role of the keyboard and display.
Perhaps this is sounding suspiciously like a socket pair again. If it is, remember this: a socket pair is more or less a dumb wire that you can send data back and forth along, but a master and slave pair have the terminal driver in between them. Because of this, pseudoterminals support most of the ioctls that virtual terminals support. However, they don’t support any capabilities on their own. When a process on the slave side writes escape sequences, the terminal driver can’t send coloured text to the master side, of course; it simply sends the escape sequences verbatim, and the master parses them and decides how to format what is displayed.
I’ll actually defer discussion of how you get open file descriptors to the master and slave ends of a pty for now, and just discuss how they behave once opened.
As I said, the slave end of a pty is very much like the file
/dev/tty1. It can control a session just as a virtual terminal can. In particular, on Linux, when a session leader for a session with no ctty opens a pty that is not controlling any session, and does not use the
O_NOCTTY flag, that slave pty will become the ctty for the session. The
TIOCNOTTY ioctls also work just as well for slave ptys (see part four). The pty will remember a foreground process group of the session that the slave end controls. If you write ^C to the master end, then the foreground process group of the slave end will be sent SIGINT; but this behaviour can be configured with
tcsetattr(3); it is precisely analogous to entering Ctrl+C at the keyboard, since the master end conceptually replaces the keyboard. And so on.
On the other hand, the master end of a pty is quite different. It cannot act as a ctty, at least not in Linux. In fact, on my system, you can do a
TIOCSCTTY on a master pty, and the
ioctl(2) will return 0 (which usually means success), but you won’t actually get it as your ctty. If you write ^C to the slave end of a pty, the master end just receives the literal character. It’s precisely analogous to a process writing
/dev/tty1; as we know, that does not have the same effect as entering Ctrl+C on the keyboard. Note however that you can use
tcsetattr(3) on the master end; the result will be the same as if you did it on the slave end. However, the master end has no way of being notified when the slave end has been
ioctl(2)ed, nor vice versa.
Prior to writing this tutorial, I didn’t know a lot about ptys myself, so I wrote a program called ttycat to explore their functionality. It connects to either the master or slave end of a pty, reads from it whenever it can, and lets you send data to write to it. So it behaves rather like
nc(1), except that it works with ptys instead of Internet sockets. You can check out a copy of the source from my github account. I wrote it to be fully POSIX-compliant, so it should work on your system without a hitch. (Let me know if this isn’t the case.)
Let’s first consider how echoing works. First we start
ttycat +echo /dev/ptmx. This will open up a master pty (more on how this works later) and use
tcsetattr(3) to turn on echoing. It will print out a message telling us the name of the slave pty, say,
/dev/pts/5. Then run
ttycat /dev/pts/5. Now type in
foo on the master and hit Enter. You should see that
foo appears on the slave and it appears again on the master. Now enter
bar on the slave. You will see
bar appear on the master, but not echoed on the slave. We can draw an analogy with a virtual terminal. In the former case, it is as though you typed in
foo at the keyboard, and it was echoed on the screen as well as sent to the process reading. Since the master end serves as both keyboard and screen, it seems the string twice. In the latter case, it is as though a process wrote
foo, and it appeared on the screen. It doesn’t get echoed back to the process—that would be nonsensical. Press Ctrl+D on either the master or the slave end and both programs will terminate. Now try this again with
-echo instead of
+echo, and you’ll see that when you type in
foo at the master, it no longer echoes back on that end, but it still goes through to the slave end.
Next, try a flag like
+iuclc. It doesn’t matter whether you set it on the master end or the slave end. Remember that
IUCLC means that uppercase characters in input are converted into lowercase characters. With a virtual terminal, input is what a process reads. With a pty, input is what a process reads from the slave end, so you have to enter it on the master end. Now try entering
FOO on the master end. You’ll see
foo appear on the slave end, as well as
foo echoed back on the master end. Now enter
BAR on the slave end. In this case, since we are writing to the slave end, it’s output, and not affected by
IUCLC; you’ll see
BAR appear on the master end.
On the other hand, what if you set
+olcuc? Then typing in
foo on the slave end will result in
FOO appearing on the master end, since whatever is written to the slave is output. But entering
bar on the master is input, and not affected—
bar appears on the slave.
Canonical mode should be turned on by default, so try starting a new session with no flags and entering
foo^Ubar on the master end. To enter the
^U, you have to enter Ctrl+V Ctrl+U on the terminal; this results in ttycat receiving a literal
^U. This simulates a Ctrl+U at the keyboard, which clears the current input line. Just as expected, only
bar will appear on the slave end, and
bar is echoed back on the master end.
Next, let’s see how job control plays out. Open up the master as usual, and open the slave with
ttycat -attach /dev/pts/5 (or whatever the slave name is). On the master end, input a Ctrl+C by typing Ctrl+V Ctrl+C. You’ll see
^C echoed on the master end, because
ECHOCTL is turned on by default. (If it were turned off, then you would likely see what appears to be an blank line, since
\003 is an unprintable character.) On the slave end you’ll see the message:
---Signal received: SIGINT---. As our experiments usually go, now output a Ctrl+C by typing Ctrl+V Ctrl+C on the slave end. You should see what appears to be a blank line on the master end, because the master receives a literal Ctrl+C and this is not a printable character. If you want to see exactly what is happening, you could pipe the output of the master ttycat through
xxd -p. You’ll see that when you enter a Ctrl+C on the master end, you get
5e43 echoed back to you, that is,
^C; and when you enter a Ctrl+C on the slave end, you get
03 on the master end. Finally, press Ctrl+D on the master ttycat to end the file. You’ll see that the slave ttycat receives
SIGHUP because its ctty has hung up, whereas previously it just got an end-of-file because it was reading from the slave end.
The remaining details regarding reads, writes, and ioctls on ptys should be relatively easy to figure out—they essentially parallel the details of virtual terminals. Now let’s see how ptys can be useful.
The most obvious example of a use for ptys is in terminal emulators. I mentioned these in part one, but I haven’t gotten the chance to explain how they work, until now. You can run a shell and launch commands in a terminal emulator in exactly the same way as you would on a virtual terminal. This is because the commands running on the terminal emulator are reading from and writing to the slave end of a pty (which behaves very much like a virtual terminal), the master end of which is held by the terminal emulator process. So this is how you can imagine it:
- The terminal emulator program is started.
- The terminal emulator process obtains a file descriptor to the master end of a pty.
- The terminal emulator process
fork(2)s. The child closes the master end, calls
setsid(2), and obtains a file descriptor to the slave end,
dup(2)ing it twice so that all three standard streams point to the slave end, and acquiring it for the session in the process. The child process also sets the
TERMenvironment variable to reflect the capabilities that the terminal emulator will emulate.
- The child process
exec(2)s a shell, or whatever command the terminal emulator is configured to launch on startup. This shell and all its descendants will have file descriptors to the slave end and have the slave end as the ctty, just like how the shell and all its descendants have file descriptors to
/dev/tty1as the ctty when you log in on the first virtual terminal.
- Whatever you type in the terminal emulator window, the terminal emulator process writes to the master end, and it is received by the shell or its descendants on the slave end. Whatever the shell or its descendants output is written to the slave end; the terminal emulator process receives the output on the master end and draws characters in the window. Escape sequences written on the slave end (in accordance with the
TERMvariable and the terminfo database) are received on the master end verbatim; the terminal emulator processes them to, for example, format the output, maybe by choosing a bold font.
- If you close the terminal emulator, the master end of the pty becomes closed; the foreground process group of the slave end receives
SIGHUPand likely dies. If you press Ctrl+D at the shell, the slave end of the pty becomes closed, the terminal emulator gets EOF on the master end, and shuts down accordingly.
Pseudoterminals do not exist in Windows. The Windows program
cmd.exe does not use a pseudoterminal. Windows executables instead have a flag compiled-in that determines whether they will run in a console subsystem or a graphical subsystem; applications that choose the console subsystem will be given the black window as in
cmd.exe where input is typed and output appears. I will not discuss the Windows console subsystem any further.
The second example I will offer is
ssh(1). On a successful login, the
sshd(8) process on the remote host
fork(2)s and similarly obtains a master and slave pty, with the slave passed to a child like
bash(1). Now, whatever you type in on the local host is sent to a TCP socket on the remote host and read by
sshd, which in turn writes it to the master end of the pty. The shell and its descendants, running on the remote host, then read from the slave end. When they produce output, the output is written to the slave end of the pty, is read by
sshd(8) on the master, and written to the socket, so that it is sent back to the local host, where, more likely than not, it appears on another terminal. The net result is to simulate a cross-host pty, with the master end on the local machine and the slave end on the remote machine, by using an Internet socket to perform the forwarding. That’s how ssh “opens up a terminal”, as you might describe it, on the remote machine. Note that ssh must copy the
TERM variable from the local host to the remote host, so the processes running on the remote host will know which escape sequences can ultimately be used on the local virtual terminal or terminal emulator.
A third example is Expect. This is a programming language interpreter with syntax similar to Tcl that is primarily used to automatically communicate with processes that insist on reading from or writing to terminals instead of regular files and pipes. For example, the command
su(1) will typically refuse to read the password from standard input unless standard input is a terminal; you can use Expect to perform automated
su(1)s nevertheless. (I don’t recommend this—setuid binaries are a much better idea, provided that you write them properly—but it’s the simplest example I can use.) Expect does this using a pty, keeping the master end for itself while spawning
su(1) and having it read from the slave end. It sends the password automatically on the master end;
su(1) receives it on the slave end and can’t tell that it didn’t come from a human.
The fourth and final example I will give is
screen(1). This is a very useful program if you do a lot of work on remote hosts; you can log in once with
ssh(1) and then launch
screen as a terminal multiplexer so that you can have several shells open without having to log in over and over again. (Its other highly useful property is that it doesn’t die if the connection should happen to drop; you can
ssh(1) back in and recover your session.) It does this by opening up one pty for each virtual window you create within it. It then keeps all the master ends for itself and passes each slave end to some process that is launched in the window as it is created.
screen(1) also supports partitioning the view into two or more windows, which you can resize. It shouldn’t surprise you that it does this by performing the
TIOCSWINSZ ioctl on the master ends, causing the slave ends to receive
SIGWINCH and read the new window sizes with the
TIOCGWINSZ ioctl. (Window size is quite useful here, whereas it is not very useful on the system console and numbered virtual terminals.)
Now for the nitty-gritty. How do you get a file descriptor to a pseudoterminal?
I suppose that in principle some operating system designer could implement a system call named
ptypair or something, in analogy to
socketpair(2), that would return an array that contains a master pty file descriptor and a slave pty file descriptor. For reasons unknown to me, that is not how things work in any operating system that I’m aware of. There are no anonymous pseudoterminals. All pseudoterminals are device nodes residing in
/dev, just like
/dev/tty1 and friends. There are actually two places in
/dev where ptys might live, though.
BSD-style ptys are pre-formed master-slave pairs. Each master has a name beginning with
/dev/pty, and each slave has a name beginning with
/dev/tty. In Linux, up to 256 BSD-style pty pairs are supported: the masters are named
/dev/pty[p-za-e][0-9a-f] and the slaves are named
/dev/tty[p-za-e][0-9a-f]. The choice of
tty for the slaves and
pty for the masters possibly reflects the fact that the slave end behaves like a virtual terminal, whereas the master end is a whole different kind of animal.
Traditionally, the BSD-style ptys are installed with owner
tty, and permissions
rw-rw-rw-. You open a BSD-style master pty just like any other file, with
open(2). (You have to open the master before the slave; on Linux the error is
EIO if you try to open the slave first.) You can only open a given master pty once (Linux again gives
EIO if you try to open it when it’s already open), although you can
dup(2) the file descriptor to a master pty with no problem. The slave, on the other hand, can be opened arbitrarily many times while the master is open. So you probably want to change the permissions on the slave to prevent other users from messing around with your pty. The function
grantpt(3) is provided to do this for you. It takes a master file descriptor as an argument and changes the owner of the slave end to match the real user ID of the calling process, and changes the permissions to
rw--w----1. (You would also have to do this because someone else might have used this pty-tty pair first and you wouldn’t be able to open the slave otherwise because they
grantpt(3)ed it to themselves first.) On many systems, this function is implemented using a helper binary called
pt_chown, which is installed setuid root so that it has the appropriate privilege to call
In the BSD-style pty system, then, when a process wants to open a master pty, it just chooses one of the
/dev/pty* files. In fact, it might have to try
open(2)ing them one-by-one until it succeeds in finding one that’s free. It then passes the master’s file descriptor to
ptsname(3), which returns the name of the slave device node. Typically, this is where the process
fork(2)s, with the child opening the slave.
It’s often said that the BSD-style ptys suffer from race conditions, though I can’t figure out precisely what race conditions people are talking about. I did see that apparently it used to be possible, on some systems, for several processes to open the same master pty. I imagine this issue has been fixed—as I said above, on Linux at least, you can’t open a master pty when it’s already open. Perhaps another possible race condition is that there’s a brief interval between when a process opens a master pty and when it calls
grantpt(3), during which an attacker can potentially open the slave. I’m not sure. I don’t know much about BSD-style ptys. They are deprecated both because of the alleged race conditions and because their naming scheme imposes a limit on the number of pty-tty pairs available; so the documentation I can find of them is sparse. The Linux system I have running in my virtual machine doesn’t even have BSD-style pty support.
BSD-style ptys have been superseded by Unix98 ptys. On Linux these should available as long as you have mounted the devpts file system. (If you ever get weird errors about the system being out of ptys, it may be because devpts is not mounted and the program is falling back to BSD-style ptys—this happened to me once, with an ex-host (of wcipeg.com).) Here, a process obtains a master pty file descriptor by opening the master multiplexer device
/dev/ptmx. This causes a corresponding slave node to appear in the devpts file system, usually mounted on
/dev/pts, such as
/dev/pts/5. As with BSD-style ptys, the process can find out the name of the slave node using
ptsname(3). The process, after opening the master end, calls
grantpt(3) as usual to make it possible for its child to open the slave end; however, this is now race-free because the slave end cannot be opened until
unlockpt(3) has additionally been called on the master file descriptor. (On Linux, this uses the
TIOCSPTLCK ioctl request code with argument 0.) So the correct sequence is to
open(2) the slave end. I noted that with BSD-style ptys, you must open the master before the slave, otherwise an error will occur. With Unix98 ptys, it’s not even possible to do it the wrong way around, because the slave isn’t created until you open the master. The slave node is deleted after the master is closed.
Well, I think that’s just about it. I’ve said everything I wanted to say about pseudoterminals, and everything I wanted to say about terminals in general. I’m glad I collected all this information in one place—even if nobody else ever reads it, at least I’ll be able to refer to it myself. But I hope you did read it, and I hope that you learned something about terminals, maybe even enough so that you could rewrite ttycat, heh. (It still has a few bugs: for example, Ctrl+\ doesn’t work properly.) Maybe terminal confusion isn’t so terminal after all. (Sorry!)
1 The reason why the slave end should be group-writable is so that other users can send you messages (that will hopefully get through to you from whatever process is reading the master end) using the
write(1) program, which is installed setgid. This is usually harmless, provided that other users aren’t trolling you by flooding your terminal emulator with gibberish, but you can of course always turn it off by changing the permissions on the slave end—after all, you now own it.
Man… There’re certainly people reading it, you don’t know how much you have helped me to understand this topic!
I’m using mac with iTerm2, it seems that the controlling tty for iTerm2 is ttys000 (as well as the shell), instead of one of the master tty device name. May I ask why is that?
Hey i have an old xp computer ant want to make it a client of my newer vista home premium computer. I hear that there is a program out there that when u boot up the (client computer) u come straight to the welcome screen on the base computer (the one with vista on it) some how they are connected threw the network. Any ideas?.
Pingback: Hello, World! Глубокое погружение в Терминалы - Дорвеи и Сателлиты
This was a fantastic read. Thank you!