团队项目·冰球模拟器——任务间通信、数据共享等设计

1 前言

在这一项目中,我们采用了多线程的方式来处理不同任务的需求。在不同任务间必定会存在有一定的资源共享的情况,最简单的办法就是使用全局变量,但是这会带来一定的问题,如:资源读写的冲突等等。当然了,我们也可以使用一些常见的方法,如互斥量、信号量等等来解决这类问题。不过,Xenomai Native Skin 本身就提供了大量常用算法,简化我们的开发过程。因此,下文将着介绍如何利用 Xenomai Native Skin 带有的 API 来实现本团队项目的任务间通信。

2 需求分析

需要进行资源共享的内容有如下几点:

  1. 系统事件,如中断等。
  2. 其它任务事件。
  3. 击球器坐标及速度。
  4. 插值命令队列。

3 实现

3.1 系统事件及其它任务事件

系统事件主要指来自 Linux 或用户的信号。在本项目中,主程序接收并处理信号 SIGINT 和 SIGTERM。注册信号处理函数使用 Linux 标准库 signal.h,代码如下:

signal(SIGINT, terminate_signal_handler);
signal(SIGTERM, terminate_signal_handler);

其中terminate_signal_handler()是处理函数。由于在结束进程前应通知各线程自行终止,因此设计一个 Xenomai Native 事件,各线程(任务)应定时检查相应的事件位,若终止事件发生,应尽快清理内存并结束。

由于本项目中的事件总数较少,且一个事件变量至少可以存放32个事件位(长整型),故在本项目中仅使用一个事件变量。

声明事件及事件位别名如下:

extern RT_EVENT event;                            // 声明事件
namespace event_mask {                            // 使用命名空间来减少冲突
    const unsigned long kNone = 0x0;              // 用于清空所有事件
    const unsigned long kRequest = 0x01;          // 新请求
    const unsigned long kDone = 0x02;             // 插值完成
    const unsigned long kTerminate = 0x04;        // 任务中断
    const unsigned long kError = 0x80000000;      // 任意错误发生
    const unsigned long kAny= 0xffffffff;         // 任意事件
}

terminate_signal_handler()即发送终止事件的函数如下:

void terminate_signal_handler(int n) {
    rt_printf("[main] catch signal: %d
", n);              // 输出调试信息
    rt_event_signal(&event,                                 // 事件变量为 event
                    event_mask::kTerminate);                // 终止事件事件位置位
}

接收中止事件示例如下:

rt_event_wait(&event,                          // 事件变量
              event_mask::kTerminate,          // 终止事件
              &mask,                           // 返回值
              EV_ANY,                          // EV_ANY 表示任一事件发生时返回
              TM_NONBLOCK);                    // 不阻塞
if (mask & event_mask::kTerminate)             // 函数返回可能有多种原因,需要判断
    goto TERMINATED;                           // 跳转到后处理。可能会在多重循环中,故使用 goto 语句

接收插值请求如下:

rt_event_wait(&event,
              event_mask::kRequest | event_mask::kTerminate,  // 同时接收多个事件
              &mask,                                          // 返回值
              EV_ANY,                                         // 任一事件发生时返回
              TM_INFINITE);                                   // 不限时阻塞
if (mask & event_mask::kTerminate)                            // 如果是终止事件,则直接跳出
    goto TERMINATED;

3.2 击球器坐标和速度

由于这一个资源比较特殊,约定只有一个线程(任务)可写。为了减少开发时间,直接采用内存共享的方式。在读的时候,为了减少发生冲突的可能性,可以先复制再读。

3.3 插值命令队列

3.3.1 声明及初始化

插值命令有个重要特点:在本次插值结束后才会进行下一次插值,也就是典型的先进先出(FIFO)队列模型。而在 Xenomai 中,Native Skin 就提供了 queue 这一数据模型,故可以直接使用。设计中,命令对象放在堆中,在队列中传递的是指针。

由于有两个坐标轴,故需要采用两个队列变量,声明及初始化如下:

extern RT_QUEUE queue_axis_x, queue_axis_y;

rt_queue_create(&queue_axis_x,                             // 队列变量
                "axis_x",                                  // 队列名,必须保证唯一
                64 * sizeof(InterpolationConfigure *),     // 队列的内存大小
                64,                                        // 队列长度
                Q_FIFO | Q_SHARED);                        // 队列类型
rt_queue_create(&queue_axis_y, "axis_y", 64 * sizeof(InterpolationConfigure *), 64, Q_FIFO | Q_SHARED);       // 同上

3.3.2 发送端

发送消息的代码比较简单,如下所示:

auto new_cmd = (InterpolationConfigure **)                          // 类型
               rt_queue_alloc(&queue,                               // 需要申请空间的队列变量
                              sizeof(InterpolationConfigure *));    // 空间大小
*new_cmd = new TrapezoidInterpolation();         // 配置命令参数
(*new_cmd)->set_time(time);
(*new_cmd)->set_position(position);
(*new_cmd)->set_velocity(velocity);
(*new_cmd)->set_acceleration(acceleration);
return rt_queue_send(&queue,                               // 类型
                     new_cmd,                              // 消息内容所在地址
                     sizeof(InterpolationConfigure *),     // 大小
                     Q_NORMAL);                            // 发送方式:普通

3.3.3 接收端

由于同一个任务函数会被创建两次,故不能在编译期就确定相应的队列变量,需要在运行时再进行绑定。而 Xenomai Native Skin 提供了一个这样的 API,叫rt_queue_bind(),可以实现这种要求。绑定如下:

RT_QUEUE queue_command;
if (rt_queue_bind(&queue_command, axis->name, TM_NONBLOCK)) {        // 尝试绑定对应的队列变量
    rt_printf("[traj_%s] queue not found
", axis->name);
    goto TRAJECTORY_GENERATED_TERMINATED;
} else {                                                             // 若成功,返回 0
    rt_printf("[traj_%s] queue bind
", axis->name);
}

在绑定之后则可以读队列了,由于传递的是指针变量,而且信息本身也是指针变量,则会涉及到较复杂的内存处理,代码如下:

rt_queue_receive(&queue_command,                         // 队列变量名
                 &msg,                                   // 返回消息。注:返回值本身是地址值
                 TM_INFINITE);                           // 阻塞
memcpy(&interpolation, msg, sizeof(Interpolation *));    // 直接复制指向的内存,并跳过类型检查
rt_queue_free(&queue_command, msg);                      // 释放队列消息所在的内存

个人认为,这一个 API 设计得并不是很好,因为在申请空间时,提供的是消息本身的内容;而在发送接收消息时,直接返回的值却是指向消息内容的地址值,两个 API 的参数类型不一致,不利于开发。

4 后记

本项目并没有使用太多复杂的消息通讯方式,而且出于简化开发过程的原因,有些本应该用更好的方式处理的却没有用。比如:在读写击球器坐标那里应当使用互斥量来保证资源不冲突等等。希望以后能更好地利用好各种线程间通讯的消息模型。

原文地址:https://www.cnblogs.com/passerby233/p/RTCSD_proj_communication.html