注:机翻,未校。
The TTY demystified
Real teletypes in the 1940s.
1940 年代真正的电传打字机。
The TTY subsystem is central to the design of Linux, and UNIX in general. Unfortunately, its importance is often overlooked, and it is difficult to find good introductory articles about it. I believe that a basic understanding of TTYs in Linux is essential for the developer and the advanced user.
TTY 子系统是 Linux 和一般 UNIX 设计的核心。不幸的是,它的重要性经常被忽视,很难找到关于它的好的介绍性文章。我相信对 Linux 中的 TTY 有基本的了解对于开发人员和高级用户来说是必不可少的。
Beware, though: What you are about to see is not particularly elegant. In fact, the TTY subsystem — while quite functional from a user’s point of view — is a twisty little mess of special cases. To understand how this came to be, we have to go back in time.
但要注意:你即将看到的并不是特别优雅。事实上,TTY子系统虽然从用户的角度来看功能相当强大,但却是一团曲折的特殊情况。要理解这是如何发生的,我们必须回到过去。
History
In 1869, the stock ticker was invented. It was an electro-mechanical machine consisting of a typewriter, a long pair of wires and a ticker tape printer, and its purpose was to distribute stock prices over long distances in realtime. This concept gradually evolved into the faster, ASCII-based teletype. Teletypes were once connected across the world in a large network, called Telex, which was used for transferring commercial telegrams, but the teletypes weren’t connected to any computers yet.
在 1869 年,股票行情机被发明出来。它是一种机电装置,由打字机、一对长电线和一个打印机组成,其目的是实时地将股票价格分发到远距离。这一概念逐渐演变为更快的基于 ASCII 的电传打字机(teletype)。电传打字机曾通过一个名为 Telex 的大型网络连接在一起,用于传输商业电报,但当时这些电传打字机还未与任何计算机连接。
Meanwhile, however, the computers — still quite large and primitive, but able to multitask — were becoming powerful enough to be able to interact with users in realtime. When the command line eventually replaced the old batch processing model, teletypes were used as input and output devices, because they were readily available on the market.
另一方面,尽管计算机仍然相当庞大和原始,但它们已经能够多任务处理,并且变得足够强大,可以实时与用户交互。当命令行最终取代了旧的批处理模型时,电传打字机被用作输入和输出设备,因为它们在市场上很容易获得。
There was a plethora of teletype models around, all slightly different, so some kind of software compatibility layer was called for. In the UNIX world, the approach was to let the operating system kernel handle all the low-level details, such as word length, baud rate, flow control, parity, control codes for rudimentary line editing and so on. Fancy cursor movements, colour output and other advanced features made possible in the late 1970s by solid state video terminals such as the VT-100, were left to the applications.
市面上有许多型号的电传打字机,都略有不同,因此需要某种软件兼容性层。在 UNIX 世界中,方法是让操作系统内核处理所有低级细节,例如字长、波特率、流量控制、奇偶校验、用于基本行编辑的控制代码等。在 1970 年代后期,VT-100 等固态视频终端实现了花哨的光标移动、颜色输出和其他高级功能,这些都留给了应用程序。
In present time, we find ourselves in a world where physical teletypes and video terminals are practically extinct. Unless you visit a museum or a hardware enthusiast, all the TTYs you’re likely to see will be emulated video terminals — software simulations of the real thing. But as we shall see, the legacy from the old cast-iron beasts is still lurking beneath the surface.
在现在的时代,我们发现自己身处一个物理电传打字机和视频终端几乎灭绝的世界。除非你参观博物馆或硬件爱好者的展览,否则你很可能看到的所有 TTY 都是模拟的视频终端——即真实设备的软模拟。但是,正如我们将要看到的,来自那些古老铸铁机器的遗产仍然潜伏在表面之下。
The use cases
A user types at a terminal (a physical teletype). This terminal is connected through a pair of wires to a UART (Universal Asynchronous Receiver and Transmitter) on the computer. The operating system contains a UART driver which manages the physical transmission of bytes, including parity checks and flow control. In a naïve system, the UART driver would then deliver the incoming bytes directly to some application process. But such an approach would lack the following essential features:
用户在终端(一个物理电传打字机)上输入数据。这个终端通过一对电线连接到计算机上的 UART (通用异步收发器)。操作系统包含一个 UART驱动程序 ,负责管理字节的物理传输,包括奇偶校验和流量控制。在一个简单的系统中,UART驱动程序会直接将接收到的字节传递给某个应用程序进程。但这种方法缺乏以下几个重要特性:
Line editing. Most users make mistakes while typing, so a backspace key is often useful. This could of course be implemented by the applications themselves, but in accordance with the UNIX design philosophy, applications should be kept as simple as possible. So as a convenience, the operating system provides an editing buffer and some rudimentary editing commands (backspace, erase word, clear line, reprint), which are enabled by default inside the line discipline. Advanced applications may disable these features by putting the line discipline in raw mode instead of the default cooked (or canonical) mode. Most interactive applications (editors, mail user agents, shells, all programs relying on curses
or readline
) run in raw mode, and handle all the line editing commands themselves. The line discipline also contains options for character echoing and automatic conversion between carriage returns and linefeeds. Think of it as a primitive kernel-level sed(1)
, if you like.
行编辑: 大多数用户在输入时会犯错误,因此一个退格键通常是非常有用的。虽然这些功能当然可以由应用程序自己实现,但根据 UNIX 设计哲学,应用程序应尽可能保持简单。因此,操作系统提供了一个编辑缓冲区和一些基本的编辑命令(退格、删除单词、清空行、重新打印),这些在 行控制 中默认启用。更高级的应用程序可以通过将行控制设置为 原始 模式而不是默认的 处理(或 * 规范 *)模式来禁用这些功能。大多数交互式应用程序(如编辑器、邮件用户代理、shell 以及所有依赖于 curses
或 readline
的程序)都在原始模式下运行,并自行处理所有行编辑命令。行控制还包含字符回显和回车与换行自动转换的选项。如果你愿意,可以把它想象成一个原始的内核级 sed (1)
。
Incidentally, the kernel provides several different line disciplines. Only one of them is attached to a given serial device at a time. The default discipline, which provides line editing, is called N_TTY
(drivers/char/n_tty.c
, if you’re feeling adventurous). Other disciplines are used for other purposes, such as managing packet switched data (ppp, IrDA, serial mice), but that is outside the scope of this article.
顺便提一下,内核提供了几种不同的行控制。一次只会有一个行控制附加到给定的串行设备上。提供行编辑功能的默认控制被称为 N_TTY
(如果你感兴趣,可以查看drivers/char/n_tty.c
)。其他控制则用于其他目的,例如管理分组交换数据(ppp、IrDA、串行鼠标),但这超出了本文的范围。
Session management. The user probably wants to run several programs simultaneously, and interact with them one at a time. If a program goes into an endless loop, the user may want to kill it or suspend it. Programs that are started in the background should be able to execute until they try to write to the terminal, at which point they should be suspended. Likewise, user input should be directed to the foreground program only. The operating system implements these features in the TTY driver (drivers/char/tty_io.c
).
会话管理。 用户可能希望同时运行多个程序,并逐个与它们进行交互。如果一个程序进入了无限循环,用户可能想要终止或挂起它。在后台启动的程序应该能够执行,直到它们尝试向终端写入数据,此时应将其挂起。同样,用户输入应仅定向到前台程序。操作系统在TTY驱动程序(drivers/char/tty_io.c
)中实现了这些功能。
An operating system process is “alive” (has an execution context), which means that it can perform actions. The TTY driver is not alive; in object oriented terminology, the TTY driver is a passive object. It has some data fields and some methods, but the only way it can actually do something is when one of its methods gets called from the context of a process or a kernel interrupt handler. The line discipline is likewise a passive entity.
操作系统中的进程是 “活动状态”(具有 执行上下文),这意味着它可以执行动作。TTY 驱动程序并不是活动状态;用面向对象的术语来说,TTY 驱动程序是一个被动对象。它有一些数据字段和方法,但它能够实际执行某些操作的唯一方式是当其某个方法在进程上下文或内核中断处理程序中被调用时。行控制同样也是一个被动实体。
Together, a particular triplet of UART driver, line discipline instance and TTY driver may be referred to as a TTY device, or sometimes just TTY. A user process can affect the behaviour of any TTY device by manipulating the corresponding device file under /dev
. Write permissions to the device file are required, so when a user logs in on a particular TTY, that user must become the owner of the device file. This is traditionally done by the login(1)
program, which runs with root privileges.
一个特定的 UART 驱动程序、行控制实例和 TTY 驱动程序的组合可以称为 * TTY 设备 *,有时也简称为 TTY。用户进程可以通过操作 /dev
下相应的设备文件来影响任何 TTY 设备的行为。对设备文件的写权限是必需的,因此当用户在某个特定的 TTY 上登录时,该用户必须成为设备文件的所有者。这通常是由以 root 权限运行的 login (1)
程序完成的。
The physical line in the previous diagram could of course be a long-distance phone line:
上图中的物理线路当然可以是长途电话线路:
This does not change much, except that the system now has to handle a modem hangup situation as well.
这没有太大变化,只是系统现在还必须处理调制解调器挂断的情况。
Let’s move on to a typical desktop system. This is how the Linux console works:
让我们继续看一个典型的桌面系统。Linux 控制台的工作原理如下:
The TTY driver and line discipline behave just like in the previous examples, but there is no UART or physical terminal involved anymore. Instead, a video terminal (a complex state machine including a frame buffer of characters and graphical character attributes) is emulated in software, and rendered to a VGA display.
TTY 驱动程序和行控制的行为与之前的例子相同,但不再涉及 UART 或物理终端。相反,视频终端(一个复杂的状态机,包括一个字符的帧缓冲区和图形字符属性)在软件中被模拟,并呈现到 VGA 显示器上。
The console subsystem is somewhat rigid. Things get more flexible (and abstract) if we move the terminal emulation into userland. This is how xterm(1)
and its clones work:
控制台子系统相对较为僵化。如果我们将终端仿真移到用户空间,事情就变得更加灵活(和抽象)。这就是xterm(1)
及其克隆程序的工作原理:
To facilitate moving the terminal emulation into userland, while still keeping the TTY subsystem (session management and line discipline) intact, the pseudo terminal or pty was invented. And as you may have guessed, things get even more complicated when you start running pseudo terminals inside pseudo terminals, à la screen(1)
or ssh(1)
.
为了方便将终端仿真移到用户空间,同时保持 TTY 子系统(会话管理和行控制)完好无损,发明了 pseudo terminal 或 pty。正如你可能猜到的,当开始在伪终端内部运行伪终端时,例如使用 screen (1)
或 ssh (1)
,事情变得更加复杂。
Now let’s take a step back and see how all of this fits into the process model.
现在让我们退后一步,看看所有这些如何适应流程模型。
Processes
A Linux process can be in one of the following states:
Linux 进程可能处于以下状态之一:
R | Running or runnable (on run queue) |
---|---|
D | Uninterruptible sleep (waiting for some event) |
S | Interruptible sleep (waiting for some event or signal) |
T | Stopped, either by a job control signal or because it is being traced by a debugger. |
Z | Zombie process, terminated but not yet reaped by its parent. |
By running ps l
, you can see which processes are running, and which are sleeping. If a process is sleeping, the WCHAN
column (“wait channel”, the name of the wait queue) will tell you what kernel event the process is waiting for.
通过运行 ps l
,您可以查看哪些进程正在运行,哪些进程处于休眠状态。如果某个进程处于休眠状态,WCHAN
列(“等待通道”,即等待队列的名称)将告诉您该进程正在等待哪个内核事件。
$ ps l
F UID PID PPID PRI NI VSZ RSS WCHAN STAT TTY TIME COMMAND
0 500 5942 5928 15 0 12916 1460 wait Ss pts/14 0:00 -/bin/bash
0 500 12235 5942 15 0 21004 3572 wait S+ pts/14 0:01 vim index.php
0 500 12580 12235 15 0 8080 1440 wait S+ pts/14 0:00 /bin/bash -c (ps l) >/tmp/v727757/1 2>&1
0 500 12581 12580 15 0 4412 824 - R+ pts/14 0:00 ps l
The “wait
” wait queue corresponds to the wait(2)
syscall, so these processes will be moved to the running state whenever there’s a state change in one of their child processes. There are two sleeping states: Interruptible sleep and uninterruptible sleep. Interruptible sleep (the most common case) means that while the process is part of a wait queue, it may actually also be moved to the running state when a signal is sent to it. If you look inside the kernel source code, you will find that any kernel code which is waiting for an event must check if a signal is pending after schedule() returns, and abort the syscall in that case.
“wait” 等待队列对应于 wait(2) 系统调用,因此每当其中一个子进程的状态发生变化时,这些进程就会被移动到运行状态。有两种睡眠状态:可中断睡眠和不间断睡眠。可中断休眠(最常见的情况)意味着,虽然进程是等待队列的一部分,但当向其发送信号时,它实际上也可能被移动到运行状态。如果你查看一下内核源代码,你会发现任何等待事件的内核代码都必须在 schedule() 返回后检查信号是否处于待处理状态,并在那种情况下中止 syscall。
In the ps
listing above, the STAT
column displays the current state of each process. The same column may also contain one or more attributes, or flags:
在上面的 ps 列表中,STAT 列显示每个进程的当前状态。同一列还可能包含一个或多个属性或标志:
s | This process is a session leader. |
---|---|
+ | This process is part of a foreground process group. |
These attributes are used for job control.
这些属性用于作业控制。
Jobs and sessions
Job control is what happens when you press ^Z
to suspend a program, or when you start a program in the background using &
. A job is the same as a process group. Internal shell commands like jobs
, fg
and bg
can be used to manipulate the existing jobs within a session. Each session is managed by a session leader, the shell, which is cooperating tightly with the kernel using a complex protocol of signals and system calls.
作业控制是指当您按 ^Z 挂起程序,或者当您在后台使用 & 启动程序时发生的情况。作业与进程组相同。内部 shell 命令(如 jobs、fg 和 bg)可用于操作会话中的现有作业。每个会话都由一个会话领导者(即 shell)管理,该 shell 使用复杂的信号和系统调用协议与内核紧密协作。
The following example illustrates the relationship between processes, jobs and sessions:
以下示例说明了进程、作业和会话之间的关系:
The following shell interactions…
…correspond to these processes…
…and these kernel structures.
-
TTY Driver ( /dev/pts/0 ).
Size: 45x13 Controlling process group: (101) Foreground process group: (103) UART configuration (ignored, since this is an xterm): Baud rate, parity, word length and much more. Line discipline configuration: cooked/raw mode, linefeed correction, meaning of interrupt characters etc. Line discipline state: edit buffer (currently empty), cursor position within buffer etc.
pipe0
Readable end (connected to PID 104 as file descriptor 0) Writable end (connected to PID 103 as file descriptor 1) Buffer
The basic idea is that every pipeline is a job, because every process in a pipeline should be manipulated (stopped, resumed, killed) simultaneously. That’s why
kill(2)
allows you to send signals to entire process groups. By default,fork(2)
places a newly created child process in the same process group as its parent, so that e.g. a^C
from the keyboard will affect both parent and child. But the shell, as part of its session leader duties, creates a new process group every time it launches a pipeline.
其基本思想是,每个流水线都是一个工作,因为流水线中的每个进程都应该同时纵(停止、恢复、终止)。这就是为什么 kill(2) 允许您向整个进程组发送信号。默认情况下,fork(2) 将新创建的子进程与其父进程组放在同一个进程组中,因此例如,来自键盘的 ^C 将同时影响父进程和子进程。但是,作为其会话领导者职责的一部分,shell 每次启动管道时都会创建一个新的进程组。
The TTY driver keeps track of the foreground process group id, but only in a passive way. The session leader has to update this information explicitly when necessary. Similarly, the TTY driver keeps track of the size of the connected terminal, but this information has to be updated explicitly, by the terminal emulator or even by the user.
TTY 驱动程序跟踪前台进程组 ID,但只是以被动方式。必要时,会话领导者必须明确更新此信息。同样,TTY 驱动程序会跟踪所连接终端的大小,但这些信息必须由终端仿真器甚至用户明确更新。
As you can see in the diagram above, several processes have /dev/pts/0
attached to their standard input. But only the foreground job (the ls | sort
pipeline) will receive input from the TTY. Likewise, only the foreground job will be allowed to write to the TTY device (in the default configuration). If the cat process were to attempt to write to the TTY, the kernel would suspend it using a signal.
如上图所示,多个进程将 /dev/pts/0 附加到其标准输入。但只有前台作业(ls | sort 管道)会接收来自 TTY 的输入。同样,仅允许前台作业写入 TTY 设备(在默认配置中)。如果 cat 进程尝试写入 TTY,内核将使用信号挂起它。
Signal madness
Now let’s take a closer look at how the TTY drivers, the line disciplines and the UART drivers in the kernel communicate with the userland processes.
现在,让我们仔细看看内核中的 TTY 驱动程序、线路规则和 UART 驱动程序如何与用户空间进程进行通信。
UNIX files, including the TTY device file, can of course be read from and written to, and further manipulated by means of the magic ioctl(2)
call (the Swiss army-knife of UNIX) for which lots of TTY related operations have been defined. Still, ioctl
requests have to be initiated from processes, so they can’t be used when the kernel needs to communicate asynchronously with an application.
UNIX 文件,包括 TTY 设备文件,当然可以被读取和写入,并且可以通过神奇的 ioctl (2)
调用(UNIX 的瑞士军刀)进一步操作,为此定义了许多与 TTY 相关的操作。然而,ioctl
请求必须从进程中发起,因此在内核需要与应用程序进行 * 异步 * 通信时,无法使用它。
In The Hitchhiker’s Guide to the Galaxy, Douglas Adams mentions an extremely dull planet, inhabited by a bunch of depressed humans and a certain breed of animals with sharp teeth which communicate with the humans by biting them very hard in the thighs. This is strikingly similar to UNIX, in which the kernel communicates with processes by sending paralyzing or deadly signals to them. Processes may intercept some of the signals, and try to adapt to the situation, but most of them don’t.
在《银河系漫游指南》中,道格拉斯·亚当斯提到了一颗极其乏味的星球, inhabited by a bunch of depressed humans 和一类锐齿动物,这些动物通过狠狠咬人类的大腿与他们交流。这与UNIX系统有着惊人的相似之处,在UNIX中,内核通过向进程发送使其瘫痪或致命的信号来进行通信。进程可以拦截一些信号,并尝试适应这种情况,但大多数进程并不会这样做。
So a signal is a crude mechanism that allows the kernel to communicate asynchronously with a process. Signals in UNIX aren’t clean or general; rather, each signal is unique, and must be studied individually.
因此,信号是一种粗糙的机制,允许内核与进程异步通信。在UNIX中,信号并不是干净或通用的;相反,每个信号都是独特的,必须单独研究。每种信号具有其特定的含义和处理方式,这使得理解和管理信号变得复杂。
You can use the command kill -l
to see which signals your system implements. This is what it may look like:
您可以使用命令 kill -l 来查看您的系统实现了哪些信号。这是它可能的样子:
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT
17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU
25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH
29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN
35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4
39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12
47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14
51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10
55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6
59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
As you can see, signals are numbered starting with 1. However, when they are used in bitmasks (e.g. in the output of ps s
), the least significant bit corresponds to signal 1.
如您所见,信号从 1 开始编号。但是,当它们在位掩码中使用时(例如在ps s的输出中),最低有效位对应于信号1。
This article will focus on the following signals: SIGHUP
, SIGINT
, SIGQUIT
, SIGPIPE
, SIGCHLD
, SIGSTOP
, SIGCONT
, SIGTSTP
, SIGTTIN
, SIGTTOU
and SIGWINCH
.
本文将重点介绍以下信号:SIGHUP、SIGINT、SIGQUIT、SIGPIPE、SIGCHLD、SIGSTOP、SIGCONT、SIGTSTP、SIGTTIN、SIGTTOU 和 SIGWINCH。
SIGHUP
- Default action: Terminate
默认操作:终止 - Possible actions: Terminate, Ignore, Function call
可能的操作:终止、忽略、函数调用
SIGHUP
is sent by the UART driver to the entire session when a hangup condition has been detected. Normally, this will kill all the processes. Some programs, such as nohup(1)
and screen(1)
, detach from their session (and TTY), so that their child processes won’t notice a hangup.
当检测到挂断情况时,UART 驱动程序会将 SIGHUP 发送到整个会话。通常,这将终止所有进程。一些程序,比如 nohup(1) 和 screen(1),会从它们的会话(和 TTY)中分离出来,这样它们的子进程就不会注意到挂断。
SIGINT
- Default action: Terminate
默认操作:终止 - Possible actions: Terminate, Ignore, Function call
可能的操作:终止、忽略、函数调用
SIGINT
is sent by the TTY driver to the current foreground job when the interactive attention character (typically ^C
, which has ASCII code 3) appears in the input stream, unless this behaviour has been turned off. Anybody with access permissions to the TTY device can change the interactive attention character and toggle this feature; additionally, the session manager keeps track of the TTY configuration of each job, and updates the TTY whenever there is a job switch.
SIGINT
是由 TTY 驱动程序发送给当前前台作业的,当输入流中出现交互式注意字符(通常是 ^C
,其 ASCII 码为 3 )时,除非这种行为已被关闭。任何具有 TTY 设备访问权限的人都可以更改交互式注意字符并切换此功能;此外,会话管理器还会跟踪每个作业的 TTY 配置,并在发生作业切换时更新 TTY。
SIGQUIT
- Default action: Core dump
默认操作:核心转储 - Possible actions: Core dump, Ignore, Function call
可能的操作:核心转储、忽略、函数调用
SIGQUIT
works just like SIGINT
, but the quit character is typically ^\
and the default action is different.
SIGQUIT 的工作方式与 SIGINT 类似,但退出字符通常为 ^\,并且默认操作不同。
SIGPIPE
- Default action: Terminate
默认操作:终止 - Possible actions: Terminate, Ignore, Function call
可能的操作:终止、忽略、函数调用
The kernel sends SIGPIPE
to any process which tries to write to a pipe with no readers. This is useful, because otherwise jobs like yes | head
would never terminate.
内核将 SIGPIPE 发送到任何尝试写入没有读取器的管道的进程。这很有用,因为否则像 yes |头永远不会终止。
SIGCHLD SIGCHLD
- Default action: Ignore
默认操作:忽略 - Possible actions: Ignore, Function call
可能的操作:忽略、函数调用
When a process dies or changes state (stop/continue), the kernel sends a SIGCHLD
to its parent process. The SIGCHLD
signal carries additional information, namely the process id, the user id, the exit status (or termination signal) of the terminated process and some execution time statistics. The session leader (shell) keeps track of its jobs using this signal.
当进程死亡或更改状态(停止/继续)时,内核会向其父进程发送 SIGCHLD。SIGCHLD 信号带有附加信息,即进程 ID、用户 ID、终止进程的退出状态(或终止信号)和一些执行时间统计信息。会话领导者 (shell) 使用此信号跟踪其作业。
SIGSTOP SIGSTOP
- Default action: Suspend
默认操作:挂起 - Possible actions: Suspend
可能的操作:挂起
This signal will unconditionally suspend the recipient, i.e. its signal action can’t be reconfigured. Please note, however, that SIGSTOP
isn’t sent by the kernel during job control. Instead, ^Z
typically triggers a SIGTSTP
, which can be intercepted by the application. The application may then e.g. move the cursor to the bottom of the screen or otherwise put the terminal in a known state, and subsequently put itself to sleep using SIGSTOP
.
该信号将无条件暂停接收者,即其信号动作无法重新配置。但请注意,在作业控制期间,内核不会发送 SIGSTOP。相反,^Z 通常会触发 SIGTSTP,该应用程序可以拦截该 SIGTSTP。然后,应用程序可以将光标移动到屏幕底部,或以其他方式将终端置于已知状态,然后使用SIGSTOP将自身置于睡眠状态。
SIGCONT
- Default action: Wake up
默认操作:唤醒 - Possible actions: Wake up, Wake up + Function call
可能的操作:唤醒、唤醒 + 函数调用
SIGCONT
will un-suspend a stopped process. It is sent explicitly by the shell when the user invokes the fg
command. Since SIGSTOP
can’t be intercepted by an application, an unexpected SIGCONT
signal might indicate that the process was suspended some time ago, and then un-suspended.
SIGCONT 将取消暂停已停止的进程。当用户调用 fg 命令时,shell 会显式发送它。由于应用程序无法拦截 SIGSTOP,因此意外的 SIGCONT 信号可能表明该进程在一段时间前已暂停,然后取消暂停。
SIGTSTP SIGTSTP
- Default action: Suspend
默认操作:挂起 - Possible actions: Suspend, Ignore, Function call
可能的操作:挂起、忽略、函数调用
SIGTSTP
works just like SIGINT
and SIGQUIT
, but the magic character is typically ^Z
and the default action is to suspend the process.
SIGTSTP 的工作方式类似于 SIGINT 和 SIGQUIT,但魔术字符通常是 ^Z,默认操作是暂停进程。
SIGTTIN
- Default action: Suspend
默认操作:挂起 - Possible actions: Suspend, Ignore, Function call
可能的操作:挂起、忽略、函数调用
If a process within a background job tries to read from a TTY device, the TTY sends a SIGTTIN
signal to the entire job. This will normally suspend the job.
如果后台作业中的进程尝试从 TTY 设备读取数据,则 TTY 会向整个作业发送 SIGTTIN 信号。这通常会暂停作业。
SIGTTOU
- Default action: Suspend
默认操作:挂起 - Possible actions: Suspend, Ignore, Function call
可能的操作:挂起、忽略、函数调用
If a process within a background job tries to write to a TTY device, the TTY sends a SIGTTOU
signal to the entire job. This will normally suspend the job. It is possible to turn off this feature on a per-TTY basis.
如果后台作业中的进程尝试写入 TTY 设备,则 TTY 会向整个作业发送 SIGTTOU 信号。这通常会暂停作业。可以按 TTY 关闭此功能。
SIGWINCH
- Default action: Ignore
默认操作:忽略 - Possible actions: Ignore, Function call
可能的操作:忽略、函数调用
As mentioned, the TTY device keeps track of the terminal size, but this information needs to be updated manually. Whenever that happens, the TTY device sends SIGWINCH
to the foreground job. Well-behaving interactive applications, such as editors, react upon this, fetch the new terminal size from the TTY device and redraw themselves accordingly.
如前所述,TTY 设备会跟踪终端大小,但需要手动更新此信息。每当发生这种情况时,TTY 设备都会将 SIGWINCH 发送到前台作业。运行良好的交互式应用程序(例如编辑器)会对此做出反应,从 TTY 设备获取新的终端大小并相应地重新绘制它们。
An example
Suppose that you are editing a file in your (terminal based) editor of choice. The cursor is somewhere in the middle of the screen, and the editor is busy executing some processor intensive task, such as a search and replace operation on a large file. Now you press ^Z
. Since the line discipline has been configured to intercept this character (^Z
is a single byte, with ASCII code 26), you don’t have to wait for the editor to complete its task and start reading from the TTY device. Instead, the line discipline subsystem instantly sends SIGTSTP
to the foreground process group. This process group contains the editor, as well as any child processes created by it.
假设您正在您选择的(基于终端的)编辑器中编辑文件。光标位于屏幕中间的某个位置,编辑器正忙于执行一些处理器密集型任务,例如对大文件执行搜索和替换操作。现在按 ^Z。由于行规程已配置为拦截此字符(^Z 是单个字节,ASCII 代码为 26),因此您不必等待编辑器完成其任务并开始从 TTY 设备读取。取而代之的是,线路规程子系统会立即将 SIGTSTP 发送到前台流程组。此进程组包含编辑器以及它创建的任何子进程。
The editor has installed a signal handler for SIGTSTP
, so the kernel diverts the process into executing the signal handler code. This code moves the cursor to the last line on the screen, by writing the corresponding control sequences to the TTY device. Since the editor is still in the foreground, the control sequences are transmitted as requested. But then the editor sends a SIGSTOP
to its own process group.
编辑器为 SIGTSTP 安装了信号处理程序,因此内核将进程转移到执行信号处理程序代码上。此代码通过将相应的控制序列写入 TTY 设备,将光标移动到屏幕上的最后一行。由于编辑器仍处于前台,因此将按请求传输控制序列。但随后,编辑器会向自己的进程组发送一个 SIGSTOP。
The editor has now been stopped. This fact is reported to the session leader using a SIGCHLD
signal, which includes the id of the suspended process. When all processes in the foreground job have been suspended, the session leader reads the current configuration from the TTY device, and stores it for later retrieval. The session leader goes on to install itself as the current foreground process group for the TTY using an ioctl
call. Then, it prints something like “[1]+ Stopped” to inform the user that a job was just suspended.
编辑器现已停止。此事实使用 SIGCHLD 信号报告给会话领导者,该信号包括挂起进程的 ID。当前台作业中的所有进程都已挂起时,会话领导者将从 TTY 设备读取当前配置,并将其存储以供以后检索。会话领导者继续使用 ioctl 调用将自身安装为 TTY 的当前前台进程组。然后,它打印类似“[1]+ 已停止”的内容,以通知用户作业刚刚暂停。
At this point, ps(1)
will tell you that the editor process is in the stopped state (“T
”). If we try to wake it up, either by using the bg
built-in shell command, or by using kill(1)
to send SIGCONT
to the process(es), the editor will start executing its SIGCONT
signal handler. This signal handler will probably attempt to redraw the editor GUI by writing to the TTY device. But since the editor is now a background job, the TTY device will not allow it. Instead, the TTY will send SIGTTOU
to the editor, stopping it again. This fact will be communicated to the session leader using SIGCHLD
, and the shell will once again write “[1]+ Stopped” to the terminal.
此时,ps(1) 会告诉您编辑器进程处于停止状态 (“T”)。如果我们尝试唤醒它,无论是使用 bg 内置的 shell 命令,还是使用 kill(1) 将 SIGCONT 发送到进程,编辑器将开始执行其 SIGCONT 信号处理程序。此信号处理器可能会尝试通过写入 TTY 设备来重绘编辑器 GUI。但是由于编辑器现在是后台作业,因此 TTY 设备将不允许这样做。相反,TTY 会将 SIGTTOU 发送给编辑器,然后再次停止它。这一事实将使用 SIGCHLD 传达给会话领导者,并且 shell 将再次向终端写入“[1]+ 已停止”。
When we type fg
, however, the shell first restores the line discipline configuration that was saved earlier. It informs the TTY driver that the editor job should be treated as the foreground job from now on. And finally, it sends a SIGCONT
signal to the process group. The editor process attempts to redraw its GUI, and this time it will not be interrupted by SIGTTOU
since it is now a part of the foreground job.
但是,当我们键入 fg 时,shell 首先恢复之前保存的行规程配置。它通知 TTY 驱动程序,从现在开始,编辑器作业应被视为前台作业。最后,它向进程组发送 SIGCONT 信号。编辑器进程尝试重新绘制其 GUI,这一次它不会被 SIGTTOU 打断,因为它现在是前台工作的一部分。
Flow control and blocking I/O
Run yes
in an xterm
, and you will see a lot of “y
” lines swooshing past your eyes. Naturally, the yes
process is able to generate “y
” lines much faster than the xterm
application is able to parse them, update its frame buffer, communicate with the X server in order to scroll the window and so on. How is it possible for these programs to cooperate?
在 xterm 中运行 yes,你会看到很多“y”线从你的眼睛掠过。当然,yes 进程生成“y”行的速度比 xterm 应用程序解析它们、更新其帧缓冲区、与 X 服务器通信以滚动窗口等速度要快得多。这些项目怎么可能合作?
The answer lies in blocking I/O. The pseudo terminal can only keep a certain amount of data inside its kernel buffer, and when that buffer is full and yes
tries to call write(2)
, then write(2)
will block, moving the yes
process into the interruptible sleep state where it remains until the xterm
process has had a chance to read off some of the buffered bytes.
答案在于阻塞 I/O。伪终端只能在其内核缓冲区中保留一定数量的数据,当该缓冲区已满并且 yes 尝试调用 write(2) 时,write(2) 将阻塞,将 yes 进程移动到可中断的休眠状态,直到 xterm 进程有机会读取一些缓冲的字节。
The same thing happens if the TTY is connected to a serial port. yes
would be able to transmit data at a much higher rate than, say, 9600 baud, but if the serial port is limited to that speed, the kernel buffer soon fills up and any subsequent write(2)
calls block the process (or fail with the error code EAGAIN
if the process has requested non-blocking I/O).
如果 TTY 连接到串行端口,也会发生同样的事情。yes 将能够以比 9600 波特率高得多的速率传输数据,但如果串行端口限制为该速度,内核缓冲区很快就会填满,任何后续的 write(2) 调用都会阻塞该进程(或者如果该进程请求了非阻塞 I/O,则失败并显示错误代码 EAGAIN)。
What if I told you, that it is possible to explicitly put the TTY in a blocking state even though there is space left in the kernel buffer? That until further notice, every process trying to write(2)
to the TTY automatically blocks. What would be the use of such a feature?
如果我告诉你,即使内核缓冲区中还有剩余空间,也可以明确地将 TTY 置于阻塞状态,该怎么办?在另行通知之前,每个尝试将 (2) 写入 TTY 的进程都会自动阻塞。这样的功能有什么用?
Suppose we’re talking to some old VT-100 hardware at 9600 baud. We just sent a complex control sequence asking the terminal to scroll the display. At this point, the terminal gets so bogged down with the scrolling operation, that it’s unable to receive new data at the full rate of 9600 baud. Well, physically, the terminal UART still runs at 9600 baud, but there won’t be enough buffer space in the terminal to keep a backlog of received characters. This is when it would be a good time to put the TTY in a blocking state. But how do we do that from the terminal?
假设我们正在与一些波特率为 9600 波特的旧 VT-100 硬件交谈。我们刚刚发送了一个复杂的控制序列,要求终端滚动显示。此时,终端因滚动操作而陷入困境,以至于无法以 9600 波特的全速率接收新数据。嗯,从物理上讲,终端 UART 仍然以 9600 波特率运行,但终端中没有足够的缓冲区空间来保持积压的接收字符。这时是将 TTY 置于阻塞状态的好时机。但是我们如何从终端做到这一点呢?
We have already seen that a TTY device may be configured to give certain data bytes a special treatment. In the default configuration, for instance, a received ^C
byte won’t be handed off to the application through read(2)
, but will instead cause a SIGINT
to be delivered to the foreground job. In a similar way, it is possible to configure the TTY to react on a stop flow byte and a start flow byte. These are typically ^S
(ASCII code 19) and ^Q
(ASCII code 17) respectively. Old hardware terminals transmit these bytes automatically, and expect the operating system to regulate its flow of data accordingly. This is called flow control, and it’s the reason why your xterm
sometimes appears to lock up when you accidentally press ^S
.
我们已经看到,TTY 设备可以配置为对某些数据字节进行特殊处理。例如,在默认配置中,接收到的 ^C 字节不会通过 read(2) 传递给应用程序,而是会导致 SIGINT 被传递到前台作业。以类似的方式,可以将 TTY 配置为对停止流字节和启动流字节做出反应。它们通常分别是 ^S(ASCII 代码 19)和 ^Q(ASCII 代码 17)。旧的硬件终端会自动传输这些字节,并期望操作系统相应地调节其数据流。这就是所谓的流量控制,这就是为什么当你不小心按下^S时,你的xterm有时会出现锁定的原因。
There’s an important difference here: Writing to a TTY which is stopped due to flow control, or due to lack of kernel buffer space, will block your process, whereas writing to a TTY from a background job will cause a SIGTTOU
to suspend the entire process group. I don’t know why the designers of UNIX had to go all the way to invent SIGTTOU
and SIGTTIN
instead of relying on blocking I/O, but my best guess is that the TTY driver, being in charge of job control, was designed to monitor and manipulate whole jobs; never the individual processes within them.
这里有一个重要的区别:写入由于流控制或由于内核缓冲区空间不足而停止的 TTY 将阻塞您的进程,而从后台作业写入 TTY 将导致 SIGTTOU 挂起整个进程组。我不知道为什么 UNIX 的设计者必须不遗余力地发明 SIGTTOU 和 SIGTTIN,而不是依赖于阻塞 I/O,但我最好的猜测是,负责作业控制的 TTY 驱动程序被设计用来监控和操作整个作业;永远不要是它们内部的单个过程。
Configuring the TTY device
To find out what the controlling TTY for your shell is called, you could refer to the ps l
listing as described earlier, or you could simply run the tty(1)
command.
要了解 shell 的控制 TTY 叫什么,您可以参考前面描述的 ps l
列表,或者您可以简单地运行 tty(1)
命令。
A process may read or modify the configuration of an open TTY device using ioctl(2)
. The API is described in tty_ioctl(4)
. Since it’s part of the binary interface between Linux applications and the kernel, it will remain stable across Linux versions. However, the interface is non-portable, and applications should rather use the POSIX wrappers described in the termios(3)
man page.
进程可以使用 ioctl(2) 读取或修改打开的 TTY 设备的配置。tty_ioctl(4) 中描述了 API。由于它是 Linux 应用程序和内核之间的二进制接口的一部分,因此它将在 Linux 版本中保持稳定。但是,该接口是不可移植的,应用程序应使用 termios(3) 手册页中描述的 POSIX 包装器。
I won’t go into the details of the termios(3)
interface, but if you’re writing a C program and would like to intercept ^C
before it becomes a SIGINT
, disable line editing or character echoing, change the baud rate of a serial port, turn off flow control etc. then you will find what you need in the aforementioned man page.
我不会详细介绍 termios(3) 接口的细节,但是如果您正在编写一个 C 程序,并希望在它变成 SIGINT 之前拦截 ^C,禁用行编辑或字符回显,更改串行端口的波特率,关闭流量控制等,那么您将在上述手册页中找到所需的内容。
There is also a command line tool, called stty(1)
, to manipulate TTY devices. It uses the termios(3)
API.
还有一个命令行工具,叫做 stty(1)
,用来操作 TTY 设备。它使用了 termios(3)
API。
Let’s try it !
让我们试试看!
$ stty -a
speed 38400 baud; rows 73; columns 238; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 1; time = 0;
-parenb -parodd cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk brkint ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany imaxbel -iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke
The -a
flag tells stty to display all settings. By default, it will look at the TTY device attached to your shell, but you can specify another device with -F
.
-a 标志告诉 stty 显示所有设置。默认情况下,它将查看连接到 shell 的 TTY 设备,但您可以使用 -F
指定另一个设备。
Some of these settings refer to UART parameters, some affect the line discipline and some are for job control. All mixed up in a bucket for monsieur. Let’s have a look at the first line:
其中一些设置引用 UART 参数,有些影响行规则,有些是作业控制的。所有混杂在一起,好似给用户的一桶杂物。让我们来看第一行:
speed | UART | The baud rate. Ignored for pseudo terminals. |
---|---|---|
rows, columns | TTY driver | Somebody’s idea of the size, in characters, of the terminal attached to this TTY device. Basically, it’s just a pair of variables within kernel space, that you may freely set and get. Setting them will cause the TTY driver to dispatch a SIGWINCH to the foreground job. |
line | Line discipline | The line discipline attached to the TTY device. 0 is N_TTY . All valid numbers are listed in /proc/tty/ldiscs . Unlisted numbers appear to be aliases for N_TTY , but don’t rely on it. |
斐夷所非 注
“monsieur” 在法语中指“先生”或“绅士”。在这里,作者以一种轻松诙谐的方式暗示用户需要处理一堆混合在一起的设置,就像是一个杂乱无章的“桶”,需要用户来分辨和理解。
Try the following: Start an xterm
. Make a note of its TTY device (as reported by tty
) and its size (as reported by stty -a
). Start vim
(or some other full-screen terminal application) in the xterm
. The editor queries the TTY device for the current terminal size in order to fill the entire window. Now, from a different shell window, type:
请尝试以下操作:启动 xterm。记下其 TTY 设备(由 tty 报告)及其大小(由 stty -a 报告)。在 xterm 中启动 vim(或其他一些全屏终端应用程序)。编辑器在 TTY 设备中查询当前终端大小,以填充整个窗口。现在,从不同的 shell 窗口中,键入:
stty -F X rows Y
where X is the TTY device, and Y is half the terminal height. This will update the TTY data structure in kernel memory, and send a SIGWINCH
to the editor, which will promptly redraw itself using only the upper half of the available window area.
其中 X 是 TTY 设备,Y 是终端高度的一半。这将更新内核内存中的 TTY 数据结构,并向编辑器发送 SIGWINCH,编辑器将立即仅使用可用窗口区域的上半部分重新绘制自身。
The second line of stty -a
output lists all the special characters. Start a new xterm
and try this:
stty -a 输出的第二行列出了所有特殊字符。开始一个新的 xterm 并尝试以下操作:
stty intr o
Now “o
”, rather than ^C
, will send a SIGINT
to the foreground job. Try starting something, such as cat
, and verify that you can’t kill it using ^C
. Then, try typing “hello
” into it.
现在,“o”(而不是 ^C)将向前台作业发送 SIGINT。尝试启动某些东西,例如 cat,并验证您无法使用 ^C 杀死它。然后,尝试在其中输入“你好”。
Occasionally, you may come across a UNIX system where the backspace key doesn’t work. This happens when the terminal emulator transmits a backspace code (either ASCII 8 or ASCII 127) which doesn’t match the erase
setting in the TTY device. To remedy the problem, one usually types stty erase ^H
(for ASCII 8) or stty erase ^?
(for ASCII 127). But please remember that many terminal applications use readline
, which puts the line discipline in raw mode. Those applications aren’t affected.
有时,您可能会遇到 UNIX 系统,其中退格键不起作用。当终端仿真器传输的退格代码(ASCII 8 或 ASCII 127)与 TTY 设备中的擦除设置不匹配时,会发生这种情况。为了解决这个问题,通常输入 stty erase ^H(对于 ASCII 8)或 stty erase ^?(适用于 ASCII 127)。但请记住,许多终端应用程序使用 readline,它将线路规则置于原始模式。这些应用程序不受影响。
Finally, stty -a
lists a bunch of switches. As expected, they are listed in no particular order. Some of them are UART-related, some affect the line discipline behaviour, some are for flow control and some are for job control. A dash (-
) indicates that the switch is off; otherwise it is on. All of the switches are explained in the stty(1)
man page, so I’ll just briefly mention a few:
最后,stty -a 列出了一堆开关。不出所料,它们没有特定的顺序列出。其中一些与 UART 相关,一些影响线路纪律行为,一些用于流量控制,一些用于作业控制。破折号 (-) 表示开关已关闭;否则,它处于开启状态。stty(1) 手册页中对所有开关进行了说明,因此我仅简要介绍几个:
icanon
toggles the canonical (line-based) mode. Try this in a new xterm
:
icanon 切换规范(基于行)模式。在新的 xterm 中尝试以下操作:
stty -icanon; cat
Note how all the line editing characters, such as backspace and ^U
, have stopped working. Also note that cat
is receiving (and consequently outputting) one character at a time, rather than one line at a time.
请注意所有行编辑字符(如退格键和 ^U)已停止工作。另请注意,cat 一次接收(并因此输出)一个字符,而不是一次接收一行。
echo
enables character echoing, and is on by default. Re-enable canonical mode (stty icanon
), and then try:
Echo 启用字符回显,并且默认情况下处于打开状态。重新启用规范模式 (stty icanon),然后尝试:
stty -echo; cat
As you type, your terminal emulator transmits information to the kernel. Usually, the kernel echoes the same information back to the terminal emulator, allowing you to see what you type. Without character echoing, you can’t see what you type, but we’re in cooked mode so the line editing facilities are still working. Once you press enter, the line discipline will transmit the edit buffer to cat
, which will reveal what your wrote.
当您键入时,您的终端仿真器会将信息传输到内核。通常,内核会将相同的信息回显到终端仿真器,使您可以查看键入的内容。如果没有字符回声,你就看不到你输入的内容,但我们处于熟食模式,所以行编辑工具仍在工作。一旦你按下回车键,行规程会将编辑缓冲区传输到猫,猫会显示你写的内容。
tostop
controls whether background jobs are allowed to write to the terminal. First try this:
ToStop 控制是否允许后台作业写入终端。首先尝试这个:
stty tostop; (sleep 5; echo hello, world) &
The &
causes the command to run as a background job. After five seconds, the job will attempt to write to the TTY. The TTY driver will suspend it using SIGTTOU
, and your shell will probably report this fact, either immediately, or when it’s about to issue a new prompt to you. Now kill the background job, and try the following instead:
& 使命令作为后台作业运行。五秒钟后,作业将尝试写入 TTY。TTY 驱动程序将使用 SIGTTOU 暂停它,您的 shell 可能会立即报告此事实,或者在即将向您发出新提示时报告。现在终止后台作业,并尝试以下操作:
stty -tostop; (sleep 5; echo hello, world) &
You will get your prompt back, but after five seconds, the background job transmits hello, world
to the terminal, in the middle of whatever you were typing.
您将返回提示,但五秒钟后,后台作业会在您输入的任何内容中间将你好世界传输到终端。
Finally, stty sane
will restore your TTY device configuration to something reasonable.
最后,stty sane 会将您的 TTY 设备配置恢复到合理的状态。
Conclusion
Further details can be found in the various man pages I’ve mentioned, as well as in the glibc manual (info libc
, “Job Control”).
更多细节可以在我提到的各种手册页以及 glibc 手册(info libc,“Job Control”)中找到。
via:
-
The TTY demystified Posted Friday 25-Jul-2008 17:46