跳转至

执行器管理

本文是对官网文章个人的总结概括,有意读者可阅读官网文章:https://micro.ros.org/docs/concepts/client_library/execution_management/

介绍

​ 节点是ROS框架的根基,节点内的回调函数更是灵魂。ROS中通信机制有很多,话题,服务,动作等等,所以对节点的回调函数的执行处理是一个很重要的问题。

​ ROS1中网络线程接收到的所有消息均放在FIFO队列中,回调也是以FIFO的方式调用,没有执行管理。在ROS2中,随着DDS(数据分发服务)的引入,executor(执行器)的概念也被创建来支持执行管理。大致流程是查询->获取->执行

  • ROS2中消息被缓冲在DDS中,executor 会收集它所管理的所有节点中的句柄,配置一个等待集(wait set)并发给rcl层,并阻塞。
  • rcl层返回一个就绪集(ready set),列出哪些句柄此刻有消息。
  • Executor 遍历就绪集,从中提取数据,调用相应回调函数。

​ 但ROS2标准rclcpp执行器具有局限性。例如定时器优先于所有其他 DDS 句柄,非定时器句柄采用非抢占式轮询调度,以及每个句柄仅考虑一个输入数据(即使可能有多个输入数据可用),ROS也没有机制来强制节点内回调函数的执行顺序。这些特点致使其难以保证执行器的确定性和实时性。

​ 鉴于以上问题,在开发microros的执行器时,官方开发了高级执行管理机制,并将其用于ROS中,改进了标准ROS2的rclcpp库。

rclcpp标准执行器分析

体系结构

  • 弱引用管理:执行器实例只维护指向节点的弱指针(weak po1inter)。弱指针是c++中的一个概念。通俗理解就是通过一个中间人去访问对象。执行器访问节点时会判断节点是否存在,如果节点已销毁,执行器就会跳过,而不会强行访问导致bug。所以我们可以安全地销毁节点,而无需通知Executor。
  • 依赖DDS队列:执行器自己不维护回调队列,而是依赖DDS来缓冲消息。它只是需要了,从中提取数据。
  • 缺乏优先级:执行器内部层次,在一个任务中,不对回调进行优先级排序或分类。
  • 非实时感知:执行器与操作系统层次,没有利用底层操作系统调度器的实时特性来更好地控制执行顺序。可以理解为没有配置任务优先级。

不太好的特性

  • 计时器优先级最高。执行器总是先处理定时器。如果定时器任务太多(过载),DDS 里的普通消息(如传感器数据)可能永远轮不到处理。
  • 非定时器句柄采用非抢占式循环调度。
  • 每一句柄只考虑一条消息。会严重加剧延迟。

处理模式的分析

​ 这一大部分文章主要分析了移动机器人和实时嵌入式系统的处理模式,由此分析推导出microros中执行器应具有的特点.

移动机器人:

  • 机器人的感知-规划-行动流水线
  • 多速率传感器同步
  • 高优先级处理路径

实时嵌入式系统

  • 时间触发轮询
  • 逻辑执行时间 (LET)

rclc执行器

特征

rclc执行器支持所有事件类型:

  • 订阅
  • 计时器
  • 服务
  • 客户
  • 警戒条件
  • 行动
  • 生命周期

还提供了如下新功能:

  • 触发执行
  • 用户自定义顺序执行
  • 多线程和调度配置
  • 用于周期性进程调度数据同步的LET语义

顺序执行

  • 配置过程中,用户可定义句柄的顺序:即添加顺序,先添加,先检查先执行。
  • 在配置时,用户可以定义句柄是仅在有新数据可用时调用(ON_NEW_DATA)还是始终调用回调(ALWAYS)。rclc_executor_add_subscription(&exe_sense, &sense_IMU, &my_sub_cb2, ON_NEW_DATA);
  • 运行时,所有句柄均按照用户定义的顺序进行处理。
    • 如果句柄配置为 ON_NEW_DATA,则仅当有新数据可用时才会调用相应的回调函数。
    • 如果句柄的配置为 ALWAYS,则相应的回调函数也始终为 ALWAYS。如果没有数据可用,则回调函数将不带任何数据(例如,空指针)被调用
    • 辨析:执行器被触发和回调被调用是两个概念。

触发条件

  • 给定一组句柄,触发条件(基于这些句柄的输入数据是否可用)决定何时开始处理所有回调
  • 可选方案:
    • ALL:当所有句柄都有输入数据时触发。
    • ANY:当至少一个句柄有输入数据可用时触发。
    • ONE:当用户指定的句柄有输入数据时触发。
    • User-defined: 用户自定义复杂的触发逻辑,甚至可以包含硬件中断。可以满足更个性化的需求。

逻辑执行时间(LET语义)

LET语义的核心是通过固定时间读取,固定时间写入的策略,保证实时系统中数据的一致性。具体流程如下,在一个周期中:

  • 周期开始(读输入): 周期开始时执行器会读取所有相关的输入数据,并保存为一份本地副本
  • 周期进行中(处理计算):

    执行器拿着本地副本中的数据,按预定顺序执行回调函数。关键在于,执行期间,即使DDS收到了新的数据,执行器也不会使用,从而保证本周期内数据的一致性。

  • 周期结束(写输出):

    周期结束时,统一将结果发布,即使有的结果早就算出,也要等到最后统一发布。

逻辑执行是一个特殊情况,正常情况下,执行器触发后,对数据是即取即用,对逐一读取句柄时,在执行该回调函数之前,才会从DDS队列中请求新数据。

多线程和调度配置

这个特点支持用户可以创建多个executor,把优先级高,低的callback分配给不同的executor,然后创建不同优先级的任务,让executor在不同任务中运行。

注意

  • 通过将回调分配给不同的 Executor 实例,可以避免标准 ROS 2 执行器中“一个慢任务阻塞所有任务”的现象。
  • 危险性:

    if Executors are running in multiple threads, publishing needs to be atomic

当执行器在多线程中运行时,发布操作一定要是原子性的。microros现在库尚未提供这种线程安全的保护,所以需要用户手动实现。

void motor_callback(const void * msgin) {

    // 在发布前获取锁
    if (xSemaphoreTake(pub_mutex, portMAX_DELAY) == pdTRUE) {
        // 【原子操作区】此时没人能打断发布过程
        rcl_publish(&shared_publisher, &tx_msg, NULL);

        // 发布完立即释放锁
        xSemaphoreGive(pub_mutex);
    }
}

执行器常用API

配置阶段

  1. 初始化参数 :
    • 回调总数
    • 回调顺序
    • 触发条件(可选,默认值:ANY)
    • 数据通信语义(可选,默认为 ROS2)
  2. 执行顺序与逻辑
    • 顺序定义: 代码中添加句柄(Handle)的顺序,直接决定了运行时回调的 顺序处理顺序
    • 回调执行条件:
      • ON_NEW_DATA
      • ALWAYS
    • 执行器触发条件:
      • trigger_any (默认):任意一个回调有新数据就触发(兼容标准 ROS 2 行为)。
      • trigger_all:所有回调都有新数据时才触发。
      • trigger_one:指定某个特定句柄收到数据时触发。
      • user_defined_function:用户自定义复杂逻辑。
    • 通信语义设置:
      • ROS2 (默认)
      • LET(逻辑执行时间)

执行阶段

可用的旋转函数:

  • spin_some:运行一次。rclc_executor_spin_some(&executor, 1000);在1000ns内等待执行,只执行一次,就立即返回,1000ns是最大等待时间。
  • spin_period:按固定周期运行。不会返回,在1000周期内执行一次,然后休眠;该周期结束,开始下一次周期。
  • spin:无限循环运行。