百度Apollo-Cyber RT 协程实现原理

509 阅读5分钟

简介

CyberRT 在user space 实现了自己的调度器,本文主要是来讲解 Schduler 的调度单元 CRoutine(协程)。 Apollo 在 3.0 之前一直使用的是ROS,其在机器人领域应用广泛,但是在自动驾驶领域并没有广泛的使用。 ROS 应用于自动驾驶领域的不足: 调度的不确定性:各节点以独立进程运行,节点运行顺序无法确定,因而业务逻辑的调度顺序无法保证; 运行效率:ROS 为分布式系统,存在通信开销。 Cyber RT的特色: 高性能:无锁对象,协程(coroutine),自适应通信机制; 确定性:可配置的任务以及任务调度,通过协程将调度从内核空间转移到用户空间; 模块化:在框架内实现组件以及节点,即可完成系统任务; 便利性:创建和使用任务。 协程的概念在很早时间就被提起,现在提到协程都会有以下几个特点:1、共享栈和非共享栈:区别在于协程是否共享一个栈;2、对称和非对称:区别在于协程的地位是否对等,对称就是对等,非对称就是协程之间的地位不是对等的;3 、基于ucontext或者自己写汇编:主要是上下文切换的实现方式不同,ucontext是libc的一族函数,但是调用时还是会进入kernel,但写汇编只需要在userspace。总的来说,croutine是一种非共享栈、非对称、自己写汇编实现的协程。

源码解读

内部实现的框架图: image.png croutine.h

class CRoutine {
 public:
  explicit CRoutine(const RoutineFunc &func);
  virtual ~CRoutine();

  // static interfaces
  static void Yield();
  static void Yield(const RoutineState &state);
  static void SetMainContext(const std::shared_ptr<RoutineContext> &context);
  static CRoutine *GetCurrentRoutine();
  static char **GetMainStack();

  // public interfaces
  bool Acquire();
  void Release();

  // It is caller's responsibility to check if state_ is valid before calling
  // SetUpdateFlag().
  void SetUpdateFlag();

  // acquire && release should be called before Resume
  // when work-steal like mechanism used
  RoutineState Resume();
  RoutineState UpdateState();
  RoutineContext *GetContext();
  char **GetStack();

  void Run();
  void Stop();
  void Wake();
  void HangUp();
  void Sleep(const Duration &sleep_duration);

  // getter and setter
  RoutineState state() const;
  void set_state(const RoutineState &state);

  uint64_t id() const;
  void set_id(uint64_t id);

  const std::string &name() const;
  void set_name(const std::string &name);

  int processor_id() const;
  void set_processor_id(int processor_id);

  uint32_t priority() const;
  void set_priority(uint32_t priority);

  std::chrono::steady_clock::time_point wake_time() const;

  void set_group_name(const std::string &group_name) {
    group_name_ = group_name;
  }

  const std::string &group_name() { return group_name_; }

 private:
  CRoutine(CRoutine &) = delete;
  CRoutine &operator=(CRoutine &) = delete;

  std::string name_;
  std::chrono::steady_clock::time_point wake_time_ =
      std::chrono::steady_clock::now();

  RoutineFunc func_;
  RoutineState state_;

  std::shared_ptr<RoutineContext> context_;

  std::atomic_flag lock_ = ATOMIC_FLAG_INIT;
  std::atomic_flag updated_ = ATOMIC_FLAG_INIT;

  bool force_stop_ = false;

  int processor_id_ = -1;
  uint32_t priority_ = 0;
  uint64_t id_ = 0;

  std::string group_name_;

  static thread_local CRoutine *current_routine_;
  static thread_local char *main_stack_;
};

关键的变量: func_: 协程要执行的函数体 context_: 存放对应协程的上下文,类型为 RoutineContext, RoutineContext 中存放的一个是栈 stack,一个是寄存器 sp。 context_pool:一个全局的对象池,进程所有的context指针都放在这个对象池中。这个对象池CCObjectPool的实现位于concurrent_object_pool.h state_: 协程的状态

enum class RoutineState { READY, FINISHED, SLEEP, IO_WAIT, DATA_WAIT };

SLEEP, IO_WAIT, DATA_WAIT 只和 READY之间互相切换状态,READY可以和任意一个状态进行切换,FINISHED只能从READY切换。

// croutine.cc

CRoutine::CRoutine(const std::function<void()> &func) : func_(func) {
  std::call_once(pool_init_flag, [&]() {
    uint32_t routine_num = common::GlobalData::Instance()->ComponentNums();
    auto &global_conf = common::GlobalData::Instance()->Config();
    if (global_conf.has_scheduler_conf() &&
        global_conf.scheduler_conf().has_routine_num()) {
      routine_num =
          std::max(routine_num, global_conf.scheduler_conf().routine_num());
    }
    context_pool.reset(new base::CCObjectPool<RoutineContext>(routine_num));
  });

  context_ = context_pool->GetObject();
  if (context_ == nullptr) {
    AWARN << "Maximum routine context number exceeded! Please check "
             "[routine_num] in config file.";
    context_.reset(new RoutineContext());
  }

  MakeContext(CRoutineEntry, this, context_.get());
  state_ = RoutineState::READY;
  updated_.test_and_set(std::memory_order_release);
}

关键函数是MakeContext函数。 routine_context.cc

//  The stack layout looks as follows:
//
//              +------------------+
//              |      Reserved    |
//              +------------------+
//              |  Return Address  |   f1
//              +------------------+
//              |        RDI       |   arg
//              +------------------+
//              |        R12       |
//              +------------------+
//              |        R13       |
//              +------------------+
//              |        ...       |
//              +------------------+
// ctx->sp  =>  |        RBP       |
//              +------------------+
void MakeContext(const func &f1, const void *arg, RoutineContext *ctx) {
  ctx->sp = ctx->stack + STACK_SIZE - 2 * sizeof(void *) - REGISTERS_SIZE; // 找到sp合适的位置
  std::memset(ctx->sp, 0, REGISTERS_SIZE); // 初始化寄存器为0
  // 设置sp到栈顶
#ifdef __aarch64__
  char *sp = ctx->stack + STACK_SIZE - sizeof(void *);
#else
  char *sp = ctx->stack + STACK_SIZE - 2 * sizeof(void *);
#endif
  *reinterpret_cast<void **>(sp) = reinterpret_cast<void *>(f1); // f1 存入栈中
  sp -= sizeof(void *); // 指向下一个指针位置
  *reinterpret_cast<void **>(sp) = const_cast<void *>(arg); // arg存储到栈中
}

注释中可以看出执行完MakeContext之后的效果。 构造完croutine之后,执行时最核心的函数是resume和Yield函数,这两个函数进行上下文的切换,将当前上下文切换到对应协程的上下文。

inline void CRoutine::Yield(const RoutineState &state) {
  auto routine = GetCurrentRoutine();
  routine->set_state(state);
  SwapContext(GetCurrentRoutine()->GetStack(), GetMainStack());
}

inline void CRoutine::Yield() {
  SwapContext(GetCurrentRoutine()->GetStack(), GetMainStack());
}

RoutineState CRoutine::Resume() {
  if (cyber_unlikely(force_stop_)) {
    state_ = RoutineState::FINISHED;
    return state_;
  }

  if (cyber_unlikely(state_ != RoutineState::READY)) {
    AERROR << "Invalid Routine State!";
    return state_;
  }

  current_routine_ = this;
  SwapContext(GetMainStack(), GetStack());
  current_routine_ = nullptr;
  return state_;
}

主要的函数实现是SwapContext

inline void SwapContext(char** src_sp, char** dest_sp) {
  ctx_swap(reinterpret_cast<void**>(src_sp), reinterpret_cast<void**>(dest_sp));
}
.globl ctx_swap
.type  ctx_swap, @function
ctx_swap:
      pushq %rdi
      pushq %r12
      pushq %r13
      pushq %r14
      pushq %r15
      pushq %rbx
      pushq %rbp
      movq %rsp, (%rdi)

      movq (%rsi), %rsp
      popq %rbp
      popq %rbx
      popq %r15
      popq %r14
      popq %r13
      popq %r12
      popq %rdi
      ret

汇编的主要含义是将上下文进行切换,比如resume就是将上下文从GetMainStack()切换到GetStack()。 主要的使用例子可以见Processor::Run函数。(processor.cc)

总结

协程主要是一种用户自己控制任务的切换的一种模式,但是还是在单线程中执行,实现更小粒度的并发。协程的实现不需要进入kernel space,性能较好,共享数据不需要加锁。

参考

自动驾驶平台Apollo 3.5阅读手记:Cyber RT中的协程(Coroutine)_cyberrt 协程非对称-CSDN博客