> For the complete documentation index, see [llms.txt](https://book.bsdcn.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://book.bsdcn.org/4.3-bsd-unix-cao-zuo-xi-tong-she-ji-yu-shi-xian/di-san-bu-fen-io-xi-tong/9.8.-zhong-duan-cao-zuo.md).

# 9.8 终端操作

考察过终端 I/O 系统的总体结构、数据结构和它控制的硬件之后，我们继续描述它的运作：考察一台泛化的终端硬件设备驱动和正常的终端行规程。各驱动中的自动配置例程不再赘述——它们的运作与第 8 章所述相同。

**open**

终端字符设备的特殊文件每次打开，都调用硬件驱动的 open 例程。open 例程检查所请求的设备配置进了系统并在自动配置期间被找到，然后初始化 `tty` 结构。设备尚未打开时，设置默认模式和线路速度。`tty` 状态置为 TS\_WOPEN（等待打开）。接着，设备若支持调制解调器控制线，open 例程使能 DTR 输出线，阻塞等待 DCD 输入线置位。多数驱动支持用 **设备标志** 覆盖调制解调器控制；这些标志在系统配置文件中设置，保存在驱动数据结构中。设备的标志中与终端线路号对应的位若已置位，输入端的调制解调器控制线即被无视。线路上检测到载波信号时，终端状态置 TS\_CARR\_ON 位。然后驱动经行规程的 open 入口把控制交给初始（或当前）行规程。

设备首次打开时的默认行规程是 0 号规程，即旧终端驱动。它的 open 例程 `ttyopen()` 检查打开线路的进程是否已被指派进程组；没有，就把它指派到终端的进程组 `t_pgrp`。终端也没有进程组时，用打开进程的 PID。这样，接纳新登录的程序初次打开终端线路，就把进程和终端的进程组设成了同一个值。线路原先未打开，则线路的终端大小信息清零，表示大小未知。线路随即标记为打开（状态位 TS\_OPEN）。

**输出行规程**

线路打开后，对所得文件描述符的 write 产生在终端线路上传输的输出。对字符设备写导致调用设备 write 入口 `d_write`，带设备号和描述待写数据的 `uio` 结构。终端硬件驱动用设备号找到正确的 `tty` 结构，再带 `tty` 结构和 `uio` 结构调用行规程的 `l_write` 入口。

行规程的 write 例程承担输出翻译和流控的大部分工作：负责从调用例程的用户进程把数据复制进内核，并把翻译后的数据放上终端的输出队列交给硬件驱动。终端驱动的 write 例程 `ttwrite()` 先检查终端线路是否仍有载波；没有则返回错误。然后检查当前进程是否被允许立即向终端写。用户可以设置 tty 选项只允许前台进程输出。该选项启用、且终端线路是进程的控制终端时，仅当进程在前台进程组（即进程与终端的进程组相同）才可立即输出。进程被禁止输出时，向其进程组发 SIGTTOU 信号把它挂起；用户把进程组移到前台后，write 会再次尝试。

确认写被允许后，`ttwrite()` 进入循环：把待写数据复制进内核，检查需要的输出翻译，把数据放上终端的输出队列。队列在全部字符处理完之前填满时，靠阻塞防止队列过满。队列大小的上限（高水位线）取决于输出线路速度。必须等输出排空才能继续时，`ttwrite()` 在 `tty` 结构状态中置 TS\_ASLEEP 标志，让传输完成中断处理程序在队列降到低水位线（同样取决于线路速度）时唤醒它。队列大小的检查和随后的入睡必须保证入睡后中断一定发生；见图 9.2。

```c
struct tty *tp;

    s = spltty();
    if (tp->t_outq.c_cc > high-water-mark) {
        ttstart(tp);
        tp->t_state |= TS_ASLEEP;
        sleep(&tp->t_outq, TTOPRI);
    }
    splx(s);
```

图 9.2 行规程中检查输出队列

错误、权限和流控都检查完之后，`ttwrite()` 用 `uiomove()` 每次至多 100 字符地把用户数据复制进本地缓冲。若为单一大小写终端做字符映射，每个字符翻译后经 `ttyoutput()` 放上输出队列；否则缓冲分段处理。原始模式不做翻译，整个缓冲一次处理。新终端驱动还提供 LITOUT 选项，允许完整的 8 位字符输出通路。其他模式下，用 `scanc()` 配一张标出可能需要翻译或延迟的动作字符的查找表，定位无须翻译的字符组；每组用 `b_to_q()` 放入输出队列。末尾的特殊字符用 `ttyoutput()` 输出。无论哪种情况，`ttwrite()` 都必须检查 C-list 块是否够用；不够就在 `lbolt` 上小睡（至多 1 秒）后重试。

做翻译输出的例程是 `ttyoutput()`：接受单个字符，按需处理，把结果放上输出队列。视终端模式可做以下翻译：

* 不支持制表符的终端，制表展开为空格。
* 仅大写终端上，大写字母前加反斜杠，小写字符换成大写。
* CRMOD 选项置位时，换行替换为回车加换行。（该选项也控制输入的回车映射为换行。）
* 产生打印头动作的字符后附延迟。延迟以 ASCII 字符范围之外、最高位置位、其余位指明延迟量的字符存放。

数据放上 `tty` 的输出队列后，调用 `ttstart()` 发起输出。除非输出已在进行、或已被停止字符或输出延迟挂起，`ttstart()` 调用 `tty` 的 `t_oproc` 字段指定的硬件驱动启动例程。全部数据处理完并放入输出队列后，`ttwrite()` 返回 write 成功的指示，实际的串行字符传输由设备驱动异步管理。

**输出上半部**

设备驱动处理字符传输的硬件专有操作，以及输出的同步与流控。`start()` 例程的结构在各种驱动间差别很小。输出机制按硬件类型分两大类。第一类是支持直接存储器访问（DMA）输出的设备，可以直接从 C-list 块取数据（DEC DH-11 终端多路复用器是这类接口的例子）。4.3BSD 在 VAX 上支持的 DMA 接口都接在 UNIBUS 上，因此输出缓冲必须用 UNIBUS 适配器中的映射寄存器映射到 UNIBUS 地址空间（见第 8 章）。为避免每次传输的映射分配开销，UNIX 把 C-list 块放在一段连续的物理内存中，第一台终端设备打开时把这段映射到 UNIBUS。传输的物理内存基址由映射区域的 UNIBUS 地址加数据在 C-list 区域中的偏移算出。

驱动 start 例程用 `ndqb()` 计算输出队列第一个 C-list 块中可在一次操作中传输的连续字符数。终端处于允许输出延迟的模式时，给 `ndqb()` 一个掩码以区别延迟字符。延迟字符若是第一个待传字符，终端线路标记 TS\_TIMEOUT，用 `timeout()` 调度延迟结束，到时调用 `ttrstrt()` 恢复输出。否则算出 UNIBUS 地址和数据块中的字符数，把设备用这些信息编程。设备从主存取数据，逐个字符发送，传输完成时中断 CPU。

第二类终端接口一次传输一个字符；DEC DZ-11 终端多路复用器就是这样。每个字符装入设备的输出字符寄存器传输，CPU 必须等传输完成中断才能发送下一个字符。这种工作方式中断量大，中断时需要的信息大多预先算好：指向下一个待传字符的指针、待传字符块的范围、以及接收下一个字符的硬件设备寄存器的地址。这一策略称为伪 DMA（pseudo-DMA）；预计算的信息存在一个 `pdma` 结构中。一小段汇编语言例程接收每次硬件传输完成中断，发送下一个字符并返回。没有剩余字符时，它带完成传输的线路号调用指定的 C 语言中断例程。于是正常驱动看到的是 DMA 输出的假象——直到整个字符块传完它才被调用。

输出操作发起后，终端状态标记 TS\_BUSY，当前传输完成前不再尝试新的传输。

**输出下半部**

一个字符块传输完成时，硬件多路复用器中断 CPU；传输中断例程带设备部件号被调用。多数情况下，设备有一个驱动可读的寄存器，指明设备的哪些线路完成了传输操作。对每条完成输出的线路，中断例程清除 TS\_BUSY 标志，用 `ndflush()` 把已传输的字符从输出队列移除。这些步骤完成一节输出。

接着调用行规程的 start 例程发起下一个操作；如前所述，它一般只是调用终端 `t_oproc` 字段指定的驱动启动例程。start 例程此时检查输出队列是否已降到低水位线；降了，再查上半部是否在等输出队列的空间。TS\_ASLEEP 标志置位则唤醒输出进程；`t_wsel` 中记录有选择输出的进程，则用 `selwakeup()` 通知它。然后，输出队列非空时照常启动下一个操作。

**输入下半部**

与输出不同，终端输入不由系统调用发起，而是在终端线路收到键盘或其他输入设备的字符时异步到来。因此终端系统的输入处理大多发生在中断时。多数硬件多路复用器在任何线路上收到字符都中断。它们通常提供存储仓（silo），存放收到的字符及收到字符的线路号和任何关联状态信息，直到设备处理程序来取。使用存储仓可以避免 CPU 尚未处理完一个收字中断时下一个字符到来而丢字符。许多设备上，系统可以把设备编程成存储仓部分或全部填满才中断，省去逐字符中断。但驱动随后必须定期检查设备，免得没有后续输入触发中断时字符滞留在存储仓里。输入流控字符的处理不能太迟，用户也察觉得到字符回显的延迟。设备若还能编程成第一个字符进仓后一小段时间即中断，驱动连定期检查也省了。4.3BSD 中带这种定时器的设备一律使用存储仓中断；其他终端驱动先用逐字符中断，输入速率高到值得动用存储仓告警和周期扫描时再切换。

设备接收器中断发生、或定时器例程发现输入时，接收中断例程从输入存储仓逐个读取字符及其线路号和状态信息。正常字符作为输入经接收 `tty` 的 `l_rint` 入口交给终端行规程：

```c
(*linesw[tp->t_line].l_rint)(input-character, tp);
```

带硬件检测奇偶错收到的字符，在只期望单一奇偶（偶或奇）时被丢弃。检测到 **断开**（break，全 0 位、长于正常字符）时，依终端模式向行规程传中断字符或空字符。

正常终端行规程的接收中断（`l_rint`）例程是 `ttyinput()`。9.1 节所述终端输入的解释就在这里做。需要时回显输入字符。原始模式下，字符不加解释放进裸输入队列。除此之外，`ttyinput()` 的工作大半是检查有特殊含义的字符并采取相应动作；其余字符放进裸队列。规范模式下，收到的字符若是回车或其他使当前行对读终端的程序可用的字符，裸队列的内容加入规范化队列，并调用 `ttwakeup()` 通知等待输入的进程。其他模式下，每处理一个字符就调用 `ttwakeup()`：它唤醒任何睡在裸队列上等输入供 `read` 的进程，并通知选择输入的进程。终端若经 `fcntl` 和 FASYNC 标志设置了信号驱动 I/O，则向控制终端的进程组发 SIGIO 信号。

`ttyinput()` 还必须检查输入队列不得过大、耗尽 C-list 块的供应；达到上限（255 字符）时丢弃输入字符。TANDEM 选项置位时，队列到半满就输出停止字符（通常 XOFF 即 Ctrl-S）施行端到端流控。

到这里为止，全部处理都是异步的，无论终端设备上有无 `read` 调用在等。这样，预键入得以用到输入队列的上限。

**输入上半部**

终于，有人对终端设备的文件描述符做 `read` 调用。与所有读字符特殊设备的调用一样，这导致调用设备驱动的 `d_read` 入口，带设备号和描述数据目的地的 `uio` 结构。终端设备驱动用设备号找到设备的 `tty` 结构，再调用行规程的 `l_read` 入口处理系统调用。

终端驱动的 `l_read` 入口是 `ttread()`。与 `ttwrite()` 一样，`ttread()` 先检查终端线路仍有载波，没有则返回错误。然后检查当前进程是否在当前控制终端的进程组：不在，其进程组被 SIGTTIN 信号停止，直到它的进程组取得终端控制权。最后，`ttread()` 检查相应队列（规范模式下是规范化队列，原始或 cbreak 模式下是裸队列）中有无数据。没有数据时，终端若用非阻塞 I/O 则返回 EWOULDBLOCK 错误；否则睡在裸队列的地址上。`ttread()` 被唤醒后从头重启处理，因为入睡期间终端状态或进程组可能已经变了。

`ttread()` 等待的队列中有字符后，用 `getc()` 一次一个地摘下，用 `ureadc()` 复制到用户缓冲。原始模式下，字符处理时不再检查。其他模式下，某些字符从队列摘除时得到特殊处理：CRMOD 标志置位时回车翻译为换行；延迟挂起字符使当前进程组被 SIGTSTP 停止。此外，规范或 cbreak 模式下，文件结束字符终止 read 而不传回用户程序；若它前面没有字符，read 返回零个字符，被用户程序解释为文件结束。字符逐个处理并返回用户，直到 `uio` 结构中的字符计数到零、队列耗尽，或（规范模式下）遇到行结束符。调用返回时，返回的字符数就是请求计数随字符处理递减的量。

读完成后，终端若处于 TANDEM 模式且曾因队列渐满发过停止字符，队列现在不满 20% 时发启动字符（通常 XON 即 Ctrl-Q）。

**stop 例程**

终端设备上的字符输出为求效率尽量以大块进行。但有两类事件应使未决的输出操作停下。第一类是收到停止字符——输出应尽快停止；接收输出的设备有时是缓冲有限的打印机或其他输出设备。第二类是收到导致输出被丢弃的特殊字符——可能因信号而起。两种情况下，终端行规程都调用字符设备驱动的 `d_stop` 入口停止当前输出操作；提供两个参数：`tty` 结构和一个指明输出要冲掉还是挂起的标志。若是冲掉输出，行规程在调用设备 stop 例程后清空输出队列的全部数据。

`d_stop` 例程的实现依赖硬件。不同驱动靠关闭发送器挂起输出，或把当前字符计数清零来停止输出。伪 DMA 驱动可以改动当前字符块的限度，使伪 DMA 例程在当前字符传完后即调用传输完成中断例程。当停止伴随冲掉数据、且被中止的输出操作会引起中断时，多数驱动在 `tty` 状态设置 TS\_FLUSH 标志；传输完成中断例程运行时注意到 TS\_FLUSH，不去更新输出队列的字符计数（中断发生时队列多半已被冲掉）。输出要停但不冲掉时，`tty` 状态置 TS\_TTSTOP 标志；驱动必须以可从当前位置恢复的方式停止输出。

**ioctl 例程**

9.3 节描述了终端驱动和行规程的用户界面，其中大部分经 `ioctl` 系统调用访问。这些调用多数操纵终端行规程的软件选项；有些也影响异步串行端口硬件的运作——尤其硬件线路速度、字长和奇偶都由这些设置导出。因此，`ioctl` 调用由当前行规程和硬件驱动共同处理。

对字符特殊文件做 `ioctl` 时，设备驱动的 `d_ioctl` 例程带设备号、`ioctl` 命令和指向数据缓冲的指针被调用。与读写例程一样，多数终端驱动的 `ioctl` 例程找到设备的 `tty` 结构后把控制交给行规程。视行规程的返回值，驱动可能再做处理，如图 9.3 所示。行规程的 `ioctl` 例程做多数公共命令，包括更改行规程和更改终端参数：检测到错误返回错误号，命令处理成功返回零，命令不认识返回负一。前两种情况下，硬件驱动检查命令是否可能影响串行端口参数的编程；可能则必须按新模式重置硬件参数，然后返回行规程的错误指示。有些驱动实现操纵调制解调器控制输出的额外 `ioctl` 命令；这些命令行规程不认识，必须由驱动处理。

```c
error = (*linesw[tp->t_line].l_ioctl)(tp, cmd, data, flag);
if (error >= 0) {
    if (cmd == TIOCSETP || cmd == TIOCSETN ||
        cmd == TIOCLBIS || cmd == TIOCLBIC ||
        cmd == TIOCLSET)
        dhparam(unit);   /* 重置硬件参数 */
    return (error);
}
switch (cmd) {
    /* 处理硬件专有命令 */
}
```

图 9.3 处理行规程的错误返回

**调制解调器变迁**

UNIX 在终端线路上使用调制解调器控制线的方式已在 9.7 节介绍。多数终端多路复用器至少支持 4.3BSD 所用的调制解调器控制线集合；不支持的则表现为载波永远置位。设备打开时使能 DTR 输出，然后检查载波输入的状态；载波输入的状态后来变化时，必须由驱动检测并处理。有些设备有单独的中断报告调制解调器状态变化；另一些把这种变化随接收字符的其他状态信息一起报告。VAX 上的一种设备（DZ-11）在调制解调器控制线变化时不中断，驱动必须定期检查它们的状态。检测到变化时，带载波输入的新状态调用行规程的 `l_modem` 例程通知它。

正常终端驱动的调制解调器例程 `ttymodem()` 维护 `tty` 结构中 TS\_CARR\_ON 标志的状态并处理相应的状态变化：检测到载波建立时，唤醒任何等 open 完成的进程；打开的线路上载波掉线时，向控制终端的进程组发挂起信号 SIGHUP，并为所有被停止的进程发继续信号（SIGCONT），同时冲掉终端队列。`ttymodem()` 的返回值指示驱动是否应维持 DTR 输出——值为零则应关闭 DTR。`ttymodem()` 还实现一个冷僻的终端选项：用载波线做流控握手，载波掉线即停输出，恢复即续。

**关闭终端设备**

对终端设备的最后一个引用关闭时，调用设备驱动的 close 例程。行规程和硬件驱动都可能需要妥善收尾。先调行规程的 close 例程：标准行规程 close 入口 `ttylclose()` 在载波仍在时等未决输出排空，然后冲掉输入队列并把终端行规程设回默认。注意，等输出完成期间 close 可能被信号打断。硬件驱动可以清除未决操作（如断开的传输）。若 TIOCHPCL `ioctl` 设置过 TS\_HUPCLS 状态位，则关闭 DTR 挂起线路。最后，`ttyclose()` 冲掉全部队列并清除全部终端状态。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://book.bsdcn.org/4.3-bsd-unix-cao-zuo-xi-tong-she-ji-yu-shi-xian/di-san-bu-fen-io-xi-tong/9.8.-zhong-duan-cao-zuo.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
