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


    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 后记

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

  • 相关阅读:
    linux进程管理类
    linux关机重启指令
    linux分区及磁盘挂载
    linux的运行级别
    property
    访问限制机制
    类的组合与封装
    继承与派生
    logging模块
    re模块
  • 原文地址:https://www.cnblogs.com/passerby233/p/RTCSD_proj_communication.html
Copyright © 2020-2023  润新知