本文是Linux HMM(Heterogeneous Memory Management)原理与实现详解的补充篇。有些内容重复,就当复习了。
目录
引言
HMM (Heterogeneous Memory Management) 是 Linux 内核提供的一种机制,旨在实现 CPU 和异构设备(如 GPU、FPGA 等)之间的统一虚拟地址空间(Unified Virtual Addressing, UVA)。通过 HMM,设备可以直接访问进程的虚拟地址,无需在用户空间和设备内存之间显式拷贝数据,从而实现零拷贝和高效的异构计算。
主要优势:
- 统一地址空间:CPU 和设备使用相同的虚拟地址
- 按需迁移:页面可在 CPU 内存和设备内存间自动迁移
- 透明性:对应用程序透明,无需修改现有代码
- 高效同步:通过 MMU Notifier 机制实现内存一致性
HMM 核心概念
2.1 设计目标
HMM 的核心目标是允许设备透明地访问进程虚拟地址空间,主要解决以下问题:
- 地址空间统一:消除 CPU 和设备间的地址空间隔离
- 内存一致性:确保 CPU 和设备看到一致的内存视图
- 动态迁移:支持页面在不同内存域间迁移
- 细粒度控制:支持页级别的访问权限控制
2.2 工作模式
HMM 支持两种主要工作模式:
2.2.1 镜像模式(Mirroring)
设备直接访问系统内存中的页面,页表由 CPU MMU 管理。
2.2.2 设备私有内存模式(Device Private Memory)
页面迁移到设备私有内存,CPU 访问时触发迁移回系统内存。
关键数据结构
3.1 struct hmm_range
这是 HMM 操作的核心结构,定义在 include/linux/hmm.h:
struct hmm_range {
struct mmu_interval_notifier *notifier; // MMU 失效通知器
unsigned long notifier_seq; // 序列号,检测并发修改
unsigned long start; // 虚拟地址范围起始
unsigned long end; // 虚拟地址范围结束
unsigned long *hmm_pfns; // PFN 数组(输出)
unsigned long default_flags; // 默认标志(读/写权限)
unsigned long pfn_flags_mask; // PFN 标志掩码
void *dev_private_owner; // 设备私有页所有者
};
字段说明:
- notifier: 关联的
mmu_interval_notifier,用于追踪内存失效事件 - notifier_seq: 序列号,通过 seqcount 机制检测并发修改
- start/end: 要查询的虚拟地址范围 [start, end)
- hmm_pfns: 输出数组,存储每个页面的 PFN 和标志位
- default_flags: 默认请求标志(如
HMM_PFN_REQ_FAULT、HMM_PFN_REQ_WRITE) - pfn_flags_mask: 允许每页独立设置的标志掩码
- dev_private_owner: 用于识别设备私有页的所有者指针
3.2 enum hmm_pfn_flags
HMM 使用巧妙的编码方式,将 PFN 和标志位编码在同一个 unsigned long 中:
enum hmm_pfn_flags {
/* 输出标志 - 高位 */
HMM_PFN_VALID = 1UL << (BITS_PER_LONG - 1), // PFN 有效
HMM_PFN_WRITE = 1UL << (BITS_PER_LONG - 2), // 可写
HMM_PFN_ERROR = 1UL << (BITS_PER_LONG - 3), // 错误(无法访问)
/* 粘性标志 - 从输入传递到输出 */
HMM_PFN_DMA_MAPPED = 1UL << (BITS_PER_LONG - 4), // 已 DMA 映射
HMM_PFN_P2PDMA = 1UL << (BITS_PER_LONG - 5), // P2P DMA 页
HMM_PFN_P2PDMA_BUS = 1UL << (BITS_PER_LONG - 6), // 总线地址 P2P
/* Order 编码位置 */
HMM_PFN_ORDER_SHIFT = (BITS_PER_LONG - 11),
/* 输入标志 */
HMM_PFN_REQ_FAULT = HMM_PFN_VALID, // 要求缺页
HMM_PFN_REQ_WRITE = HMM_PFN_WRITE, // 要求可写
HMM_PFN_FLAGS = ~((1UL << HMM_PFN_ORDER_SHIFT) - 1),
};
编码格式(64位系统):
63 62 61 60 ... 11 10 9 8 ... 0
| | | | | | | |
| | | | | | +--------+-- PFN (页帧号)
| | | | +--+-------------- Order (页面大小 2^order)
| | | +------------------------ ERROR
| | +--------------------------- WRITE
| +------------------------------ VALID
+--------------------------------- (保留)
3.3 struct hmm_vma_walk
页表遍历的私有上下文结构:
struct hmm_vma_walk {
struct hmm_range *range; // 关联的 hmm_range
unsigned long last; // 最后处理的地址(用于重试)
};
3.4 辅助函数
// 从 hmm_pfn 提取 struct page
static inline struct page *hmm_pfn_to_page(unsigned long hmm_pfn)
{
return pfn_to_page(hmm_pfn & ~HMM_PFN_FLAGS);
}
// 从 hmm_pfn 提取物理地址
static inline phys_addr_t hmm_pfn_to_phys(unsigned long hmm_pfn)
{
return __pfn_to_phys(hmm_pfn & ~HMM_PFN_FLAGS);
}
// 获取页面映射的 order(大小为 2^order 字节)
static inline unsigned int hmm_pfn_to_map_order(unsigned long hmm_pfn)
{
return (hmm_pfn >> HMM_PFN_ORDER_SHIFT) & 0x1F;
}
核心机制实现
4.1 MMU Notifier 集成
HMM 依赖 mmu_interval_notifier 机制跟踪 CPU 页表的变化。
4.1.1 工作原理
当 CPU 页表发生变化时(如 unmap、写保护、页面迁移等),MMU Notifier 会触发回调通知设备驱动:
struct mmu_interval_notifier {
struct interval_tree_node interval_tree;
const struct mmu_interval_notifier_ops *ops;
struct mm_struct *mm;
struct hlist_node deferred_item;
unsigned long seq;
};
struct mmu_interval_notifier_ops {
bool (*invalidate)(struct mmu_interval_notifier *interval_sub,
const struct mmu_notifier_range *range,
unsigned long cur_seq);
};
4.1.2 序列号机制
使用 seqcount 实现无锁的并发检测:
// 开始读取序列号
unsigned long seq = mmu_interval_read_begin(notifier);
// 执行操作...
// 检查是否有并发修改
if (mmu_interval_check_retry(notifier, seq)) {
// 发生并发修改,需要重试
return -EBUSY;
}
4.2 hmm_range_fault() 核心流程
这是 HMM 的核心函数,负责查询和缺页处理:
int hmm_range_fault(struct hmm_range *range)
{
struct hmm_vma_walk hmm_vma_walk = {
.range = range,
.last = range->start,
};
struct mm_struct *mm = range->notifier->mm;
int ret;
mmap_assert_locked(mm);
do {
/* 检查是否有并发修改 */
if (mmu_interval_check_retry(range->notifier,
range->notifier_seq))
return -EBUSY;
/* 遍历页表 */
ret = walk_page_range(mm, hmm_vma_walk.last, range->end,
&hmm_walk_ops, &hmm_vma_walk);
/*
* 当返回 -EBUSY 时,循环重启,hmm_vma_walk.last 被设置为
* 尚未存储到 pfns 的地址。所有 < last 的条目已设置为输出值,
* >= last 的条目仍为输入值。
*/
} while (ret == -EBUSY);
return ret;
}
流程说明:
- 初始化上下文:创建
hmm_vma_walk结构 - 并发检测:检查
notifier_seq是否变化 - 页表遍历:调用
walk_page_range()遍历多级页表 - 重试机制:遇到
-EBUSY时重试(如缺页、迁移等) - 返回结果:填充
hmm_pfns数组
4.3 缺页判断逻辑
static unsigned int hmm_pte_need_fault(
const struct hmm_vma_walk *hmm_vma_walk,
unsigned long pfn_req_flags,
unsigned long cpu_flags)
{
struct hmm_range *range = hmm_vma_walk->range;
/* 合并每页请求和默认标志 */
pfn_req_flags &= range->pfn_flags_mask;
pfn_req_flags |= range->default_flags;
/* 不需要任何操作 */
if (!(pfn_req_flags & HMM_PFN_REQ_FAULT))
return 0;
/* 需要写权限?*/
if ((pfn_req_flags & HMM_PFN_REQ_WRITE) &&
!(cpu_flags & HMM_PFN_WRITE))
return HMM_NEED_FAULT | HMM_NEED_WRITE_FAULT;
/* CPU 页表无效则需要缺页 */
if (!(cpu_flags & HMM_PFN_VALID))
return HMM_NEED_FAULT;
return 0;
}
判断逻辑:
- 不设置 REQ_FAULT:只查询当前状态,不触发缺页
- REQ_FAULT + REQ_WRITE:要求可写,触发写时复制(COW)
- REQ_FAULT:要求页面有效,触发换入/分配
页表遍历与缺页处理
5.1 页表遍历回调
HMM 定义了完整的页表遍历回调:
static const struct mm_walk_ops hmm_walk_ops = {
.pud_entry = hmm_vma_walk_pud, // PUD 级别
.pmd_entry = hmm_vma_walk_pmd, // PMD 级别
.pte_hole = hmm_vma_walk_hole, // 页表空洞
.hugetlb_entry = hmm_vma_walk_hugetlb_entry, // 大页
.test_walk = hmm_vma_walk_test, // VMA 测试
.walk_lock = PGWALK_RDLOCK, // 使用读锁
};
5.2 PMD 级别处理
static int hmm_vma_walk_pmd(
pmd_t *pmdp, unsigned long start,
unsigned long end, struct mm_walk *walk)
{
struct hmm_vma_walk *hmm_vma_walk = walk->private;
struct hmm_range *range = hmm_vma_walk->range;
pmd_t pmd;
again:
pmd = pmdp_get_lockless(pmdp); // 无锁读取 PMD
/* PMD 不存在 */
if (pmd_none(pmd))
return hmm_vma_walk_hole(start, end, -1, walk);
/* PMD 迁移中 */
if (thp_migration_supported() && is_pmd_migration_entry(pmd)) {
if (hmm_range_need_fault(...)) {
hmm_vma_walk->last = addr;
pmd_migration_entry_wait(walk->mm, pmdp);
return -EBUSY; // 等待迁移完成
}
return hmm_pfns_fill(start, end, range, 0);
}
/* PMD 不存在(swap out 等)*/
if (!pmd_present(pmd)) {
if (hmm_range_need_fault(...))
return -EFAULT;
return hmm_pfns_fill(start, end, range, HMM_PFN_ERROR);
}
/* 透明大页 */
if (pmd_trans_huge(pmd)) {
pmd = pmdp_get_lockless(pmdp);
if (!pmd_trans_huge(pmd))
goto again; // 并发拆分,重试
return hmm_vma_handle_pmd(walk, addr, end, hmm_pfns, pmd);
}
/* 遍历 PTE */
ptep = pte_offset_map(pmdp, addr);
if (!ptep)
goto again;
for (; addr < end; addr += PAGE_SIZE, ptep++, hmm_pfns++) {
int r = hmm_vma_handle_pte(walk, addr, end, pmdp, ptep, hmm_pfns);
if (r) {
/* hmm_vma_handle_pte() 已调用 pte_unmap() */
return r;
}
}
pte_unmap(ptep - 1);
return 0;
}
5.3 PTE 级别处理
static int hmm_vma_handle_pte(struct mm_walk *walk, unsigned long addr,
unsigned long end, pmd_t *pmdp, pte_t *ptep,
unsigned long *hmm_pfn)
{
struct hmm_vma_walk *hmm_vma_walk = walk->private;
struct hmm_range *range = hmm_vma_walk->range;
pte_t pte = ptep_get(ptep);
uint64_t pfn_req_flags = *hmm_pfn;
uint64_t new_pfn_flags = 0;
/* PTE 为空 */
if (pte_none_mostly(pte)) {
required_fault = hmm_pte_need_fault(hmm_vma_walk, pfn_req_flags, 0);
if (required_fault)
goto fault;
goto out;
}
/* PTE 不在内存中 */
if (!pte_present(pte)) {
swp_entry_t entry = pte_to_swp_entry(pte);
/* 设备私有页 - 由调用者拥有 */
if (is_device_private_entry(entry) &&
page_pgmap(pfn_swap_entry_to_page(entry))->owner ==
range->dev_private_owner) {
cpu_flags = HMM_PFN_VALID;
if (is_writable_device_private_entry(entry))
cpu_flags |= HMM_PFN_WRITE;
new_pfn_flags = swp_offset_pfn(entry) | cpu_flags;
goto out; // 直接返回设备私有页的 PFN
}
/* 普通 swap 页 */
if (!non_swap_entry(entry))
goto fault; // 触发换入
/* 设备独占页 */
if (is_device_exclusive_entry(entry))
goto fault;
/* 迁移中 */
if (is_migration_entry(entry)) {
pte_unmap(ptep);
hmm_vma_walk->last = addr;
migration_entry_wait(walk->mm, pmdp, addr);
return -EBUSY; // 等待迁移完成
}
/* 其他错误 */
pte_unmap(ptep);
return -EFAULT;
}
/* 正常页面 */
cpu_flags = pte_to_hmm_pfn_flags(range, pte);
required_fault = hmm_pte_need_fault(hmm_vma_walk, pfn_req_flags, cpu_flags);
if (required_fault)
goto fault;
/* 特殊页面(非正常映射)*/
if (!vm_normal_page(walk->vma, addr, pte) &&
!is_zero_pfn(pte_pfn(pte))) {
if (hmm_pte_need_fault(hmm_vma_walk, pfn_req_flags, 0)) {
pte_unmap(ptep);
return -EFAULT;
}
new_pfn_flags = HMM_PFN_ERROR;
goto out;
}
new_pfn_flags = pte_pfn(pte) | cpu_flags;
out:
*hmm_pfn = (*hmm_pfn & HMM_PFN_INOUT_FLAGS) | new_pfn_flags;
return 0;
fault:
pte_unmap(ptep);
/* 触发缺页 */
return hmm_vma_fault(addr, end, required_fault, walk);
}
5.4 缺页处理
static int hmm_vma_fault(unsigned long addr, unsigned long end,
unsigned int required_fault, struct mm_walk *walk)
{
struct hmm_vma_walk *hmm_vma_walk = walk->private;
struct vm_area_struct *vma = walk->vma;
unsigned int fault_flags = FAULT_FLAG_REMOTE;
WARN_ON_ONCE(!required_fault);
hmm_vma_walk->last = addr;
/* 需要写权限?*/
if (required_fault & HMM_NEED_WRITE_FAULT) {
if (!(vma->vm_flags & VM_WRITE))
return -EPERM; // VMA 不允许写
fault_flags |= FAULT_FLAG_WRITE;
}
/* 逐页触发缺页 */
for (; addr < end; addr += PAGE_SIZE)
if (handle_mm_fault(vma, addr, fault_flags, NULL) &
VM_FAULT_ERROR)
return -EFAULT;
return -EBUSY; // 返回 EBUSY,触发重试
}
5.5 透明大页处理
static int hmm_vma_handle_pmd(struct mm_walk *walk, unsigned long addr,
unsigned long end, unsigned long hmm_pfns[],
pmd_t pmd)
{
struct hmm_vma_walk *hmm_vma_walk = walk->private;
struct hmm_range *range = hmm_vma_walk->range;
unsigned long pfn, npages, i;
unsigned int required_fault;
unsigned long cpu_flags;
npages = (end - addr) >> PAGE_SHIFT;
cpu_flags = pmd_to_hmm_pfn_flags(range, pmd);
/* 检查是否需要缺页 */
required_fault = hmm_range_need_fault(hmm_vma_walk, hmm_pfns,
npages, cpu_flags);
if (required_fault)
return hmm_vma_fault(addr, end, required_fault, walk);
/* 填充所有页面的 PFN */
pfn = pmd_pfn(pmd) + ((addr & ~PMD_MASK) >> PAGE_SHIFT);
for (i = 0; addr < end; addr += PAGE_SIZE, i++, pfn++) {
hmm_pfns[i] &= HMM_PFN_INOUT_FLAGS; // 保留粘性标志
hmm_pfns[i] |= pfn | cpu_flags; // 设置 PFN 和标志
}
return 0;
}
并发控制与同步
6.1 锁机制
HMM 使用读写锁保护页表遍历:
static const struct mm_walk_ops hmm_walk_ops = {
...
.walk_lock = PGWALK_RDLOCK, // 使用 mmap_lock 读锁
};
特点:
- 允许多个读者并发遍历
- 不阻塞页表更新(使用无锁读取)
- 通过序列号检测并发修改
6.2 无锁读取
pmd = pmdp_get_lockless(pmdp); // 无锁读取 PMD
6.3 序列号检测
do {
/* 检查序列号 */
if (mmu_interval_check_retry(range->notifier, range->notifier_seq))
return -EBUSY;
ret = walk_page_range(...);
} while (ret == -EBUSY);
6.4 内存屏障
/* 确保读取顺序 */
rmb(); // Read Memory Barrier
DMA 映射扩展
7.1 HMM-DMA 架构
HMM 提供了 DMA 映射的扩展接口,支持将 HMM PFN 映射到 DMA 地址:
struct hmm_dma_map {
size_t dma_entry_size; // 每个 DMA 条目的大小
unsigned long *pfn_list; // PFN 列表
dma_addr_t *dma_list; // DMA 地址列表
struct dma_iova_state state; // IOVA 状态
};
7.2 分配 DMA 映射
int hmm_dma_map_alloc(struct device *dev, struct hmm_dma_map *map,
size_t nr_entries, size_t dma_entry_size)
{
bool dma_need_sync = false;
bool use_iova;
/* 检查 DMA 限制 */
#ifdef CONFIG_DMA_NEED_SYNC
dma_need_sync = !dev->dma_skip_sync;
#endif
if (dma_need_sync || dma_addressing_limited(dev))
return -EOPNOTSUPP;
map->dma_entry_size = dma_entry_size;
/* 分配 PFN 列表 */
map->pfn_list = kvcalloc(nr_entries, sizeof(*map->pfn_list),
GFP_KERNEL | __GFP_NOWARN);
if (!map->pfn_list)
return -ENOMEM;
/* 尝试分配 IOVA */
use_iova = dma_iova_try_alloc(dev, &map->state, 0,
nr_entries * PAGE_SIZE);
/* 如果不使用 IOVA 且需要 unmap,分配 DMA 地址列表 */
if (!use_iova && dma_need_unmap(dev)) {
map->dma_list = kvcalloc(nr_entries, sizeof(*map->dma_list),
GFP_KERNEL | __GFP_NOWARN);
if (!map->dma_list)
goto err_dma;
}
return 0;
err_dma:
kvfree(map->pfn_list);
return -ENOMEM;
}
7.3 映射单个 PFN
dma_addr_t hmm_dma_map_pfn(struct device *dev, struct hmm_dma_map *map,
size_t idx, struct pci_p2pdma_map_state *p2pdma_state)
{
struct dma_iova_state *state = &map->state;
unsigned long *pfns = map->pfn_list;
struct page *page = hmm_pfn_to_page(pfns[idx]);
phys_addr_t paddr = hmm_pfn_to_phys(pfns[idx]);
size_t offset = idx * map->dma_entry_size;
dma_addr_t dma_addr;
/* 已映射?*/
if ((pfns[idx] & HMM_PFN_DMA_MAPPED) &&
!(pfns[idx] & HMM_PFN_P2PDMA_BUS)) {
if (dma_use_iova(state))
return state->addr + offset;
if (dma_need_unmap(dev))
return map->dma_list[idx];
}
/* 检查 P2P DMA */
switch (pci_p2pdma_state(p2pdma_state, dev, page)) {
case PCI_P2PDMA_MAP_THRU_HOST_BRIDGE:
pfns[idx] |= HMM_PFN_P2PDMA;
break;
case PCI_P2PDMA_MAP_BUS_ADDR:
pfns[idx] |= HMM_PFN_P2PDMA_BUS | HMM_PFN_DMA_MAPPED;
return pci_p2pdma_bus_addr_map(p2pdma_state, paddr);
default:
break;
}
/* 使用 IOVA */
if (dma_use_iova(state)) {
ret = dma_iova_link(dev, state, paddr, offset,
map->dma_entry_size, DMA_BIDIRECTIONAL, 0);
if (ret)
goto error;
dma_addr = state->addr + offset;
} else {
/* 普通 DMA 映射 */
dma_addr = dma_map_page(dev, page, 0, map->dma_entry_size,
DMA_BIDIRECTIONAL);
if (dma_mapping_error(dev, dma_addr))
goto error;
if (dma_need_unmap(dev))
map->dma_list[idx] = dma_addr;
}
pfns[idx] |= HMM_PFN_DMA_MAPPED;
return dma_addr;
error:
pfns[idx] &= ~HMM_PFN_P2PDMA;
return DMA_MAPPING_ERROR;
}
7.4 解除映射
bool hmm_dma_unmap_pfn(struct device *dev, struct hmm_dma_map *map, size_t idx)
{
const unsigned long valid_dma = HMM_PFN_VALID | HMM_PFN_DMA_MAPPED;
struct dma_iova_state *state = &map->state;
unsigned long *pfns = map->pfn_list;
if ((pfns[idx] & valid_dma) != valid_dma)
return false;
/* P2P 总线地址无需 unmap */
if (pfns[idx] & HMM_PFN_P2PDMA_BUS)
;
/* IOVA */
else if (dma_use_iova(state)) {
dma_iova_unlink(dev, state, idx * map->dma_entry_size,
map->dma_entry_size, DMA_BIDIRECTIONAL, 0);
}
/* 普通 DMA */
else if (dma_need_unmap(dev))
dma_unmap_page(dev, map->dma_list[idx], map->dma_entry_size,
DMA_BIDIRECTIONAL);
pfns[idx] &= ~(HMM_PFN_DMA_MAPPED | HMM_PFN_P2PDMA | HMM_PFN_P2PDMA_BUS);
return true;
}
性能优化策略
8.1 大页支持
HMM 通过 HMM_PFN_ORDER_SHIFT 编码页面大小:
static inline unsigned long hmm_pfn_flags_order(unsigned long order)
{
return order << HMM_PFN_ORDER_SHIFT;
}
// PMD 大页:order = PMD_SHIFT - PAGE_SHIFT (通常为 9,即 2MB)
// PUD 大页:order = PUD_SHIFT - PAGE_SHIFT (通常为 18,即 1GB)
优势:
- 减少页表遍历次数
- 减少 TLB miss
- 提高 DMA 效率
8.2 批量操作
static int hmm_pfns_fill(unsigned long addr, unsigned long end,
struct hmm_range *range, unsigned long cpu_flags)
{
unsigned long i = (addr - range->start) >> PAGE_SHIFT;
for (; addr < end; addr += PAGE_SIZE, i++) {
range->hmm_pfns[i] &= HMM_PFN_INOUT_FLAGS;
range->hmm_pfns[i] |= cpu_flags;
}
return 0;
}
8.3 无锁读取
pmd = pmdp_get_lockless(pmdp); // 避免锁竞争
8.4 IOVA 重用
对于支持 IOVA 的设备,可以重用 IOVA 空间,避免频繁的 TLB 刷新:
if (dma_use_iova(state))
return state->addr + offset; // 重用 IOVA
8.5 分段处理
为避免长时间持锁,HMM 支持分段处理大范围:
#define MAX_WALK_BYTE (2UL << 30) // 2GB
do {
hmm_range->end = min(hmm_range->start + MAX_WALK_BYTE, end);
// 处理当前段...
hmm_range->start = hmm_range->end;
} while (hmm_range->end < end);
总结
9.1 核心特性
- 统一地址空间:CPU 和设备共享虚拟地址
- 按需缺页:支持透明的页面换入/迁移
- 细粒度控制:页级别的权限和属性控制
- 高效同步:基于 MMU Notifier 的无锁同步
- 大页优化:支持 THP、Hugetlb 等大页
- DMA 集成:提供 DMA 映射扩展接口
9.2 适用场景
- GPU 计算:CUDA Unified Memory、OpenCL SVM
- 机器学习:大规模张量运算
- 数据库加速:FPGA/GPU 加速查询
- 视频处理:GPU 加速编解码
- 科学计算:异构并行计算
9.3 限制与约束
- 不支持 VM_IO/VM_PFNMAP:I/O 映射和纯物理映射不支持
- 需要 MMU Notifier:依赖 CONFIG_MMU_NOTIFIER
- 性能开销:页表遍历和同步有一定开销
- 内存碎片:频繁迁移可能导致碎片
- 调试复杂:并发问题难以调试
9.4 未来发展
- 更细粒度的迁移策略:基于访问模式的自适应迁移
- 更好的大页支持:自动大页合并和拆分
- 跨 NUMA 节点优化:优化跨节点访问性能
参考文档:
- Documentation/mm/hmm.rst
- include/linux/hmm.h
- mm/hmm.c
- LWN Articles on HMM
162

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



