4. Linux TPM子系统架构

Linux TPM 子系统位于 drivers/char/tpm/,它把不同厂商、不同总线(LPC、SPI、I2C、CRB 等)的 TPM 设备抽象成统一的内核对象,再通过字符设备与 sysfs 向用户空间暴露能力。理解这一层的四个关键对象——tpm_class、struct tpm_chip、tpm_transmit() 以及字符设备/sysfs 接口——是后续编写应用、调试驱动、排查权限问题的前提。

一、核心原理:四层抽象与一条主路径

TPM 子系统采用“类—设备—传输—接口”的分层设计。tpm_class 是设备模型中的 class,用于把 TPM 设备归类,并在 /sys/class/tpm/ 下生成统一视图;struct tpm_chip 是每个 TPM 实例的核心数据结构,保存设备号、版本、厂商 ID、超时、命令缓冲区、tpm_transmit 回调等;tpm_transmit() 是内核内部发送 TPM 命令的公共入口,负责加锁、分片、重试与超时;字符设备 /dev/tpm0 与 /dev/tpmrm0 则把命令收发能力交给用户空间。

一条典型的命令路径是:用户空间写 /dev/tpm0 → 字符设备 file_operations → tpm_transmit() → 具体驱动(如 tpm_tis、tpm_crb)→ 硬件寄存器/FIFO → 返回响应。理解这条路径,就能定位“命令发不出去”“返回超时”“权限被拒”等问题发生在哪一层。

Linux TPM 子系统架构与命令路径 用户空间 tpm2-tools / tss2 /dev/tpm0 /dev/tpmrm0 /sys/class/tpm/ 内核接口层 字符设备 tpm_fops / tpmrm_fops sysfs 属性 caps / pcrs / timeouts tpm_class /sys/class/tpm/tpmN tpm_chip 核心对象 传输层 tpm_transmit() / tpm_transmit_cmd() 加锁 · 分片 · 重试 · 超时 · 响应校验 驱动层 tpm_tis tpm_crb tpm_spi tpm_i2c

二、tpm_chip:每个 TPM 实例的“身份证”

struct tpm_chip 定义在 include/linux/tpm.h,是驱动注册时最重要的对象。它包含设备号 dev、厂商与版本信息、命令/响应缓冲区、超时参数、ops 回调集合以及 flags 状态位。驱动通过 tpm_chip_alloc() 分配,填充 ops 后调用 tpm_chip_register() 注册,内核会自动创建字符设备与 sysfs 节点。

struct tpm_chip {
    struct device dev;
    struct cdev cdev;
    unsigned int flags;
    u8 nr_commands;
    struct tpm_class_ops *ops;
    struct tpm_bios_log log;
    u32 timeout_a, timeout_b, timeout_c, timeout_d;
    u8 *data_buffer;
    ...
};

其中 ops 是驱动与子系统之间的契约,典型成员包括 send、recv、status、req_cancel、req_complete_mask 等。只要实现这些回调,任何总线上的 TPM 都能接入统一框架。

三、tpm_transmit:命令收发的公共入口

tpm_transmit() 是内核内部发送命令的核心函数,它不直接操作硬件,而是调用 chip->ops->send() 与 recv()。其关键职责包括:对 chip->tpm_mutex 加锁,保证同一时刻只有一个命令在途;根据 TPM_HEADER_SIZE 与 chip->data_buffer 处理分片;按 timeout_a/b/c/d 设置等待时间;对响应做长度与标签校验。用户空间通过字符设备写入命令后,最终都会汇聚到这里。

关键点: tpm_transmit() 是“内核内部”的公共入口,用户空间不能直接调用它,但所有 /dev/tpm0 的读写都会经过它。调试时若看到 tpm_transmit: tpm_send: error -62,说明命令已到达传输层,但硬件或驱动返回了错误。

四、字符设备与 sysfs 接口概览

TPM 子系统向用户空间提供两类主要接口:字符设备与 sysfs。字符设备 /dev/tpm0 提供原始命令通道,适合 tpm2-tools 等工具直接发送二进制命令;/dev/tpmrm0 是内核资源管理器,负责内核侧上下文管理,避免多进程争用。sysfs 则位于 /sys/class/tpm/tpmN/,用于读取能力、PCR、超时等只读信息。

接口 路径/对象 典型用途 读写属性
字符设备 /dev/tpm0 发送原始 TPM 命令,tpm2-tools 默认通道 读写
资源管理器 /dev/tpmrm0 内核管理上下文,多进程安全使用 读写
sysfs 能力 /sys/class/tpm/tpm0/caps 查看厂商、版本、命令数量 只读
sysfs PCR /sys/class/tpm/tpm0/pcrs 读取 PCR 摘要,校验度量值 只读
sysfs 超时 /sys/class/tpm/tpm0/timeouts 查看 timeout_a/b/c/d 参数 只读

五、案例与场景:从命令到硬件的完整链路

假设使用 tpm2_pcrread 读取 PCR0。用户空间打开 /dev/tpm0,写入 TPM2_PCR_Read 命令;字符设备 tpm_fops 的 write 回调把数据交给 tpm_transmit();后者加锁后调用 tpm_tis_send(),把命令写入 TIS 寄存器;硬件返回响应后,tpm_tis_recv() 读回数据,tpm_transmit() 校验并返回;用户空间 read 得到 PCR 值。若权限不足,会在打开设备时返回 EACCES;若 TPM 被禁用,则 tpm_transmit() 返回 -ENODEV。

实操建议: 排查时先看 dmesg | grep -i tpm 确认驱动是否绑定;再用 ls -l /dev/tpm* 检查设备节点;最后用 cat /sys/class/tpm/tpm0/caps 确认能力。三步即可判断问题在驱动、设备节点还是权限层。

六、误区与复盘

  • 误区一:认为 /dev/tpm0 可以直接被多个进程并发写。实际上原始字符设备没有内核上下文管理,多进程并发会导致命令交错,应使用 /dev/tpmrm0。
  • 误区二:把 tpm_transmit() 当成用户空间 API。它是内核内部函数,用户空间只能通过字符设备或 sysfs 间接使用。
  • 误区三:忽略 tpm_chip 的 ops 差异。不同总线驱动的 send/recv 实现不同,超时和分片行为也可能不同,调试时需结合具体驱动。
  • 复盘要点:先确认 tpm_chip 是否注册成功,再确认字符设备是否创建,最后确认 tpm_transmit() 的返回值。这条链路能把大部分 TPM 集成问题定位到具体层。

掌握 tpm_class、tpm_chip、tpm_transmit() 以及字符设备与 sysfs 接口,就掌握了 Linux TPM 子系统的骨架。后续无论是编写 tpm2-tools 脚本、调试 SPI TPM 驱动,还是做系统集成中的权限与并发设计,都可以沿着这条“用户空间 → 字符设备 → 传输层 → 驱动 → 硬件”的主路径逐层分析。