深入探索Xenomai实时系统
1. 核心代码分析
首先来看一段核心代码:
task = vrtx_current_task();
/*
* Set up a few status bits the VRTX way, so that inquiries
* about the task state will return proper information.
*/
task->vrtxtcb.TCBSTAT = TBSMBOX;
if (timeout)
task->vrtxtcb.TCBSTAT |= TBSDELAY;
/* We have to wait for a message now. */
xnsynch_sleep_on(&mb->synchbase, timeout, XN_RELATIVE);
/* Are we waking up due to a Linux signal, or some unblocking call? */
if (xnthread_test_info(&task->threadbase, XNBREAK)) {
*errp = -EINTR;
goto unlock_and_exit;
}
/* Did we reach the timeout limit? */
if (xnthread_test_info(&task->threadbase, XNTIMEO)) {
*errp = ER_TMO;
goto unlock_and_exit;
}
done:
/*
* Ok, we got a message, let's reset the mailbox before passing
* it on to the caller.
*/
msg = mb->msg;
mb->msg = NULL;
*errp = RET_OK;
unlock_and_exit:
xnlock_put_irqrestore(&nklock, s);
return msg;
这段代码主要实现了一个等待消息的功能。具体步骤如下:
1. 获取当前任务。
2. 设置任务的状态位,根据是否有超时设置不同的状态。
3. 调用
xnsynch_sleep_on
函数等待消息。
4. 检查是否因为Linux信号或解锁调用而唤醒,如果是则设置错误码并跳转到退出。
5. 检查是否达到超时限制,如果是则设置错误码并跳转到退出。
6. 如果成功获取到消息,重置邮箱并设置返回码为成功。
7. 最后释放锁并返回消息。
2. Xenomai的核心构建块与皮肤
Xenomai的核心在实现核心活动中承担了大部分工作,主要使用的锚点对象是同步构建块,它由Xenomai核心导出,用于建模各种IPC特性。
Xenomai系统的一个显著特点是缺乏中央或主API进行应用开发,它认为所有RTOS API都是平等的。开发者可以选择适合Xenomai灵活模型的任何皮肤,甚至开发新的皮肤来移植尚未支持的RTOS应用。
Xenomai自带了多种实时API:
| API名称 | 描述 |
| ---- | ---- |
| POSIX接口 | 由Gilles Chanteperdrix维护,旨在符合1003.1b标准,可作为glibc服务的直接替代品,适用于LinuxThreads和NPTL - enabled Glibc。 |
| VxWorks模拟器 | 模仿WIND内核5.x API。 |
| pSOS + 模拟器 | 基于pSOS 2.x核心API定义。 |
| VRTX模拟器 | 支持VRTX32和VRTX/sa系统调用接口。 |
| uITRON兼容皮肤 | 基于规范修订版3.02 (E级)。 |
| RTAI 3.x模拟器 | 用于将内核模块中的遗留应用移植到Xenomai。 |
| 原生API | 作为Xenomai开发工作的一部分设计,类似于传统RTOS API,方便非POSIX RTOS背景的开发者开发应用。 |
| 实时驱动模型(RTDM) | 为实时设备驱动的用户和开发者提供统一接口,解决了双内核系统中应用的实时和非实时运行阶段的约束问题,符合POSIX语义。 |
3. Xenomai的工作原理
Xenomai与其他适用于Linux的双内核系统(如RTAI或RTLinux)的根本区别在于它与原生Linux环境有更高的集成度。对于Xenomai项目,保持常规Linux编程模型对实时应用可用与保证在任何硬件上实现最低延迟同样重要。
然而,使用协内核技术在程序员尝试利用Linux丰富的POSIX环境时会带来一些可用性问题。例如,实时应用可能需要调用具有不可预测延迟的Linux服务,如访问磁盘文件或建立网络通信。
3.1 实时影子
Xenomai的实时线程源自通过标准POSIX API创建的常规Linux任务,因此在非关键时间模式下,Xenomai线程继承了Linux任务调用常规Linux服务的能力。
当Linux任务提升到实时应用领域时,会附加一个Xenomai特定的扩展,即实时影子。实时影子允许匹配的Linux任务在实时模式下由Xenomai协内核调度。
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(常规Linux任务):::process --> B(实时影子):::process
B --> C(Xenomai协内核调度):::process
3.2 新的系统调用集
Xenomai通过添加几组新的系统调用来扩展目标操作系统,这些系统调用由皮肤实现。每次加载皮肤模块时,它会向Xenomai核心导出其提供的服务集,核心再将应用请求分派到合适的皮肤。
为了实现这一点,Xenomai使用I - pipe功能拦截常规Linux系统调用调度器,并将额外的系统调用导向实现它们的皮肤模块。在用户空间,系统调用包装器以一组C函数的形式存在于库中,实时应用必须链接这些库。
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(VxWorks应用):::process --> B(libvxworks):::process
B --> C(Xenomai核心):::process
C --> D(VxWorks皮肤):::process
C --> E(常规Linux子系统):::process
3.3 内核特性共享与域迁移
实时线程可以调用标准glibc服务和Xenomai引入的额外系统调用。在设置和清理任务时,应用线程可能满足于Linux内核提供的尽力而为的延迟,但在主要处理工作期间,可能需要严格的实时保证。
在协内核环境中,两个内核并发运行且不同步活动,Xenomai的一个内核具有绝对优先级。因此,必须防止实时线程在由Xenomai核心控制时调用常规Linux内核代码,否则会导致不安全的重入,损害整个系统。
解决冲突的方法有两个方面:
- Xenomai线程在任何给定时间只能由Xenomai协内核或Linux内核之一控制。协内核管理时,可使用Xenomai的确定性系统调用;Linux内核控制时,可进入常规Linux系统调用,但无实时保证。Xenomai将这些上下文称为主(Xenomai控制)和次(Linux控制)模式。
- 当发出系统调用时,Xenomai会自动将线程置于正确的模式,这种模式切换机制称为域迁移。
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(中断、系统调用等事件):::process --> B(实时应用):::process
B --> C(Xenomai域):::process
B --> D(Linux域):::process
C --> E(IRQ控制):::process
D --> F(IRQ控制):::process
4. 实时驱动模型(RTDM)
开发双内核架构的实时设备驱动一直是实时编程中最具挑战性的任务之一,尤其是当驱动需要完成的工作不仅仅是将从线路上轮询到的几个字节通过FIFO发送到用户空间进程时。
4.1 面临的问题
主要存在以下问题:
-
接口设计难题
:设备驱动开发者需要为应用程序设计专门的解决方案来调用驱动以提交请求并获取结果。例如,提供实时安全的FIFO,或者使用系统内存结合常见的IPC,在负责内核空间工作的实时辅助程序和用户空间进程之间共享。这些非标准协议在将驱动移植到其他实时扩展时很难复用。
-
API依赖问题
:由于常规Linux内核API不适合实现驱动,开发者必须基于底层实时扩展提供的特定API来开发。例如,基于协内核的设备驱动不能使用普通的Linux互斥锁或在队列中等待,而必须从实时内核获取等效资源。因此,更换到另一个Linux实时扩展可能需要将驱动代码移植到新的API。
4.2 RTDM的出现与作用
为了解决这些问题,Jan Kiszka和Jörg Langenberg提出了实时驱动模型(RTDM),并且从早期开始就被Xenomai支持。RTDM作为一个中介层,连接实时应用进程和设备驱动提供的服务。
设备驱动分为两类:
| 驱动类型 | 描述 |
| ---- | ---- |
| 协议驱动 | 提供套接字接口,适合管理与实时设备的面向消息的通信。用户空间库将标准套接字调用(如socket()、send()/sendto()、recv()/recvfrom()等)包装成Xenomai系统调用,这些调用会触发操作所需协议族的设备驱动中的回调函数。 |
| 命名设备 | 类似于Linux设备驱动模型中的字符设备,由驱动定义一个任意的文本标签命名。该命名设备在标准Linux设备层次结构中没有对应物,仅存在于RTDM层,与常规Linux设备命名空间并行。用户空间库将标准POSIX 1003.1的I/O通信例程(如open()、read()/write()、ioctl()等)包装成Xenomai系统调用,这些调用会触发与open()最初传递的名称匹配的设备驱动。 |
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(应用程序):::process --> B(设备驱动):::process
B --> C(RTDM皮肤):::process
C --> D(Xenomai核心):::process
D --> E(Linux系统调用接口):::process
5. RTDM驱动示例
以下是一个基于RTDM的命名设备驱动示例,它实现了一个由内核空间的实时任务执行的简单硬件轮询循环,该循环会定期唤醒等待某些事件信号的用户空间进程。
struct rtdev_context {
rtdm_task_t task;
rtdm_event_t event;
};
static void task_body(void *arg)
{
struct rtdev_context *ctx = (struct rtdev_context *)arg;
/*
* Tell RTDM that we want to be scheduled periodically, with a
* 100 microsecond base period.
*/
rtdm_task_set_period(&ctx->task, 100000);
while (1) {
/* Wait for the next release point in the timeline. */
rtdm_task_wait_period();
poll_hardware();
/* Signal the event userland waits for. */
rtdm_event_pulse(&ctx->event);
}
}
int rtdev_open(struct rtdm_dev_context *context,
rtdm_user_info_t *user_info, int oflags)
{
struct rtdev_context *ctx = context->dev_private;
/*
* Userland just called open("/dev/rtdev", ...); so set up a
* driver context for the new channel. We first initialize the
* event flag, then tell RTDM to start the polling task, with
* priority 72.
*/
rtdm_event_init(&ctx->event, 0);
return rtdm_task_init(&ctx->task, "rtdev", &task_body, ctx, 72, 0);
}
int rtdev_close(struct rtdm_dev_context *context,
rtdm_user_info_t *user_info)
{
struct rtdev_context *ctx = context->dev_private;
/* Userland just closed the file; do some housekeeping here. */
rtdm_event_destroy(&ctx->event);
rtdm_task_destroy(&ctx->task);
return 0;
}
int rtdev_ioctl_rt(struct rtdm_dev_context *context,
rtdm_user_info_t * user_info, unsigned int request, void *arg)
{
struct rtdev_context *ctx = context->dev_private;
int ret;
/*
* The only request this driver honors is to wait for the next
* pulse sent by the polling task, then copy back some data
* received from the hardware. We won't specify what this data
* may be here.
*/
switch (request) {
case DEMO_RTIOC_RTDEV_WAIT:
/* Wait for task_body() to wake us up. */
ret = rtdm_event_wait(&ctx->event);
if (!ret)
ret = rtdm_safe_copy_to_user(user_info, arg,
&hwdata, sizeof(hwdata));
break;
default:
ret = -EINVAL;
}
return ret;
}
static struct rtdm_device device = {
.struct_version = RTDM_DEVICE_STRUCT_VER,
.device_flags = RTDM_NAMED_DEVICE,
.context_size = sizeof(struct rtdev_context),
.device_name = "rtdev",
.open_rt = NULL,
.open_nrt = rtdev_open,
.ops = {
.close_rt = NULL,
.close_nrt = rtdev_close,
.ioctl_rt = rtdev_ioctl_rt,
.ioctl_nrt = NULL,
},
.device_class = RTDM_CLASS_DEMO,
.device_sub_class = RTDM_SUBCLASS_RTDEV,
.driver_name = "rtdev",
.driver_version = RTDM_DRIVER_VER(0, 0, 0),
.peripheral_name = "RTDM-compliant demo device driver",
.proc_name = device.device_name,
};
int __init __rtdev_init(void)
{
/* Make this driver known to RTDM. */
return rtdm_dev_register(&device);
}
void __rtdev_exit(void)
{
rtdm_dev_unregister(&device, 1000);
}
在
rtdm_device
结构中,可以看到驱动实现的每个接口例程的条目,如
open_rt()
和
open_nrt()
。RTDM会根据调用上下文调用这些例程,对于Xenomai调度器控制的实时操作调用
open_rt()
,对于Linux调度器控制的标准操作调用
open_nrt()
。
6. 使用RTDM驱动的应用示例
以下是一个使用上述驱动的典型应用程序示例,该代码需要链接一个包装RTDM调用的Xenomai库,Xenomai的POSIX接口提供了这样的包装。
void *test_thread(void *arg)
{
int ret;
for (;;) {
ret = ioctl(fd, DEMO_RTIOC_RTDEV_WAIT, &hwdata);
if (ret) {
perror("ioctl failed");
pthread_exit(EXIT_FAILURE);
}
}
}
int main(int argc, char *const argv[])
{
struct sched_param param = { .sched_priority = 70 };
pthread_attr_t attr;
pthread_t tid;
int fd;
mlockall(MCL_CURRENT | MCL_FUTURE);
// 后续代码可继续完善初始化和线程创建等操作
}
这个应用程序创建了一个实时线程,通过RTDM包装的
open()
和
ioctl()
调用与驱动进行通信。
综上所述,Xenomai实时系统通过其独特的设计和机制,如实时影子、新的系统调用集、域迁移以及实时驱动模型(RTDM),解决了双内核系统在实时编程中的诸多问题,为开发者提供了强大而灵活的实时编程环境。无论是对于实时设备驱动的开发还是实时应用程序的编写,Xenomai都提供了有效的解决方案和丰富的工具。
超级会员免费看

1935

被折叠的 条评论
为什么被折叠?



