[TOC]

drivers/tty TTY子系统(TTY Subsystem) Linux终端和串口的核心框架

历史与背景

这项技术是为了解决什么特定问题而诞生的?

TTY子系统是Linux/Unix系统中历史最悠久、最核心的子系统之一。它的诞生是为了解决一个根本性的问题:如何为用户进程与各种文本输入/输出设备(即“终端”)之间提供一个统一、抽象的交互接口

它主要解决了以下几个核心问题:

  1. 硬件的抽象:早期的计算机通过物理的电传打字机(Teletypewriter, TTY)进行交互。后来出现了各种串行终端(Serial Terminal)。TTY子系统将这些五花八门的硬件抽象成一个标准的字符设备,使得用户进程可以用同样的方式(read, write)与它们交互。
  2. 输入处理与行编辑:用户在终端上输入时,难免会打错字。TTY子系统引入了“线路规程”(Line Discipline)的概念,提供了一套通用的输入处理逻辑,包括行缓冲、退格(Backspace)删除、擦除整行(Ctrl+U)等编辑功能。这使得应用程序(如Shell)无需自己实现这些复杂的编辑逻辑。
  3. 会话管理与信号:TTY子系统是Unix进程和作业控制(Job Control)的基石。它负责解释特殊的控制字符,例如将Ctrl+C转换为SIGINT信号发送给前台进程组,将Ctrl+Z转换为SIGTSTP信号来挂起进程。
  4. 支持虚拟终端:随着图形界面和网络的发展,物理终端逐渐被淘汰。TTY子系统演进出了**伪终端(Pseudo Terminal, PTY)**的概念,它在软件中模拟了一个物理终端,使得像xterm这样的终端模拟器、ssh这样的远程登录会话,以及容器(Docker)都能与Shell等命令行程序进行无缝交互。

它的发展经历了哪些重要的里程碑或版本迭代?

  • 源于Unix:TTY子系统的核心设计思想直接继承自最初的Unix系统,专为物理电传打字机和串行终端设计。
  • 伪终端(PTY)的引入:这是一个决定性的里程碑。为了支持远程登录(telnet, rlogin, ssh)和图形界面的终端模拟器,PTY被发明出来。它由一对主从设备(Master/Slave)组成,应用程序(如ssh服务器)通过主设备端(PTM)写入和读取,而Shell等客户端进程则在从设备端(PTS)上运行,认为自己正与一个真实的终端交互。
  • Unix 98 PTY模型:早期的BSD PTY模型需要大量预先创建的设备文件(/dev/ptyXX),管理不便且数量有限。Unix 98 PTY模型引入了/dev/ptmx这个“伪终端复用器”,每次打开它都会动态地创建一个新的主从设备对,对应的从设备出现在/dev/pts/目录下。这成为了现代Linux系统的标准。
  • 与Serial驱动的整合:内核中的所有串口(Serial Port)驱动都被设计为TTY驱动的一种,它们负责与硬件(如8250 UART芯片)通信,并将数据流接入TTY核心。

目前该技术的社区活跃度和主流应用情况如何?

TTY子系统是任何Linux/Unix-like系统的基石,其稳定性和重要性无与伦-比。它不是一个经常添加新功能的领域,但其复杂性和历史遗留问题使其成为一个需要持续精细维护的核心部分。

  • 主流应用
    • 所有命令行交互:你在任何终端中输入的每一个命令,都在通过一个TTY(通常是PTY)设备。
    • 远程服务器管理:每一次SSH登录都会创建一个PTY会话。
    • 容器化docker exec -it等命令通过创建一个PTY来为容器内的进程提供一个交互式终端。
    • 嵌入式与物联网:通过串口与微控制器、调试接口、工业设备等进行通信。

核心原理与设计

它的核心工作原理是什么?

TTY子系统是一个经典的三层架构模型,位于用户空间应用程序和物理/虚拟硬件之间。

  1. 上层:TTY核心 (tty_core.c)
    • 这是整个子系统的中央枢纽。它向用户空间提供了标准的字符设备接口(/dev/tty*, /dev/pts/*等)和对应的文件操作(open, read, write, ioctl)。
    • 它管理着核心数据结构struct tty_struct,这个结构代表一个打开的终端连接,并将下面两层连接在一起。
  2. 中层:线路规程(Line Discipline, n_tty.c)
    • 这是TTY子系统的“大脑”。它是一个数据处理层,所有流经TTY的数据都会经过它的处理。
    • 输入路径(硬件 -> 进程):当用户在键盘上敲击字符时,数据从底层驱动上来,线路规程负责:
      • 回显(Echo):将用户输入的字符再发送回终端显示出来。
      • 行缓冲(Canonical Mode):将字符暂存起来,直到用户按下回车键,才将一整行数据提交给上层等待read()的进程。
      • 编辑:处理退格、删除字符等编辑操作。
      • 特殊字符解释:捕获Ctrl+C, Ctrl+Z等特殊字符,并将其转换成发送给进程的信号。
    • 输出路径(进程 -> 硬件):当进程write()数据时,线路规程负责输出处理,例如,它可能会根据设置(ONLCR标志)自动将换行符\n转换为回车+换行\r\n
  3. 下层:TTY驱动 (serial/, vt/, pty.c)
    • 这是TTY子系统的“手脚”,直接与硬件或虚拟设备打交道。
    • 它实现了一组struct tty_operations回调函数,如.write()(将字符发送到硬件)、.open()(初始化硬件)等。
    • 它负责从硬件接收字符,然后调用tty_flip_buffer_push()等函数将数据“喂”给线路规程进行处理。
    • 常见的TTY驱动类型包括:串口驱动虚拟终端驱动(VT)伪终端驱动(PTY)

它的主要优势体现在哪些方面?

  • 高度标准化:为所有形式的终端交互提供了单一、一致的编程接口。
  • 功能强大:内置了丰富的行编辑、会话管理和信号处理功能。
  • 彻底解耦:将应用程序与具体的终端硬件或实现完全分离开来。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 历史包袱与复杂性:TTY子系统是内核中最古老、最复杂的子系统之一,其代码和大量的ioctl选项充满了历史痕迹,学习和调试的门槛较高。
  • 性能开销:线路规程的逐字符处理引入了不可避免的性能开销。对于需要通过串口进行高速、大块原始数据传输的场景(例如,传输固件文件),TTY的终端语义处理会成为不必要的负担。在这种情况下,通常会将线路规程设置为“原始模式”(raw mode)以最小化开销。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

TTY子系统是任何需要模拟传统终端交互的场景下的唯一且标准的解决方案。

  • 交互式Shell:所有命令行Shell(bash, zsh等)都运行在一个TTY设备上。
  • 串口通信:使用minicom, screen等工具通过串口(如USB-to-Serial适配器)与外部设备(如Arduino, 路由器控制台)通信。
  • 文本编辑器vim, emacs, nano等在终端中运行的编辑器,完全依赖TTY接口进行屏幕渲染和按键输入。
  • 远程登录ssh服务器为每个登录会话创建一个PTY,将网络套接字的数据流与远端的Shell进行桥接。

是否有不推荐使用该技术的场景?为什么?

  • 进程间通信(IPC):不推荐。如果只是为了在两个进程间交换数据,应该使用更简单高效的管道(Pipe)套接字(Socket)。PTY是重量级的,专门用于模拟终端,用在这里是大材小用。
  • 网络通信:不推荐。进程间的网络通信应使用网络套接字(Network Socket)
  • 高速原始数据流:如上所述,如果一个串行设备只是用来传输二进制数据流,而不需要任何终端编辑或控制字符功能,那么虽然仍需通过TTY设备访问,但也应该立即将其设置为原始模式,以避免不必要的性能损失。

对比分析

请将其 与 其他相似技术 进行详细对比。

特性 TTY Subsystem Raw Character Device Pipes / Sockets
核心功能 提供面向终端的、经过处理的双向字节流,附带会话管理。 提供未经处理的、原始的双向字节流。 提供通用的、面向连接或数据报的双向通信通道。
数据模型 面向行(在规范模式下),有编辑、回显、信号等语义。 面向字节,所读即所写,无任何中间处理。 面向字节流(TCP)或数据包(UDP),是通用的数据传输机制。
典型设备 /dev/ttyS0 (串口), /dev/pts/5 (伪终端)。 /dev/urandom, 某些专用硬件的驱动接口。 N/A (通过系统调用创建,不在/dev下)。
主要用途 人机交互,模拟终端会话。 与硬件直接交互,传输原始数据。 进程间/机器间通信 (IPC/Network)
复杂性 非常高。包含复杂的线路规程和ioctl状态机。 。通常只有简单的read/write操作。 中等。API丰富,但概念比TTY清晰。
适用场景 任何需要让程序认为它在和“人”通过终端交互的场景。 驱动程序需要一个简单的、无加工的字节流接口给用户空间。 任何形式的程序间数据交换。

与其他子系统的差异

  1. console 的区别:

    • console 是系统的默认输出设备,用于显示内核日志和启动信息。
    • tty 是更广义的终端设备管理子系统,console 只是其中的一部分。
  2. serial 的区别:

    • serial 子系统专注于串口设备的管理,而 tty 子系统管理所有类型的终端设备,包括串口。
  3. pts 的区别:

    • pts 是伪终端设备的实现,属于 tty 子系统的一部分,专门用于虚拟终端。

总结

tty 是 Linux 内核中用于管理终端设备的核心子系统,涵盖了物理终端、串口终端和伪终端等多种设备类型。它在用户与系统交互、调试和远程登录等场景中扮演着重要角色。尽管其历史可以追溯到早期的电传打字机,但在现代计算中,tty 的概念已经被扩展和抽象,成为操作系统中不可或缺的一部分。

include/linux/console.h

console_is_registered 检查控制台是否已注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//检查con->node是否未哈希
/* Variant of console_is_registered() when the console_list_lock is held. */
static inline bool console_is_registered_locked(const struct console *con)
{
lockdep_assert_console_list_lock_held();
return !hlist_unhashed(&con->node);
}
/*
* console_is_registered - 检查控制台是否已注册
* @con:要检查的控制台的 struct console 指针
*
* 上下文:流程上下文。获取控制台列表锁定时可能会进入睡眠状态。
* 返回:如果控制台在控制台列表中,则为 true,否则为 false。
*
* 如果之前注册的控制台返回 false,则可以假定控制台的取消注册已完全完成,包括删除控制台列表后的 exit() 回调。
*/
static inline bool console_is_registered(const struct console *con)
{
bool ret;

console_list_lock(); //互斥加锁
ret = console_is_registered_locked(con); //检查con->node是否未哈希
console_list_unlock(); //互斥解锁
return ret;
}

drivers/tty/n_tty.c

n_tty_ops

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static struct tty_ldisc_ops n_tty_ops = {
.owner = THIS_MODULE,
.num = N_TTY,
.name = "n_tty",
.open = n_tty_open,
.close = n_tty_close,
.flush_buffer = n_tty_flush_buffer,
.read = n_tty_read,
.write = n_tty_write,
.ioctl = n_tty_ioctl,
.set_termios = n_tty_set_termios,
.poll = n_tty_poll,
.receive_buf = n_tty_receive_buf,
.write_wakeup = n_tty_write_wakeup,
.receive_buf2 = n_tty_receive_buf2,
.lookahead_buf = n_tty_lookahead_flow_ctrl,
};

/**

n_tty_init

1
2
3
4
void __init n_tty_init(void)
{
tty_register_ldisc(&n_tty_ops);
}

drivers/tty/tty_ldisc.c

tty_register_ldisc - 内核注册新的行规程(line discipline)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* tty_register_ldisc - 内核注册新的行规程(line discipline)
* @new_ldisc:指向 ldisc 对象的指针
*
* 在内核中安装一个新的 line discipline。该规则被设置为未引用,然后从此时开始对内核可用。
*
* 锁定:需要 %tty_ldiscs_lock 来防止 ldisc 比赛
*/
int tty_register_ldisc(struct tty_ldisc_ops *new_ldisc)
{
unsigned long flags;

if (new_ldisc->num < N_TTY || new_ldisc->num >= NR_LDISCS)
return -EINVAL;

raw_spin_lock_irqsave(&tty_ldiscs_lock, flags);
tty_ldiscs[new_ldisc->num] = new_ldisc;
raw_spin_unlock_irqrestore(&tty_ldiscs_lock, flags);

return 0;
}
EXPORT_SYMBOL(tty_register_ldisc);

drivers/tty/tty_io.c

tty_class_init: 注册TTY设备类

此代码片段的核心作用是在Linux内核中注册一个名为 “tty” 的设备类 (struct class)。这个类是所有终端设备 (TTY devices) 的容器, 包括物理串口、虚拟控制台和伪终端等。注册这个类是内核TTY子系统初始化的基础步骤, 它使得后续的TTY驱动程序能将自己创建的设备归入这个统一的分类下, 并最终在sysfs中表现为/sys/class/tty/目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/*
* tty_devnode: devtmpfs 的回调函数, 用于在创建设备节点时设置其名称和权限.
*
* @dev: 指向正在为其创建设备节点的 device 结构体的只读指针.
* @mode: 指向一个 umode_t 类型变量的指针, 用于接收该设备节点应有的文件权限. 如果此参数为NULL, 则函数不应修改它.
* @return: 可以返回一个字符串来覆盖默认的设备节点名称, 或者返回NULL以使用默认名称.
*/
static char *tty_devnode(const struct device *dev, umode_t *mode)
{
/*
* 检查 mode 指针是否为空. 如果是, 表示调用者不关心权限设置, 直接返回.
*/
if (!mode)
return NULL;
/*
* 检查当前设备的设备号 (dev->devt) 是否匹配两个特殊设备之一.
* dev->devt 是一个 dev_t 类型的值, 它结合了主设备号和次设备号.
* MKDEV() 是一个宏, 用于根据主设备号和次设备号创建一个 dev_t 值.
* TTYAUX_MAJOR 是 TTY 辅助设备的主设备号 (通常是 5).
*
* dev->devt == MKDEV(TTYAUX_MAJOR, 0) 检查的是不是 /dev/tty (当前控制终端).
* dev->devt == MKDEV(TTYAUX_MAJOR, 2) 检查的是不是 /dev/ptmx (创建伪终端对的主设备).
*/
if (dev->devt == MKDEV(TTYAUX_MAJOR, 0) ||
dev->devt == MKDEV(TTYAUX_MAJOR, 2))
/*
* 如果是这两个特殊设备之一, 就将其文件权限设置为 0666.
* 这意味着文件的所有者、所属组以及其他所有用户都对它有读和写的权限.
* 这是这两个特定设备节点的标准权限.
*/
*mode = 0666;
/*
* 总是返回 NULL. 这意味着我们不想修改设备节点的默认名称, 只想在特定情况下修改其权限.
*/
return NULL;
}

/*
* 定义一个全局的、常量 struct class 实例, 名为 tty_class.
* 这个结构体描述了 "tty" 这个设备类的属性.
*/
const struct class tty_class = {
/*
* .name: 类的名称. 这将导致在 sysfs 中创建一个名为 "/sys/class/tty" 的目录.
*/
.name = "tty",
/*
* .devnode: 指向上面定义的 tty_devnode 回调函数.
* 当任何属于 tty_class 的设备被创建时, 内核的 devtmpfs 文件系统会调用这个函数
* 来帮助确定最终在 /dev 目录下创建的设备节点文件的权限.
*/
.devnode = tty_devnode,
};

/*
* tty_class_init: TTY类的初始化函数.
* 标记为 __init, 表示此函数仅在内核启动期间执行, 其占用的内存之后可以被回收.
*/
static int __init tty_class_init(void)
{
/*
* 调用 class_register() 函数, 将我们上面定义的 tty_class 注册到内核中.
* 注册成功后, 其他驱动程序就可以通过 tty_class 将它们创建的设备归入此类.
* 函数返回0表示成功, 负值表示错误.
*/
return class_register(&tty_class);
}

/*
* postcore_initcall 是一个宏, 用于将 tty_class_init 函数注册为一个初始化调用.
* "postcore" 意味着这个函数将在内核核心子系统(如内存管理, 调度器)初始化之后,
* 但在大多数设备驱动初始化之前被调用. 这个时机非常合适, 因为它确保了当串口等TTY驱动
* 开始工作时, "tty" 这个类已经准备就绪了.
*/
postcore_initcall(tty_class_init);