ceph源码中的几处语法分析

本文选取了ceph中的几处代码进行分析,旨在读懂代码的基础之上,提炼出关键知识点和思想进行学习。

1. MDSContext::vec

目标代码
class MDSContext : public Context
{
public:
template<template<typename> class A>
  using vec_alloc = std::vector<MDSContext*, A<MDSContext*>>;
  using vec = vec_alloc<std::allocator>;

};
关键词
  • Template template parameter(模板模板参数)
  • alias templates(模板别名)
语法分析

在分析这段代码之前,先来看两个更简单的例子:

template<class T>
struct Alloc
{
    // 实现自己的内存分配器
};
template<class T>
using Vec = std::vector<T, Alloc<T>>;

int main()
{
    Vec<int> as;
    return 0;
}
template<class T>
struct Base
{
    int val;
};
//没什么实际意义,只是为了说明用法
template<class T>
using A = Base<T>;

int main()
{
    Base<int> inst1;
    A<int> inst2;
    return 0;
}

以上两个例子,就是C++11新增的特性——模板别名,通过using语句为复杂的模板格式起一个简单的别名,极大的简化后续使用时的书写复杂度。

而这里的MDSContext::vec要比以上两个例子更复杂些,一是有模板嵌套,二是两次别名,但其实原理还是一样的,拆开来看就可以了。具体分析直接见下方代码注释吧:

class MDSContext
{
public:
template<template<typename> class A> // A是一个模板,就这么简单
  // 用std::alocator来实例化vec_alloc,即vec_alloc<std::allocator>,allocator即替换A来进行实例化
  using vec_alloc = std::vector<MDSContext*, A<MDSContext*>>;
  using vec = vec_alloc<std::allocator>;

// 上面的最后一个using其实就是最后一步的彻底特例化了,如果去除这步,手动传参也是可以的
template<template<typename> class A>
  using vec_alloc = std::vector<MDSContext*, A<MDSContext*>>;
    
// 一句话总结,其实就是定义了std::vector<MDSContext*, std::allocator<MDSContext*>>的别名
};


int main()
{
    // 等价的定义,第一种写法最简单
    MDSContext::vec a;
    MDSContext::vec_alloc<std::allocator> b;
    std::vector<MDSContext*, std::allocator<MDSContext*>> c;
    return 0;
}

用法就是如上面所示,借此部分代码学习了模板别名的知识点,但是对于这部分的代码,以我目前的认知来看是没看出有什么好处的,因为这段代码绕了一圈,其实vec就是vector<MDSContext*>,第一个using允许用户自定义allocator,但是第二个using又显示使用了std::allocator,这还有什么意义呢?把本来泛型的东西又给特例化了,MDSContext::vec相比vector<MDSContext*>的并没有更简单,而且可读性也差了很多。后面再看有没有新的理解吧。

2. C_IO_Wrapper

目标代码

下面这段代码做了什么?

Context *fin = new C_IO_Wrapper(this, new C_MDS_BootStart(this, MDS_BOOT_INITIAL));

  bool const ready = objecter->wait_for_map(
      mdsmap->get_last_failure_osd_epoch(),
      fin);

其中类的继承体系如下:

graph BT
MDSContext-->Context
tmp[MDSHolder<MDSIOContextBase>]-->MDSContext
MDSIOContext-->tmp
C_IO_Wrapper-->MDSIOContext
MDSInternalContext-->tmp
C_MDS_BootStart-->MDSInternalContext

为了省去再去翻代码,这里直接把相关类定义的源码列出来了。

/*
 * Context - abstract callback class
 */
class Context {
  Context(const Context& other);
  const Context& operator=(const Context& other);

 protected:
  virtual void finish(int r) = 0;

  // variant of finish that is safe to call "synchronously."  override should
  // return true.
  virtual bool sync_finish(int r) {
    return false;
  }

 public:
  Context() {}
  virtual ~Context() {}       // we want a virtual destructor!!!
  virtual void complete(int r) {
    finish(r);
    delete this;
  }
  virtual bool sync_complete(int r) {
    if (sync_finish(r)) {
      delete this;
      return true;
    }
    return false;
  }
};

/**
 * Completion which has access to a reference to the global MDS instance.
 *
 * This class exists so that Context subclasses can provide the MDS pointer
 * from a pointer they already had, e.g. MDCache or Locker, rather than
 * necessarily having to carry around an extra MDS* pointer. 
 */
class MDSContext : public Context
{
public:
template<template<typename> class A>
  using vec_alloc = std::vector<MDSContext*, A<MDSContext*>>;
  using vec = vec_alloc<std::allocator>;

template<template<typename> class A>
  using que_alloc = std::deque<MDSContext*, A<MDSContext*>>;
  using que = que_alloc<std::allocator>;

  void complete(int r) override;
  virtual MDSRank *get_mds() = 0;
};

void MDSContext::complete(int r) {
  MDSRank *mds = get_mds();
  ceph_assert(mds != nullptr);
  ceph_assert(mds->mds_lock.is_locked_by_me());
  dout(10) << "MDSContext::complete: " << typeid(*this).name() << dendl;
  return Context::complete(r);
}

class MDSIOContextBase : public MDSContext
{
public:
  MDSIOContextBase(bool track=true);
  virtual ~MDSIOContextBase();
  MDSIOContextBase(const MDSIOContextBase&) = delete;
  MDSIOContextBase& operator=(const MDSIOContextBase&) = delete;

  void complete(int r) override;

};

/**
 * Completion for an I/O operation, takes big MDSRank lock
 * before executing finish function.
 */
class MDSIOContext : public MDSHolder<MDSIOContextBase>
{
public:
  explicit MDSIOContext(MDSRank *mds_) : MDSHolder(mds_) {}
};


template<class T>
class MDSHolder : public T
{
public:
  MDSRank* get_mds() override {
    return mds;
  }

protected:
  MDSHolder() = delete;
  MDSHolder(MDSRank* mds) : mds(mds) {
    ceph_assert(mds != nullptr);
  }

  MDSRank* mds;
};

/**
 * General purpose, lets you pass in an MDS pointer.
 */
class MDSInternalContext : public MDSHolder<MDSContext>
{
public:
  MDSInternalContext() = delete;

protected:
  explicit MDSInternalContext(MDSRank *mds_) : MDSHolder(mds_) {}
};


class C_MDS_BootStart : public MDSInternalContext {
  MDSRank::BootStep nextstep;
public:
  C_MDS_BootStart(MDSRank *m, MDSRank::BootStep n)
    : MDSInternalContext(m), nextstep(n) {}
  void finish(int r) override {
    mds->boot_start(nextstep, r);
  }
};

/**
 * This class is used where you have an MDSInternalContext but
 * you sometimes want to call it back from an I/O completion.
 */
class C_IO_Wrapper : public MDSIOContext
{
protected:
  bool async;
  MDSContext *wrapped;
  void finish(int r) override {
    wrapped->complete(r);
    wrapped = nullptr;
  }
public:
  C_IO_Wrapper(MDSRank *mds_, MDSContext *wrapped_) :
    MDSIOContext(mds_), async(true), wrapped(wrapped_) {
    ceph_assert(wrapped != NULL);
  }

  ~C_IO_Wrapper() override {
    if (wrapped != nullptr) {
      delete wrapped;
      wrapped = nullptr;
    }
  }
  void complete(int r) final;
  void print(ostream& out) const override {
    out << "io_wrapper(" << wrapped << ")";
  }
};
关键词
  • Mixin
  • 回调
语法分析

这段代码看起来继承链比较多,但其实几个中间类对逻辑本身没有影响,只是添加了一些额外内容。下面对这段代码进行分析:

  1. 通过mixin添加rank(非主逻辑相关)
// MDSHolder实现了mixin语义,这里相当于为MDSContext添加了一个rank
class MDSInternalContext : public MDSHolder<MDSContext>
{
public:
  MDSInternalContext() = delete;

protected:
  explicit MDSInternalContext(MDSRank *mds_) : MDSHolder(mds_) {}
};
  1. 定义通用抽象回调基类
// 通用抽象回调类,用户在complete()中实现自己的回调调用逻辑
class Context {
 protected:
  virtual void finish(int r) = 0;

 public:
  virtual void complete(int r) {
    finish(r);
    delete this;
  }

};
  1. C_IO_Wrapper类实现回调调用逻辑
class C_IO_Wrapper : public MDSIOContext
{
protected:
  MDSContext *wrapped;
  // 通过wrapped->complete(r)触发真正的回调实现
  void finish(int r) override {
    wrapped->complete(r);
    wrapped = nullptr;
  }
public:
  C_IO_Wrapper(MDSRank *mds_, MDSContext *wrapped_) :
    MDSIOContext(mds_), async(true), wrapped(wrapped_) {
    ceph_assert(wrapped != NULL);
  }

  void complete(int r) final;

};
  1. 用户对回调的处理逻辑部分

注:为了简明分析逻辑,此部分是从多处提取的关键代码,而并非是连续完整的


// 1.基类Context指针指向的是派生类C_IO_Wrapper对象,C_MDS_BootStart是真正的回调类,其complete函数则是真正的回调实现
Context *fin = new C_IO_Wrapper(this, new C_MDS_BootStart(this, MDS_BOOT_INITIAL));

// 2.这里的Context* c即上面的new C_MDS_BootStart(this, MDS_BOOT_INITIAL),其会被push进waiting_for_map
void Objecter::_wait_for_new_map(Context *c, epoch_t epoch, int err)
{
  // 
  waiting_for_map[epoch].push_back(pair<Context *, int>(c, err));
  _maybe_request_map();
}

// 3.收到osdmap时,会遍历waiting_for_map,触发回调。但是注意这里p是Context*,
// 而前面push进来的是C_MDS_BootStart*,所以会产生运行时多态

void Objecter::handle_osd_map(MOSDMap *m)
{
    
    for (list<pair<Context*, int> >::iterator i = p->second.begin();
     i != p->second.end(); ++i) {
      i->first->complete(i->second);
    } 
    
}

// 4.回调是什么:C_MDS_BootStart只实现了finish,所以complete会调用顶层Context::complete(),
// 而finish则是自己实现的版本,最终也就是mds->boot_start(nextstep, r)
class Context {
  
  virtual void complete(int r) {
    finish(r);
    delete this;
  }

};


class C_MDS_BootStart : public MDSInternalContext {
  MDSRank::BootStep nextstep;
public:
  C_MDS_BootStart(MDSRank *m, MDSRank::BootStep n)
    : MDSInternalContext(m), nextstep(n) {}
  void finish(int r) override {
    mds->boot_start(nextstep, r);
  }
};

所以,这段代码的功能总结下来是很清晰明了的:
注册了一个等待osdmap的回调事件,通过C_MDS_BootStart来承载,待收到osdmap后触发回调:mds->boot_start(MDS_BOOT_INITIAL, r)

3. MDSGatherBuilder

目标代码
MDSGatherBuilder gather(g_ceph_context,
            new C_MDS_BootStart(this, MDS_BOOT_OPEN_ROOT));
     
            
inotable->load(gather.new_sub());

其中相关的定义如下:

/*
 * The C_GatherBuilder remembers each C_Context created by
 * C_GatherBuilder.new_sub() in a C_Gather.  When a C_Context created
 * by new_sub() is complete(), C_Gather forgets about it.  When
 * C_GatherBuilder notices that there are no C_Context left in
 * C_Gather, it calls complete() on the C_Context provided as the
 * second argument of the constructor (finisher).
 *
 * How to use C_GatherBuilder:
 *
 * 1. Create a C_GatherBuilder on the stack
 * 2. Call gather_bld.new_sub() as many times as you want to create new subs
 *    It is safe to call this 0 times, or 100, or anything in between.
 * 3. If you didn't supply a finisher in the C_GatherBuilder constructor,
 *    set one with gather_bld.set_finisher(my_finisher)
 * 4. Call gather_bld.activate()
 *
 * Example:
 *
 * C_SaferCond all_done;
 * C_GatherBuilder gb(g_ceph_context, all_done);
 * j.submit_entry(1, first, 0, gb.new_sub()); // add a C_Context to C_Gather
 * j.submit_entry(2, first, 0, gb.new_sub()); // add a C_Context to C_Gather
 * gb.activate(); // consume C_Context as soon as they complete()
 * all_done.wait(); // all_done is complete() after all new_sub() are complete()
 *
 * The finisher may be called at any point after step 4, including immediately
 * from the activate() function.
 * The finisher will never be called before activate().
 *
 * Note: Currently, subs must be manually freed by the caller (for some reason.)
 */
template <class ContextType, class GatherType>
class C_GatherBuilderBase
{
public:
  C_GatherBuilderBase(CephContext *cct_)
    : cct(cct_), c_gather(NULL), finisher(NULL), activated(false)
  {
  }
  C_GatherBuilderBase(CephContext *cct_, ContextType *finisher_)
    : cct(cct_), c_gather(NULL), finisher(finisher_), activated(false)
  {
  }

  ContextType *new_sub() {
    if (!c_gather) {
      c_gather = new GatherType(cct, finisher);
    }
    return c_gather->new_sub();
  }
  void activate() {
    if (!c_gather)
      return;
    ceph_assert(finisher != NULL);
    activated = true;
    c_gather->activate();
  }
  void set_finisher(ContextType *finisher_) {
    finisher = finisher_;
    if (c_gather)
      c_gather->set_finisher(finisher);
  }


private:
  CephContext *cct;
  GatherType *c_gather;
  ContextType *finisher;
  bool activated;
};


/*
 * C_Gather
 *
 * ContextType must be an ancestor class of ContextInstanceType, or the same class.
 * ContextInstanceType must be default-constructable.
 *
 * BUG:? only reports error from last sub to have an error return
 */
template <class ContextType, class ContextInstanceType>
class C_GatherBase {
private:
  CephContext *cct;
  int result;
  ContextType *onfinish;
#ifdef DEBUG_GATHER
  std::set<ContextType*> waitfor;
#endif
  int sub_created_count;
  int sub_existing_count;
  mutable Mutex lock;
  bool activated;

  class C_GatherSub : public ContextInstanceType {
    C_GatherBase *gather;
  public:
    C_GatherSub(C_GatherBase *g) : gather(g) {}
    void complete(int r) override {
      // Cancel any customized complete() functionality
      // from the Context subclass we're templated for,
      // we only want to hit that in onfinish, not at each
      // sub finish.  e.g. MDSInternalContext.
      Context::complete(r);
    }
    void finish(int r) override {
      gather->sub_finish(this, r);
      gather = 0;
    }
    ~C_GatherSub() override {
      if (gather)
  gather->sub_finish(this, 0);
    }
  };
  
public:
  C_GatherBase(CephContext *cct_, ContextType *onfinish_)
    : cct(cct_), result(0), onfinish(onfinish_),
      sub_created_count(0), sub_existing_count(0),
      lock("C_GatherBase::lock", true, false), //disable lockdep
      activated(false)
  {
    mydout(cct,10) << "C_GatherBase " << this << ".new" << dendl;
  }

  void set_finisher(ContextType *onfinish_) {
    Mutex::Locker l(lock);
    ceph_assert(!onfinish);
    onfinish = onfinish_;
  }
  void activate() {
    lock.Lock();
    ceph_assert(activated == false);
    activated = true;
    if (sub_existing_count != 0) {
      lock.Unlock();
      return;
    }
    lock.Unlock();
    delete_me();
  }
  ContextType *new_sub() {
    Mutex::Locker l(lock);
    ceph_assert(activated == false);
    sub_created_count++;
    sub_existing_count++;
    ContextType *s = new C_GatherSub(this);
#ifdef DEBUG_GATHER
    waitfor.insert(s);
#endif
    mydout(cct,10) << "C_GatherBase " << this << ".new_sub is " << sub_created_count << " " << s << dendl;
    return s;
  }

};

/**
 * No-op for callers expecting MDSInternalContext
 */
class C_MDSInternalNoop : public MDSContext
{
public:
  void finish(int r) override {}
  void complete(int r) override { delete this; }
protected:
  MDSRank* get_mds() override final {ceph_abort();}
};

using MDSGather = C_GatherBase<MDSContext, C_MDSInternalNoop>;
using MDSGatherBuilder = C_GatherBuilderBase<MDSContext, MDSGather>;

关键词
语法分析
  1. gather定义
template <class ContextType, class GatherType>
class C_GatherBuilderBase
{
public:
  C_GatherBuilderBase(CephContext *cct_)
    : cct(cct_), c_gather(NULL), finisher(NULL), activated(false)
  {
  }
  C_GatherBuilderBase(CephContext *cct_, ContextType *finisher_)
    : cct(cct_), c_gather(NULL), finisher(finisher_), activated(false)
  {
  }
  
private:
  CephContext *cct;
  GatherType *c_gather;
  ContextType *finisher;
  bool activated;
};

// 如名称所述:C_GatherBuilderBase是gather构建器模板,C_GatherBase是gather模板
// MDSGatherBuilder是gather构建器的mds特例化版本,MDSGather是gather的mdscontext特例化版本
using MDSGather = C_GatherBase<MDSContext, C_MDSInternalNoop>;
using MDSGatherBuilder = C_GatherBuilderBase<MDSContext, MDSGather>;

using MDSGather = C_GatherBase<MDSContext, C_MDSInternalNoop>;
using MDSGatherBuilder = C_GatherBuilderBase<MDSContext, MDSGather>;

MDSGatherBuilder gather(g_ceph_context,
            new C_MDS_BootStart(this, MDS_BOOT_OPEN_ROOT));
            
// 相当于
C_MDS_BootStart *tmp = new C_MDS_BootStart(this, MDS_BOOT_PREPARE_LOG);
C_GatherBuilderBase<MDSContext,C_GatherBase<MDSContext, C_MDSInternalNoop>>(g_ceph_context,tmp) gather;

可以看到,C_GatherBuilderBase有两个版本的构造函数,这里使用的是第二个版本。另外在代码中也会经常见到第一个版本构造的用法,先只传递CephContext*参数,随后通过set_finisher设置finisher,两者是等价的。

  1. gather.new_sub()
// 调用第二个模板参数的new_sub(),即C_GatherBase<MDSContext, C_MDSInternalNoop>::new_sub()
template <class ContextType, class GatherType>
class C_GatherBuilderBase
{
public:
  ContextType *new_sub() {
    if (!c_gather) {
      c_gather = new GatherType(cct, finisher);
    }
    return c_gather->new_sub();
  }

};

// new_sub()最终是new了一个C_GatherSub*,但是C_GatherSub继承自第二个模板参数
// 所以相当于最终new_sub()返回的是一个指向C_MDSInternalNoop的派生类对象的基类MDSContext指针
template <class ContextType, class ContextInstanceType>
class C_GatherBase {

  class C_GatherSub : public ContextInstanceType {
    C_GatherBase *gather;
  public:
    C_GatherSub(C_GatherBase *g) : gather(g) {}

  };

  ContextType *new_sub() {
    // 创建新的sub同时增加计数
    sub_created_count++;
    sub_existing_count++;
    ContextType *s = new C_GatherSub(this);
  }
private:
  int sub_created_count;
  int sub_existing_count;
};
// 所以最终gather.new_sub()返回的就是一个指向C_MDSInternalNoop的派生类对象的指针
// 而C_MDSInternalNoop的关键在于它的complete()直接销毁,不会走通用的回调逻辑去调finish()
using MDSGather = C_GatherBase<MDSContext, C_MDSInternalNoop>;

class C_MDSInternalNoop : public MDSContext
{
public:
  void finish(int r) override {}
  void complete(int r) override { delete this; }
protected:
  MDSRank* get_mds() override final {ceph_abort();}
};
};

这里补充一点,前面的代码注释中可以看到,C_GatherBase对其模板参数有两个要求:

  • ContextType必须是ContextInstanceType的基类
  • ContextInstanceType必须是默认构造的

那么为什么要有这两个限制呢?就是因为这里的new_sub():

  • new_sub()真正构建是ContextType *s = new C_GatherSub(this);而C_GatherSub是ContextInstanceType的派生类,但是new_sub()返回的确实ContextType类型的指针,那显然ContextInstanceType必须继承自ContextType,否则就是用一个类指针去指向了一个毫无关联的类对象,是无法通过编译的。
  • 再者,C_GatherSub的构造函数为:C_GatherSub(C_GatherBase *g) : gather(g),其中并未调用基类ContextInstanceType的构造函数,当然这里也不知道基类构造函数的版本,所以如果ContextInstanceType实现了自定义带参版的构造函数,这里便会构造失败。因此要求ContextInstanceType必须可以默认构造。顺便提一下这里说的默认构造的要求并不是狭义的编译器自动生成的构造函数,是指不需要显示地传入实参的构造函数,ContextInstanceType实现自己的构造函数也可以,只要可以不传参实例化就行。
  1. inotable->load(gather.new_sub())等
    gather创建之后,执行了多步操作传入了gather.new_sub(),即为这些操作全部传入了回调,但是由于C_MDSInternalNoop::complete()中直接不会调用finish而是直接销毁,所以这些回调全部是无实际意义的。

  2. gather.activate()

// 和new_sub()相同,最终是C_GatherBase::activate()
template <class ContextType, class ContextInstanceType>
class C_GatherBase {
  void activate() {
    lock.Lock();
    ceph_assert(activated == false);
    activated = true;
    if (sub_existing_count != 0) {
      lock.Unlock();
      return;
    }
    lock.Unlock();
    delete_me();
  }
  
  // 调用onfinish->complete(),onfinish为C_GatherBase的第二个参数,所以最终会调用
  // C_MDS_BootStart::complete(),而根据上一个例子,最终的有实际逻辑的调用是:mds->boot_start(nextstep, r)
  // 所以activate()在这里实际上就是激活下一步的boot start
  void delete_me() {
    if (onfinish) {
      onfinish->complete(result);
      onfinish = 0;
    }
    delete this;
  }

void MDSContext::complete(int r) {
  MDSRank *mds = get_mds();
  ceph_assert(mds != nullptr);
  ceph_assert(mds->mds_lock.is_locked_by_me());
  dout(10) << "MDSContext::complete: " << typeid(*this).name() << dendl;
  return Context::complete(r);
}
  ContextType *onfinish;
};
总结

注:这里的语法较为复杂,不保证自己理解的完全正确,如有不同的看法欢迎指正,一起学习进步

先说特定目标代码这部分,MDSGatherBuilder gather(g_ceph_context, C_MDS_BootStart(this, MDS_BOOT_PREPARE_LOG))创建一个gather,其每次new_sub()产生的回调是不做任何事的;而C_MDS_BootStart则是最后的有意义的回调类,它通过gather.activate()触发,触发下一阶段的boot_start。

再说"gather"的通用逻辑和设计(重点):

// 通用的gather版本和gather构建器
typedef C_GatherBase<Context, Context> C_Gather;
typedef C_GatherBuilderBase<Context, C_Gather > C_GatherBuilder;

C_GatherBuilder为gather构建器,其模板参数有两个:回调类型和gather类型,实例化之后其内部c_gather指针会指向一个C_GatherBase模板特例化的一个的实例,C_GatherBase就是一直提到的gather,之后的所有逻辑都是围绕这个gather进行。C_GatherBase模板也有两个模板参数,一个回调类型ContextType和一个sub回调类型ContextInstanceType,其中ContextType必须是ContextInstanceType的基类。理解gather的3大要点:onfinish、new_sub、以及activate():

  • gather内部维护了一个finish指针ContextType *onfinish,gather既可以在初始时便指定onfinish,也可以初始时不指定但随后通过set_finish显示指定
  • gather的每次new_sub()都会生成一个sub回调,并增加内部计数,每次sub回调在被调用时,则减少计数。
  • activate()触发回调事件集合,onfinish是整个gather中唯一会实际执行的回调,其发生在所有sub回调和activate中的最后一个的执行时,在activate执行之前,是绝对不会执行最终回调的。

下面通过一个实例来说明:

  1. 创建一个新的gather,并指定了一个finish回调
  2. gather.new_sub()生成回调sub1
  3. gather.new_sub()生成回调sub2
  4. 接下来就看[sub1、sub2、activate]这三者的调用时间关系,谁最后执行谁就负责调用finish->complete()执行真正的回调,
    这里就是gather的关键,只有最后执行的才会执行真正的回调
    下面是以时间轴的形式模拟了两个场景:
  sub1:减少计数  -->   sub2:减少计数  -->   activate:finish->complete() 
  |-----------------------------------------------------------------|

  sub2:减少计数  -->   activate:activated = true   -->   sub1:finish->complete()
  |---------------------------------------------------------------------------|

至此gather的用法已经比较清晰了,可见gather的设计目的是为了将多个回调事件的完成与一个回调事件的触发关联起来,可以试一对一或者一对多,当这些全部sub回调事件都被调用了(但不会真正执行),就会执行真正的finish回调。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值