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