TEE-TA学习轨迹第十篇:tee_supplicant守护进程分析
·
tee_supplicant.c 源码执行流程完整分析
我们按照 程序启动 → 初始化配置 → 设备就绪 → 主循环接收请求 → 请求解析分发 → 功能处理 → 返回响应的完整执行时序,结合源码逐段拆解流程,并对应到之前调试遇到的 TA 加载问题。
一、程序入口:main 函数初始化全流程
程序从 main() 启动,整体分为 参数解析 → 路径初始化 → 插件加载 → 守护进程化 → 设备打开 → 就绪通知 → 进入主循环 7 个阶段。
1. 命令行参数解析
static struct option long_options[] = {
/* long name | has argument | flag | short value */
{ "help", no_argument, 0, 'h' },
{ "daemonize", no_argument, 0, 'd' },
{ "fs-parent-path", required_argument, 0, 'f' },
{ "ta-path", required_argument, 0, 'l' },
{ "ta-dir", required_argument, 0, 't' },
{ "plugin-path", required_argument, 0, 'p' },
{ "rpmb-cid", required_argument, 0, 'r' },
{ 0, 0, 0, 0 }
};
while ((opt = getopt_long(argc, argv, "hdf:l:t:p:r:",
long_options, &long_index )) != -1) {
switch (opt) {
case 'h' :
return usage(EXIT_SUCCESS);
break;
case 'd':
daemonize = true;
break;
case 'f':
supplicant_params.fs_parent_path = optarg;
break;
case 'l':
supplicant_params.ta_load_path = optarg;
break;
case 't':
supplicant_params.ta_dir = optarg;
break;
case 'p':
supplicant_params.plugin_load_path = optarg;
break;
case 'r':
supplicant_params.rpmb_cid = optarg;
break;
default:
return usage(EXIT_FAILURE);
}
}
- 用 getopt_long 解析长短参数,所有配置写入全局结构体 supplicant_params;
- 核心配置项:TA 加载路径、安全文件系统根路径、插件路径、RPMB 设备 CID、是否后台守护运行;
- 兼容性处理:--ta-dir 是旧版参数,和 --ta-path 互斥,同时使用直接报错退出。
2. TA 加载路径初始化:set_ta_path()
之前调试过程出现「TA not found」的核心关联逻辑:
static void set_ta_path(void) {
// 1. 拆分冒号分隔的多路径,统计路径数量
// 2. 分配指针数组
// 3. 如果是旧版 ta-dir 模式:路径 = 基础路径 + / + ta_dir
// 如果是新版 ta-path 模式:直接使用传入的完整路径
// 4. 所有路径存入全局 char **ta_path 数组
char *ta_path_str = NULL;
char *p = NULL;
char *saveptr = NULL;
char *new_path = NULL;
size_t n = 0;
const char *path = supplicant_params.ta_load_path;
int path_len = -1;
if (!path)
path = TEEC_LOAD_PATH;
ta_path_str = strdup(path);
if (!ta_path_str)
goto err;
p = ta_path_str;
while (strtok_r(p, ":", &saveptr)) {
p = NULL;
n++;
}
n++; /* NULL terminator */
ta_path = calloc(n, sizeof(char *));
if (!ta_path)
goto err;
n = 0;
strcpy(ta_path_str, path);
p = ta_path_str;
while ((new_path = strtok_r(p, ":", &saveptr))) {
if (!supplicant_params.ta_load_path) {
char full_path[PATH_MAX] = { 0 };
path_len = snprintf(full_path, PATH_MAX, "%s/%s", new_path,
supplicant_params.ta_dir);
if (path_len < 0 || path_len >= PATH_MAX)
goto err_path;
ta_path[n++] = strdup(full_path);
} else {
path_len = strnlen(new_path, PATH_MAX);
if (path_len == PATH_MAX)
goto err_path;
ta_path[n++] = strdup(new_path);
}
p = NULL;
}
free(ta_path_str);
return;
err:
EMSG("out of memory");
exit(EXIT_FAILURE);
err_path:
EMSG("Path exceeds maximum path length");
exit(EXIT_FAILURE);
}
- 支持多路径搜索,加载 TA 时会按顺序遍历所有目录;
- 关键设计:后续加载 TA 时,会用 TA 的 UUID 字符串拼接文件名,在这些路径下查找 .ta 文件——这就是为什么 TA 文件必须以 UUID 命名,而不能自定义成 example_ta.ta。
3. 插件加载与守护进程化
if (plugin_load_all() != 0) { // 加载所有动态插件,扩展功能
EMSG("failed to load plugins");
exit(EXIT_FAILURE);
}
if (daemonize) {
if (pipe(pipefd) < 0) {
EMSG("pipe(): %s", strerror(errno));
exit(EXIT_FAILURE);
}
e = make_daemon(pipefd); // fork 子进程,脱离终端,重定向标准IO
if (e < 0) {
EMSG("make_daemon(): %d", e);
exit(EXIT_FAILURE);
}
}
- make_daemon 是自定义的守护进程实现:父进程通过管道等待子进程初始化完成(打开设备成功)后再退出,保证服务启动可靠性;
- 子进程执行 setsid、chdir("/")、重定向 stdin/stdout/stderr 到 /dev/null,符合标准守护进程规范。
4. 打开特权设备节点
if (dev) {
arg.fd = open_dev(dev, &arg.gen_caps); // 指定设备名
} else {
arg.fd = get_dev_fd(&arg.gen_caps); // 自动遍历 /dev/teepriv0 ~ 9
}
关键细节
- 打开的是 /dev/teepriv*,而不是 CA 用的 /dev/tee*:
- /dev/tee0:普通 CA 程序使用,权限低,只能调用 TA;
- /dev/teepriv0:supplicant 专用特权设备,权限高,能处理 RPC、共享内存管理、TA 加载等内核级请求。
- open_dev 内部调用 TEE_IOC_VERSION ioctl,校验必须是 OP-TEE 实现,同时获取设备能力 gen_caps(比如是否支持注册式共享内存)。
5. 就绪通知与进入主循环
sd_notify_ready(); // 通知 systemd 服务已就绪
if (daemonize)
write(pipefd[1], "", 1); // 唤醒父进程退出
while (!arg.abort) {
if (!process_one_request(&arg))
arg.abort = true;
}
- systemd 集成:支持 systemd 的服务状态通知,符合现代 Linux 服务规范;
- 主循环:持续调用 process_one_request 处理 RPC 请求,出错则置位 abort 退出循环。
二、核心主循环:单条 RPC 请求的完整处理流程
process_one_request() 是整个程序的核心调度函数,每调用一次处理一条来自安全世界的 RPC 请求,完整流程分为 6 步。
第1步:准备缓冲区,阻塞等待请求
request.recv.num_params = RPC_NUM_PARAMS;
params = (struct tee_ioctl_param *)(&request.send + 1);
params->attr = TEE_IOCTL_PARAM_ATTR_META;
num_waiters_inc(arg);
if (!read_request(arg->fd, &request))
return false;
- 初始化请求/响应共用缓冲区 union tee_rpc_invoke,最多支持 5 个参数;
- 标记支持元参数(用于并发线程调度);
- 增加「空闲等待线程数」计数器;
- read_request 是阻塞调用:通过 TEE_IOC_SUPPL_RECV ioctl 陷入内核,安全世界没有发起 RPC 时,线程会休眠挂起;当安全世界发起 RPC 回调时,内核唤醒线程,返回请求数据。
第2步:解析请求参数
if (!find_params(&request, &func, &num_params, ¶ms, &num_meta))
return false;
find_params 做三件事:
- 跳过开头的元参数(meta param),提取真正的业务参数;
- 取出 RPC 命令号 func、参数数量 num_params、参数数组指针 params;
- 校验:元参数只能出现在参数列表最前面,不能夹杂在业务参数中间,格式非法直接返回失败。
第3步:并发调度:按需创建工作线程
if (num_meta && !num_waiters_dec(arg) && !spawn_thread(arg))
return false;
这是 OP-TEE supplicant 的 动态线程模型核心设计:
- 如果请求带元参数,说明这是一个可异步处理的标准请求;
- 先把当前等待线程数减 1(因为当前线程马上要去处理这个请求了);
- 如果减完后等待线程数为 0,说明没有空闲线程了,就调用 spawn_thread 新建一个工作线程,继续等待新请求;
- 当前线程继续处理本条请求,处理完后回到等待状态。
设计目的:按需创建线程,平时只有一个主线程等待,并发上来时动态扩容,兼顾资源占用和并发能力。
第4步:命令分发:switch 匹配处理函数
根据 RPC 命令号 func 分发到对应业务处理函数,这是所有功能的入口:
switch (func) {
case OPTEE_MSG_RPC_CMD_LOAD_TA:
ret = load_ta(num_params, params);
break;
case OPTEE_MSG_RPC_CMD_FS:
ret = tee_supp_fs_process(num_params, params);
break;
case OPTEE_MSG_RPC_CMD_RPMB:
ret = process_rpmb(num_params, params);
break;
case OPTEE_MSG_RPC_CMD_SHM_ALLOC:
ret = process_alloc(arg, num_params, params);
break;
case OPTEE_MSG_RPC_CMD_SHM_FREE:
ret = process_free(num_params, params);
break;
case OPTEE_MSG_RPC_CMD_GPROF:
ret = prof_process(num_params, params, "gmon-");
break;
case OPTEE_MSG_RPC_CMD_SOCKET:
ret = tee_socket_process(num_params, params);
break;
case OPTEE_MSG_RPC_CMD_FTRACE:
ret = prof_process(num_params, params, "ftrace-");
break;
case OPTEE_MSG_RPC_CMD_PLUGIN:
ret = plugin_process(num_params, params);
break;
default:
EMSG("Cmd [0x%" PRIx32 "] not supported", func);
/* Not supported. */
ret = TEEC_ERROR_NOT_SUPPORTED;
break;
}
所有处理函数执行完成后,都返回标准 TEEC 错误码。
第5步:写入返回值,发送响应
request.send.ret = ret;
return write_response(arg->fd, &request);
- 把处理结果写入响应结构体的 ret 字段;
- write_response 通过 TEE_IOC_SUPPL_SEND ioctl 把响应数据发回内核,内核再转交安全世界;
- 本次请求处理完成,线程回到循环开头,继续等待下一条请求。
三、核心功能模块源码流程详解
1. TA 加载流程:load_ta()(对应之前调试的 TA not found 报错)
这是你调试中最核心的函数,完整执行步骤:
static uint32_t load_ta(size_t num_params, struct tee_ioctl_param *params)
{
// 1. 参数校验:必须2个参数,第0个是value(存UUID),第1个是memref(存输出缓冲区)
if (num_params != 2 || get_value(num_params, params, 0, &val_cmd) ||
get_param(num_params, params, 1, &shm_ta))
return TEEC_ERROR_BAD_PARAMETERS;
// 2. 把字节数组格式的UUID转换成TEEC_UUID结构体
uuid_from_octets(&uuid, (void *)val_cmd);
// 3. 核心:从文件系统加载TA二进制
size = shm_ta.size;
ta_found = TEECI_LoadSecureModule(&uuid, shm_ta.buffer, &size);
// 4. 处理结果
if (ta_found != TA_BINARY_FOUND) {
EMSG(" TA not found");
return TEEC_ERROR_ITEM_NOT_FOUND;
}
MEMREF_SIZE(params + 1) = size;
// 5. 缓冲区协商:如果缓冲区不够,返回SHORT_BUFFER并带出所需大小
if (shm_ta.buffer && size > shm_ta.size)
return TEEC_ERROR_SHORT_BUFFER;
return TEEC_SUCCESS;
}
对应我们调试的报错
- 安全世界调用 sys_open_ta_bin 发起 RPC
- supplicant 执行 load_ta
- TEECI_LoadSecureModule 遍历 ta_path 路径,用 UUID 拼接文件名 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.ta 查找文件
- 找不到就返回 TA not found,错误码 0xffff0008 逐层传回 CA。
这就是为什么把文件命名为 example_ta.ta 会加载失败——底层是严格按 UUID 字符串匹配文件名的。
2. 共享内存分配:process_alloc()
static uint32_t process_alloc(struct thread_arg *arg, ...)
{
// 1. 取出请求的内存大小
get_value(num_params, params, 0, &val);
// 2. 根据设备能力选择分配模式
if (arg->gen_caps & TEE_GEN_CAP_REG_MEM)
shm = register_local_shm(arg->fd, val->b); // 注册模式:用户态分配+内核注册
else
shm = alloc_shm(arg->fd, val->b); // 分配模式:内核分配+用户态mmap
// 3. 存入全局链表管理,返回shm ID给安全世界
val->c = shm->id;
push_tshm(shm);
return TEEC_SUCCESS;
}
- 共享内存由全局单链表 shm_head 统一管理,每个 SHM 有唯一 ID,安全世界只通过 ID 引用;
- 两种模式本质都是让同一块物理内存同时映射到安全世界和用户态,实现零拷贝数据交互。
3. 其他代理功能的统一模式
- 文件系统代理(FS):安全世界发起文件读写请求 → supplicant 收到后执行对应的 open/read/write/unlink 系统调用 → 结果通过共享内存返回;
- RPMB 代理:安全世界发 RPMB 读写帧 → supplicant 调用底层 RPMB 驱动交互 → 返回响应帧;
- Socket 代理:安全世界需要联网 → supplicant 创建 socket、执行 connect/send/recv → 透传数据。
核心设计思想:安全世界只定义请求协议,所有和 Linux 系统资源的交互,全部由 supplicant 在用户态代理完成,安全世界不需要也不能直接操作系统调用。
四、多线程模型与线程安全
1. 工作线程入口:thread_main()
static void *thread_main(void *a) {
num_waiters_dec(arg); // 抵消spawn_thread里提前加的计数
while (!arg->abort) {
if (!process_one_request(arg))
arg->abort = true;
}
return NULL;
}
每个新建的工作线程,和主线程一样,都是循环调用 process_one_request,抢着处理请求,本质是「自发自调度的线程池」。
2. 线程安全保障
- 共享内存链表操作(push/pop/find)全部加 shm_mutex 互斥锁;
- 等待线程计数器 num_waiters 操作全部加 arg->mutex 互斥锁;
- 所有锁操作都做了错误检查,加锁失败直接终止进程,避免死锁和数据竞争。
五、完整调用链路总结(以 TA 加载为例)
结合之前的调试场景,一次 TA 加载的全链路是:
- CA 调用 TEEC_OpenSession → 内核 OP-TEE 驱动 → 触发 SMC 进入安全世界;
- 安全世界发现 TA 未加载,发起 LOAD_TA 类型的 RPC 请求,陷入 REE;
- Linux 内核收到 RPC,唤醒阻塞在 TEE_IOC_SUPPL_RECV 上的 tee-supplicant 线程;
- tee-supplicant 解析请求,执行 load_ta,按 UUID 在文件系统中查找 .ta 文件;
- 找到后读取 TA 内容到共享内存,返回成功;找不到则返回 ITEM_NOT_FOUND;
- 响应通过内核传回安全世界,安全世界校验签名、加载 TA,继续完成会话打开;
- 最终结果逐层返回,CA 收到成功或 0xffff0008 错误。
整条链路中,tee-supplicant 就是安全世界伸向普通世界的 代理手脚,所有需要访问 Linux 资源的操作,都要经过它中转。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)