AMD KFD的BO设计分析系列7-2:GPU GART 实现深度解析--绑定机制与性能优化

目录


GART的基本原理请查看:AMD KFD的BO设计分析系列7-1:GPU GART 实现深度解析–基础架构与工作原理

1. GART 绑定机制

1.1 绑定流程概览

ttm_tt_bind()
    ↓
amdgpu_ttm_backend_bind()
    ↓
amdgpu_gart_bind()              ← 本章重点
    ├─> 安全检查
    ├─> 记录映射(调试)
    ├─> amdgpu_gart_map()       ← 更新页表
    │   └─> amdgpu_gmc_set_pte_pde()  ← 硬件特定格式
    ├─> 内存屏障 mb()
    ├─> 刷新 HDP 缓存
    └─> 刷新所有 VM Hub TLB

1.2 amdgpu_gart_bind() 详解

int amdgpu_gart_bind(struct amdgpu_device *adev, uint64_t offset,
                     int pages, struct page **pagelist,
                     dma_addr_t *dma_addr, uint64_t flags)
{
    int r, i;

    // ========== 1. 安全检查 ==========
    if (!adev->gart.ready) {
        WARN(1, "trying to bind memory to uninitialized GART!\n");
        return -EINVAL;
    }

    // ========== 2. 记录映射关系(调试) ==========
#ifdef CONFIG_DRM_AMDGPU_GART_DEBUGFS
    unsigned t, p;
    t = offset / AMDGPU_GPU_PAGE_SIZE;
    p = t / AMDGPU_GPU_PAGES_IN_CPU_PAGE;
    for (i = 0; i < pages; i++, p++)
        adev->gart.pages[p] = pagelist ? pagelist[i] : NULL;
#endif

    // ========== 3. 页表未映射则跳过 ==========
    if (!adev->gart.ptr)
        return 0;

    // ========== 4. 更新页表 ==========
    r = amdgpu_gart_map(adev, offset, pages, dma_addr, flags,
                        adev->gart.ptr);

    // ========== 5. 内存屏障 ==========
    mb();  // 确保所有 PTE 写入完成

    // ========== 6. 刷新 HDP 缓存 ==========
    amdgpu_asic_flush_hdp(adev, NULL);

    // ========== 7. 刷新所有 VM Hub 的 TLB ==========
    for (i = 0; i < adev->num_vmhubs; i++)
        amdgpu_gmc_flush_gpu_tlb(adev, 0, i, 0);

    return 0;
}

1.3 参数详解

参数类型含义示例
adevstruct amdgpu_device *GPU 设备-
offsetuint64_tGART 起始偏移(字节)0x80000000
pagesint要绑定的 CPU 页数256 (1MB)
pageliststruct page **CPU 页面指针数组ttm->pages
dma_addrdma_addr_t *DMA 地址数组ttm_dma->dma_address
flagsuint64_tPTE 标志位READ|WRITE|WC

offset 计算示例:

// ttm_tt 绑定到 GART
struct amdgpu_ttm_tt *gtt = ...;
uint64_t offset = (u64)bo_mem->start << PAGE_SHIFT;

// bo_mem->start 是页面索引
// 左移 PAGE_SHIFT 得到字节偏移
// 例如: start=2048, PAGE_SHIFT=12 → offset=0x800000 (8MB)

1.4 amdgpu_gart_map() - 页表填充

int amdgpu_gart_map(struct amdgpu_device *adev, uint64_t offset,
                    int pages, dma_addr_t *dma_addr, uint64_t flags,
                    void *dst)
{
    uint64_t page_base;
    unsigned t, i;

    // 转换为 GPU 页索引
    t = offset / AMDGPU_GPU_PAGE_SIZE;

    // 遍历每个 CPU 页
    for (i = 0; i < pages; i++) {
        page_base = dma_addr[i];  // CPU 页的 DMA 地址

        // 一个 CPU 页可能包含多个 GPU 页
        // 例如:64KB CPU 页 = 16 个 4KB GPU 页
        for (unsigned j = 0; j < AMDGPU_GPU_PAGES_IN_CPU_PAGE; j++) {
            // 调用硬件特定函数设置 PTE
            amdgpu_gmc_set_pte_pde(adev, dst, t,
                                   page_base, flags);
            
            page_base += AMDGPU_GPU_PAGE_SIZE;  // 下一个 GPU 页
            t++;  // 下一个 PTE 索引
        }
    }
    return 0;
}

处理 CPU 页 vs GPU 页差异:

CPU 页 (64KB):
┌────────────────────────────────────────────────┐
│         DMA Address: 0xF0000000                │
└────────────────────────────────────────────────┘
                    ↓ 拆分为 16 个 GPU 页
GPU 页 (4KB 每个):
┌─────┬─────┬─────┬─────┬───┬─────┐
│ 0K  │ 4K  │ 8K  │ 12K │...│ 60K │
└─────┴─────┴─────┴─────┴───┴─────┘
  ↓     ↓     ↓     ↓         ↓
PTE[t] PTE[t+1] ...        PTE[t+15]

每个 PTE 存储:
PTE[t+0] = 0xF0000000 | flags
PTE[t+1] = 0xF0001000 | flags
PTE[t+2] = 0xF0002000 | flags
...
PTE[t+15] = 0xF000F000 | flags

1.5 amdgpu_gmc_set_pte_pde() - 硬件 PTE 格式

void amdgpu_gmc_set_pte_pde(struct amdgpu_device *adev,
                            void *cpu_pt_addr,
                            uint32_t gpu_page_idx,
                            uint64_t addr,
                            uint64_t flags)
{
    uint64_t *pte = (uint64_t *)cpu_pt_addr;
    
    // 组合物理地址和标志位
    pte[gpu_page_idx] = (addr & AMDGPU_PTE_ADDR_MASK) | flags;
}

PTE 格式(64位):

 63                                                    12 11        0
┌──────────────────────────────────────────────────────┬──────────┐
│         Physical Address (52 bits)                   │  Flags   │
│         页对齐 (低 12 位为 0)                          │ (12 bits)│
└──────────────────────────────────────────────────────┴──────────┘

标志位详解:

位域名称含义
[0]VALIDPTE 有效1
[1]READABLE可读1
[2]WRITEABLE可写1
[3]EXECUTE可执行(某些 GPU)0/1
[4:5]CACHE缓存策略00=UC, 01=WC, 10=Cached
[6]SNOOPEDCache coherent0/1
[7]SYSTEM系统内存1
[8:11]FRAGFragment size0-9 (4KB-2GB)

标志位计算示例:

uint64_t amdgpu_ttm_tt_pte_flags(struct amdgpu_device *adev,
                                 struct ttm_tt *ttm,
                                 struct ttm_mem_reg *mem)
{
    uint64_t flags = 0;
    
    // 基础权限
    flags |= AMDGPU_PTE_VALID;      // 位 0
    flags |= AMDGPU_PTE_READABLE;   // 位 1
    
    if (ttm->page_flags & TTM_PAGE_FLAG_WRITE)
        flags |= AMDGPU_PTE_WRITEABLE;  // 位 2
    
    // 缓存策略
    switch (ttm->caching_state) {
    case tt_cached:
        flags |= AMDGPU_PTE_SNOOPED;  // Cache coherent
        break;
    case tt_wc:
        flags |= AMDGPU_PTE_WC;       // Write-combine
        break;
    case tt_uncached:
        // 无额外标志,默认 uncached
        break;
    }
    
    // 系统内存标记
    flags |= AMDGPU_PTE_SYSTEM;
    
    return flags;
}

实际 PTE 值示例:

物理地址: 0x00000000F0123000
标志位:   VALID | READABLE | WRITEABLE | WC | SYSTEM
         = 0x00000007

PTE = 0x00000000F0123000 | 0x00000007
    = 0x00000000F0123007

GPU 读取此 PTE 后:
- 访问 GPU VA → 转换到物理地址 0xF0123000
- 权限: 可读可写
- 缓存: Write-combine 模式

1.6 内存屏障与缓存刷新

(1) mb() - 内存屏障
mb();  // Memory Barrier

作用:

  • 确保之前的所有内存写入完成
  • 防止 CPU 乱序执行导致 PTE 未写入就刷新 TLB
  • x86 编译为 mfence 指令

必要性示例:

// 错误:无内存屏障
pte[0] = addr0 | flags;
pte[1] = addr1 | flags;
// CPU 可能乱序执行,先刷新 TLB
flush_tlb();  // PTE 可能还在 CPU cache

// 正确:有内存屏障
pte[0] = addr0 | flags;
pte[1] = addr1 | flags;
mb();  // 强制完成所有写入
flush_tlb();  // PTE 已写入内存
(2) amdgpu_asic_flush_hdp() - HDP 刷新
amdgpu_asic_flush_hdp(adev, NULL);

HDP (Host Data Path):

  • CPU 访问 VRAM 的数据路径
  • 有自己的写缓存
  • 需要显式刷新确保 VRAM 可见

工作流程:

CPU 写入 PTE
    ↓
CPU Cache
    ↓ (mb() 刷新)
HDP Write Cache  ← 仍在这里
    ↓ (flush_hdp() 刷新)
VRAM (页表)
    ↓
GPU 可见

实现(硬件特定):

void amdgpu_asic_flush_hdp(struct amdgpu_device *adev,
                           struct amdgpu_ring *ring)
{
    if (adev->asic_funcs->flush_hdp)
        adev->asic_funcs->flush_hdp(adev, ring);
    
    // 典型实现:
    // WREG32(mmHDP_MEM_COHERENCY_FLUSH_CNTL, 1);
    // 等待完成
}

2. GART 解绑机制

2.1 解绑流程

int amdgpu_gart_unbind(struct amdgpu_device *adev, uint64_t offset, int pages)
{
    unsigned t, p, i;

    // ========== 1. 安全检查 ==========
    if (!adev->gart.ready)
        return -EINVAL;

    // ========== 2. 清除调试数组 ==========
#ifdef CONFIG_DRM_AMDGPU_GART_DEBUGFS
    t = offset / AMDGPU_GPU_PAGE_SIZE;
    p = t / AMDGPU_GPU_PAGES_IN_CPU_PAGE;
    for (i = 0; i < pages; i++, p++)
        adev->gart.pages[p] = NULL;
#endif

    // ========== 3. 页表未映射则跳过 ==========
    if (!adev->gart.ptr)
        return 0;

    // ========== 4. 用 dummy page 填充 PTE ==========
    t = offset / AMDGPU_GPU_PAGE_SIZE;
    for (i = 0; i < pages; i++) {
        for (unsigned j = 0; j < AMDGPU_GPU_PAGES_IN_CPU_PAGE; j++) {
            amdgpu_gmc_set_pte_pde(adev, adev->gart.ptr, t,
                                   adev->dummy_page_addr,
                                   AMDGPU_PTE_VALID | AMDGPU_PTE_READABLE);
            t++;
        }
    }

    // ========== 5. 刷新机制(同绑定) ==========
    mb();
    amdgpu_asic_flush_hdp(adev, NULL);
    for (i = 0; i < adev->num_vmhubs; i++)
        amdgpu_gmc_flush_gpu_tlb(adev, 0, i, 0);

    return 0;
}

2.2 为什么用 Dummy Page?

对比方案:

方案PTE 值GPU 访问行为优缺点
清零 PTE0x0 (Invalid)页面错误/崩溃GPU hang 风险
Dummy Pagedummy_addr | VALID读到零值安全降级
保留旧映射原地址读到已释放页内存安全问题

实际场景:

// BO 解绑过程中 GPU 可能仍在访问
ttm_tt_unbind(ttm);amdgpu_gart_unbind();  // 用 dummy page 填充
    ↓
GPU 仍在执行着色器,访问已解绑地址
    ↓
读到 dummy page (全零) → 安全
    vs.
读到无效地址 → GPU hang/崩溃 ❌

Dummy Page 标志:

flags = AMDGPU_PTE_VALID | AMDGPU_PTE_READABLE;
// 注意:
// - VALID: GPU 不会产生页面错误
// - READABLE: 允许读取(返回零)
// - 无 WRITEABLE: 写操作被忽略或产生错误(取决于硬件)

3. TLB 管理

3.1 TLB 概述

TLB (Translation Lookaside Buffer):

  • GPU 的页表缓存
  • 存储最近使用的 PTE
  • 避免每次访问都查页表

层级结构:

GPU 内存访问
    ↓
L1 TLB (每个 CU/Shader Engine)
    ↓ Miss
L2 TLB (每个 VM Hub)
    ↓ Miss
Page Table Walk (访问 VRAM 中的页表)
    ↓
获取 PTE

3.2 VM Hub 架构

什么是 VM Hub?

  • GPU 有多个独立的虚拟内存管理单元
  • 每个引擎有自己的 Hub 和 TLB

典型配置(RDNA/CDNA):

Hub ID名称用途TLB 大小
0GFX Hub图形引擎256 entries
1MM Hub多媒体(视频编解码)128 entries
2VC HubVCN (视频编码)64 entries

为什么需要多个 Hub?

  • 并行处理:图形和视频可同时运行
  • 隔离性:不同引擎的内存访问互不干扰
  • 性能:每个 Hub 专门优化

3.3 TLB 刷新机制

void amdgpu_gmc_flush_gpu_tlb(struct amdgpu_device *adev,
                              uint32_t vmid,
                              uint32_t vmhub,
                              uint32_t flush_type)
{
    // 调用硬件特定函数
    if (adev->gmc.gmc_funcs->flush_gpu_tlb)
        adev->gmc.gmc_funcs->flush_gpu_tlb(adev, vmid, vmhub, flush_type);
}

参数说明:

参数含义典型值
vmid虚拟机 ID(上下文 ID)0-15
vmhubVM Hub 索引0=GFX, 1=MM
flush_type刷新类型0=全部, 1=部分

硬件实现示例(MMIO 寄存器):

// Vega/RDNA 系列
static void gmc_v9_0_flush_gpu_tlb(struct amdgpu_device *adev,
                                   uint32_t vmid,
                                   uint32_t vmhub,
                                   uint32_t flush_type)
{
    // 1. 准备刷新命令
    uint32_t req = VM_INVALIDATE_REQ;
    
    // 2. 写入刷新请求寄存器
    WREG32_NO_KIQ(hub->vm_inv_eng0_req + eng, req);
    
    // 3. 等待确认
    for (i = 0; i < adev->usec_timeout; i++) {
        ack = RREG32_NO_KIQ(hub->vm_inv_eng0_ack + eng);
        if (ack & (1 << vmid))
            break;
        udelay(1);
    }
    
    if (i >= adev->usec_timeout)
        DRM_ERROR("Timeout waiting for VM flush ACK!\n");
}

3.4 TLB 刷新时机

操作是否刷新 TLB原因
绑定✅ 是新 PTE 需要可见
解绑✅ 是旧 PTE 需要失效
修改标志✅ 是PTE 内容改变
只读 PTE❌ 否无状态变化
GPU 重置✅ 是清空所有缓存

刷新开销:

单次刷新时间:   ~10-50 微秒
影响范围:       停止所有内存访问
批量优化:       合并多次刷新 → 1 次

3.5 批量刷新优化

问题:

// 绑定 1000 个页面
for (i = 0; i < 1000; i++) {
    amdgpu_gart_bind(adev, offset + i * PAGE_SIZE, 1, ...);
    // 每次都刷新 TLB → 1000 次刷新!
}

优化:

// 批量绑定
amdgpu_gart_bind(adev, offset, 1000, ...);
// 只刷新一次 TLB → 性能提升 100x

实现机制:

int amdgpu_gart_bind(...)
{
    // 更新所有 PTE
    for (i = 0; i < pages; i++)
        update_pte(i);
    
    mb();  // 确保所有 PTE 写入完成
    
    // 只刷新一次
    flush_tlb();  // ← 批量操作的关键
}

4. 性能优化

4.1 地址转换延迟

TLB 命中 vs 未命中:

场景延迟说明
L1 TLB 命中1-2 cycles最快
L2 TLB 命中10-20 cycles较快
Page Table Walk100-200 cycles访问 VRAM
VRAM 页表在系统内存1000+ cycles极慢(不应发生)

影响:

高 TLB 命中率 (95%+):
  平均延迟 = 0.95 * 2 + 0.05 * 100 = 6.9 cycles 

低 TLB 命中率 (50%):
  平均延迟 = 0.50 * 2 + 0.50 * 100 = 51 cycles 

4.2 优化策略对比表

优化方法实现性能提升适用场景难度
大页支持使用 2MB/1GB 页10-100x TLB 效率大型连续 BO
批量刷新合并 TLB 刷新5-10x 绑定速度多页面绑定
预取优化连续地址访问2-3x 带宽利用线性访问模式硬件自动
TLB 预热提前绑定热点数据减少首次访问延迟已知访问模式
GART 分区分离不同用途区域减少冲突多引擎并行

4.3 大页支持详解

页面大小对比:

页大小TLB Entry 覆盖需要的 Entry 数(1GB 数据)
4KB4KB262,144
2MB2MB512
1GB1GB1

PTE Fragment 字段:

// 在 PTE 标志中指定
flags |= AMDGPU_PTE_FRAG(frag_size);

// frag_size 编码:
// 0 = 4KB (2^12)
// 1 = 8KB (2^13)
// 2 = 16KB (2^14)
// ...
// 9 = 2MB (2^21)

启用大页的条件:

  1. CPU 页大小 >= 目标大页大小
  2. 物理地址对齐到大页边界
  3. 连续的物理内存

实现示例:

// 检查是否可用 2MB 页
if (num_pages >= 512 &&  // 至少 2MB
    (dma_addr & 0x1FFFFF) == 0 &&  // 2MB 对齐
    is_contiguous(dma_addr, 512)) {  // 连续
    
    flags |= AMDGPU_PTE_FRAG(9);  // 使用 2MB 页
    // 只需 1 个 PTE 覆盖 512 个 4KB 页
}

4.4 GART 地址空间布局优化

典型布局(优化后):

GPU 虚拟地址空间:
┌─────────────────────────────┐ 0x0000000000000000
│   保留区域 (1GB)             │
├─────────────────────────────┤ 0x0000000040000000
│   GART - 图形专用 (4GB)      │ ← GFX Hub 主要区域
│   - Staging buffers         │   TLB 命中率高
│   - 纹理溢出                 │
├─────────────────────────────┤ 0x0000000140000000
│   GART - 计算专用 (4GB)      │ ← Compute 隔离区域
│   - Userptr                 │   减少 TLB 冲突
│   - Compute 输入/输出        │
├─────────────────────────────┤ 0x0000000240000000
│   GART - 视频专用 (2GB)      │ ← MM Hub 专用
│   - DMA-BUF                 │
│   - 视频帧缓冲                │
├─────────────────────────────┤ 0x00000002C0000000
│   AGP Aperture (可选)        │
├─────────────────────────────┤ 0x0000000300000000
│   VRAM Aperture             │ ← 显存直接映射
│   (物理 VRAM 大小)           │   无 TLB 转换
└─────────────────────────────┘

分区优势:

  • 不同引擎访问不同区域 → 减少 TLB 冲突
  • 专用区域可预热 TLB
  • 便于监控和调试

4.5 性能监控

关键指标:

指标正常值异常值问题
TLB 命中率>95%<80%访问模式分散
Page Walk 次数<5% 访问>20%TLB 太小或抖动
GART 利用率<80%>95%接近耗尽
绑定延迟<1ms/1000页>10ms可能碎片化

5. 调试与监控

5.1 Debugfs 接口

# 查看 GART 信息
cat /sys/kernel/debug/dri/0/amdgpu_gtt_mm

# 输出示例:
# GART: size=16384MB, used=2048MB, free=14336MB
# num_gpu_pages: 4194304
# num_cpu_pages: 4194304
# table_size: 33554432 (32MB)

详细映射信息(CONFIG_DRM_AMDGPU_GART_DEBUGFS):

cat /sys/kernel/debug/dri/0/amdgpu_gart_pages | head -20

# 输出:
# [0] 0xF0000000 (CPU page 0x...)
# [1] 0xF0001000 (CPU page 0x...)
# [2] NULL (unmapped)

5.2 Trace Points

# 启用 GART 绑定跟踪
echo 1 > /sys/kernel/debug/tracing/events/amdgpu/amdgpu_gart_bind/enable

# 运行应用
./my_gpu_app

# 查看跟踪日志
cat /sys/kernel/debug/tracing/trace

# 示例输出:
# amdgpu_gart_bind: offset=0x80000000 pages=256 flags=0x7
# amdgpu_gart_bind: offset=0x80100000 pages=512 flags=0x7

5.3 常见问题诊断

问题 1: “GART bind failed”

原因:

  • GART 未初始化(ready=false)
  • DMA 地址无效
  • 页表未映射

诊断:

dmesg | grep -i gart

# 查找:
# - "GART: num cpu pages ..." (初始化日志)
# - "Failed to allocate GART table" (分配失败)
# - "trying to bind memory to uninitialized GART" (未就绪)
问题 2: GPU Hang (TLB 相关)

症状:

  • GPU 访问违例
  • dmesg 显示 “VM fault”

诊断:

# 查看 VM fault 日志
dmesg | grep -i "vm fault"

# 输出示例:
# [drm:amdgpu_job_timedout] *ERROR* VM fault (0x01) at page 0x80001234
# 
# 分析:
# - 地址 0x80001234 在 GART 范围内
# - 检查该地址是否已绑定
# - 可能是 TLB 未刷新或 PTE 错误

验证 TLB 刷新:

// 添加调试日志
printk("Flushing TLB for vmhub %d\n", i);
amdgpu_gmc_flush_gpu_tlb(adev, 0, i, 0);
printk("TLB flush completed\n");
问题 3: 性能下降

诊断步骤:

  1. 检查 TLB 命中率(需要性能计数器)

  2. 查看 GART 使用率

    cat /sys/kernel/debug/dri/0/amdgpu_gtt_mm
    # 如果 used > 95%,可能频繁换页
    
  3. 监控绑定频率

    # 统计 1 秒内的绑定次数
    perf record -e probe:amdgpu_gart_bind -a sleep 1
    perf report
    

5.4 调试技巧

技巧 1: 验证 PTE 内容

void dump_pte(struct amdgpu_device *adev, uint64_t offset)
{
    uint64_t *pte = (uint64_t *)adev->gart.ptr;
    unsigned idx = offset / AMDGPU_GPU_PAGE_SIZE;
    
    printk("PTE[%u] = 0x%016llx\n", idx, pte[idx]);
    printk("  Physical: 0x%016llx\n", pte[idx] & ~0xFFF);
    printk("  Valid: %d\n", !!(pte[idx] & AMDGPU_PTE_VALID));
    printk("  Readable: %d\n", !!(pte[idx] & AMDGPU_PTE_READABLE));
    printk("  Writeable: %d\n", !!(pte[idx] & AMDGPU_PTE_WRITEABLE));
}

技巧 2: 强制 TLB 刷新测试

// 测试 TLB 刷新是否有效
amdgpu_gart_bind(...);
// 故意不刷新 TLB
// 然后访问 → 应该失败(证明 TLB 缓存了旧值)

amdgpu_gart_bind(...);
flush_all_tlb();  // 强制刷新
// 访问 → 应该成功

技巧 3: 监控 HDP 刷新

// 测试 HDP 缓存影响
write_pte(...);
// 不刷新 HDP
read_from_gpu();  // 可能读到旧 PTE

write_pte(...);
flush_hdp();
read_from_gpu();  // 应该读到新 PTE

小结

关键要点

  1. 绑定机制

    • 三层函数:bind → map → set_pte_pde
    • 必须刷新 mb、HDP、TLB
    • 处理 CPU 页 vs GPU 页差异
  2. TLB 管理

    • 多个 VM Hub,各自独立 TLB
    • 刷新开销大,需要批量优化
    • 命中率直接影响性能
  3. 性能优化

    • 大页减少 TLB 压力 10-100x
    • 批量刷新减少开销 5-10x
    • 地址空间分区减少冲突
  4. 调试方法

    • Debugfs 查看状态
    • Trace points 跟踪操作
    • 性能计数器监控 TLB
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DeeplyMind

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值