1. 概述
本文档详细分析AMD GPU驱动中KFD (Kernel Fusion Driver) 子系统对用户态指针 (userptr) 内存的处理机制。当应用程序通过 KFD_IOC_ALLOC_MEM_FLAGS_USERPTR 标志分配内存时,驱动需要将用户空间已分配的内存映射到GPU地址空间,使GPU能够直接访问用户态内存。
1.1 核心函数
- 入口函数:
kfd_ioctl_alloc_memory_of_gpu()(kfd_chardev.c) - 核心实现:
amdgpu_amdkfd_gpuvm_alloc_memory_of_gpu()(amdgpu_amdkfd_gpuvm.c) - 页面初始化:
init_user_pages()(amdgpu_amdkfd_gpuvm.c)
2. 用户态接口处理流程
文章:AMD rocr-libhsakmt分析系列6-1: userptr的register实现机制详解(上篇)分析了libhsakmt中在svm的支持下如何使用userptr,本文是KFD内核的实现分析,由于对内核的理解不是很深入,如果有错误,请读友们拍砖。
2.1 IOCTL 入口处理
在 kfd_ioctl_alloc_memory_of_gpu() 函数中,当检测到 KFD_IOC_ALLOC_MEM_FLAGS_USERPTR 标志时:
if (flags & KFD_IOC_ALLOC_MEM_FLAGS_USERPTR) {
/* 检查 userptr 是否对应于其他设备的本地内存 */
vma = find_vma(current->mm, args->mmap_offset);
if (vma && (vma->vm_flags & VM_IO)) {
unsigned long pfn;
follow_pfn(vma, args->mmap_offset, &pfn);
/* 转换为 doorbell 类型 */
flags |= KFD_IOC_ALLOC_MEM_FLAGS_DOORBELL;
flags &= ~KFD_IOC_ALLOC_MEM_FLAGS_USERPTR;
offset = (pfn << PAGE_SHIFT);
} else {
/* 验证地址对齐 */
if (offset & (PAGE_SIZE - 1)) {
pr_debug("Unaligned userptr address:%llx\n", offset);
return -EINVAL;
}
cpuva = offset;
}
}
关键点:
- 设备内存检测: 通过
find_vma()检查用户地址是否映射到IO内存区域(VM_IO标志) - 特殊处理: 如果是设备本地内存,转换为doorbell类型,获取物理页帧号(PFN)
- 地址验证: 普通userptr必须页对齐(PAGE_SIZE),否则返回 -EINVAL
- CPU虚拟地址保存: 将用户提供的地址保存到
cpuva变量
3. GPU内存对象分配
3.1 域和标志设置
在 amdgpu_amdkfd_gpuvm_alloc_memory_of_gpu() 中:
else if (flags & ALLOC_MEM_FLAGS_USERPTR) {
domain = AMDGPU_GEM_DOMAIN_GTT; // GPU访问域设为GTT
alloc_domain = AMDGPU_GEM_DOMAIN_CPU; // 实际分配域为CPU
//userptr 放在offset变量中,见libhsakmt中的分析
if (!offset || !*offset)
return -EINVAL;
user_addr = untagged_addr(*offset); // 去除地址标签位
}
技术要点:
- 双域设计:
domain: GTT域,表示GPU通过GTT (Graphics Translation Table) 访问alloc_domain: CPU域,表示内存实际在系统RAM中
- 地址处理:
untagged_addr()移除ARMv8.5-A的MTE (Memory Tagging Extension) 标签位
3.2 BO (Buffer Object) 创建
memset(&bp, 0, sizeof(bp));
bp.size = size;
bp.byte_align = 1;
bp.domain = alloc_domain; // CPU域
bp.flags = alloc_flags;
bp.type = bo_type; // ttm_bo_type_device
bp.resv = NULL;
ret = amdgpu_bo_create(adev, &bp, &bo);
if (user_addr)
bo->flags |= AMDGPU_AMDKFD_USERPTR_BO;
创建的BO特性:
- 分配在系统内存: 通过
AMDGPU_GEM_DOMAIN_CPU - 无sg表: 对于userptr,sg字段稍后由HMM (Heterogeneous Memory Management) 处理
- BO标记: 设置
AMDGPU_AMDKFD_USERPTR_BO标志
3.3 内存对象结构初始化
kgd_mem就是KFD驱动的bo,在amdgpu_bo的基础上添加了一些AI多卡应用需要的信息。该类型的理解请参考专栏:AMD KFD BO设计深度剖析——解锁GPU存储核心技术。
*mem = kzalloc(sizeof(struct kgd_mem), GFP_KERNEL);
INIT_LIST_HEAD(&(*mem)->bo_va_list);
mutex_init(&(*mem)->lock);
(*mem)->alloc_flags = flags;
(*mem)->va = va;
(*mem)->domain = domain; // GTT域
(*mem)->mapped_to_gpu_memory = 0;
(*mem)->process_info = avm->process_info;
3.4 添加到BO列表
add_kgd_mem_to_kfd_bo_list(*mem, avm->process_info, user_addr);
static void add_kgd_mem_to_kfd_bo_list(struct kgd_mem *mem,
struct amdkfd_process_info *process_info,
bool userptr)
{
struct ttm_validate_buffer *entry = &mem->validate_list;
struct amdgpu_bo *bo = mem->bo;
INIT_LIST_HEAD(&entry->head);
entry->num_shared = 1;
entry->bo = &bo->tbo;
mutex_lock(&process_info->lock);
if (userptr)
list_add_tail(&entry->head, &process_info->userptr_valid_list);
else
list_add_tail(&entry->head, &process_info->kfd_bo_list);
mutex_unlock(&process_info->lock);
}
关键设计:
- 分离管理: userptr BOs添加到
userptr_valid_list,普通BOs添加到kfd_bo_list - 验证缓冲区: 通过
ttm_validate_buffer结构管理,用于批量预留和验证
4. 用户页面初始化 (核心机制)
4.1 初始化流程概述
init_user_pages() 函数是userptr机制的核心,完成四个关键步骤:
| 步骤 | 函数 | 作用 | 关键操作 |
|---|---|---|---|
| 1 | amdgpu_ttm_tt_set_userptr() | 保存用户地址到TTM | 设置 gtt->userptr,引用 usertask |
| 2 | amdgpu_mn_register() | 注册MMU通知器 | 插入interval tree,监控地址范围 |
| 3 | amdgpu_ttm_tt_get_user_pages() | 获取用户页面 | HMM fault,转换PFN到page数组 |
| 4 | ttm_bo_validate() | 验证BO | 在GTT域验证,建立GPU映射 |
4.2 关键代码段
步骤1: TTM Userptr设置
int amdgpu_ttm_tt_set_userptr(struct ttm_tt *ttm, uint64_t addr, uint32_t flags)
{
struct amdgpu_ttm_tt *gtt = (void *)ttm;
gtt->userptr = addr; // 保存用户虚拟地址
gtt->usertask = current->group_leader; // 保存进程任务结构
get_task_struct(gtt->usertask); // 增加引用计数
return 0;
}
步骤2: MMU Notifier注册
int amdgpu_mn_register(struct amdgpu_bo *bo, unsigned long addr)
{
// 关键点:使用区间树管理地址范围
while ((it = interval_tree_iter_first(&amn->objects, addr, end))) {
// 查找重叠区间并合并
interval_tree_remove(&node->it, &amn->objects);
addr = min(it->start, addr);
end = max(it->last, end);
}
// 插入新的监控区间
node->it.start = addr;
node->it.last = end;
interval_tree_insert(&node->it, &amn->objects);
return 0;
}
关键技术:
- 区间树: 使用红黑树interval tree高效管理地址范围,O(log n)复杂度
- 地址合并: 自动合并重叠的监控区间,避免冗余
- MMU回调: 当用户空间修改页表时(munmap、mprotect等),内核回调驱动
步骤3: HMM页面获取
int amdgpu_ttm_tt_get_user_pages(struct amdgpu_bo *bo, struct page **pages)
{
// 1. 验证VMA
vma = find_vma(mm, start);
if (unlikely(!vma || start < vma->vm_start))
return -EFAULT;
// 2. 设置HMM范围
range->start = start;
range->end = start + ttm->num_pages * PAGE_SIZE;
range->default_flags = range->flags[HMM_PFN_VALID];
range->default_flags |= is_readonly ? 0 : range->flags[HMM_PFN_WRITE];
// 3. 注册并触发页面故障
hmm_range_register(range, mirror);
hmm_range_wait_until_valid(range, HMM_RANGE_DEFAULT_TIMEOUT);
hmm_range_fault(range, 0); // 确保页面在内存中
// 4. 转换PFN为page结构
for (i = 0; i < ttm->num_pages; i++)
pages[i] = hmm_device_entry_to_page(range, pfns[i]);
return 0;
}
HMM机制关键点:
- HMM Range注册: 向HMM子系统注册监控范围
- 页面故障处理:
hmm_range_fault()触发缺页异常,确保页面在物理内存中 - PFN转换: 将物理页帧号(PFN)转换为内核
struct page*指针 - 读写权限: 根据BO的只读属性设置
HMM_PFN_WRITE标志
步骤4: BO验证
amdgpu_bo_reserve(bo, true); // 预留BO
amdgpu_bo_placement_from_domain(bo, mem->domain); // GTT域放置策略
ttm_bo_validate(&bo->tbo, &bo->placement, &ctx); // TTM验证
amdgpu_bo_unreserve(bo); // 释放预留
作用: 确保BO在GTT域验证成功,建立GPU可访问的内存映射
5. GPU映射处理
5.1 映射流程概述
amdgpu_amdkfd_gpuvm_map_memory_to_gpu() 负责将userptr BO映射到GPU虚拟地址空间。
| 步骤 | 操作 | 关键检查 | 作用 |
|---|---|---|---|
| 1 | 检测invalid状态 | atomic_read(&mem->invalid) | 判断userptr是否失效 |
| 2 | 检查系统域 | bo->tbo.mem.mem_type == TTM_PL_SYSTEM | 确认BO是否已迁移到GTT |
| 3 | 建立VM映射 | map_bo_to_gpuvm() | 在GPU虚拟内存中建立映射 |
| 4 | 更新页目录 | vm_update_pds() | 更新GPU页表结构 |
5.2 关键代码
Invalid检测:
// 使用mmap_sem保护,确保MMU notifier不会并发运行
if (amdgpu_ttm_tt_get_usermm(bo->tbo.ttm)) {
down_write(¤t->mm->mmap_sem);
is_invalid_userptr = atomic_read(&mem->invalid);
up_write(¤t->mm->mmap_sem);
}
// Userptr在系统域视为invalid,延迟映射
if (amdgpu_ttm_tt_get_usermm(bo->tbo.ttm) &&
bo->tbo.mem.mem_type == TTM_PL_SYSTEM)
is_invalid_userptr = true;
映射和同步:
map_bo_to_gpuvm(adev, entry, ctx.sync, is_invalid_userptr); // 建立GPUVM映射
vm_update_pds(vm, ctx.sync); // 更新页目录
技术要点:
- 延迟映射: invalid或在系统域的userptr跳过映射,等待restore worker处理
- mmap_sem保护: 确保与MMU notifier互斥,避免竞态
- 同步机制: 通过
amdgpu_sync确保GPU操作完成
6. 失效和恢复机制
6.1 失效触发流程
当用户空间修改内存映射时(munmap、mremap、mprotect等),触发MMU Notifier回调:
| 步骤 | 操作 | 函数/机制 | 作用 |
|---|---|---|---|
| 1 | 标记失效 | atomic_set(&mem->invalid, 1) | 设置失效标志 |
| 2 | 移动列表 | userptr_valid_list → userptr_inval_list | 更新BO状态 |
| 3 | 调度恢复 | schedule_delayed_work(restore_userptr_work) | 启动恢复工作队列 |
6.2 恢复工作队列
核心流程:
static void amdgpu_amdkfd_restore_userptr_worker(struct work_struct *work)
{
// 1. 获取进程引用
usertask = get_pid_task(process_info->pid, PIDTYPE_PID);
mm = get_task_mm(usertask);
mutex_lock(&process_info->lock);
// 2. 更新失效页面
if (update_invalid_user_pages(process_info, mm))
goto unlock_out;
// 3. 验证失效页面
if (!list_empty(&process_info->userptr_inval_list)) {
if (validate_invalid_user_pages(process_info))
goto unlock_out;
}
// 4. 原子更新并恢复
if (atomic_cmpxchg(&process_info->evicted_bos, evicted_bos, 0) == evicted_bos)
kgd2kfd_resume_mm(mm);
unlock_out:
mutex_unlock(&process_info->lock);
mmput(mm);
put_task_struct(usertask);
// 失败则重新调度
if (evicted_bos)
schedule_delayed_work(&process_info->restore_userptr_work,
msecs_to_jiffies(AMDGPU_USERPTR_RESTORE_DELAY_MS));
}
6.3 更新失效页面
关键操作:
static int update_invalid_user_pages(struct amdkfd_process_info *process_info,
struct mm_struct *mm)
{
// 1. 迁移失效BO到CPU域
list_for_each_entry_safe(mem, tmp_mem, &process_info->userptr_valid_list, ...) {
if (!atomic_read(&mem->invalid))
continue;
amdgpu_bo_reserve(bo, true);
amdgpu_bo_placement_from_domain(bo, AMDGPU_GEM_DOMAIN_CPU);
ttm_bo_validate(&bo->tbo, &bo->placement, &ctx);
amdgpu_bo_unreserve(bo);
list_move_tail(&mem->validate_list.head, &process_info->userptr_inval_list);
}
// 2. 重新获取用户页面
list_for_each_entry(mem, &process_info->userptr_inval_list, ...) {
amdgpu_ttm_tt_get_user_pages(bo, bo->tbo.ttm->pages);
amdgpu_ttm_tt_get_user_pages_done(bo->tbo.ttm);
atomic_cmpxchg(&mem->invalid, invalid, 0); // 清除失效标志
}
return 0;
}
6.4 验证失效页面
核心流程:
static int validate_invalid_user_pages(struct amdkfd_process_info *process_info)
{
// 1. 预留所有BO和页表
ttm_eu_reserve_buffers(&ticket, &resv_list, false, &duplicates);
// 2. 验证BO并更新GPUVM
list_for_each_entry_safe(mem, tmp_mem, &process_info->userptr_inval_list, ...) {
// 验证BO回GTT域
if (bo->tbo.ttm->pages[0]) {
amdgpu_bo_placement_from_domain(bo, mem->domain);
ttm_bo_validate(&bo->tbo, &bo->placement, &ctx);
}
// 移回valid列表
list_move_tail(&mem->validate_list.head, &process_info->userptr_valid_list);
// 更新GPU页表
list_for_each_entry(bo_va_entry, &mem->bo_va_list, bo_list) {
if (bo_va_entry->is_mapped)
update_gpuvm_pte(..., bo_va_entry, &sync);
}
}
// 3. 更新页目录
process_update_pds(process_info, &sync);
ttm_eu_backoff_reservation(&ticket, &resv_list);
return ret;
}
6.5 技术要点
| 机制 | 实现方式 | 优势 |
|---|---|---|
| 延迟恢复 | AMDGPU_USERPTR_RESTORE_DELAY_MS (1ms) | 聚合多次失效事件,减少恢复开销 |
| 原子操作 | atomic_cmpxchg(&mem->invalid, ...) | 确保并发安全,无锁编程 |
| 批量处理 | 一次处理所有inval_list中的BOs | 减少锁竞争,提高效率 |
| 状态迁移 | CPU域 ← invalid ← GTT域 | 完整的生命周期管理 |
7. 技术亮点总结
7.1 HMM (Heterogeneous Memory Management)
| 关键点 | 实现机制 | 优势 |
|---|---|---|
| 统一地址空间 | CPU和GPU共享相同的虚拟地址 | 简化编程模型,无需地址转换 |
| 自动迁移 | 页面在CPU/GPU之间自动迁移 | 透明的数据移动,降低开发复杂度 |
| 透明性 | 应用层无需显式数据传输 | 提高开发效率,减少错误 |
7.2 MMU Notifier机制
| 关键点 | 实现机制 | 优势 |
|---|---|---|
| 实时同步 | 用户空间内存变化实时通知驱动 | 保证内存一致性 |
| 区间树管理 | 使用红黑树interval tree管理地址范围 | O(log n)时间复杂度,高效查找 |
| 并发保护 | mmap_sem确保与页表修改的同步 | 避免竞态条件 |
7.3 失效恢复机制
| 关键点 | 实现机制 | 优势 |
|---|---|---|
| 延迟恢复 | delayed_work聚合多次失效事件 | 减少系统开销,提高效率 |
| 原子操作 | atomic_cmpxchg确保并发安全 | 无锁编程,提高性能 |
| 批量处理 | 一次恢复所有失效的userptr BOs | 减少锁竞争,提高吞吐量 |
7.4 双域设计
| 关键点 | 实现机制 | 优势 |
|---|---|---|
| domain | GTT域(GPU访问路径) | 明确GPU访问方式 |
| alloc_domain | CPU域(物理分配位置) | 内存实际在系统RAM中 |
| 灵活性 | 支持内存在不同域之间迁移 | 适应不同访问模式 |
8. 性能优化策略
| 优化策略 | 实现方式 | 核心函数/机制 | 性能收益 |
|---|---|---|---|
| 页面预取 | HMM机制支持页面预取 | hmm_range_fault() | 减少页面故障延迟,提前准备数据 |
| 批量操作 | 批量预留多个BOs | ttm_eu_reserve_buffers() | 减少锁竞争和上下文切换,提高吞吐量 |
| 批量验证 | process_validate_vms() | 一次性处理多个VM,降低系统调用开销 | |
| 延迟更新 | 延迟恢复工作队列 | AMDGPU_USERPTR_RESTORE_DELAY_MS (1ms) | 聚合多次失效事件,减少恢复次数 |
| 延迟调度 | schedule_delayed_work() | 避免频繁的上下文切换 | |
| 零拷贝传输 | GPU直接访问用户内存 | HMM + userptr机制 | 消除CPU-GPU数据拷贝,节省带宽和时间 |
| 统一虚拟地址 | 共享地址空间 | 避免地址转换开销 | |
| 无锁设计 | 原子操作 | atomic_read(), atomic_cmpxchg() | 减少锁竞争,提高并发性能 |
| 读写信号量 | down_read(), down_write() | 允许多读单写,提高并发度 |
9. 潜在问题和注意事项
| 问题类别 | 具体问题 | 影响 | 缓解措施 |
|---|---|---|---|
| 内存固定开销 | Userptr页面需要固定在内存中 | 增加内存压力,页面无法被换出 | 限制userptr总量,使用内存限制机制 |
| 大量userptr分配 | 可能导致系统内存不足 | amdgpu_amdkfd_reserve_mem_limit() 限制 | |
| 长期占用物理内存 | 影响其他进程可用内存 | 及时释放不用的userptr | |
| 页面故障延迟 | 首次访问触发页面故障 | GPU访问延迟增加 | 使用页面预取优化 |
| 失效后恢复 | hmm_range_fault() 等待时间 | 延迟工作队列批量处理 | |
| 页表更新 | 需要等待GPU完成 | 异步同步机制 (amdgpu_sync) | |
| 并发复杂性 | MMU notifier与GPU操作并发 | 需要复杂的同步机制 | 使用mmap_sem + process_info->lock |
| 多VM并发访问 | 可能出现死锁 | 严格的锁获取顺序 | |
| 原子操作竞争 | atomic_cmpxchg可能失败需重试 | 循环重试机制 | |
| 进程生命周期 | 进程异常退出 | MMU notifier需要正确清理 | amdgpu_mn_unregister() 清理 |
| task_struct引用计数 | usertask 管理不当导致泄露 | get_task_struct() / put_task_struct() | |
| mm_struct有效性 | 进程退出后mm可能无效 | get_task_mm() / mmput() 保护 | |
| 页面迁移 | CPU/GPU域切换 | TTM验证开销 | 减少不必要的域切换 |
| 并发迁移 | 可能导致数据不一致 | 使用eviction fence同步 |
10. 应用场景
| 应用领域 | 具体场景 | 技术优势 | 典型应用 |
|---|---|---|---|
| 零拷贝数据传输 | 大数据处理 | GPU直接访问用户缓冲区,无需拷贝 | 数据库查询加速、日志分析 |
| 视频处理 | 避免CPU-GPU内存拷贝 | 实时视频编解码、图像处理 | |
| 科学计算 | 减少数据传输开销 | 气象模拟、分子动力学 | |
| HSA (Heterogeneous System Architecture) | 统一内存模型 | CPU和GPU共享地址空间 | 异构计算平台 |
| 共享数据结构 | 直接访问复杂数据结构 | 图数据库、指针追踪算法 | |
| 无缝协作 | CPU和GPU交替访问同一数据 | 流水线式处理 | |
| 机器学习 | 大模型训练 | 减少数据传输开销 | Transformer、LLM训练 |
| 推理加速 | 直接访问输入数据 | 实时推理服务 | |
| 数据增强 | CPU预处理,GPU直接消费 | 图像增强、数据预处理 | |
| 图形渲染 | 纹理数据 | 用户空间准备纹理,GPU直接读取 | 游戏引擎、3D渲染 |
| 顶点数据 | 避免顶点缓冲拷贝 | CAD软件、建模工具 | |
| 高性能计算 | MPI通信 | 减少进程间数据拷贝 | 并行计算框架 |
| 流式处理 | 连续数据流零拷贝传输 | 实时数据分析 | |
| 内存映射文件 | 大文件处理 | 文件内容直接映射给GPU | 文件索引、搜索引擎 |
| 持久化内存 | GPU直接访问持久化数据 | 数据库系统 |
11. 代码流程图
用户调用 KFD_IOC_ALLOC_MEM_FLAGS_USERPTR
|
v
kfd_ioctl_alloc_memory_of_gpu()
|
+-- 检查VMA是否为IO内存 (find_vma)
| |
| +-- 是 -> 转换为DOORBELL
| +-- 否 -> 验证地址对齐
|
v
amdgpu_amdkfd_gpuvm_alloc_memory_of_gpu()
|
+-- 设置域: domain=GTT, alloc_domain=CPU
+-- 创建BO (amdgpu_bo_create)
+-- 分配kgd_mem结构
+-- 添加到userptr_valid_list
|
v
init_user_pages()
|
+-- amdgpu_ttm_tt_set_userptr()
| |
| +-- 保存user_addr到gtt->userptr
| +-- 引用usertask
|
+-- amdgpu_mn_register()
| |
| +-- 创建HMM mirror
| +-- 插入interval_tree
| +-- 注册MMU notifier回调
|
+-- amdgpu_ttm_tt_get_user_pages()
| |
| +-- find_vma验证地址
| +-- hmm_range_register()
| +-- hmm_range_fault() 触发页面故障
| +-- 转换PFN到struct page*
|
+-- ttm_bo_validate()
|
+-- 在GTT域验证BO
+-- 建立GPU映射
映射到GPU (用户调用map操作)
|
v
amdgpu_amdkfd_gpuvm_map_memory_to_gpu()
|
+-- 检查is_invalid_userptr
+-- add_bo_to_vm()
+-- map_bo_to_gpuvm()
+-- vm_update_pds() 更新页目录
MMU Notifier回调 (用户munmap/mprotect等)
|
v
amdgpu_mn_invalidate_range_start()
|
+-- atomic_set(&mem->invalid, 1)
+-- 移动到userptr_inval_list
+-- schedule_delayed_work(restore_userptr_work)
恢复工作队列
|
v
amdgpu_amdkfd_restore_userptr_worker()
|
+-- update_invalid_user_pages()
| |
| +-- 迁移到CPU域
| +-- amdgpu_ttm_tt_get_user_pages() 重新获取页面
| +-- atomic_cmpxchg清除invalid标志
|
+-- validate_invalid_user_pages()
|
+-- ttm_bo_validate() 验证回GTT域
+-- update_gpuvm_pte() 更新页表
+-- 移动回userptr_valid_list
12. 总结
KFD的userptr机制通过HMM、MMU Notifier和TTM的深度集成,实现了CPU和GPU之间的零拷贝内存共享。关键技术包括:
- HMM统一内存管理: 透明的页面迁移和故障处理
- MMU Notifier实时监控: 自动跟踪用户空间内存变化
- 双域设计: 灵活的内存域管理
- 失效恢复机制: 自动恢复失效的userptr映射
- 批量优化: 减少锁竞争和系统调用开销
amdgpu的userptr bo的实现,还是依托于drm框架下的ttm,以及hmm、mn机制,想深入理解的话,请查看专栏:linux drm子系统。
这套机制为HSA和高性能计算提供了坚实的基础,使得GPU能够高效、安全地访问用户态内存。看起来复杂的很,我也是在反复调试的过程中总结了下流程,但是否有更好的实现技术或思路,我无法给出定论。既然AMD的大佬们都使用这种方案,那说明该方案有合理性和普适性,欢迎读友们讨论。
2390

被折叠的 条评论
为什么被折叠?



