Terminal
Overview
History
Historically, users accessed a UNIX system using a terminal connected via a serial line (an RS-232 connection).
On early UNIX systems, the terminal lines connected to the system were represented by character devices with names of the form /dev/ttyn.
Job Control
The protocol for allowing a user to move between multiple process groups (or jobs) within a single login session.
Process Group
Jobs
The processes belonging to a single command are called a process group or job, e.g., command like:
$ grep -vE '^ *($|#)' /etc/ssh/sshd_config | less &
[1] 3308
[1]+ Stopped grep --color=auto -vE '^ *($|#)' /etc/ssh/sshd_config | less
$ ps -H -o sid,pgid,tpgid,ppid,pid,cmd
SID PGID TPGID PPID PID CMD
3298 3298 3315 3165 3298 /bin/bash
3298 3307 3315 3298 3307 grep --color=auto -vE ^ *($|#) /etc/ssh/sshd_config
3298 3307 3315 3298 3308 less
3298 3315 3315 3298 3315 ps -H -o sid,pgid,tpgid,ppid,pid,cmd
Processes can be put in another process group using the setpgid
function, provided the process group belongs to the same session.
Foreground Job
The shell can give unlimited access to the controlling terminal to only one process group at a time. This is called the foreground job on that controlling terminal
Background Job
Other process groups managed by the shell that are executing without such access to the terminal are called background jobs.
Orphaned Process Group
Process groups that continue running even after the session leader has terminated are marked as orphaned process groups.
When a process group becomes an orphan, its processes are sent a SIGHUP
signal.
Session
Usually, new sessions are created by
the system login program
process disconnecting from its controlling terminal when it calls
setsid
to become the leader of a new session
Controlling Terminal
A shell that supports job control must arrange to control which job can use the terminal at any time. Otherwise there might be multiple jobs trying to read from the terminal at once, and confusion about which process should receive the input typed by the user.
One of the attributes of a process. Child processes created with fork
inherit this attribute from its parent.
A session leader that has control of a terminal is called the controlling process of that terminal.
API
POSIX termios
Linux ioctl
Pseudo-terminal Interface
The way programs like xterm
and emacs
implement their terminal emulation functionality.
Slave
The slave end of the pseudoterminal provides an interface that behaves exactly like a classical terminal for processes expecting to be connected to a terminal.
Master
Anything that is written on the master end by applications such as
network login services (
ssh(1)
,rlogin(1)
,telnet(1)
) data read from the pseudoterminal master is sent across the network to a client program that is connected to a terminal or terminal emulatorterminal emulators, data read from the pseudoterminal master is interpreted by the emulators in the same way a real terminal would interpret the data
script(1)
,screen(1)
, andexpect(1)
is provided to the process on the slave end as though it was input typed on a terminal.
Represented by the device file /dev/ptmx
is a character file with major number 5 and minor number 2.
The GNU Library API
#include <stdio.h>
#include <unistd.h>
#include <pty.h>
int main()
{
int master, slave;
pid_t child;
ssize_t c;
char buf[1024];
child = forkpty(&master, NULL, NULL, NULL);
if (child == -1)
return -1;
else if (child == 0) {
printf("[c] hi\n");
} else {
printf("[p] master: %u\n", master);
c = read(master, buf, sizeof(buf));
printf("[p] read %u bytes from master end:\n", c);
fflush(stdout);
write(1, buf, c);
puts("");
return 0;
}
}
$ gcc pty.c -lutil
$ ./a.out
[p] master: 3
[p] read 8 bytes from master end:
[c] hi
Python
tty
import tty
print(tty.tcgetattr(0))
Pseudo-terminal
Usage
We can use the function spawn
of the Python standard library pty
to open a new pseudo-terminal, spawn a new process, and connect its control terminal to the slave end.
import pty
pty.spawn("/bin/bash")
$ tty
/dev/pts/0
$ python -c 'import pty;pty.spawn("/bin/bash")'
$ tty
/dev/pts/1
Implementation
From the source code, we can see that the pty.spawn
function will call a function named fork
which will call os.forkpty
internally to create a new process and a new pair of pseudo-terminal.
pid, master_fd = fork()
if pid == CHILD:
os.execlp(argv[0], *argv)
The new child process will call os.setsid()
to become the process group leader.
def fork():
...
try:
pid, fd = os.forkpty()
except (AttributeError, OSError):
pass
else:
if pid == CHILD:
try:
os.setsid()
...
return pid, fd
From the implementation of os.forkpty
found in cpython/Modules/posixmodule.c
, we can see that it calls forkpty(3)
to setup the pseudo-terminal.
pid = forkpty(&master_fd, NULL, NULL, NULL);
...
return Py_BuildValue("(Ni)", PyLong_FromPid(pid), master_fd);
After getting the original terminal attributes with tcgetattr
and putting stdin in raw mode, as the code:
try:
mode = tcgetattr(STDIN_FILENO)
setraw(STDIN_FILENO)
the function pty.spawn
will then pass data between master and slave ends in a loop, the parent read input from its stdin and then write it back into the master end:
def _copy(master_fd, master_read=_read, stdin_read=_read):
...
if os.get_blocking(master_fd):
# If we write more than tty/ndisc is willing to buffer, we may block
# indefinitely. So we set master_fd to non-blocking temporarily during
# the copy operation.
os.set_blocking(master_fd, False)
try:
_copy(master_fd, master_read=master_read, stdin_read=stdin_read)
finally:
# restore blocking mode for backwards compatibility
os.set_blocking(master_fd, True)
return
high_waterlevel = 4096
stdin_avail = master_fd != STDIN_FILENO
stdout_avail = master_fd != STDOUT_FILENO
i_buf = b''
o_buf = b''
while 1:
rfds = []
wfds = []
if stdin_avail and len(i_buf) < high_waterlevel:
rfds.append(STDIN_FILENO)
if stdout_avail and len(o_buf) < high_waterlevel:
rfds.append(master_fd)
if stdout_avail and len(o_buf) > 0:
wfds.append(STDOUT_FILENO)
if len(i_buf) > 0:
wfds.append(master_fd)
rfds, wfds, _xfds = select(rfds, wfds, [])
...
if master_fd in wfds:
n = os.write(master_fd, i_buf)
i_buf = i_buf[n:]
if stdin_avail and STDIN_FILENO in rfds:
data = stdin_read(STDIN_FILENO)
if not data:
stdin_avail = False
else:
i_buf += data
On the other hand, the parent receives the child's output from the master end and then writes it back to its stdout.
if STDOUT_FILENO in wfds:
try:
n = os.write(STDOUT_FILENO, o_buf)
o_buf = o_buf[n:]
except OSError:
stdout_avail = False
if master_fd in rfds:
# Some OSes signal EOF by returning an empty byte string,
# some throw OSErrors.
try:
data = master_read(master_fd)
except OSError:
data = b""
if not data: # Reached EOF.
return # Assume the child process has exited and is
# unreachable, so we clean up.
o_buf += data
We can use strace
to trace pseudo-terminal-related system calls invoked by Python and see that how the input data are read from and write to the master end.
$ strace -o pty.strace python3 -c 'import pty;pty.spawn("/bin/bash");'
$ cat pty.strace
...
openat(AT_FDCWD, "/dev/ptmx", O_RDWR) = 3
ioctl(3, TIOCGPTN, [1]) = 0
ioctl(3, TIOCSPTLCK, [0]) = 0
ioctl(3, TIOCGPTPEER, 0x102) = 4
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f609f731a10) = 17666
close(4) = 0
ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(0, SNDCTL_TMR_CONTINUE or TCSETSF, {B38400 -opost -isig -icanon -echo ...}) = 0
ioctl(0, TCGETS, {B38400 -opost -isig -icanon -echo ...}) = 0
select(4, [0 3], [], [], NULL) = 1 (in [3])
read(3, "\33[?2004h\33[1;32m\342\224\200[\33[1;34msg-ded"..., 1024) = 209
write(1, "\33[?2004h\33[1;32m\342\224\200[\33[1;34msg-ded"..., 209) = 209
select(4, [0 3], [], [], NULL) = 1 (in [0])
read(0, "l", 1024) = 1
write(3, "l", 1) = 1
select(4, [0 3], [], [], NULL) = 1 (in [3])
read(3, "l", 1024) = 1
write(1, "l", 1) = 1
select(4, [0 3], [], [], NULL) = 1 (in [0])
read(0, "s", 1024) = 1
write(3, "s", 1) = 1
select(4, [0 3], [], [], NULL) = 1 (in [3])
read(3, "s", 1024) = 1
write(1, "s", 1) = 1
select(4, [0 3], [], [], NULL) = 1 (in [0])
read(0, ";", 1024) = 1
write(3, ";", 1) = 1
select(4, [0 3], [], [], NULL) = 1 (in [3])
read(3, ";", 1024) = 1
write(1, ";", 1) = 1
select(4, [0 3], [], [], NULL) = 1 (in [0])
read(0, "d", 1024) = 1
Internal
UART driver, which manages the physical transmission of bytes, line discipline instance and TTY driver (drivers/char/tty_io.c
) are referred to as a TTY device.
TTY Line Discipline
Every character received by the kernel (both from devices and users) is passed through a preselected TTY Line Discipline.
TTY line discipline process all incoming and outgoing character from/to a tty device in two modes:
canonical mode providing an editing buffer and editing commands likes backspace, erase word, clear line, and reprint
raw mode
The default discipline providing line editing is called N_TTY which is implemented in drivers/char/n_tty.c
.
TTY Driver
Characters Handling
2 queues exist for handling input characters:
from the terminal device to the reading processes
output characters transmitted from processes to the terminal
If terminal echoing is enabled, then the terminal driver automatically appends a copy of any input character to the end of the output queue, so that input characters are also output on the terminal.
Reference
Last updated