在开发实时应用时,我们希望软件具备良好的跨平台和可移植,既能在实时linux也能在RTOS上工作,为实现这个目的,我们会选择使用POSIX API来设计实时应用,实现在不同的操作系统和平台上的可移植性。这样只要有POSIX支持,应用程序从一个操作系统移植到另一个操作系统,无需对源代码进行较大的修改,可以减少很多重复工作(想象很美好)。
但是,实时应用除关注接口基本功能一致外,还关注接口的实时性(执行时间确定性),那不同的实时操作系统的POSIX底层实现/行为实不实时呢?
以精确定时为例,精确定时是我们常用的操作系统服务之一,比如工业以太网(ECAT、PN...)中的通信周期,通信周期的准确控制离不开精确定时;在无线基站和终端的slot调度中,精确定时确保了数据传输的有序与高效;在实时仿真中,仿真步长的精确控制更是模拟真实场景的关键等等,这些都离不开POSIX定时接口。
本文首先简要概述POSIX标准,随后深入剖析POSIX定时接口在常用开放内核源码的RTOS(实时操作系统)上的上的实现原理做简单介绍。通过本文的介绍,希望能为读者在实际应用中提供有价值的参考,避免在RTOS中使用POSIX遇到不必要的困扰。
POSIX是 IEEE(Institue of Electrical and Electronics Engineers,电气和电子工程师学会)为了规范各种 UNIX 操作系统提供的 API 接口而定义的一系列互相关联标准的总称,其正式称呼为 IEEE1003,国际标准名称为 ISO / IEC9945。此标准源于一个大约开始于 1985 年的项目。
POSIX 这个名称是由理查德 · 斯托曼(Richard Stallman)应 IEEE 的要求而提议的一个易于记忆的名称。它是 Portable Operating System Interface(可移植操作系统接口)的缩写,而 X 则表明其对 Unix API 的传承。POSIX更能正确表示这一系列相关标准,所以术语POSIX最初被用作IEEE Std 1003.1-1988的同义词,即POSIX.1(n=1)。这保持了符号“POSIX”可读性的优点,而不会与POSIX系列标准产生歧义。
关于有关POSIX. 1的背景、受众和目的的更多信息,参看该链接https://www.opengroup.org/austin/papers/backgrounder.html
POSIX标准涉及操作系统方方面面,我们主要关注以下两个标准:
IEEE Std 1003.1标准,最初只用于UNIX系统。POSIX定义了一系列应用程序编程接口(API),用于在源代码级别实现可移植性。POSIX最早发布于1988年,最新版本于2018年发布。它提供了一种标准化的应用程序环境配置文件(AEP),用于实现实时和嵌入式应用程序的支持。通过遵循POSIX标准,开发人员可以编写与POSIX兼容的源代码,从而实现在不同的操作系统和平台上的可移植性。这使得开发人员能够更轻松地将应用程序从一个操作系统移植到另一个操作系统,而无需对源代码进行较大的修改。
POSIX.13(IEEE Std 1003.13)对实时和嵌入式应用程序的支持:IEEE Std 1003.13-2003是一个针对实时系统的标准,提供了对嵌入式应用程序的支持,实时配置文件可总结如下。该标准定义了几个实时系统Profiles,所有Profiles都包括部分或全部POSIX.1、.1b和.1c,以及开发平台的POSIX.2和.2a的部分内容,这些Profiles分别为PSE51、PSE52、PSE53和PSE54。
综上,PSOXI标准不仅包含应用开发时常说的POSIX API,还包括文件系统、网络、shell等整个应用程序的运行环境。
目前符合 POSIX 标准协议的操作系统有:UNIX、BSD、Linux、iOS、Android、SylixOS、VxWorks、RTEMS、LynxOS 等,由于这些OS对 POSIX 的支持,其他兼容 POSIX 系统上的应用程序可以非常方便地移植到这些系统上。
这里简单介绍几个常用RTOS的POSIX支持情况:
RTOS | POSIX支持情况 |
---|---|
PREEMPT-RT | 完全兼容 |
Xenomai | 完全兼容,实时部分xenomai内核和libcobalt提供,非实时部分linux内核和glibc提供 |
VxWorks | User space(RTP):POSIX.13 PSE52 (少数接口存在限制)Kernel space:POSIX.1部分接口和POSIX.1可选功能中的一些实时接口 |
RTEMS | POSIX 1003.1b-1993. POSIX 1003.1b-1993。POSIX 1003.1h/D3.Open Group Single UNIX Specification.单进程; |
SylixOS | 兼容IEEE 1003(ISO/IEC 9945)操作系统接口规范兼容POSIX 1003.1b(ISO/IEC 9945-1)实时编程的标准 |
Zephry | PSE54 |
EcOS 3.0 | ISO/IEC 9945—1(少部分省略) |
FreeRTOS | 实验性,仅实现了约 20% 的 POSIX API |
RT-Thread | 未找到明确说明,从源码上看兼容大部分POSIX |
详见Zephyr开发文档https://docs.zephyrproject.org/latest/samples/posix/philosophers/README.html
Zephyr支持路线图 https://static.sched.com/hosted_files/eoss24/40/ZDS-2024-POSIX-Roadmap-for-Zephyr-LTSv3.pdf
FreeRTOS-Plus-POSIX 可实现 POSIX 线程 API 的小子集。借助此子集,熟悉 POSIX API 的应用程序开发者可使用类似线程原语的 POSIX 开发 FreeRTOS 应用程序。FreeRTOS-Plus-POSIX 仅实现了约 20% 的 POSIX API。因此,无法仅使用此包装器将现有的 POSIX 兼容应用程序或 POSIX 兼容库移植到 FreeRTOS 内核上运行。
FreeRTOS-Plus-POSIX 实现了部分 IEEE Std 1003.1-2017 版《开放组技术标准基础规范》,第 7 期。FreeRTOS-Plus-POSIX 包括以下 POSIX 线程标头文件的实现,详细信息请参阅 FreeRTOS-Plus-POSIX API 文档了解。
需要注意的是:FreeRTOS posix接口不支持动态创建任务,即开始调度后不能再创建新的任务。
在实时应用场景中,精确定时是我们常用的接口,比如工业以太网ECAT、PN中的通信周期,无线基站/终端中的slot调度,实时仿真中的仿真步长等等。
POSIX中常用的定时接口有nanosleep()、clock_nanosleep()、timer_create()/timer_settime()等,但这些RTOS实现的POSIX实时吗?或者说定时精度如何?
在POSIX标准中,对nanosleep()
睡眠时间分辨率的定义为1/HZ,即操作系统的Tick周期时间,意味着实际睡眠分辨率与系统时钟滴答周期近似,下表总结了常见RTOS的nanosleep()
底层实现及精度,从标准上看,这些RTOS都符合POSIX标准,但HZ不足以支撑很多us级定时应用场景。
RTOS | 实现原理 | 精度 |
---|---|---|
PREEMPT-RT | 高精度hrtimer,每次定时最终由硬件timer中断handler中唤醒 | ns |
Xenomai | 高精度hrtimer,每次定时最终由硬件timer中断handler中唤醒 | ns |
VxWorks | 睡眠时间转换对齐到Tick | Tick |
RTEMS | ||
SylixOS | 未启用LW_CFG_TIME_HIGH_RESOLUTION_EN时,睡眠时间转换为Tick,每个Tick中统一处理;启用LW_CFG_TIME_HIGH_RESOLUTION_EN后,CPU不足一个Tcik的nanosleep转换为while(1)死等时间到达。 | Tick或ns |
Zephry | 睡眠时间转换为Tick | Tick |
EcOS 3.0 | 睡眠时间转换为Tick | Tick |
FreeRTOS | 睡眠时间转换为Tick | Tick |
RT-Thread | 5.x版本虽然实现了hrtimer逻辑,但是你用时会发现是个半成品,可以选择一个硬件Timer来实现hrtimer底层定时,但是这样每次只能有一个任务处于定时状态(用全局变量来维护timer的参数,且ctime层 hrtime、cputime接口职责隔离不清晰,无可扩展性,可以看出"能跑就行",质量堪忧);默认情况下还是基于Tick。 | Tick或ns |
这里不得不提一下,一些操作系统所宣传的ns级延时,基本指的是逻辑如下,对于用惯了xenomai/Peempt-RT的hrtimer来说,不明白CPU死等的方式有什么值得宣传的(这让我想起了郭天祥...)。
udeley(){
do {
从硬件读取当前时间;
} while (当前时间未到达指定时间);
}
计时器管理器提供的服务包括:
在xenomai中,我们通常会通过如下方式来定时触发周期任务:
sev.sigev_notify = SIGEV_THREAD_ID | SIGEV_SIGNAL;
sev.sigev_signo = SIGUSR1;
sev.sigev_value.sival_ptr = &timerid;
sev.sigev_notify_thread_id = get_thread_pid();
/*timer create*/
timer_create(CLOCK_MONOTONIC, &sev, &timerid);
/* Start the timer */
timer_overrun = timer_getoverrun(timerid);
/*get current time*/
err = clock_gettime(CLOCK_MONOTONIC, &now);
tspec.it_value = now;
tspec.it_value.tv_sec += 0;
tspec.it_value.tv_nsec += 2000000;
tsnorm(&tspec.it_value);
tspec.it_interval.tv_sec = 0;
tspec.it_interval.tv_nsec = 1000000; /**/
timer_settime(timerid, TIMER_ABSTIME, &tspec, NULL);
while (1)
s = sigwait(&GlobalSignalMaskSet, &sig);
/*.............*/
}
但是,PREEMPT-RT和xenomai中关于timer的接口底层实现有区别:
大家都知道,linux中断分为上半部和下半部,未启用PREEMPT-RT时,上半部表示硬件中断上下文,即响应中断就直接执行中断上半部;
启用PREEMPT-RT后,有所不同,通常上半部由中断线程来处理,即中断产生后,唤醒中断线程来处理中断上半部,此时中断上半部在线程上下文执行。并不是所有中断都是中断线程来执行,比如系统Timer中断就是不能中断线程化的,还是在硬件中断上下执行。对于中断下半部,基本没有变化,还是由softirq、workqueue线程等执行。
linux内核中,进程创建的每个timer都会对应内核中高精度timer的一个对象,这些hrtimer用红黑树组织,所有timer最后由硬件timer中断驱动运行,运行原理如下:
那PREEMPT-RT中,POSIX API底层用的是HRTIMER_MODE_HARD还是HRTIMER_MODE_SOFT?
API | timer类型 |
---|---|
nanosleep()clock_nanosleep() | HRTIMER_MODE_HARD |
timer_create()+timer_settime() | 4.16版本以前高精度定时器总是在硬中断里面执行定时器回调函数,所以timer相关接口定时精确,详见https://www.spinics.net/lists/kernel/msg3208465.html4.16版本及以后版本增加HRTIMER_MODE_SOFT,timer系列接口定时不再精确(不实时,抖动大)。 |
原理,详见本博客博文xenomai时间子系统。
xenomai内核提供的所有POSIX接口都是实时的。
我们仅比较了两个实时应用常见API在不同RTOS中的实现,应该明白,POSIX只是一个API标准,不同的系统底层实现不同,我们在将我们的实时任务移植适配到不同的RTOS的时候,需要事先评估用到的POSIX接口在这些RTOS上的实时行为与我们的期待是否相符。
https://www.baeldung.com/linux/posix
https://unix.stackexchange.com/questions/11983/what-exactly-is-posix
https://static.sched.com/hosted_files/eoss2023/2e/Zephyr RTOS - Posix.pdf
VxWorks_7_programmers_guide.pdf
https://docs.rtems.org/branches/master/posix-users/index.html
https://ecos.sourceware.org/docs-latest/ref/posix-standard-support.html