CyberRT 源码阅读笔记

809 阅读7分钟

源码地址 GitHub - ApolloAuto/apollo: An open autonomous driving platform

文档地址 apollo/docs/04_CyberRT at master · ApolloAuto/apollo

阅读版本 v8.0.0

Schedule

源码路径 cyber/scheduler

概览:schedule的功能是为 CRoutine(其实就是线程池中的task)提供调度策略,基于 linux kernel api 和 用户层CRoutine发射逻辑 实现。其中利用 linux kernel api 实现对线程调度策略修改(假设当前linux有RT补丁)、CPU亲和性修改;利用对CRoutine发射逻辑配置实现有限线程数量下的优先级调度。

说明:接下对源码部分(源码集中在apollo/cyber/scheduler目录)的 api、类关系、使用方式(配置方式) 和 linux kernel api使用 进行分析,之后会从使用方式进行分析。

话不多说,整一个框图说明类关系(先用不严谨的类图,重点标黑)。图中描述Classic调度策略的实现方式。

模块通过scheduler持有和调用CalssicContext和Processor实现对Task(CRoutine直接叫task,虽然CRoutine类有协程的追求但是终究没有派上用场)的执行,具体行为依据 .conf 的配置。其中体现schedule的地方包括两方面,其一是根据.conf中processor配置调用linux kernel api 实现配置Processor内thread的调度策略(SCHED_OTHER、SCHED_FIFO、SCHED_RR)、优先级(Prio)和cpu亲和性(绑定单core或者绑定多core);其二是根据.conf中tasks配置的优先级配置选择向group发送的先后顺序,group聚合多processor与多task间的关系。这些schedule实现都在ProcessorContext中。

备注:.conf中,processor_num是一个group中包含的线程数量,cpuset是这组线程的cpu亲和性配置。group与其他元素具体关系见下问图。

DataFusion

源码路径 cyber/data

概览:datafusion的功能是将多路输入合并成一帧数据输出的模块。在apollo中,node通过datafusion实现对read结果的合并。

DataFusion由 DataDispather、CacheBuffer、DataVisitor三部分组成。reader(或者说subscriber)的结果通过DataDispather发送和缓存在CacheBuffer中,DataVision被事件触发将一组数据发送给具体的CRoutine。

数据流:接下来我们主要关注数据流。

线程关系:整个模块都没有单独开线程。但应该注意到不同Reader向DataDispatcher发送数据的调用来自可能来自不同线程,故每个CacheBuffer都具有自己的mutex。还要注意,TryFetch并非都是在Reader发数据的线程中调用,故ChannelBuffer访问CacheBuffer时也有加锁。这里也能感受到,CacheBuffer上再封装一层,有利于降低加解锁的频率。

Fusion逻辑:当前开源的代码中仅有一种Fusion逻辑,即第一路数据触发对外部的通知 和 输出当前各路最新数据。很可能的落地方式是第一路数据触发外部回调并返回每路下一帧数据(帧序号在DataVisitor)。

问题:这里的逻辑过于简单,明显无法解决数据波动和少量丢帧情况,或者说不够鲁棒。估计实际生产代码有基于DataFusion的更多实现,AllLatest只是最初级的方案仅用于开源工程。

Event

通过代码插桩+文件打印的方式记录必要的输入输出时间戳。此处的思路是将必要的调试信息转换成event进行抽象和记录,突出信息的轻量。

CM/Transport

通信功能基础分为 Transmitter、Receiver 和 service_discovery。service_discovery此处不必多说,与业务逻辑关系较弱。Transmitter(即writer底层实现)和Receiver(即reader底层实现)都具有 SHM、RTPS、INTRA、HYBRID 的实现,对应不同的通信方式。Dispathcer也具有四种具体实现,具有单例属性,作为 Receiver 底层 Listener 运行的实体(调用回调和触发Listen),也持有具体的内存对象。这部分的组合类/使用接口为 Transport,具有进程单例属性,负责初始和释放 所有通信方式的Dispatcher,负责提供 Transmitter、Receiver的Create接口。Transmitter 和 Receiver 是 Writer 和 Reader 的通信实现,service_discovery 是二者的服务发现实现。

通信方式说明:

  • SHM,进程间共享内存/UDP 通信,效率和带宽很好。
  • RTPS,发布订阅模型,能够提供实时性、可靠性和灵活性,支持QoS通信质量策略调整。跨机模式往往采用UDP,故存在QoS调整等找补策略。
  • INTRA,Inter-Process 通信,也就是进程内消息传递。
  • HYBRID,混合/自动选择通信方式,根据具体需求自动选择以上三种方式之一。

这里仔细了解SHM类型的通信实现方式。主要关注点为 Transmitter 发送逻辑、Receiver 被动接收触发逻辑、数据缓存、数据同步。

折叠内容描述在sys/shm.hsys/socket.h层的发送和接受逻辑,也描述数据缓存、数据同步逻辑。Listen触发部分位于ShmDispatcher中,存一个线程会每隔100ms询问一次Notifier,这个询问是带有超时的阻塞接口,收到数据即刻线程中调用回调。ConditionNotifier实现会每50ms查询一次共享内存计数器,MulticastNotifier依赖内核poll。同一个类型的Dispatcher是单例,故同类型消息回调都是串行的。在消息回调中开启耗时操作将会影响全局消息接收。

transport/shm

代码分析如下:

  1. Block 类的实现 (block.hblock.cc):
    • Block 类是共享内存(Shared Memory, SHM)中的一个块,用于存储消息数据。
    • 它包含了读写锁的逻辑,使用 std::atomic<int32_t> 类型的 lock_num_ 变量来控制对块的访问。
    • TryLockForWriteTryLockForRead 方法分别尝试获取写锁和读锁,如果块正在被写入,则读锁获取会失败。
    • ReleaseWriteLockReleaseReadLock 方法用于释放写锁和读锁。
    • msg_size_msg_info_size_ 分别存储消息的大小和消息元信息的大小。
  1. ConditionNotifier 类的实现 (condition_notifier.hcondition_notifier.cc):
    • 继承自 NotifierBase
    • ConditionNotifier 类用于在共享内存中通知读写事件。
    • 它使用 System V 的共享内存机制,通过 shmgetshmat 系统调用来创建和映射共享内存。
    • Notify 方法用于通知有新的可读信息,Listen 方法用于等待通知并获取可读信息。
    • Indicator 结构体用于存储通知的序列号和相关信息。
  1. MulticastNotifier 类的实现 (multicast_notifier.hmulticast_notifier.cc):
    • 继承自 NotifierBase
    • MulticastNotifier 类使用多播(Multicast)来通知读写事件。
    • 它创建 UDP 套接字并绑定到多播地址和端口,用于发送和接收通知。
    • Notify 方法通过 UDP 多播发送通知,Listen 方法监听多播地址上的通知。
  1. Segment 类的实现 (segment.hsegment.cc):
    • Segment 类表示共享内存中的一个段,它管理多个 Block
    • 它提供了获取写入块和读取块的方法,以及销毁共享内存段的方法。
    • AcquireBlockToWriteAcquireBlockToRead 方法用于分配块以进行写入和读取。
    • ReleaseWrittenBlockReleaseReadLock 方法用于释放块。
  1. PosixSegment 和 XsiSegment 类的实现 (posix_segment.hposix_segment.cc, xsi_segment.hxsi_segment.cc):
    • 这两个类是 Segment 类的具体实现,分别使用 POSIX 共享内存和 System V 共享内存。
    • 它们负责创建、映射、销毁共享内存段,并管理状态和块的分配。
  1. SegmentFactory 类的实现 (segment_factory.hsegment_factory.cc):
    • SegmentFactory 类用于创建 Segment 对象,它根据配置选择使用 PosixSegmentXsiSegment
  1. BUILD 文件:
    • BUILD 文件是 Bazel 构建系统的配置文件,它定义了如何构建和测试代码。
    • 它包含了多个 cc_librarycc_test 规则,用于编译库和执行测试。
  1. 测试代码 (condition_notifier_test.cc):
    • 测试代码使用 Google Test 框架来验证 ConditionNotifier 类的功能。

总结:

  • 代码实现了一个基于共享内存的传输系统,包括消息块的管理、读写锁的控制、通知机制的实现,以及共享内存段的创建和销毁。
  • 使用了 System V 和 POSIX 两种共享内存机制,以及 UDP 多播通信。
  • 提供了 Bazel 构建配置和单元测试。
  • 代码风格遵循了 Apollo 项目的编码标准。

上层 CS 通信模式的实现基于 RTPS 模式。将 QoS 设置成QOS_PROFILE_SERVICES_DEFAULT 即可。

Logger

基于glog

Node & Component

node作为每个进程的单例,持有资源和提供应用层接口。重要逻辑还得看下层。