CyberRt协程介绍

目录

协程

线程切换

协程切换

问题

两个接口

初始化协程栈

举个例子

总结


协程

CyberRt作为百度阿波罗的中间件,采用了比较有特色的协程调度框架。

本文主要是为了更详细的介绍下,CyberRT协程的实现原理。

线程切换

linux下线程的调度属性,主要包括,SCHED_FIFO/SCHED_RR/SCHED_OTHER等方式。

一般来说,线程切换有如下场景:

  • 时间片用完,线程主动放弃CPU的使用权,给其他线程使用。如RR策略。

  • 该线程被其他更加高优线程抢占,如FIFO策略。

  • 该线程主动调用阻塞接口,典型的,如IO相关操作:Read/Write/。互斥量相关操作,如lock/wait。主动休眠Sleep等操作。

而对于这些操作而言,每次的切换均需要进行用户态与内核态之间的切换,导致了CPU更多的浪费在了无效的指令中。

线程切换:本质是由操作系统保存当前的寄存器的值,以及线程函数执行到的那个切换点的独立的线程栈。

如果有一种方式,可以既能够完成多路任务的切换,又能够避免内核态与用户态的开销。随之而来的解决方案就是协程。

协程切换

参照线程切换的逻辑,协程在切换过程中也需要保存当前寄存器的值,以及当前协程函数所执行到的切换点的协程栈。

问题

1. 协程切换了,寄存器保存在哪里

        CyberRt是有栈协程实现方式,将寄存器直接保存至协程的栈空间内

2. 协程切换了,如何知道上一次执行到协程栈上的具体位置

        在RoutineContext结构体预留sp指针,该指针标志着每个协程栈空间,程序所执行到了的栈顶的位置,如果协程切换出去,后续将依赖于sp指针的位置进行上下文恢复

两个接口

协程的Yield和Resume:

  • Yield:在协程函数内调用,保存当前执行上下文,主要是寄存器和栈顶指针,随后让出线程的使用权,跳出协程函数执行逻辑。
  • Resume:在线程主逻辑调用。第一次调用将进入CoroutineEntry入口处执行。后续调用将切换至上次协程Yield的地方,恢复执行上下文后继续执行协程的逻辑。

// 协程依赖的上下文,stack标识的是该协程任务依赖的运行栈
// sp标志着当前执行到的栈顶,通过sp可以在切换回协程后,找到上一次程序执行到的地方
struct RoutineContext {
  char stack[STACK_SIZE];
  char* sp = nullptr;
#if defined __aarch64__
} __attribute__((aligned(16)));
#else
};
#endif

初始化协程栈

CyberRT会根据配置的component数目,从内存池申请大块内存。随后会在创建协程的时候,从内存池内提供协程上下文空间,包括协程栈(2M)和栈顶指针(char*)。

初始化时在协程栈内存预留寄存器的存储空间,栈顶指针,协程入口函数,执行参数等信息。

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());
    }
    // 整体的空间分配。从内存池分配routine_num*RoutineContext的空间大小
    // 后续协程栈从内存空间获取一块Context即可
    context_pool.reset(new base::CCObjectPool<RoutineContext>(routine_num));
  });
  // 获取可用的buffer
  context_ = context_pool->GetObject();
  if (context_ == nullptr) {
    AWARN << "Maximum routine context number exceeded! Please check "
             "[routine_num] in config file.";
    // 若可用buffer用完,则直接从堆内存申请RoutineContext
    context_.reset(new RoutineContext());
  }
  // 初始化协程栈,指定协程入口函数为CRoutineEntry
  MakeContext(CRoutineEntry, this, context_.get());
  // 协程初始态为Ready,可直接由Processor调度
  state_ = RoutineState::READY;
  updated_.test_and_set(std::memory_order_release);
}

以X86_64为例,其栈顶指针寄存器为rsp。

MakeContext函数:此时协程Context已经分配好了,即2M内存空间+指针sp。在MakeContext里对这2M空间进行初始化。

  • 计算出sp的位置为从栈底-2*sizeof(void*)-REGISTERS_SIZE
// 用于构造协程栈
void MakeContext(const func &f1, const void *arg, RoutineContext *ctx) {
  // 计算出ctx->sp的位置为从栈底-2*sizeof(void*)-REGISTERS_SIZE
  // 预留出CroutineEntry+14个通用寄存器的存储空间
  ctx->sp = ctx->stack + STACK_SIZE - 2 * sizeof(void *) - REGISTERS_SIZE;
  std::memset(ctx->sp, 0, REGISTERS_SIZE);
#ifdef __aarch64__
  char *sp = ctx->stack + STACK_SIZE - sizeof(void *);
#else
  char *sp = ctx->stack + STACK_SIZE - 2 * sizeof(void *);
#endif
  // 在栈底位置填入CroutineEntry函数地址
  *reinterpret_cast<void **>(sp) = reinterpret_cast<void *>(f1);
  sp -= sizeof(void *);
  // 在CroutineEntry所在位置下一个地方放入arg参数地址
  *reinterpret_cast<void **>(sp) = const_cast<void *>(arg);
}

初始化后的栈空间如下所示:

  • 第一次调用该croutine的resume函数。rsp通过char* sp,获取栈顶地址,然后popq获取栈中栈帧值至物理寄存器,直至执行CroutineEntry。

 协程执行到某个地方执行yield挂起。

  • 挂起后,将当前CPU寄存器中的值保存至该协程栈中。将sp指向协程栈顶,用于resume后rsp找到栈顶。

 汇编代码:

//假设调用的是resume函数,执行
//ctx_swap(reinterpret_cast<void**>(src_sp), reinterpret_cast<void**>(dest_sp));
//此时src_cp代表main_stack的sp指针,dest_sp代表croutine_stack的sp指针。

//pushq:将后面的寄存器数据放入到sp所指向的栈当中。
//movq:将后面的值赋值给前面的
//popq: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) 
      //1. 将当前物理寄存器内容保存至main_stack。
      //2. rdi代表函数第一个入参,将当前线程栈信息保存至main_stack。

      movq (%rsi), %rsp 
      //1. rsi代表函数第二个入参,
      //2. 第二个入参为ctx->sp,保存着协程栈的栈顶地址
      //3. 将rsp指向ctx->sp,也就是指向待运行croutine_stack栈顶。
      popq %rbp
      popq %rbx
      popq %r15
      popq %r14
      popq %r13
      popq %r12
      popq %rdi
      ret //执行协程入口函数CRoutineEntry,ret 的作用就是把 %rsp 上移一个位置,并跳转到返回地址执行

举个例子

假设某线程执行函数:

void funA() {
    funB() {
        funC() {
            ...
            coroutine->Resume();
        }
    }
}

其对应的栈结构如下所示:

协程将在创建完成后,由Processor通过resume调用。main_stack代表Processor线程的主栈。Processor实现协程的调度逻辑,通过由main_stack切换至指定协程stack的方式,实现用户任务执行。

  • 由main_stack切换至就绪协程,即resume。在main_stack内保存当前CPU物理寄存器的值。主栈切换后的变化:

  • 切换至就绪协程的栈

协程入口函数:

CoroutineEntry

参数:

void* args

刚创建协程,执行MakeContext,栈状态如左图所示。指定协程入口函数和参数,预留寄存器存储空间并置为0;

创建完成后,该协程第一次Resume,CoroutineEntry未执行,栈状态如中间图所示。此时cr_stack为空,而rsp指向该协程栈的栈顶,当该函数执行后,将控制从rsp执行的位置进行栈信息的存储。

CoroutineEntry开始执行后,栈状态如右图所示。此时执行栈已经切换为协程独有的栈。

由于rsp执行新的协程栈栈顶,因此CoroutineEntry的执行栈已经切换为为该协程所分配的执行栈空间,执行栈顶主要依赖于rsp的指向位置控制。

  • 执行中的协程Yield切换回主栈。

此时将当前寄存器的值保存至内部协程栈中。

  • 协程Yield后主栈变化。修改rsp寄存器的值,将执行栈恢复至主栈,然后pop恢复寄存器的值,此时程序可以继续从主栈上次Resume的地方继续往下执行。

总结

本文主要介绍了CyberRt协程栈的切换过程。在调用Resume和Yield的接口前后的主栈和相关协程栈的变化情况。

  • CyberRT固定为协程分配的栈大小为2M,通过sp指针保存栈顶位置。
  • 初次Resume协程,将调用CroutineEntry函数,参数为void* args。后续Resume该协程将跳到上一次协程Yield的地方继续执行。
  • 切出协程时会将寄存器存储至协程栈内,并恢复主栈信息。切入协程时将寄存器加载至物理寄存器。通过这种方式在用户态模拟出了类似于线程切换的机制。

03-11
<think>嗯,用户想了解CyberRT介绍、教程和使用方法。首先,我需要整理一下关于CyberRT的基本信息。根据提供的引用资料,CyberRT是百度Apollo自动驾驶平台中的实时计算框架,用于处理高并发和低延迟的任务。它的核心功能包括高性能通信、组件化设计和灵活的调度机制。 用户可能希望了解CyberRT的架构,比如它的模块组成,比如通信层、数据中间件和调度器等。引用中提到CyberRT使用基于协程的调度策略,这点需要详细说明。另外,用户可能对如何开始使用CyberRT感兴趣,比如安装配置、创建组件、通信机制和调度策略的设置。 教程部分需要分步骤,比如环境搭建、编写第一个组件、通信配置和调度策略选择。使用注意事项如性能优化、调试工具和扩展性也需要涵盖。同时,相关问题部分要引导用户深入了解性能优化、与ROS的对比以及调试方法。需要确保回答结构清晰,引用正确,并且符合用户的中文需求。</think>### CyberRT 介绍、教程及使用方法 #### 一、CyberRT 简介 **CyberRT** 是百度 Apollo 自动驾驶平台中的**实时计算框架**,专注于高并发、低延迟的自动驾驶任务处理。其核心设计目标包括: 1. **高性能通信**:基于共享内存和零拷贝技术优化数据传输,支持 $QoS$(服务质量)配置,满足不同场景需求[^2]。 2. **组件化设计**:通过 $Component$ 封装算法模块,利用有向无环图($DAG$)描述模块间的数据流[^5]。 3. **灵活调度**:采用用户级协程($Croutine$)和优先级调度策略(如 $ClassicScheduler$ 和 $Choreography$),提升资源利用率[^4]。 #### 二、核心概念 1. **Component** - 继承自 $ComponentBase$ 或 $TimerComponent$,用于实现独立功能模块。 - 示例代码结构: ```cpp class MyComponent : public apollo::cyber::Component<InputType, OutputType> { bool Init() override { /* 初始化逻辑 */ } bool Proc(const InputType& input, OutputType& output) override { /* 处理逻辑 */ } }; ``` [^4] 2. **通信机制** - 支持 $Channel$(发布-订阅模式)和 $Service$(请求-响应模式)。 - 数据传输通过 $Transport$ 层实现,支持 $RTPS$ 协议(需配置 $qos_profile.proto$)。 3. **调度策略** - **ClassicScheduler**:基于优先级抢占式调度。 - **Choreography**:基于时间片的协作式调度,适用于多核资源分配[^4]。 #### 三、教程与使用方法 ##### 1. 环境配置 - **安装依赖**:需安装 FastRTPS、Protobuf 等库。 - **编译配置**:通过 Bazel 构建系统管理组件依赖。 ##### 2. 编写第一个 Component - **步骤**: 1. 定义组件类并实现 $Init()$ 和 $Proc()$ 方法。 2. 在 $dag$ 文件中注册组件及输入/输出通道。 3. 通过启动脚本加载 $dag$ 文件。 ##### 3. 通信配置 - **QoS 设置**:在 $qos_profile.proto$ 中定义历史深度($depth$)、可靠性策略($RELIABILITY_RELIABLE$)等参数。 - 示例配置: ```proto qos_profile { history: HISTORY_KEEP_LAST depth: 10 reliability: RELIABILITY_RELIABLE } ``` ##### 4. 调试与日志 - 使用内置日志宏输出信息: ```cpp AINFO << "Sensor data received: " << sensor_data.DebugString(); ``` 日志级别包括 $ADEBUG$、$AERROR$ 等[^3]。 #### 四、使用注意事项 1. **性能优化**:减少数据拷贝,优先使用共享内存。 2. **调试工具**:利用 $cyber_monitor$ 实时监控通道数据。 3. **扩展性**:通过自定义 $Processor$ 和 $Task$ 实现新调度策略。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值