1 前言
在这一项目中,我们采用了多线程的方式来处理不同任务的需求。在不同任务间必定会存在有一定的资源共享的情况,最简单的办法就是使用全局变量,但是这会带来一定的问题,如:资源读写的冲突等等。当然了,我们也可以使用一些常见的方法,如互斥量、信号量等等来解决这类问题。不过,Xenomai Native Skin 本身就提供了大量常用算法,简化我们的开发过程。因此,下文将着介绍如何利用 Xenomai Native Skin 带有的 API 来实现本团队项目的任务间通信。
2 需求分析
需要进行资源共享的内容有如下几点:
- 系统事件,如中断等。
- 其它任务事件。
- 击球器坐标及速度。
- 插值命令队列。
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 后记
本项目并没有使用太多复杂的消息通讯方式,而且出于简化开发过程的原因,有些本应该用更好的方式处理的却没有用。比如:在读写击球器坐标那里应当使用互斥量来保证资源不冲突等等。希望以后能更好地利用好各种线程间通讯的消息模型。