
UVM Part 1 用户态接口首篇。适用代码kernel-open/nvidia-uvm/关联源码uvm.c、uvm_fd_type.h、uvm_fd_type.c、uvm_ioctl.h、uvm_api.h1. 本篇要回答的问题前两篇讲的是内核里有哪些结构。本篇回到最上游——用户态请求是怎么进内核的CUDA 打开/dev/nvidia-uvm后一个struct file承载什么状态UVM_*ioctl 是如何被分发到具体uvm_api_*处理函数的为什么一个 fd 要区分UVM_FD_VA_SPACE/UVM_FD_MM等类型初始化为什么要做原子 CAS2. 代码地图关注点符号位置设备文件操作表uvm_fopsuvm.cstatic const struct file_operations打开设备uvm_open()uvm.cioctl 分发uvm_ioctl()uvm.c分发宏UVM_ROUTE_CMD_*uvm_api.h初始化uvm_api_initialize()uvm.cfd 类型uvm_fd_type_t,uvm_fd_type()uvm_fd_type.h/.cioctl 号 / 参数UVM_*_PARAMSuvm_ioctl.h3. 设备文件操作表uvm_fopsUVM 注册一个字符设备nvidia-uvm其file_operations极简staticconststructfile_operationsuvm_fops{.openuvm_open_entry,.releaseuvm_release_entry,.mmapuvm_mmap_entry,.unlocked_ioctluvm_unlocked_ioctl_entry,#ifNVCPU_IS_X86_64.compat_ioctluvm_unlocked_ioctl_entry,#endif.ownerTHIS_MODULE,};四个入口的分工回调作用后续篇open每个struct file建立独立address_space本篇第 4 节mmap把托管 / 信号量 / P2P 内存映射进用户 VAPart 1.2 / Part 3unlocked_ioctl所有UVM_*控制命令的总入口本篇第 6 节release关闭 fd销毁 VA space可能延迟到 kthreadPart 1.2每个*_entry包装函数用UVM_ENTRY_RET宏包裹真正实现统一做线程上下文 / 栈检查等是 UVM 的通用模式。4.uvm_open()为什么每个 fd 要独立 address_spaceuvm_open()做了一件不显眼但关键的事——给每个打开的文件分配独立的struct address_spacemappinguvm_kvmalloc(sizeof(*mapping));address_space_init_once(mapping);mapping-hostinode;mapping-a_opsinode-i_mapping-a_ops;filp-private_dataNULL;// 初始为 UVM_FD_UNINITIALIZEDfilp-f_mappingmapping;原因源码注释已说明默认同一 inode 上所有进程的struct file共享inode 的address_space那么unmap_mapping_range会跨进程一起解映射。而UVM 把 mapping offset 当作该进程文件的 VA因此必须让每个进程的映射相互隔离。此时filp-private_data NULL即UVM_FD_UNINITIALIZED尚未绑定任何 VA space。5. fd 类型系统一个 fd 可以是什么filp-private_data不是简单地存一个指针而是用低位打包类型标签uvm_fd_type.htypedefenum{UVM_FD_UNINITIALIZED,// 刚 open未初始化UVM_FD_INITIALIZING,// 正在初始化CAS 中间态UVM_FD_VA_SPACE,// 已绑定 uvm_va_space_tUVM_FD_MM,// 绑定 mm用于 va_space_mm 机制UVM_FD_TEST,// 测试用UVM_FD_COUNT}uvm_fd_type_t;#defineUVM_FD_TYPE_BITS3#defineUVM_FD_TYPE_MASK((1ULUVM_FD_TYPE_BITS)-1)因为指针天然按 8 字节对齐低 3 位恒为 0UVM 就把类型塞进低 3 位、高位存实际指针。uvm_fd_type(filp, ptr)解出{类型, 指针}uvm_fd_get_type(filp, TYPE)仅当类型匹配才返回指针。uvm_open()private_data NULLuvm_fd_type_init_cas()原子 CAS 抢占UVM_INITIALIZE 成功uvm_fd_type_set(VA_SPACE, va_space)创建失败回退允许重试UVM_MM_INITIALIZE测试路径uvm_release()UNINITINITIALIZINGVA_SPACEMMTEST6. ioctl 分发uvm_ioctl()的路由表所有控制命令进入uvm_ioctl()本质是一张大switch用一组UVM_ROUTE_CMD_*宏把 ioctl 号映射到uvm_api_*函数staticlonguvm_ioctl(structfile*filp,unsignedintcmd,unsignedlongarg){switch(cmd){caseUVM_DEINITIALIZE:return0;UVM_ROUTE_CMD_STACK_NO_INIT_CHECK(UVM_INITIALIZE,uvm_api_initialize);UVM_ROUTE_CMD_STACK_NO_INIT_CHECK(UVM_MM_INITIALIZE,uvm_api_mm_initialize);UVM_ROUTE_CMD_STACK_INIT_CHECK(UVM_REGISTER_GPU,uvm_api_register_gpu);UVM_ROUTE_CMD_STACK_INIT_CHECK(UVM_SET_PREFERRED_LOCATION,uvm_api_set_preferred_location);UVM_ROUTE_CMD_STACK_INIT_CHECK(UVM_MIGRATE,uvm_api_migrate);UVM_ROUTE_CMD_ALLOC_INIT_CHECK(UVM_ALLOC_SEMAPHORE_POOL,uvm_api_alloc_semaphore_pool);/* ...数十条... */}// 都没匹配 → 交给测试 ioctlreturnuvm_test_ioctl(filp,cmd,arg);}6.1 三种路由宏的区别宏名的差异编码了两个正交维度参数存放位置、是否需要已初始化的 VA space。宏参数缓冲VA space 检查典型命令..._STACK_NO_INIT_CHECK栈上不要求已初始化UVM_INITIALIZE、UVM_MM_INITIALIZE..._STACK_INIT_CHECK栈上要求UVM_FD_VA_SPACE绝大多数命令..._ALLOC_INIT_CHECK堆分配参数较大要求已初始化UVM_MAP_EXTERNAL_ALLOCATION、UVM_ALLOC_SEMAPHORE_POOL宏内部统一负责从用户空间copy_from_user参数、视宏而定校验 fd 已是 VA space、调用处理函数、再copy_to_user回写结果。因此每个uvm_api_*函数签名统一为NV_STATUS uvm_api_xxx(UVM_XXX_PARAMS *params, struct file *filp)。6.2 命令分类速览路由表里的命令大致可分为类别代表命令对应结构操作生命周期UVM_INITIALIZE/UVM_DEINITIALIZE创建 / 销毁uvm_va_space_tGPU 管理UVM_REGISTER_GPU/UVM_UNREGISTER_GPU改registered_gpus掩码PeerUVM_ENABLE_PEER_ACCESS改enabled_peers策略UVM_SET_PREFERRED_LOCATION/UVM_SET_ACCESSED_BY/UVM_ENABLE_READ_DUPLICATION改 rangepolicyPart 1.2迁移UVM_MIGRATE/UVM_POPULATE_PAGEABLE触发 make_residentPart 3.4外部内存UVM_MAP_EXTERNAL_ALLOCATION/UVM_IMPORT_DMA_BUFexternal / dma-buf range信号量 / P2PUVM_ALLOC_SEMAPHORE_POOL/UVM_ALLOC_DEVICE_P2P专用 range 类型工具UVM_TOOLS_*/UVM_QUERY_RESIDENCY观测Part 5.27.UVM_INITIALIZE并发安全的 VA space 创建UVM_INITIALIZE是把一个裸 fd 变成 VA space 的那一步也是并发处理的典范。多个线程可能同时对同一 fd 发UVM_INITIALIZE代码用原子 CAS保证只有一个线程真正创建old_fd_typeuvm_fd_type_init_cas(filp);// 期望 UNINIT → 换成 INITIALIZINGswitch(old_fd_type){caseUVM_FD_UNINITIALIZED:// 只有抢到的线程走这里statusuvm_va_space_create(filp-f_mapping,va_space,params-flags);if(status!NV_OK){uvm_fd_type_set(filp,UVM_FD_UNINITIALIZED,NULL);// 失败回退允许重试returnstatus;}uvm_fd_type_set(filp,UVM_FD_VA_SPACE,va_space);break;caseUVM_FD_VA_SPACE:// 已初始化仅当 flags 一致才算成功va_spaceuvm_va_space_get(filp);status(params-flags!va_space-initialization_flags)?NV_ERR_INVALID_ARGUMENT:NV_OK;break;caseUVM_FD_INITIALIZING:// 别人正在建 → 让用户重试statusNV_ERR_BUSY_RETRY;break;/* ... */}要点CAS 抢占uvm_fd_type_init_cas()把UNINIT原子换成INITIALIZING只有观察到UNINIT的线程负责创建 VA space。失败可重试创建失败时把状态复位为UNINIT其他线程只可能看到过INITIALIZING不会看到半成品VA_SPACE因此安全。幂等语义已是VA_SPACE时重复 initialize只要 flags 一致就返回成功。NV_ERR_BUSY_RETRY撞上中间态时返回让用户态重试而非阻塞等待。这一步成功后filp-private_data就指向 Part 0.2 讲的uvm_va_space_t后续所有UVM_ROUTE_CMD_STACK_INIT_CHECK命令才能通过 fd 类型检查。8. 完整入口时序uvm_api_*uvm_ioctl 路由uvm_fopsVFS用户态 (CUDA)uvm_api_*uvm_ioctl 路由uvm_fopsVFS用户态 (CUDA)open(/dev/nvidia-uvm)uvm_open()分配独立 address_spaceprivate_data UNINITioctl(fd, UVM_INITIALIZE, params)uvm_unlocked_ioctl_entryuvm_ioctl(filp, cmd, arg)uvm_api_initialize()CAS 建 va_spaceNV_OKfd 现为 VA_SPACEioctl(fd, UVM_REGISTER_GPU, ...)uvm_ioctl → 校验 fd 类型uvm_api_register_gpu()结果 copy_to_user9. 边界与坑点fd 类型必须先对UVM_ROUTE_CMD_STACK_INIT_CHECK会拒绝未初始化 fd工具类命令若在 initialize 前发出会失败。PM 锁uvm_unlocked_ioctl外层持g_uvm_global.pm.lock电源管理release若 trylock 失败会把销毁延迟到 kthreaduvm_release_deferred这在挂起 / 恢复期间尤其重要。测试 ioctl 兜底路由表未命中会落到uvm_test_ioctl仅在开启测试构建时有意义。别把UVM_FD_MM当 VA spaceUVM_MM_INITIALIZE走另一条 fd 类型用于va_space_mm机制Part 4 相关与主 VA space fd 不是一回事。10. 小结与下一篇uvm_fops只有 open / mmap / ioctl / release 四个入口open 为每个 fd 建独立address_space以隔离进程映射。fd 用低 3 位打包uvm_fd_type_tUVM_INITIALIZE通过原子 CAS 并发安全地绑定uvm_va_space_t。uvm_ioctl()用UVM_ROUTE_CMD_*宏把命令路由到统一签名的uvm_api_*宏名编码了参数放栈/堆 是否需已初始化。下一篇Part 1.211-vaspace-lifecycle深入uvm_va_space_create()/ GPU 注册 / range 创建与销毁把这些 ioctl 落到第 0.2 篇那棵结构树的增删改上。交叉引用fd 绑定的uvm_va_space_t字段见 01-four-layer-model策略类命令的语义见 Part 1.212-policy-apiUVM_MIGRATE的内核流程见 Part 3.433-make-resident-migrate。