TEE-TA学习轨迹第十篇:tee_supplicant守护进程分析

发布时间:2026/7/2 2:33:53
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/teepriv0supplicant 专用特权设备权限高能处理 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 发起 RPCsupplicant 执行 load_taTEECI_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 请求陷入 REELinux 内核收到 RPC唤醒阻塞在 TEE_IOC_SUPPL_RECV 上的 tee-supplicant 线程tee-supplicant 解析请求执行 load_ta按 UUID 在文件系统中查找 .ta 文件找到后读取 TA 内容到共享内存返回成功找不到则返回 ITEM_NOT_FOUND响应通过内核传回安全世界安全世界校验签名、加载 TA继续完成会话打开最终结果逐层返回CA 收到成功或 0xffff0008 错误。整条链路中tee-supplicant 就是安全世界伸向普通世界的代理手脚所有需要访问 Linux 资源的操作都要经过它中转。