[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
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/n_null.c TTY 空线路规程

本代码片段展示了一个极其简单但概念上很重要的 TTY(电传打字机)子系统组件:空线路规程(n_null。其核心功能是提供一个“黑洞”式的线路规程,它不执行任何实际的 I/O 操作,对于任何读或写请求,它都直接返回错误。它的主要用途是在 TTY 设备的初始化或关闭过程中的“失败路径”上,作为一个安全的、无操作的占位符,防止系统在这些过渡状态下出现未定义的行为。

实现原理分析

此代码是 TTY 线路规程插件模型的一个最简实现。

  1. TTY 线路规程(Line Discipline)的角色:

    • 在 Linux TTY 架构中,线路规程位于 TTY 核心和底层 TTY 驱动(如串口驱动)之间。它负责实现特定于会话的协议和编辑功能。
    • 最常见的线路规程是 N_TTY,它实现了标准的终端行为(行编辑、回显、特殊字符处理等)。其他规程还包括用于 PPP、SLIP 等协议的 N_PPP, N_SLIP
    • 内核允许动态地为一个 TTY 设备切换不同的线路规程。
  2. n_null 的极简实现:

    • I/O 操作: n_null_readn_null_write 是这个规程的核心。它们不做任何事情,直接返回 -EOPNOTSUPP(不支持该操作)。这意味着任何尝试通过一个设置了 n_null 规程的 TTY 文件描述符进行读写的用户空间程序,都会立即失败。
    • tty_ldisc_ops 结构: null_ldisc 结构将这些无操作的函数与线路规程的元数据(名称 "n_null" 和编号 N_NULL)绑定在一起,形成一个完整的线路规程“插件”。
  3. 模块化注册:

    • n_null_init 函数在模块加载时被调用,它通过 tty_register_ldiscnull_ldisc 注册到 TTY 核心的全局线路规程列表中。BUG_ON 确保了如果注册失败(这在正常情况下几乎不可能发生),内核会立即崩溃,表明存在严重的初始化问题。
    • n_null_exit 函数则对称地调用 tty_unregister_ldisc 来注销它。

代码分析

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
76
77
78
79
80
81
// SPDX-License-Identifier: GPL-2.0
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/tty.h>
#include <linux/module.h>

/*
* n_null.c - 用于失败路径的空线路规程
*
* Copyright (C) Intel 2017
*/

/**
* @brief n_null_read - n_null 线路规程的读操作回调。
* @param tty: TTY 结构体。
* @param file: 文件对象指针。
* @param buf: 用户空间缓冲区。
* @param nr: 要读取的字节数。
* @param cookie: 未使用。
* @param offset: 未使用。
* @return ssize_t: 始终返回 -EOPNOTSUPP (不支持该操作)。
*/
static ssize_t n_null_read(struct tty_struct *tty, struct file *file, u8 *buf,
size_t nr, void **cookie, unsigned long offset)
{
return -EOPNOTSUPP;
}

/**
* @brief n_null_write - n_null 线路规程的写操作回调。
* @param tty: TTY 结构体。
* @param file: 文件对象指针。
* @param buf: 用户空间数据缓冲区。
* @param nr: 要写入的字节数。
* @return ssize_t: 始终返回 -EOPNOTSUPP (不支持该操作)。
*/
static ssize_t n_null_write(struct tty_struct *tty, struct file *file,
const u8 *buf, size_t nr)
{
return -EOPNOTSUPP;
}

// 定义 n_null 线路规程的操作集结构体。
static struct tty_ldisc_ops null_ldisc = {
.owner = THIS_MODULE, // 关联到本内核模块
.num = N_NULL, // 线路规程的唯一编号
.name = "n_null", // 线路规程的名称
.read = n_null_read, // 读操作回调
.write = n_null_write,// 写操作回调
};

/**
* @brief n_null_init - n_null 模块的初始化函数。
* @return int: 始终返回0。
*/
static int __init n_null_init(void)
{
// 向 TTY 核心注册 n_null 线路规程。
// BUG_ON 确保如果注册失败 (不应发生),内核会立即崩溃。
BUG_ON(tty_register_ldisc(&null_ldisc));
return 0;
}

/**
* @brief n_null_exit - n_null 模块的退出函数。
*/
static void __exit n_null_exit(void)
{
// 从 TTY 核心注销 n_null 线路规程。
tty_unregister_ldisc(&null_ldisc);
}

// 注册模块的初始化和退出函数。
module_init(n_null_init);
module_exit(n_null_exit);

// 模块元数据。
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Alan Cox");
MODULE_ALIAS_LDISC(N_NULL); // 为此线路规程编号创建一个模块别名。
MODULE_DESCRIPTION("Null ldisc driver");

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);

tty_register_device_attr: TTY设备注册的核心

本代码片段展示了Linux TTY子系统的核心功能之一:tty_register_device_attr函数。这是将一个抽象的TTY线路(由tty_driverindex定义)实例化为一个具体的、在内核设备模型中可见的struct device的最终实现。其核心功能是动态分配一个struct device,为其赋予设备号(dev_t)、名称、类别(class)和sysfs属性,然后将其注册到设备模型中,并最终通过cdev(字符设备)接口,在/dev目录下创建用户可见的设备文件。

实现原理分析

此函数是连接三个主要内核子系统的枢纽:TTY子系统设备模型虚拟文件系统(VFS)

  1. 动态设备创建:

    • 与静态分配设备号的旧驱动模型不同,此函数是为TTY_DRIVER_DYNAMIC_DEV标志设计的,它允许驱动在运行时(例如,当一个USB串口被插入时)才注册设备。
    • kzalloc(sizeof(*dev), GFP_KERNEL): 它不是使用一个预先存在的struct device,而是为每一个TTY线路动态地分配一个新的struct device实例。
  2. 三大子系统的链接:

    • 设备号 (dev_t): MKDEV(driver->major, driver->minor_start) + index计算出全局唯一的设备号。这个dev_t是内核的“通用语言”,是连接一切的关键。
    • 链接到设备模型:
      • dev->class = &tty_class;: 将新设备归入tty类。这决定了它在sysfs中的位置(/sys/class/tty/)。
      • dev->parent = device;: 设置其父设备,构建出硬件的拓扑结构(例如,一个tty设备是某个USB设备的子节点)。
      • dev_set_name(...): 设置设备名(如ttyS0),这也是它在sysfs中显示的名字。
      • device_register(dev): 将这个配置好的struct device正式注册到内核设备模型中,使其在sysfs中可见。
    • 链接到VFS (tty_cdev_add):
      • 这是创建字符设备文件的关键一步。cdev是字符设备的内核抽象。
      • tty_cdev_add函数会分配一个cdev结构,将其与tty_driver提供的文件操作(file_operations)关联起来,并使用之前计算好的dev_t,将其注册到VFS。
      • 当这个函数成功执行后,用户空间的udev守护进程会收到一个KOBJ_ADD uevent事件,并根据dev_t/dev目录下自动创建对应的设备文件(如/dev/ttyS0)。
  3. 同步与生命周期管理:

    • dev_set_uevent_suppress(dev, 1): 这是一个巧妙的同步技巧。它在设备注册的早期阶段抑制uevent的发送,防止用户空间(如udev)在设备尚未完全初始化(特别是cdev还未添加)时就尝试访问它。
    • kobject_uevent(&dev->kobj, KOBJ_ADD): 在所有步骤都完成后,手动触发一个”ADD” uevent,通知用户空间设备已完全就绪。
    • dev->release = tty_device_create_release: 设置了release回调函数。当这个struct device的最后一个引用被put_device释放时,内核会自动调用此函数来kfreekzalloc分配的内存,确保了无内存泄露。

代码分析

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/**
* @brief 注册一个tty设备(不带自定义属性的简化版本)。
* @param driver tty驱动。
* @param index tty线路号。
* @param device 关联的父设备,可为NULL。
* @return struct device* 成功则返回创建的设备指针,失败返回错误指针。
*/
struct device *tty_register_device(struct tty_driver *driver, unsigned index,
struct device *device)
{
/* 调用功能更全的版本,drvdata和attr_grp传NULL。 */
return tty_register_device_attr(driver, index, device, NULL, NULL);
}
EXPORT_SYMBOL(tty_register_device);

/**
* @brief tty设备的release回调函数。
* @param dev 指向要释放的设备。
* @details 当通过kzalloc动态分配的tty设备的引用计数降为0时,
* 内核设备模型会自动调用此函数来释放内存。
*/
static void tty_device_create_release(struct device *dev)
{
dev_dbg(dev, "releasing...\n");
kfree(dev);
}

/**
* @brief 注册一个带自定义属性的tty设备(核心实现)。
* @param driver tty驱动。
* @param index tty线路号。
* @param device 关联的父设备,可为NULL。
* @param drvdata 要设置的驱动私有数据。
* @param attr_grp 要设置的sysfs属性组。
* @return struct device* 成功则返回创建的设备指针,失败返回错误指针。
*/
struct device *tty_register_device_attr(struct tty_driver *driver,
unsigned index, struct device *device,
void *drvdata,
const struct attribute_group **attr_grp)
{
char name[64];
/* 根据主设备号和次设备号起始值计算出唯一的设备号(dev_t)。 */
dev_t devt = MKDEV(driver->major, driver->minor_start) + index;
struct ktermios *tp;
struct device *dev;
int retval;

/* ... 检查index是否越界 ... */

/* 根据驱动类型生成设备名(如 "ttyS0" 或 "ttySTM0")。 */
if (driver->type == TTY_DRIVER_TYPE_PTY)
pty_line_name(driver, index, name);
else
tty_line_name(driver, index, name);

/* 动态分配一个device结构体。 */
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev)
return ERR_PTR(-ENOMEM);

/* 填充device结构体的核心成员。 */
dev->devt = devt; /* 设备号 */
dev->class = &tty_class; /* 所属的类 */
dev->parent = device; /* 父设备 */
dev->release = tty_device_create_release; /* 释放回调函数 */
dev_set_name(dev, "%s", name); /* 设备名 */
dev->groups = attr_grp; /* sysfs属性组 */
dev_set_drvdata(dev, drvdata); /* 驱动私有数据 */

/* 暂时抑制uevent事件,防止用户空间访问一个半初始化的设备。 */
dev_set_uevent_suppress(dev, 1);

/* 将此设备注册到内核设备模型,使其在sysfs中可见。 */
retval = device_register(dev);
if (retval)
goto err_put;

/* 对于非动态分配次设备号的驱动... */
if (!(driver->flags & TTY_DRIVER_DYNAMIC_ALLOC)) {
/* ... 清理可能残留的旧termios数据 ... */
tp = driver->termios[index];
if (tp) {
driver->termios[index] = NULL;
kfree(tp);
}

/*
* 添加字符设备(cdev),这是将设备链接到VFS的关键,
* 它使得/dev下的设备文件可以被open/read/write。
*/
retval = tty_cdev_add(driver, devt, index, 1);
if (retval)
goto err_del;
}

/* 取消抑制uevent。 */
dev_set_uevent_suppress(dev, 0);
/* 手动发送一个ADD uevent,通知用户空间设备已完全就绪。 */
kobject_uevent(&dev->kobj, KOBJ_ADD);

return dev;

/* 错误处理回滚路径 */
err_del:
device_del(dev);
err_put:
put_device(dev);

return ERR_PTR(retval);
}
EXPORT_SYMBOL_GPL(tty_register_device_attr);

/**
* @brief 注销一个tty设备。
* @param driver tty驱动。
* @param index tty线路号。
*/
void tty_unregister_device(struct tty_driver *driver, unsigned index)
{
/*
* 这是一个高级辅助函数,它根据类和设备号找到对应的
* device并调用device_unregister()来完整地移除它。
*/
device_destroy(&tty_class, MKDEV(driver->major, driver->minor_start) + index);
if (!(driver->flags & TTY_DRIVER_DYNAMIC_ALLOC)) {
/* 从VFS中删除字符设备。 */
cdev_del(driver->cdevs[index]);
driver->cdevs[index] = NULL;
}
}
EXPORT_SYMBOL(tty_unregister_device);

drivers/tty/tty_port.c

tty_port_register_device & ..._serdev: TTY/Serdev 设备的动态注册

本代码片段展示了Linux TTY核心层提供给驱动程序的一系列高级API,用于将一个tty_port(代表硬件端口)注册为一个用户可见的设备。其核心功能是提供一个分层的、多功能的注册接口,最关键的是tty_port_register_device_attr_serdev函数,它能够动态地决定是将一个物理串口注册为一个传统的、供用户空间直接访问的TTY设备(如/dev/ttyS0),还是注册为一个serdev(Serial Device)总线控制器,供内核中的其他驱动(如GPS、Bluetooth驱动)绑定。

实现原理分析

这段代码是TTY子系统高度抽象和模块化的体现。它通过一系列封装函数,为驱动作者提供了不同层次的控制,并优雅地集成了serdev框架。

  1. 最底层:tty_port_link_device:

    • 职责: 这是最基础的操作,它只做一件事:在tty_driverports数组中,建立一个从线路号(index)到tty_port指针链接
    • 作用: 它仅仅是告知TTY核心:“嘿,对于线路X,它的硬件抽象是这个tty_port结构体”。它本身不创建任何设备节点或sysfs条目。
    • 使用场景: 内核文档明确指出这是“最后的手段”,只应在无法使用更高级的register函数时才使用。
  2. 标准注册:tty_port_register_device_attr:

    • 职责: 这是标准的“端口到设备”的注册函数。
    • 实现: 它是一个封装,执行两个步骤:
      1. 调用tty_port_link_device来建立逻辑链接。
      2. 调用tty_register_device_attr(TTY核心的内部函数),这个函数负责所有繁重的工作:创建struct device实例,在/sys下创建设备目录和属性文件,并通过cdev接口在/dev下创建字符设备文件。
  3. serdev集成与动态决策 (tty_port_register_device_attr_serdev):

    • 职责: 这是功能最强大的注册函数,它实现了TTY和serdev之间的动态二选一。
    • serdev是什么?: serdev框架用于管理那些连接到串口上的设备(如GPS模块)。在这种情况下,内核本身需要一个驱动来和GPS模块通信,而不是让用户空间直接读写原始的串口数据。此时,物理串口就扮演了一个“总线控制器”的角色。
    • 实现逻辑:
      1. 链接: 首先,它和标准注册一样,调用tty_port_link_device
      2. 尝试serdev: 然后,它调用serdev_tty_port_register。这个函数会检查设备树,看当前串口节点下是否有子节点。如果有,就意味着有一个设备连接到了这个串口。serdev框架会为这个连接创建一个serdev_device,并将物理串口注册为一个serdev_controller
      3. 决策点: if (PTR_ERR(dev) != -ENODEV)。这是决策的关键。
        • 如果serdev_tty_port_register成功,或者返回了除-ENODEV之外的任何错误,函数会直接返回。它不会继续创建TTY设备节点。这意味着端口被serdev框架“接管”了。
        • 只有当serdev_tty_port_register返回-ENODEV(表示“没有找到设备”,即设备树中没有对应的子节点)时,这个if条件才为假。
      4. 回退到TTY: 在if条件为假的情况下,代码会回退(fallback)到调用tty_register_device_attr,将这个端口注册为一个标准的TTY设备。
    • 这个“尝试-回退”的逻辑使得同一个驱动代码可以无缝支持两种不同的用例,而具体行为由设备树来配置。
  4. 注销 (tty_port_unregister_device):

    • 它的逻辑与..._serdev注册函数完美镜像。
    • 它首先尝试serdev_tty_port_unregister。如果成功(返回0),说明它是一个serdev控制器,清理完成,函数返回。
    • 如果失败(返回非0),说明它不是serdev控制器,于是它回退到调用tty_unregister_device来清理标准的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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/**
* @brief 链接一个tty_port到一个tty_driver。
* @param port 设备的tty_port。
* @param driver 该设备的tty_driver。
* @param index tty的线路号。
* @details 这仅建立一个内部指针链接,是注册过程的基础。
*/
void tty_port_link_device(struct tty_port *port,
struct tty_driver *driver, unsigned index)
{
if (WARN_ON(index >= driver->num))
return;
/* 在驱动的ports数组中,将指定线路号指向对应的tty_port。 */
driver->ports[index] = port;
}
EXPORT_SYMBOL_GPL(tty_port_link_device);

/**
* @brief 注册一个TTY设备。
* @param port 设备的tty_port。
* @param driver tty_driver。
* @param index tty线路号。
* @param device 父设备,可为NULL。
* @return struct device* 成功则返回创建的设备指针,失败返回错误指针。
*/
struct device *tty_port_register_device(struct tty_port *port,
struct tty_driver *driver, unsigned index,
struct device *device)
{
/* 调用更通用的版本,不带自定义属性。 */
return tty_port_register_device_attr(port, driver, index, device, NULL, NULL);
}
EXPORT_SYMBOL_GPL(tty_port_register_device);

/**
* @brief 注册一个带自定义属性的TTY设备。
* @param port 设备的tty_port。
* @param driver tty_driver。
* @param index tty线路号。
* @param device 父设备。
* @param drvdata 驱动私有数据。
* @param attr_grp 自定义sysfs属性组。
* @return struct device* 成功则返回创建的设备指针,失败返回错误指针。
*/
struct device *tty_port_register_device_attr(struct tty_port *port,
struct tty_driver *driver, unsigned index,
struct device *device, void *drvdata,
const struct attribute_group **attr_grp)
{
/* 首先,建立逻辑链接。 */
tty_port_link_device(port, driver, index);
/* 然后,调用TTY核心的函数来完成设备创建和注册。 */
return tty_register_device_attr(driver, index, device, drvdata,
attr_grp);
}
EXPORT_SYMBOL_GPL(tty_port_register_device_attr);

/**
* @brief 注册一个TTY设备或Serdev设备。
* @param port 设备的tty_port。
* @param driver tty_driver。
* @param index tty线路号。
* @param host 串口硬件设备。
* @param parent 父设备。
* @param drvdata 驱动私有数据。
* @param attr_grp 属性组。
* @return struct device* 成功则返回创建的设备指针,失败返回错误指针。
*/
struct device *tty_port_register_device_attr_serdev(struct tty_port *port,
struct tty_driver *driver, unsigned index,
struct device *host, struct device *parent, void *drvdata,
const struct attribute_group **attr_grp)
{
struct device *dev;

/* 总是先建立逻辑链接。 */
tty_port_link_device(port, driver, index);

/* 尝试将此端口注册为一个serdev控制器。 */
dev = serdev_tty_port_register(port, host, parent, driver, index);
/*
* 如果serdev注册成功,或返回了除-ENODEV之外的任何错误,
* 就直接返回。-ENODEV表示没有找到serdev客户端(即设备树中无子节点)。
*/
if (PTR_ERR(dev) != -ENODEV) {
/* 如果我们注册了一个serdev设备,就跳过创建字符设备(cdev)。 */
return dev;
}

/* 回退路径:如果没有serdev客户端,则注册为一个普通的TTY设备。 */
return tty_register_device_attr(driver, index, parent, drvdata,
attr_grp);
}
EXPORT_SYMBOL_GPL(tty_port_register_device_attr_serdev);

/**
* @brief 注销一个TTY或serdev设备。
* @param port 设备的tty_port。
* @param driver tty_driver。
* @param index tty线路号。
*/
void tty_port_unregister_device(struct tty_port *port,
struct tty_driver *driver, unsigned index)
{
int ret;

/* 首先,尝试将其作为一个serdev设备来注销。 */
ret = serdev_tty_port_unregister(port);
/* 如果成功(返回0),说明它是一个serdev设备,工作完成。 */
if (ret == 0)
return;

/* 如果它不是一个serdev设备,则作为一个普通的TTY设备来注销。 */
tty_unregister_device(driver, index);
}
EXPORT_SYMBOL_GPL(tty_port_unregister_device);

include/uapi/linux/tty_flags.h

ASYNC_* 标志: 用户空间对串口驱动行为的底层控制

本代码片段是一个UAPI(User API)头文件,它定义了一系列ASYNCB_*(位索引)和ASYNC_*(位掩码)宏。其核心功能是向用户空间应用程序(如setserialagetty等)提供一个标准的、稳定的API,用于通过ioctl(TIOCSSERIAL)系统调用来精细地控制串口驱动的底层行为。这些标志是struct serial_struct结构体中flags字段的“语言”。这个文件构成了内核UPF_*标志与用户空间之间公开的、有文档记录的契约

实现原理分析

此文件的核心原理与内核中的标志位定义类似,即位掩码,但它的服务对象是用户空间。

  1. 双层宏定义:

    • ASYNCB_*: 定义了每个标志所对应的比特位索引号(bit number)。例如,ASYNCB_SPD_HI4。这种定义方式便于使用{test,set,clear}_bit这类位操作函数(虽然这主要在内核中使用,但定义风格保持了一致)。
    • ASYNC_*: 使用位移操作 (1U << ASYNCB_*),将位索引号转换为可以直接用于按位与/或操作的位掩码。例如,ASYNC_SPD_HI就是 (1U << 4),即0x10。用户空间程序直接使用这些ASYNC_*掩码。
  2. 用户空间与内核的契约:

    • 这个头文件位于内核源码的include/uapi目录下,意味着它会被导出并安装到系统的/usr/include/linux目录,供所有用户空间的C程序包含。
    • 它定义了一个ABI(Application Binary Interface)。一旦发布,这些宏的值就不应再改变,否则会破坏已编译的用户空间程序的兼容性。
    • 如前一个分析所述,内核中的UPF_*标志被定义为与这些ASYNC_*标志完全等值,从而实现了用户空间设置与内核驱动标志之间的无缝传递。
  3. 标志的演进与废弃:

    • 注释中包含了非常重要的历史信息。许多标志后面都带有[x],表示**“已废弃”(defunct)**。例如ASYNCB_SPLIT_TERMIOS。这说明serial_struct接口是一个历史悠久的、有些陈旧的API,它的一些功能已经被更现代的termios接口所取代。
    • #ifndef __KERNEL__: 这个预处理器块中的宏只对用户空间可见。它们定义了一些内核曾经使用但现在已废弃、且从未真正暴露给用户的内部标志。这是一种向后兼容和信息隐藏的手段。

代码分析

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
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _LINUX_TTY_FLAGS_H
#define _LINUX_TTY_FLAGS_H

/*
* async_struct (和 serial_struct) flags 字段的定义,
* 也被 tty_port 的 flags 结构共享。
*
* ASYNCB_* 的定义是为了方便使用位操作函数。
*
* [0..ASYNCB_LAST_USER] 范围内的位是用户空间定义、可见和可修改的。
* [x] 表示该标志已废弃,不再使用。
*/
#define ASYNCB_HUP_NOTIFY 0 /*!< 在callout端口上,挂断或关闭时通知getty */
#define ASYNCB_FOURPORT 1 /*!< (PC-ism) 像AST Fourport卡一样设置OUT1, OUT2 */
#define ASYNCB_SAK 2 /*!< 安全注意键 (Secure Attention Key) */
#define ASYNCB_SPLIT_TERMIOS 3 /*!< [x] 为dialin/callout使用独立的termios */
#define ASYNCB_SPD_HI 4 /*!< (legacy) 使用 57600 bps 替代 38400 bps */
#define ASYNCB_SPD_VHI 5 /*!< (legacy) 使用 115200 bps 替代 38400 bps */
#define ASYNCB_SKIP_TEST 6 /*!< 自动配置时跳过UART测试 */
#define ASYNCB_AUTO_IRQ 7 /*!< 自动配置时自动探测IRQ */
#define ASYNCB_SESSION_LOCKOUT 8 /*!< [x] 基于会话锁定cua设备 */
#define ASYNCB_PGRP_LOCKOUT 9 /*!< [x] 基于进程组锁定cua设备 */
#define ASYNCB_CALLOUT_NOHUP 10 /*!< [x] 不为cua设备执行挂断 */
#define ASYNCB_HARDPPS_CD 11 /*!< 当CD线变高时调用hardpps */
#define ASYNCB_SPD_SHI 12 /*!< (legacy) 使用 230400 bps 替代 38400 bps */
#define ASYNCB_LOW_LATENCY 13 /*!< 请求低延迟行为 */
#define ASYNCB_BUGGY_UART 14 /*!< 这是一个有bug的UART,跳过某些安全检查 */
#define ASYNCB_AUTOPROBE 15 /*!< [x] 端口被PCI/PNP代码自动探测 */
#define ASYNCB_MAGIC_MULTIPLIER 16 /*!< (16750) 使用特殊的时钟或分频器 */
#define ASYNCB_LAST_USER 16 /*!< 用户可访问的最后一个位的索引 */

/*
* 仅由内核使用的内部标志 (只读)
* 警告: 这些标志已不再使用,并被 iflags 字段中的 TTY_PORT_* 标志取代。
*/
#ifndef __KERNEL__ /* 这个块只对用户空间代码可见 */
#define ASYNCB_INITIALIZED 31 /*!< 串口已初始化 */
#define ASYNCB_SUSPENDED 30 /*!< 串口已挂起 */
#define ASYNCB_NORMAL_ACTIVE 29 /*!< 普通设备已激活 */
#define ASYNCB_BOOT_AUTOCONF 28 /*!< 启动时自动配置端口 */
#define ASYNCB_CLOSING 27 /*!< 串口正在关闭 */
#define ASYNCB_CTS_FLOW 26 /*!< 执行CTS流控 */
#define ASYNCB_CHECK_CD 25 /*!< 检查CD线状态 (即CLOCAL) */
#define ASYNCB_SHARE_IRQ 24 /*!< (已废弃) 用于多功能卡 */
#define ASYNCB_CONS_FLOW 23 /*!< 控制台的流控 */
#define ASYNCB_FIRST_KERNEL 22 /*!< 内核私有标志的起始位 */
#endif

/* 掩码定义 */
#define ASYNC_HUP_NOTIFY (1U << ASYNCB_HUP_NOTIFY)
#define ASYNC_FOURPORT (1U << ASYNCB_FOURPORT)
#define ASYNC_SAK (1U << ASYNCB_SAK)
/* ... 其他ASYNC_*掩码的定义 ... */
#define ASYNC_LOW_LATENCY (1U << ASYNCB_LOW_LATENCY)
#define ASYNC_BUGGY_UART (1U << ASYNCB_BUGGY_UART)
#define ASYNC_MAGIC_MULTIPLIER (1U << ASYNCB_MAGIC_MULTIPLIER)

/* 包含了所有用户可访问标志的掩码 */
#define ASYNC_FLAGS ((1U << (ASYNCB_LAST_USER + 1)) - 1)
/* 所有已废弃标志的掩码 */
#define ASYNC_DEPRECATED (ASYNC_SPLIT_TERMIOS | ASYNC_SESSION_LOCKOUT | \
ASYNC_PGRP_LOCKOUT | ASYNC_CALLOUT_NOHUP | ASYNC_AUTOPROBE)
/* ... 其他组合掩码 ... */
#define ASYNC_SPD_CUST (ASYNC_SPD_HI|ASYNC_SPD_VHI)
#define ASYNC_SPD_WARP (ASYNC_SPD_HI|ASYNC_SPD_SHI)
#define ASYNC_SPD_MASK (ASYNC_SPD_HI|ASYNC_SPD_VHI|ASYNC_SPD_SHI)

#ifndef __KERNEL__
/* 这些标志不再使用 (并且总是对用户空间屏蔽) */
/* ... */
#endif

#endif

drivers/tty/tty_baudrate.c

tty_termios_baud_rate & tty_termios_encode_baud_rate: termios 波特率的编解码器

本代码片段展示了Linux TTY核心层用于转换termios波特率的两个核心辅助函数。它们是termios接口与驱动程序可理解的数值波特率之间的双向编解码器

  1. 解码 (tty_termios_baud_rate): 将termios->c_cflag中编码的Bxxxx符号,翻译成一个具体的数值波特率(如115200)。
  2. 编码 (tty_termios_encode_baud_rate): 将一个具体的数值波特率,反向编码成termios->c_cflag中的**Bxxxx符号**,如果找不到精确匹配,则使用特殊的BOTHER标志。

这两个函数是连接抽象的POSIX termios接口与底层驱动所需具体数值的桥梁。

实现原理分析

此机制的核心是查表法和对**特殊标志BOTHER**的巧妙运用。

  1. 波特率表 (baud_table & baud_bits):

    • 内核维护着两个严格同步的静态数组:
      • baud_table: 存储了所有标准波特率的数值speed_t类型,即unsigned int)。
      • baud_bits: 存储了与baud_table中每个数值相对应的**Bxxxx符号**(tcflag_t类型,来自termbits.h)。
    • 例如,baud_table[13]9600,而baud_bits[13]B9600。这两个表的索引是相同的。
  2. 解码过程 (tty_termios_baud_rate):

    • 职责: Bxxxx符号 -> speed_t数值。
    • 实现:
      • cbaud = termios->c_cflag & CBAUD;: 首先,它从c_cflag中屏蔽出与波特率相关的比特位(CBAUD掩码)。Bxxxx符号本质上就是一些小整数(0-15)。
      • BOTHER处理: 如果cbaud等于BOTHER(一个特殊的标志,表示“其他速率”),这意味着用户想要一个非标准的、任意的波特率。在这种情况下,函数会直接返回存储在termios->c_ospeed字段中的具体数值。
      • CBAUDEX处理: CBAUDEX是用于更高波特率的扩展标志。如果它被设置,cbaud的值需要加上一个偏移量(15)才能得到正确的表索引。
      • 查表: 最终,它使用计算出的cbaud作为索引,在baud_table中查找并返回对应的数值。例如,如果cbaud是13,它就返回baud_table[13],即9600
  3. 编码过程 (tty_termios_encode_baud_rate):

    • 职责: speed_t数值 -> Bxxxx符号。
    • 实现: 这是一个更复杂的过程,因为它需要处理近似匹配
      • 保存数值: termios->c_ispeed = ibaud; termios->c_ospeed = obaud; 首先,它总是将用户请求的精确数值波特率保存在c_ispeedc_ospeed字段中。
      • 模糊搜索: 它遍历整个baud_table。对于每个标准速率,它会检查用户请求的速率(ibaud/obaud)是否落在一个很小的误差范围内 (obaud - oclose <= baud_table[i] && obaud + oclose >= baud_table[i])。
      • 优先标准符号: 如果找到了一个足够接近的标准速率,它就会将对应的Bxxxx符号(来自baud_bits)设置到termios->c_cflag中。这是为了向后兼容,因为许多老的应用程序只认识标准的Bxxxx符号。
      • 回退到BOTHER: 如果在整个表中都没有找到近似的匹配项,函数就会在termios->c_cflag中设置BOTHER标志。
    • 作用: 这个函数的主要调用者是底层驱动程序。当驱动程序发现自己无法精确实现用户请求的波特率,但可以实现一个非常接近的速率时,它会调用这个函数来更新termios结构,以便通过tcgetattr()系统调用将实际使用的速率(包括其对应的Bxxxx符号)报告回用户空间。

代码分析

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/*
* 用于将termios中的Bxxxx符号解码为数值波特率的查找表。
* baud_table存储数值,baud_bits存储对应的符号。
*/
static const speed_t baud_table[] = { /* ... 0, 50, 75, ... */ };
static const tcflag_t baud_bits[] = { /* ... B0, B50, B75, ... */ };

static int n_baud_table = ARRAY_SIZE(baud_table);

/**
* @brief 从termios结构中解码出输出波特率的数值。
* @param termios termios结构体。
* @return speed_t 数值波特率。
*/
speed_t tty_termios_baud_rate(const struct ktermios *termios)
{
unsigned int cbaud;

/* 从c_cflag中屏蔽出与波特率相关的位。 */
cbaud = termios->c_cflag & CBAUD;

/* 如果是BOTHER,表示使用c_ospeed中存储的任意速率。 */
if (cbaud == BOTHER)
return termios->c_ospeed;

/* 如果使用了扩展波特率标志,则调整索引值。 */
if (cbaud & CBAUDEX) {
cbaud &= ~CBAUDEX;
cbaud += 15;
}
/* 使用索引在baud_table中查找并返回数值。 */
return cbaud >= n_baud_table ? 0 : baud_table[cbaud];
}
EXPORT_SYMBOL(tty_termios_baud_rate);

/**
* @brief 从termios结构中解码出输入波特率的数值。
* @param termios termios结构体。
* @return speed_t 数值波特率。
*/
speed_t tty_termios_input_baud_rate(const struct ktermios *termios)
{
/* 输入波特率的符号存储在c_cflag的高位。 */
unsigned int cbaud = (termios->c_cflag >> IBSHIFT) & CBAUD;

/* 如果输入波特率设为B0,则它与输出波特率相同。 */
if (cbaud == B0)
return tty_termios_baud_rate(termios);

/* ... 逻辑与tty_termios_baud_rate相同 ... */
if (cbaud == BOTHER)
return termios->c_ispeed;

if (cbaud & CBAUDEX) {
cbaud &= ~CBAUDEX;
cbaud += 15;
}
return cbaud >= n_baud_table ? 0 : baud_table[cbaud];
}
EXPORT_SYMBOL(tty_termios_input_baud_rate);

/**
* @brief 将数值波特率编码回termios结构中。
* @param termios 要被修改的ktermios结构。
* @param ibaud 输入速度(数值)。
* @param obaud 输出速度(数值)。
*/
void tty_termios_encode_baud_rate(struct ktermios *termios,
speed_t ibaud, speed_t obaud)
{
int i = 0;
int ifound = -1, ofound = -1;
/* 计算一个小的误差容忍范围。 */
int iclose = ibaud/50, oclose = obaud/50;
int ibinput = 0;

/* ... 处理B0(挂断)的特殊情况 ... */

/* 总是将精确的数值速度存储在c_ispeed和c_ospeed中。 */
termios->c_ispeed = ibaud;
termios->c_ospeed = obaud;

/* ... 处理一些为了兼容性的特殊情况 ... */

/* 清除旧的波特率符号位。 */
termios->c_cflag &= ~CBAUD;
termios->c_cflag &= ~(CBAUD << IBSHIFT);

/*
* 遍历波特率表,查找与给定数值近似匹配的标准Bxxxx符号。
*/
do {
/* 检查输出波特率是否在误差范围内。 */
if (obaud - oclose <= baud_table[i] &&
obaud + oclose >= baud_table[i]) {
termios->c_cflag |= baud_bits[i];
ofound = i;
}
/* 检查输入波特率是否在误差范围内。 */
if (ibaud - iclose <= baud_table[i] &&
ibaud + iclose >= baud_table[i]) {
/* ... (处理输入输出速度相同的情况) ... */
ifound = i;
termios->c_cflag |= (baud_bits[i] << IBSHIFT);
}
} while (++i < n_baud_table);

/* 如果没有为输出波特率找到任何近似的标准符号,则设置BOTHER标志。 */
if (ofound == -1)
termios->c_cflag |= BOTHER;
/* 如果输入输出速率不同,且输入速率也找不到标准符号,则为输入设置BOTHER。 */
if (ifound == -1 && (ibaud != obaud || ibinput))
termios->c_cflag |= (BOTHER << IBSHIFT);
}
EXPORT_SYMBOL_GPL(tty_termios_encode_baud_rate);

/**
* @brief 为一个tty设备编码波特率。
* @param tty 终端设备。
* @param ibaud 输入波特率。
* @param obaud 输出波特率。
*/
void tty_encode_baud_rate(struct tty_struct *tty, speed_t ibaud, speed_t obaud)
{
/* 这是一个简单的封装,直接调用核心编码函数。 */
tty_termios_encode_baud_rate(&tty->termios, ibaud, obaud);
}
EXPORT_SYMBOL_GPL(tty_encode_baud_rate);

drivers/tty/tty_buffer.c

tty_buffer_request_room & __tty_insert_flip_string_flags: TTY Flip Buffer的写入核心

本代码片段展示了Linux TTY子系统输入缓冲的核心机制——flip_buffer的写入实现。__tty_buffer_request_room是缓冲区的空间管理器,负责在需要时动态地分配新的内存块。__tty_insert_flip_string_flags则是数据写入器,它使用空间管理器来确保有足够的空间,然后将字符数据和与之对应的标志(如TTY_NORMAL, TTY_PARITY)拷贝到缓冲区中。这些函数是连接底层硬件驱动(如uart_insert_char的调用者)与上层线路规程(line discipline)的数据中转站

实现原理分析

此机制的核心是一个生产者-消费者模型,它使用一个链表式的、分块的缓冲区,并通过内存屏障来实现高效的、近乎无锁的同步。

  1. Flip Buffer 数据结构:

    • tty_port->buf (struct tty_bufhead): 这是缓冲区的“头部”,包含了指向链表头(head)和尾(tail)的指针。
    • tty_buffer: 这是缓冲区的基本内存块,通常大小为一个页面(TTY_BUFFER_PAGE)。它包含一个字符缓冲区和一个可选的标志缓冲区flags成员表示该缓冲区是否包含标志。
    • 生产者: 底层驱动(如stm32_usart_receive_chars_pio)是生产者,负责向缓冲区写入数据。它总是操作buf->tail指向的缓冲区。
    • 消费者: TTY核心的后台工作(tty_port_default_work)是消费者,负责从缓冲区读取数据并递交给线路规程。它总是操作buf->head指向的缓冲区。
  2. 空间管理 (__tty_buffer_request_room):

    • 职责: 确保tail缓冲区中至少有size大小的可用空间。
    • 快速路径: if (!change && left >= size)。如果当前tail缓冲区有足够的剩余空间,并且不需要从“无标志”模式切换到“有标志”模式,则函数直接返回,开销极小。
    • 慢速路径 (分配):
      • tty_buffer_alloc(port, size): 当空间不足时,调用此函数从一个预分配的缓冲池(port->buf.free)中获取一个新的tty_buffer,或者如果池为空,则动态分配一个新的页面。
      • 链表链接: 新分配的缓冲区n成为新的tail。旧的tail缓冲区的next指针被原子地设置为指向n
    • 同步与内存屏障:
      • smp_store_release(&b->commit, b->used): 这是关键的同步点smp_store_release是一个带有“释放”语义的原子写操作。它有两个作用:
        1. 将旧缓冲区bcommit计数更新为其实际使用的大小used
        2. 发布(publish)所有在此操作之前对缓冲区数据(字符和标志)的写入。它确保了消费者在看到更新的commit值时,也一定能看到所有的数据。
      • smp_store_release(&b->next, n): 同样,它原子地更新next指针,并发布这个更新。这确保了消费者在处理完一个缓冲区后,能够安全地看到链表中的下一个缓冲区。
  3. 数据写入 (__tty_insert_flip_string_flags):

    • 职责: 将一个字符串及其标志高效地拷贝到flip_buffer中。
    • 分块拷贝: do { ... } while (unlikely(size > copied))。这个循环处理了需要跨越多个tty_buffer块的大数据写入。
    • 循环体:
      1. goal = min_t(...): 计算本次循环要拷贝的数据量,最大不超过一个缓冲区的大小。
      2. __tty_buffer_request_room(port, goal, ...): 为本次拷贝请求空间。
      3. memcpy(...): 将字符数据和标志数据分别拷贝到char_buf_ptrflag_buf_ptr指向的位置。
      4. tb->used += space;: 更新tail缓冲区的已用空间计数。

代码分析

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/**
* @brief 为tty缓冲区请求空间,在需要时进行扩展。
* @param port tty端口。
* @param size 需要的空间大小。
* @param flags 新缓冲区是否需要标志存储。
* @return int 成功则返回请求的大小,部分成功返回可用大小,失败返回0。
*/
static int __tty_buffer_request_room(struct tty_port *port, size_t size,
bool flags)
{
struct tty_bufhead *buf = &port->buf;
struct tty_buffer *n, *b = buf->tail;
/* 计算当前tail缓冲区的剩余空间。*/
size_t left = (b->flags ? 1 : 2) * b->size - b->used;
/* 检查是否需要从“无标志”切换到“有标志”缓冲区。*/
bool change = !b->flags && flags;

/* 快速路径:如果空间足够且无需切换类型,则直接返回。 */
if (!change && left >= size)
return size;

/* 慢速路径:需要分配新的缓冲区。 */
n = tty_buffer_alloc(port, size);
if (n == NULL)
return change ? 0 : left; /* 分配失败 */

n->flags = flags;
buf->tail = n; /* 将新的缓冲区设置为tail。 */

/*
* 关键同步点1:原子地更新旧缓冲区b的commit计数。
* release语义确保了在此之前对b的数据写入对消费者可见。
*/
smp_store_release(&b->commit, b->used);

/*
* 关键同步点2:原子地更新旧缓冲区b的next指针,将其链接到新缓冲区n。
* release语义确保了消费者在看到这个新链接时,b的commit值也已更新。
*/
smp_store_release(&b->next, n);

return size;
}

/**
* @brief 为tty缓冲区请求空间(强制需要标志)。
* @param port tty端口。
* @param size 需要的空间大小。
* @return int 可用空间大小。
*/
int tty_buffer_request_room(struct tty_port *port, size_t size)
{
return __tty_buffer_request_room(port, size, true);
}
EXPORT_SYMBOL_GPL(tty_buffer_request_room);

/**
* @brief 将一个带标志的字符串插入到flip buffer(核心实现)。
* @param port tty端口。
* @param chars 字符数据缓冲区。
* @param flags 标志数据缓冲区。
* @param mutable_flags flags缓冲区是否与chars缓冲区一样是变化的。
* @param size 要插入的数据大小。
* @return size_t 实际插入的字节数。
*/
size_t __tty_insert_flip_string_flags(struct tty_port *port, const u8 *chars,
const u8 *flags, bool mutable_flags,
size_t size)
{
bool need_flags = mutable_flags || flags[0] != TTY_NORMAL;
size_t copied = 0;

/* 循环处理,以应对数据需要跨越多个buffer块的情况。 */
do {
/* 计算本次循环要拷贝的目标大小。 */
size_t goal = min_t(size_t, size - copied, TTY_BUFFER_PAGE);
/* 请求空间。 */
size_t space = __tty_buffer_request_room(port, goal, need_flags);
struct tty_buffer *tb = port->buf.tail;

/* 如果请求不到任何空间,则中断。 */
if (unlikely(space == 0))
break;

/* 拷贝字符数据。 */
memcpy(char_buf_ptr(tb, tb->used), chars, space);

/* 根据标志类型,拷贝或填充标志数据。 */
if (mutable_flags) {
memcpy(flag_buf_ptr(tb, tb->used), flags, space);
flags += space;
} else if (tb->flags) {
memset(flag_buf_ptr(tb, tb->used), flags[0], space);
} else {
WARN_ON_ONCE(need_flags);
}

/* 更新已用空间计数和已拷贝计数。 */
tb->used += space;
copied += space;
chars += space;

} while (unlikely(size > copied));

return copied;
}
EXPORT_SYMBOL(__tty_insert_flip_string_flags);

/**
* @brief 将一个字符串插入到flip buffer,所有字符使用同一个固定标志。
* @param port tty端口。
* @param chars 字符数据。
* @param flag 应用于所有字符的标志。
* @param size 数据大小。
* @return size_t 实际插入的字节数。
*/
static inline size_t tty_insert_flip_string_fixed_flag(struct tty_port *port,
const u8 *chars, u8 flag,
size_t size)
{
/* 调用核心实现,mutable_flags为false。 */
return __tty_insert_flip_string_flags(port, chars, &flag, false, size);
}

/**
* @brief 将一个普通字符串(所有字符标志为TTY_NORMAL)插入到flip buffer。
* @param port tty端口。
* @param chars 字符数据。
* @param size 数据大小。
* @return size_t 实际插入的字节数。
*/
static inline size_t tty_insert_flip_string(struct tty_port *port,
const u8 *chars, size_t size)
{
return tty_insert_flip_string_fixed_flag(port, chars, TTY_NORMAL, size);
}

tty_buffer_alloc & tty_buffer_free: TTY Flip Buffer的内存池管理

本代码片段展示了Linux TTY flip_buffer机制的内存管理后台。其核心功能是通过tty_buffer_alloctty_buffer_free这一对函数,实现对tty_buffer内存块的高效分配与回收。这个机制采用了一种混合策略:它维护一个小的、无锁的空闲链表(free list),用于快速重用小尺寸的缓冲区,同时在需要更大尺寸或空闲链表为空时,回退到标准的内核内存分配器(kmalloc)。tty_buffer_free_all则是用于在设备关闭时彻底清理所有缓冲区的最终清理函数。

实现原理分析

此机制的设计目标是在保证高性能(特别是在中断上下文中)和内存效率之间取得平衡。

  1. 混合分配策略 (tty_buffer_alloc):

    • 职责: 分配一个新的、大小至少为sizetty_buffer
    • 尺寸对齐: size = __ALIGN_MASK(size, TTYB_ALIGN_MASK); 它首先将请求的尺寸向上对齐到一个合适的边界(通常是256字节的倍数),以减少内存碎片并标准化缓冲区大小。
    • 快速路径 (从空闲链表获取):
      • if (size <= MIN_TTYB_SIZE): 只有当请求的尺寸是小尺寸时,它才会尝试从空闲链表中获取。
      • free = llist_del_first(&port->buf.free);: 这是关键llist是一个无锁单向链表,专为单生产者/单消费者场景设计,但在这里用于多生产者(多个中断)/单消费者(清理函数)场景,llist_del_first是一个原子操作,可以安全地从链表头部移除一个节点,无需任何锁。
      • 优点: 从空闲链表获取缓冲区的速度极快,因为它不涉及调用重量级的kmalloc,并且是无锁的,非常适合在中断上下文中使用。
    • 慢速路径 (从kmalloc分配):
      • 当请求的是大尺寸缓冲区,或者空闲链表为空时,代码会回退到调用kmalloc
      • atomic_read(&port->buf.mem_used) > port->buf.mem_limit: 这是一个内存限额检查,防止单个TTY设备消耗过多的内核内存。
      • GFP_ATOMIC | __GFP_NOWARN: 使用GFP_ATOMIC标志进行分配,这意味着它是一个高性能、不可睡眠的分配请求,适合在中断上下文中使用。
    • 初始化: 无论从哪里获得缓冲区,tty_buffer_reset都会被调用,将其所有成员(used, commit, next等)重置为初始状态。
  2. 回收策略 (tty_buffer_free):

    • 职责: 回收一个不再使用的tty_buffer
    • 策略:
      • if (b->size > MIN_TTYB_SIZE): 如果缓冲区是大尺寸的,则直接调用kfree将其彻底释放回系统。
      • else if (b->size > 0): 如果是小尺寸的,则调用llist_add(&b->free, &buf->free)将其放回空闲链表的头部,以备下次快速分配。
    • 这个简单的策略旨在平衡内存占用和分配性能:频繁使用的小缓冲区被缓存起来,而不常用的大缓冲区则及时释放,避免占用过多内存。
  3. 最终清理 (tty_buffer_free_all):

    • 职责: 在TTY端口被销毁时,释放所有与之关联的缓冲区,包括正在使用的数据链表和空闲链表。
    • 实现:
      • 遍历buf->head链表,逐个kfree掉所有包含数据的缓冲区。
      • llist_del_all(&buf->free): 原子地将整个空闲链表取下。
      • 遍历这个取下的链表,逐个kfree掉所有空闲的缓冲区。
      • WARN(...): 最后,它会检查内存使用量的原子计数器mem_used是否归零,这是一个健全性检查,用于在调试时发现内存泄露。

代码分析

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/**
* @brief 重置一个tty_buffer到其初始状态。
* @param p 要重置的缓冲区。
* @param size 新的缓冲区大小。
*/
static void tty_buffer_reset(struct tty_buffer *p, size_t size)
{
p->used = 0;
p->size = size;
p->next = NULL;
p->commit = 0;
p->lookahead = 0;
p->read = 0;
p->flags = true;
}

/**
* @brief 释放一个tty端口使用的所有缓冲区。
* @param port 要释放其缓冲区的tty端口。
* @details 移除所有数据缓冲区和空闲链表中的缓冲区。
*/
void tty_buffer_free_all(struct tty_port *port)
{
struct tty_bufhead *buf = &port->buf;
struct tty_buffer *p, *next;
struct llist_node *llist;
/* ... */

/* 遍历并释放数据链表中的所有缓冲区。 */
while ((p = buf->head) != NULL) {
buf->head = p->next;
freed += p->size;
if (p->size > 0)
kfree(p);
}
/* 原子地移除整个空闲链表。 */
llist = llist_del_all(&buf->free);
/* 遍历并释放空闲链表中的所有缓冲区。 */
llist_for_each_entry_safe(p, next, llist, free)
kfree(p);

/* 重置sentinel节点,并将head/tail指向它,形成一个空的循环链表。 */
tty_buffer_reset(&buf->sentinel, 0);
buf->head = &buf->sentinel;
buf->tail = &buf->sentinel;

/* 检查内存使用计数是否归零,以发现内存泄露。 */
still_used = atomic_xchg(&buf->mem_used, 0);
WARN(still_used != freed, "仍有 %d 字节未被释放!",
still_used - freed);
}

/**
* @brief 分配一个tty缓冲区。
* @param port tty端口。
* @param size 期望的大小(字节)。
* @return struct tty_buffer* 成功则返回缓冲区指针,失败返回NULL。
*/
static struct tty_buffer *tty_buffer_alloc(struct tty_port *port, size_t size)
{
struct llist_node *free;
struct tty_buffer *p;

/* 将请求大小向上对齐。 */
size = __ALIGN_MASK(size, TTYB_ALIGN_MASK);

/* 快速路径:如果请求的是小尺寸缓冲区... */
if (size <= MIN_TTYB_SIZE) {
/* ...尝试从无锁的空闲链表中原子地获取一个。 */
free = llist_del_first(&port->buf.free);
if (free) {
p = llist_entry(free, struct tty_buffer, free);
goto found;
}
}

/* 慢速路径:空闲链表为空,或请求的是大尺寸缓冲区。 */
/* 检查是否超过了内存使用限额。 */
if (atomic_read(&port->buf.mem_used) > port->buf.mem_limit)
return NULL;
/* 使用GFP_ATOMIC进行不可睡眠的内存分配。*/
p = kmalloc(struct_size(p, data, 2 * size), GFP_ATOMIC | __GFP_NOWARN);
if (p == NULL)
return NULL;

found:
/* 重置新分配的缓冲区。 */
tty_buffer_reset(p, size);
/* 原子地增加内存使用计数。 */
atomic_add(size, &port->buf.mem_used);
return p;
}

/**
* @brief 释放一个tty缓冲区。
* @param port 拥有该缓冲区的tty端口。
* @param b 要释放的缓冲区。
*/
static void tty_buffer_free(struct tty_port *port, struct tty_buffer *b)
{
struct tty_bufhead *buf = &port->buf;

/* 原子地减少内存使用计数。 */
WARN_ON(atomic_sub_return(b->size, &buf->mem_used) < 0);

/* 策略:大尺寸缓冲区直接kfree释放回系统。 */
if (b->size > MIN_TTYB_SIZE)
kfree(b);
/* 小尺寸缓冲区则放回无锁的空闲链表中,以备重用。 */
else if (b->size > 0)
llist_add(&b->free, &buf->free);
}

flush_to_ldisc & tty_flip_buffer_push: TTY Flip Buffer的数据消费与调度

本代码片段展示了Linux TTY flip_buffer机制的后半部分——即消费者(Consumer)的实现。其核心功能是通过flush_to_ldisc这个工作队列(workqueue)处理函数,flip_buffer中读取由驱动程序(生产者)放入的数据,并将其递交给上层的线路规程(line discipline)进行处理(如行编辑、信号生成等)。tty_flip_buffer_push则是生产者用来触发这个消费过程的API。

实现原理分析

此机制是flip_buffer生产者-消费者模型的核心调度逻辑,它通过工作队列实现了从硬中断上下文进程上下文的异步工作转移,并通过内存屏障确保了数据的安全传递。

  1. 消费者 (flush_to_ldisc):

    • 职责: 作为flip_buffer唯一消费者,负责读取数据并将其“冲刷”(flush)到线路规程。
    • 上下文: 它是一个work_struct的处理函数,这意味着它总是在一个内核线程(进程上下文)中执行。这非常重要,因为它允许在处理数据时安全地睡眠(例如,如果线路规程需要等待锁)。
    • 单线程保证: mutex_lock(&buf->lock)确保了在任何时刻,只有一个CPU核心上的一个线程在执行flush_to_ldisc的逻辑,即消费者是单线程的,这极大地简化了缓冲区的读取逻辑。
    • 读取循环 (while (1)):
      1. 获取数据量: count = smp_load_acquire(&head->commit) - head->read; 这是关键的同步点smp_load_acquire是一个带有“获取”语义的原子读操作。它确保在读取commit计数的同时,也“获取”了生产者通过smp_store_release发布的所有数据commit是生产者写入的数据量,read是消费者已读取的数据量,差值就是新数据的量。
      2. 空缓冲区处理: 如果count为0,表示当前head缓冲区的数据已被处理完毕。代码会尝试将buf->head指针移动到链表中的下一个缓冲区,并释放掉旧的head
      3. 递交线路规程: rcvd = receive_buf(port, head, count); 这是数据递交的核心。receive_buf(代码未显示,通常指向tty_ldisc_receive_buf)会调用当前TTY线路规程的.receive_buf方法,将数据块和标志块的指针以及count传递给它。
      4. head->read += rcvd;: 更新已读计数。
      5. cond_resched(): 在处理完一个数据块后,主动调用调度器,检查是否有更高优先级的任务需要运行。这防止了在处理大量数据时,flush_to_ldisc长时间独占CPU,保证了系统的响应性。
  2. 生产者触发器 (tty_flip_buffer_push):

    • 职责: 由生产者(如串口驱动的ISR或DMA完成回调)调用,用于通知消费者“有新数据了,请来处理”。
    • 实现: 这是一个两步过程。
      1. tty_flip_buffer_commit(buf->tail): 发布数据。它调用smp_store_release(&tail->commit, tail->used),原子地更新tail缓冲区的commit计数。这个release操作与flush_to_ldisc中的acquire操作配对,构成了一个内存屏障,确保了数据在被消费者看到之前,已经完全写入内存。
      2. queue_work(system_unbound_wq, &buf->work): 调度消费者。它将buf->work(在tty_buffer_init中被初始化为指向flush_to_ldisc)提交到系统的工作队列中。内核的工作队列子系统会在稍后的某个时间点,在一个内核线程中执行flush_to_ldisc函数。
  3. 初始化 (tty_buffer_init):

    • 它负责建立整个flip_buffer的初始状态,包括:
      • 初始化互斥锁和无锁空闲链表。
      • 创建一个“哨兵”(sentinel)节点,使得headtail指针永远不会为NULL,简化了链表操作。
      • INIT_WORK(&buf->work, flush_to_ldisc): buf->workflush_to_ldisc函数绑定,为后续的queue_work调用做好准备。

代码分析

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
static size_t
receive_buf(struct tty_port *port, struct tty_buffer *head, size_t count)
{
u8 *p = char_buf_ptr(head, head->read);
const u8 *f = NULL;
size_t n;

if (head->flags)
f = flag_buf_ptr(head, head->read);

n = port->client_ops->receive_buf(port, p, f, count);
if (n > 0)
memset(p, 0, n);
return n;
}

/**
* @brief 将flip buffer中的数据冲刷(flush)到线路规程(ldisc)。
* @param work 指向tty_port->buf.work的工作队列结构。
* @details 这是flip buffer的“消费者”线程。
*/
static void flush_to_ldisc(struct work_struct *work)
{
struct tty_port *port = container_of(work, struct tty_port, buf.work);
struct tty_bufhead *buf = &port->buf;

/* 锁定,确保在任何时刻只有一个消费者线程在运行。 */
mutex_lock(&buf->lock);

while (1) {
struct tty_buffer *head = buf->head;
struct tty_buffer *next;
size_t count, rcvd;

/* ... 如果有更高优先级的访问者,则暂时退出 ... */

/*
* 同步点1:以acquire语义读取next指针。
* 确保在head指针前移之前,能看到旧head的commit值。
*/
next = smp_load_acquire(&head->next);
/*
* 同步点2:以acquire语义读取commit计数。
* 确保能看到所有由生产者release发布的数据。
*/
count = smp_load_acquire(&head->commit) - head->read;

/* 如果当前缓冲区已处理完毕... */
if (!count) {
if (next == NULL) /* 如果没有下一个缓冲区,则工作完成。 */
break;
buf->head = next; /* 前移head指针。 */
tty_buffer_free(port, head); /* 释放旧的head缓冲区。 */
continue;
}

/* 调用线路规程的接收函数,将数据递交给上层。 */
rcvd = receive_buf(port, head, count);
/* 更新已读计数。 */
head->read += rcvd;
/* ... (处理线路规程节流的情况) ... */
if (!rcvd)
break;

/* 主动让出CPU,防止长时间独占。 */
cond_resched();
}

mutex_unlock(&buf->lock);
}

/**
* @brief 原子地提交(发布)tail缓冲区的写入。
* @param tail 指向tail缓冲区的指针。
*/
static inline void tty_flip_buffer_commit(struct tty_buffer *tail)
{
/*
* 关键同步点:以release语义更新commit计数。
* 这会“发布”所有在此之前对tail缓冲区数据的写入,
* 使其对消费者的acquire操作可见。
*/
smp_store_release(&tail->commit, tail->used);
}

/**
* @brief 推送终端缓冲区的数据。
* @param port 要推送的tty端口。
* @details 这是由生产者(如驱动ISR)调用的API,用于触发消费者线程。
*/
void tty_flip_buffer_push(struct tty_port *port)
{
struct tty_bufhead *buf = &port->buf;

/* 步骤1:发布所有已写入的数据。 */
tty_flip_buffer_commit(buf->tail);
/* 步骤2:将消费者任务(flush_to_ldisc)提交到系统工作队列。 */
queue_work(system_unbound_wq, &buf->work);
}
EXPORT_SYMBOL(tty_flip_buffer_push);

/* ... (tty_insert_flip_string_and_push_buffer - 插入并推送的组合函数) ... */
/**
* tty_insert_flip_string_and_push_buffer - add characters to the tty buffer and
* push
* @port: tty port
* @chars: characters
* @size: size
*
* The function combines tty_insert_flip_string() and tty_flip_buffer_push()
* with the exception of properly holding the @port->lock.
*
* To be used only internally (by pty currently).
*
* Returns: the number added.
*/
int tty_insert_flip_string_and_push_buffer(struct tty_port *port,
const u8 *chars, size_t size)
{
struct tty_bufhead *buf = &port->buf;
unsigned long flags;

spin_lock_irqsave(&port->lock, flags);
size = tty_insert_flip_string(port, chars, size);
if (size)
tty_flip_buffer_commit(buf->tail);
spin_unlock_irqrestore(&port->lock, flags);

queue_work(system_unbound_wq, &buf->work);

return size;
}


/* * 翻转缓冲区的字节阈值,用于限制内存消耗。 * 实际内存限制 > 此数值的 2 倍。 */
#define TTYB_DEFAULT_MEM_LIMIT (640 * 1024UL)

/**
* @brief 初始化一个tty的缓冲区管理结构。
* @param port 要初始化的tty端口。
*/
void tty_buffer_init(struct tty_port *port)
{
struct tty_bufhead *buf = &port->buf;

mutex_init(&buf->lock); /* 初始化消费者锁 */
/* ... 初始化sentinel节点和head/tail指针 ... */
init_llist_head(&buf->free); /* 初始化无锁空闲链表 */
atomic_set(&buf->mem_used, 0); /* 初始化内存使用计数 */
/* ... */
/* 将work结构与flush_to_ldisc处理函数绑定。 */
INIT_WORK(&buf->work, flush_to_ldisc);
buf->mem_limit = TTYB_DEFAULT_MEM_LIMIT; /* 设置内存限额 */
}