深入理解PX4飞控系统:多线程并发、原子操作与单例模式完全指南

深入理解PX4飞控系统:多线程并发、原子操作与单例模式完全指南

前言

在学习PX4飞控源码时,你是否遇到过这样的困惑:

std::atomic<MulticopterRateControl*> MulticopterRateControl::_object{nullptr};

MulticopterRateControl *instance = new MulticopterRateControl(vtol);
if (instance) {
    _object.store(instance);
    _task_id = task_id_is_work_queue;
    if (instance->init()) {
        return PX4_OK;
    }
}
  • 为什么要用std::atomic
  • 为什么要在堆上创建实例?
  • 什么是单例模式?为什么需要它?
  • PX4的多线程是如何工作的?
  • 什么时候需要原子操作?

本文将系统性地解答这些问题,带你深入理解PX4的并发控制架构。适合有C++基础、正在学习PX4源码或从事飞控开发的工程师阅读。

关键词:PX4、多线程、原子操作、单例模式、线程安全、飞控系统、并发控制


目录

  1. 线程的本质:破除常见误区
  2. 多线程并发问题详解
  3. 原子操作:硬件级的并发保护
  4. 原子操作的硬件实现原理
  5. 单例模式深度剖析
  6. PX4的线程安全策略
  7. 完整的启动流程分析
  8. 什么时候需要原子操作
  9. 常见误区与最佳实践
  10. 总结与核心要点

1. 线程的本质:破除常见误区

1.1 常见误区澄清

很多初学者有这样的误解:

错误理解

  • 一个.cpp文件 = 一个线程
  • 一个类 = 一个线程
  • 一个函数 = 一个线程

正确理解

  • 线程是执行者(就像厨师)
  • 代码是指令(就像菜谱)
  • 多个线程可以执行同一份代码(多个厨师用同一本菜谱)

1.2 生活类比

想象一个厨房做饭的场景:

┌─────────────────────────────────────┐
│ 厨房 = 程序                          │
│ 菜谱(cpp文件) = 代码文件             │
│ 菜谱中的步骤(函数) = 具体操作        │
│ 厨师 = 线程(执行者)                  │
└─────────────────────────────────────┘

一个厨师(线程)可以:
✓ 读多本菜谱(多个cpp文件)
✓ 执行多个步骤(多个函数)
✓ 炒菜、切菜、煮汤(调用不同函数)

多个厨师(多线程)可能:
⚠ 同时操作同一个锅(共享变量) → 竞态条件!
⚠ 一个在加盐,另一个在加糖 → 数据竞争!

1.3 PX4中的实际例子

// mc_rate_control.cpp 文件

class MulticopterRateControl : public ModuleBase<MulticopterRateControl> {
private:
    // 静态成员变量(所有线程共享!)
    static std::atomic<MulticopterRateControl*> _object;
    
public:
    void Run();              // 工作队列线程定期调用
    int custom_command();    // 命令行线程调用
    static int task_spawn(); // 主线程调用
};

关键认识

  • 一个cpp文件中的代码被多个不同线程执行
  • 同一个函数可能被多个线程调用
  • 所有线程访问同一个共享变量(_object)

2. 多线程并发问题详解

2.1 什么是并发问题?

当多个线程同时访问共享资源时,如果没有适当的同步机制,就会出现数据竞争(Data Race)。

2.2 真实场景:同时启动冲突

// 场景:两个终端同时输入 "mc_rate_control start"

// 时刻T0:两个线程同时执行 task_spawn()

// 线程A(主线程)执行:
int task_spawn() {
    if (_object.load() == nullptr) {  // ✓ 检查通过(nullptr)
        auto instance = new MC_RateControl();  // 创建实例A
        _object.store(instance);  // 存储实例A
    }
}

// 线程B(初始化线程)同时执行:
int task_spawn() {
    if (_object.load() == nullptr) {  // ✓ 检查通过(nullptr) 
        auto instance = new MC_RateControl();  // 创建实例B
        _object.store(instance);  // 存储实例B (覆盖实例A!)
    }
}

// 灾难性结果:
// ❌ 实例A内存泄漏(没人持有指针了)
// ❌ 创建了两个控制器实例(违反单例模式)
// ❌ 两个控制器同时争夺飞机控制权 → 飞机崩溃!

2.3 场景可视化

时间轴 →

线程A(主线程):     [启动]                    [检查状态]
                      ↓                          ↓
                  task_spawn()              custom_command()
                      ↓                          ↓
                  创建实例A                  读取_object
                      ↓                          
                  _object.store()            

线程B(工作队列):        [空闲]  [执行]  [执行]  [执行] ...
                                   ↓       ↓       ↓
                               Run()   Run()   Run()
                                   ↓       ↓       ↓
                            读取_object 读取_object

线程C(命令行):                        [用户输入]
                                          ↓
                                    custom_command()
                                          ↓
                                     读取_object

共享变量_object:   [null]  [ptr]  [ptr]  [ptr]  [ptr]
                          ↑
                      ⚠ 所有线程竞争访问这里!

3. 原子操作:硬件级的并发保护

3.1 原子操作的定义

原子操作(Atomic Operation):一个不可分割、不可中断的操作,要么完全执行,要么完全不执行,不存在"执行到一半"的中间状态。

3.2 生活类比:ATM取钱

非原子操作(危险):
1. 检查余额: 1000元
2. 扣除金额: 1000 - 100 = 900元  ← ⚡这时断电!
3. 吐出钞票: (未执行)
结果: 钱被扣了,但没拿到现金!

原子操作(安全):
整个取钱过程是一个不可分割的整体
要么: 扣钱+吐钱都完成
要么: 完全不执行

3.3 CPU指令级的问题

// 看似简单的一行代码
counter = counter + 1;

// 实际编译成3条CPU指令:
LOAD  temp, counter    // 1. 从内存读取到寄存器
ADD   temp, temp, 1    // 2. 寄存器中加1
STORE counter, temp    // 3. 写回内存

// 多线程交错执行:
线程A: LOAD  (读到0)
线程B: LOAD  (也读到0)
线程A: ADD   (计算得1)
线程B: ADD   (计算得1)
线程A: STORE (写入1)
线程B: STORE (写入1)  ← 覆盖了A的结果!

// 期望: counter = 2
// 实际: counter = 1 ❌

3.4 原子操作的使用

#include <atomic>

// 普通变量(非原子)
int normal_counter = 0;

// 原子变量
std::atomic<int> atomic_counter{0};

// 普通指针(非原子)
MulticopterRateControl *normal_ptr = nullptr;

// 原子指针(PX4中使用)
std::atomic<MulticopterRateControl*> _object{nullptr};

// 原子操作示例
_object.store(instance);     // 原子存储
auto ptr = _object.load();   // 原子加载
atomic_counter.fetch_add(1); // 原子递增

4. 原子操作的硬件实现原理

4.1 三大核心技术

技术1:总线锁定(Bus Lock)
CPU多核架构:

        CPU核心0              CPU核心1
           |                     |
        L1缓存                L1缓存
           |                     |
        L2缓存                L2缓存
           |_____________________|
                    |
              内存总线 ← 🔒 这里加锁!
                    |
                主内存
                
原子操作时:
1. CPU核心0执行 _object.store()
2. 🔒 锁定内存总线 (LOCK信号)
3. 其他核心被阻塞,无法访问该地址
4. 写入完成后释放总线
5. 其他核心才能继续访问

实际效果

// 线程A (CPU核心0)执行:
_object.store(instance);
// 硬件锁定总线 → 独占访问内存

// 线程B (CPU核心1)同时尝试:
auto ptr = _object.load();
// 被硬件阻塞,必须等待线程A完成
// 不可能读到"一半被写入"的数据
技术2:内存屏障(Memory Barrier)

问题:CPU指令重排序

// 代码顺序
instance = new MulticopterRateControl();  // 步骤1
instance->init();                         // 步骤2
_object = instance;                       // 步骤3

// CPU可能优化成:
_object = instance;                       // 步骤3提前!
instance = new MulticopterRateControl();  // 步骤1
instance->init();                         // 步骤2

// 其他线程可能看到:
auto ptr = _object;  // 读到了指针
ptr->Run();          // 但对象还没创建完! 💥崩溃!

原子操作的解决方案:

// 原子操作自带内存屏障
instance = new MulticopterRateControl();
instance->init();
_object.store(instance);  // ← 这里插入内存屏障

// 编译成汇编:
// ... 创建对象的指令 ...
// ... init()的指令 ...
DMB ISH           // 🛡️ Data Memory Barrier: 内存屏障指令
STLR x0, [x1]     // 原子存储

// 保证:
// ✓ store之前的所有指令都已完成
// ✓ 不会重排序到store之后
// ✓ 其他核心看到的顺序是正确的
技术3:缓存一致性(Cache Coherence)

问题:多核缓存不一致

初始状态: _object = nullptr

CPU核心0:                    CPU核心1:
L1缓存: _object = nullptr    L1缓存: _object = nullptr
   ↓                            ↓
执行: _object = 0x1234        执行: auto p = _object
L1缓存: _object = 0x1234      L1缓存: _object = nullptr  ← 读到旧值!
主内存: _object = 0x1234      (还没刷新缓存)

原子操作触发缓存同步:

// CPU核心0执行:
_object.store(instance);  // 地址0x1234

// 硬件自动执行:
// 1. 写入核心0的L1缓存
// 2. 标记该缓存行为"已修改"(Modified)
// 3. 通过缓存一致性协议(MESI)发送消息
// 4. 使其他所有核心的该缓存行失效
// 5. 写入主内存

// CPU核心1执行:
auto ptr = _object.load();

// 硬件自动执行:
// 1. 检查L1缓存 → 发现"已失效"
// 2. 从主内存重新加载 → 读到0x1234
// 3. 更新L1缓存

4.2 ARM汇编实现对比

// C++代码
_object.store(instance);

// 编译后的ARM汇编(原子指令)
STLR  x0, [x1]    // STore-reLease: 原子存储指令
                  // 硬件保证: 整个操作不可被中断
                  // 自带内存屏障和缓存同步

// 对比普通指令(非原子)
STR   x0, [x1]    // 普通存储: 可能被中断、重排序、缓存不一致

5. 单例模式深度剖析

5.1 什么是单例模式?

单例(Singleton) = 全局只有一个实例的类

// 飞机上的飞控系统:
✓ 只能有一个姿态控制器 (不能同时有两个争夺控制权)
✓ 只能有一个位置控制器
✓ 只能有一个电机混控器

如果有两个姿态控制器:
控制器A: "向左倾斜10度!"
控制器B: "向右倾斜15度!"
飞机: "我该听谁的?" → 💥崩溃!

5.2 静态实例指针的作用

class MulticopterRateControl {
private:
    // 【静态】= 属于类,不属于某个对象
    // 【实例指针】= 指向这个类的唯一实例
    static std::atomic<MulticopterRateControl*> _object;
    //     ↑                ↑                      ↑
    //   静态成员        类型(指针)            变量名
};
为什么需要"静态"?
// ❌ 非静态成员变量(错误)
class RateController {
    MulticopterRateControl *_instance;  // 非静态成员
    
    // 问题: 这个变量属于"某个RateController对象"
    // 每创建一个对象,就有一个独立的_instance
};

RateController ctrl1;  // ctrl1有自己的_instance
RateController ctrl2;  // ctrl2有自己的_instance
// 无法实现"全局唯一"!


// ✅ 静态成员变量(正确)
class MulticopterRateControl {
    static MulticopterRateControl *_object;  // 静态成员
    
    // 这个变量属于"整个类",不属于任何对象
    // 无论创建多少个对象,_object只有一份
};

// 即使不创建任何对象,也可以访问
MulticopterRateControl::_object;  // 通过类名直接访问
为什么需要"指针"?
// ❌ 静态对象(不灵活)
class RateController {
    static MulticopterRateControl _object;  // 静态对象
};
// 问题:
// 1. 对象在程序启动时就创建(可能还不需要)
// 2. 无法控制创建时机
// 3. 占用内存即使不使用


// ✅ 静态指针(灵活)
class MulticopterRateControl {
    static MulticopterRateControl *_object;  // 静态指针
};
// 优势:
// 1. 初始状态: _object = nullptr (不占用内存)
// 2. 需要时才创建: _object = new MC()
// 3. 不需要时可以销毁: delete _object; _object = nullptr;
// 4. 可以检查是否存在: if (_object != nullptr)

5.3 完整的单例实现

// src/modules/mc_rate_control/MulticopterRateControl.hpp
class MulticopterRateControl : public ModuleBase<MulticopterRateControl> {
private:
    // 1️⃣ 静态实例指针(唯一的实例)
    static std::atomic<MulticopterRateControl*> _object;
    
    // 2️⃣ 私有构造函数(防止外部创建实例)
    MulticopterRateControl(bool vtol = false);
    
    // 3️⃣ 禁止拷贝(防止复制出多个实例)
    MulticopterRateControl(const MulticopterRateControl&) = delete;
    MulticopterRateControl& operator=(const MulticopterRateControl&) = delete;
    
public:
    // 4️⃣ 静态创建方法(控制实例创建)
    static int task_spawn(int argc, char *argv[]);
    
    // 5️⃣ 静态访问方法(获取实例)
    static MulticopterRateControl* instantiate(int argc, char *argv[]) {
        return _object.load();
    }
    
    // 6️⃣ 静态销毁方法(控制实例销毁)
    static int task_stop();
};

// 定义静态成员
std::atomic<MulticopterRateControl*> 
    MulticopterRateControl::_object{nullptr};

5.4 何时需要单例模式?

✅ 需要单例的场景
// 场景A: 硬件资源的唯一性
class GPSDriver {
    int _serial_fd;  // 串口文件描述符
    
    // 如果两个驱动同时读同一个串口:
    // 驱动A读到: $GPGGA,12345...
    // 驱动B读到: 6.78,N,123.45...
    // 数据被两个驱动分割,都无法解析! ❌
};

// 场景B: 全局状态管理
class ParameterManager {
    std::map<std::string, float> _params;
    
    // 如果有多个ParameterManager:
    // 实例A: MC_ROLL_P = 6.5
    // 实例B: MC_ROLL_P = 5.0
    // 控制器读取哪个? 配置不一致! ❌
};

// 场景C: 资源协调
class ActuatorMixer {
    void mix_and_output() {
        // 协调所有电机输出
        // 如果有两个混控器同时输出:
        // 电机接收到冲突指令! 危险! ❌
    }
};

6. PX4的线程安全策略

6.1 工作队列架构(主要模式)

PX4大部分控制器使用工作队列(Work Queue)架构,这是一种天然避免并发问题的设计。

// 工作队列特点:单线程执行,无并发冲突

WorkQueue wq_rate_ctrl("wq:rate_ctrl");

class MulticopterRateControl : public WorkItem {
private:
    // ❌ 这些变量都不需要原子保护
    float _roll_rate_setpoint{0.0f};
    float _pitch_rate_setpoint{0.0f};
    matrix::Vector3f _rates_prev{};
    
public:
    bool init() {
        // 注册到工作队列(共享线程池)
        ScheduleOnInterval(4_ms);  // 每4ms执行一次Run()
        return true;
    }
    
    void Run() override {
        // ✅ 这个函数永远只被一个工作队列线程调用
        // ✅ 不需要担心并发问题!
        
        _roll_rate_setpoint = get_setpoint();  // 普通访问
        _rates_prev = current_rates;           // 普通赋值
        
        // 完全不需要原子操作或锁!
    }
};

6.2 不同场景的同步机制

场景同步机制使用率示例
工作队列任务无需同步90%大部分控制器
静态实例指针原子操作5%ModuleBase::_object
uORB消息传递内部锁/原子通用发布订阅框架
性能计数器原子操作常见perf_counter
独立线程数据互斥锁3%传感器驱动

6.3 性能对比

// 方案1:互斥锁(慢)
pthread_mutex_t mutex;

void access_with_lock() {
    pthread_mutex_lock(&mutex);      // ~100-1000 CPU周期
                                      // 可能涉及系统调用
                                      // 可能导致线程休眠/唤醒
    auto ptr = _object;
    pthread_mutex_unlock(&mutex);    // ~100-1000 CPU周期
}

// 方案2:原子操作(快)
std::atomic<MulticopterRateControl*> _object;

void access_with_atomic() {
    auto ptr = _object.load();       // ~1-10 CPU周期
                                      // 单条硬件指令
                                      // 无系统调用
                                      // 无阻塞
}

// 🚀 性能差异: 原子操作比互斥锁快 10-100倍!

7. 完整的启动流程分析

7.1 单例启动保护

int MulticopterRateControl::task_spawn(int argc, char *argv[]) {
    // 步骤1️⃣: 原子检查
    if (_object.load() != nullptr) {
        PX4_WARN("already running");
        return -1;
    }
    
    // 步骤2️⃣: 在堆上创建实例
    MulticopterRateControl *instance = new MulticopterRateControl(vtol);
    
    if (instance) {
        // 步骤3️⃣: 原子发布
        _object.store(instance);
        // release语义保证:
        // ✓ new操作完全完成
        // ✓ 对象构造完全完成
        // ✓ 其他线程load时能看到完整的对象
        
        // 步骤4️⃣: 标记为工作队列模式
        _task_id = task_id_is_work_queue;
        
        // 步骤5️⃣: 初始化
        if (instance->init()) {
            return PX4_OK;
        }
    }
    
    // 启动失败,清理资源
    delete instance;
    _object.store(nullptr);
    _task_id = -1;
    
    return PX4_ERROR;
}

7.2 为什么在堆上创建实例?

// ❌ 栈上创建(错误)
int task_spawn() {
    MulticopterRateControl instance;  // 栈上对象
    _object = &instance;
    
    return PX4_OK;
}  // ← instance在这里被销毁! 指针变成野指针!


// ✅ 堆上创建(正确)
int task_spawn() {
    MulticopterRateControl *instance = new MulticopterRateControl();
    _object.store(instance);
    
    return PX4_OK;
}  // ← instance继续存在于堆上

堆上创建的关键优势:

  1. 生命周期可控:对象存活直到显式delete
  2. 全局访问:指针可以被多个模块共享
  3. 按需创建:只在需要时才分配内存
  4. 可以销毁:不需要时可以释放资源

8. 什么时候需要原子操作

8.1 核心原因:控制器的两个层面

// 【层面1】控制逻辑 - 不需要原子操作
class MulticopterRateControl : public WorkItem {
private:
    // ❌ 这些成员变量都不是原子的
    float _roll_rate_sp;
    float _pitch_rate_sp;
    
public:
    void Run() override {
        // ✅ 工作队列保证单线程执行
        _roll_rate_sp = calculate_rate();
    }
};


// 【层面2】生命周期管理 - 需要原子操作
class MulticopterRateControl {
private:
    // ✅ 这个指针是原子的
    static std::atomic<MulticopterRateControl*> _object;
    
public:
    static int task_spawn();   // 启动
    static int task_stop();    // 停止
};

8.2 PX4中所有需要原子操作的场景

场景1:模块生命周期管理(最重要)
// 几乎所有PX4模块都需要
template<class T>
class ModuleBase {
protected:
    // ✅ 必须是原子的
    static std::atomic<T*> _object;
    
public:
    static int task_spawn();
    static int task_stop();
};
场景2:性能计数器
class PerformanceCounter {
private:
    // ✅ 原子计数器
    std::atomic<uint64_t> _count{0};
    
public:
    void count() {
        _count.fetch_add(1);
    }
};
场景3:线程退出标志
class SensorDriver {
private:
    // ✅ 原子退出标志
    std::atomic<bool> _should_exit{false};
    
    static void* thread_entry(void *arg) {
        while (!driver->_should_exit.load()) {
            driver->read_sensor();
        }
    }
};
场景4:状态标志位
class VehicleStatus {
private:
    // ✅ 原子状态标志
    std::atomic<bool> _armed{false};
    std::atomic<uint8_t> _nav_state{0};
};
场景5:发布-订阅计数器
class uORB::DeviceNode {
private:
    // ✅ 原子版本号
    std::atomic<unsigned> _generation{0};
};

8.3 决策流程图

需要原子操作吗?
    |
    ├─ 变量是否被多个线程访问?
    │   ├─ 否 → ❌ 不需要
    │   └─ 是 → 继续
    │
    ├─ 访问模式?
    │   ├─ 只在启动时设置,之后只读 → ❌ 不需要
    │   ├─ 只被一个工作队列线程访问 → ❌ 不需要
    │   └─ 多线程读写 → 继续
    │
    ├─ 操作类型?
    │   ├─ 简单赋值/递增/标志位 → ✅ 使用原子操作
    │   └─ 复杂多步骤操作 → ✅ 使用互斥锁

9. 常见误区与最佳实践

9.1 常见误区

误区1:原子变量的所有操作都是原子的
std::atomic<int> counter{0};

counter++;              // ✅ 原子操作
counter = counter + 1;  // ❌ 非原子! (读和写分离了)

// 正确写法:
counter.fetch_add(1);   // ✅ 原子操作
误区2:过度使用原子操作
// ❌ 过度使用
class Controller {
    std::atomic<float> _gain;      // 不必要
    
    void Run() {
        // Run()只被一个线程调用,不需要原子
    }
};

9.2 最佳实践

// ✅ 实践1:封装原子操作
class ThreadSafeFlag {
private:
    std::atomic<bool> _flag{false};
    
public:
    void set() { _flag.store(true); }
    bool is_set() const { return _flag.load(); }
};

// ✅ 实践2:选择合适的内存序
// 高频操作用relaxed
_counter.fetch_add(1, std::memory_order_relaxed);

// 同步点用acquire/release
_ready.store(true, std::memory_order_release);

// ✅ 实践3:最小化原子操作范围
class Module {
    static std::atomic<Module*> _instance;  // 需要原子
    float _gain;     // 不需要(私有,单线程)
};

10. 总结与核心要点

10.1 关键认识

✅ PX4的核心理念:
   "通过架构设计避免并发,而不是通过同步机制解决并发"

核心策略:
1️⃣ 工作队列架构 → 消除大部分并发需求 (90%)
2️⃣ uORB消息传递 → 封装线程安全细节
3️⃣ 单例+原子操作 → 保护关键共享资源 (5%)
4️⃣ 最小化共享状态 → 减少同步开销

结果:
✓ 90%的代码不需要考虑线程安全
✓ 原子操作只用于少数关键场景
✓ 代码简洁,性能高效,易于维护

10.2 原子操作的三大硬件保证

1️⃣ 总线锁定 (Bus Lock)
   → 独占内存访问,其他核心被阻塞

2️⃣ 内存屏障 (Memory Barrier)
   → 禁止指令重排序,确保操作顺序

3️⃣ 缓存一致性 (Cache Coherence)
   → 强制刷新所有核心缓存,保证一致性

10.3 单例模式的五大要素

完整的单例模式 = 5个关键要素:

1️⃣ 静态实例指针
   static std::atomic<MyClass*> _object;

2️⃣ 私有构造函数
   private: MyClass() { }

3️⃣ 禁止拷贝
   MyClass(const MyClass&) = delete;

4️⃣ 静态创建方法
   static int task_spawn() { ... }

5️⃣ 原子操作保护
   _object.compare_exchange_strong(...)

10.4 记忆口诀

原子操作七大场景:
1. 模块指针要原子(生命周期管理)
2. 性能计数要原子(多线程递增)
3. 退出标志要原子(一写多读)
4. 状态标志要原子(多线程查询)
5. 版本序号要原子(发布订阅)
6. 引用计数要原子(资源管理)
7. 事件统计要原子(并发记录)

工作队列无需愁(单线程执行)
局部变量不用管(栈上独立)
只读数据很安全(启动后不变)

结语

通过本文的深入分析,我们理解了:

  1. 多线程并发的本质:多个执行者同时操作共享资源
  2. 原子操作的原理:硬件级的不可分割操作,通过总线锁定、内存屏障和缓存一致性保证线程安全
  3. 单例模式的必要性:在飞控系统中保证资源唯一性,避免控制冲突
  4. PX4的设计智慧:通过工作队列架构天然避免大部分并发问题

掌握这些知识后,你将能够:

  • ✅ 理解PX4控制器的启动和生命周期管理
  • ✅ 正确使用原子操作保护共享资源
  • ✅ 实现自己的线程安全模块
  • ✅ 避免常见的并发bug

记住:好的并发设计不是添加更多锁,而是通过架构设计避免并发问题。这正是PX4的工作队列架构如此优雅的原因! 🚀


参考资源


如果本文对你有帮助,请点赞收藏!欢迎在评论区讨论交流!

本文为原创技术文章,转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值