在 Linux 内核下实现 Ada 多任务的 “裸机” 方案
一、引言
GNAT 多任务运行时系统(GNARL)已被移植到通用英特尔 PC 兼容架构的硬件上,作为 Linux 操作系统之下的一层。GNAT Ada 编译系统非常成功,已被移植到许多处理器架构和操作系统,但直到最近,其多任务运行时系统(GNARL)还未在裸机上实现。Ada 95 编程语言的任务模型旨在允许轻量级的裸机实现,GNARL 也是如此。GNARL 中依赖特定机器和操作系统的组件通过一个名为 GNULLI 的低级任务接口隔离。此前,GNULLI 一直被实现为连接现有线程库服务的 “胶水代码”,而线程库又基于底层操作系统。Ada 任务的性能受到线程库和操作系统的限制,因为它们并非为支持 Ada 而设计,且大多不适用于实时应用。因此,若由一个小型、简单且高度可预测的 GNULLI 实现直接在硬件上支持任务实现,其效果如何仍是一个悬而未决的问题。
二、背景
(一)动机
进行 GNULLI 裸机实现有两个独立的动机:
1.
实时应用需求
:在使用商业操作系统的并发编程原语实现 Ada 任务的 GNULLI 版本中,操作系统的活动会与应用中的任务竞争,导致任务执行时间出现不可预测的变化。而裸机实现可实现硬实时应用所需的效率和可预测的执行时间。
2.
安全认证基础
:为 1997 年 Ravenscar 国际实时 Ada 研讨会提出的受限 Ada 任务子集的小型、可认证安全实现奠定基础。完整的现成操作系统增加的复杂性会阻碍认证,因此需要简单的裸机实现。
(二)硬件选择
目标硬件架构是具有英特尔 486/586 处理器的通用 PC 兼容机,因其广泛可用性和低成本而被选中。
(三)设备驱动问题
任务内核本身用处不大,有趣的应用需要进行输入输出,这就需要设备驱动。单个设备驱动的复杂性可能超过任务内核,且硬件设备接口文档往往不完善甚至保密,还经常变化。因此,开发和维护足够的硬件设备驱动非常困难。一种避免投入大量新工作的方法是重用现有操作系统(如 DOS 或 Linux)的设备驱动,但这些操作系统并非为实时使用而设计,其设备驱动会导致时间不可预测。非实时设备驱动可以重用,但必须在后台以足够低的优先级运行,以免阻碍硬实时任务的进展。
(四)前后台分离
支持前后台分离通常很有用,因为除最简单的实时系统外,所有系统都包含硬实时、软实时或非实时处理的组合。例如,系统可能有硬实时周期性任务来服务传感器和执行器,同时有软实时或非实时任务进行网络通信和数据记录等具有可变时间延迟的操作。在这种情况下,传感器和执行器的 I/O 驱动需要满足硬实时约束,而网络和磁盘 I/O 的 I/O 驱动可以从非实时操作系统重用。
传统的前后台处理实现方式是前台任务调度器有一个单一的后台任务,该后台任务再执行后台任务调度器和所有后台任务。佛罗里达州立大学在 20 世纪 80 年代末和 90 年代初开发的一系列低级 Ada 内核遵循了这一模型,其中 DOS 操作系统作为后台任务运行。最近,Yodaiken 的实时 Linux(RT - Linux)也采用了这一模型。与 DOS 相比,Linux 作为后台操作系统有很多优势,包括支持并发线程控制和独立的虚拟地址空间。
(五)RT - Linux 介绍
RT - Linux 是作为动态加载模块添加到 Linux 内核的一层软件。它在硬件中断到达常规 Linux 中断处理程序之前捕获它们,这使得 RT - Linux 层不仅可以抢占常规 Linux 任务(即使它们在内核中执行),还可以推迟 Linux 设备驱动安装的硬件中断处理程序的执行。RT - Linux 内核支持创建和调度非常轻量级的前台任务,这些任务完全在内核地址空间中执行。常规 Linux 操作系统内核和它调度的所有任务在后台以较低优先级运行。后台任务可以访问 Linux 操作系统的完整服务,但执行时间不可预测;前台进程执行时间可预测,但不能使用常规的 Linux I/O 或进行其他常规操作系统服务调用,因为它们在操作系统级别之下执行。前后台之间只能使用 RT - Linux 内核支持的有界 FIFO 缓冲区结构进行通信。此外,RT - Linux 还提供细粒度(0.8 微秒)的时钟和定时器服务,可用于精确安排时间延迟和周期性执行。
三、设计与实现问题
(一)基于 RT - Linux
本项目选择 Linux 作为后台操作系统,并尽可能重用 RT - Linux 的代码。虽然 RT - Linux 对多处理器的支持还处于实验阶段,但由于目标是实现一个非常简单的 Ada 内核,所以只关注单处理器系统。RT - Linux 中所有依赖机器和操作系统的代码都可以重用,包括将 RT - Linux 插入前台的机制、上下文切换代码和定时器驱动,FIFO 也可直接重用。仅任务调度器和同步原语需要重写,以支持 Ada 调度和锁定策略。选择用 Ada 重写这些代码,并定义 Ada 接口来导入 RT - Linux 定时器驱动和 FIFO 缓冲区的原始 C 语言代码。
(二)内核内执行限制
在 Linux 内核内和内核下执行对 “应用程序” 有一些限制。例如,不能执行使用内核陷阱实现的操作,如调用标准 C 的 malloc 分配内存或使用标准 C 库进行输入输出。必须找到这些服务的低级替代方法,或者不进行这些操作。动态内存分配可以通过调用 kmalloc 在内核地址空间中完成,但为了获得可预测的性能,应将其限制在每个内核模块初始化期间的少数调用。输入输出可以通过自定义的低级设备驱动直接完成,或者通过缓冲区路由到后台服务器,后台服务器可以使用 Linux 操作系统的完整 I/O 功能。
从概念上讲,包含前后台任务的应用程序有多个分区。前台任务存在于使用裸机运行时系统在内核地址空间中执行的实时分区;后台任务存在于一个或多个非实时分区,每个分区对应一个 Linux 进程,并使用基于 Linux 操作系统的运行时系统。
(三)Ada 95 原理实现模型
Ada 95 原理描述了一种基于优先级上限锁定概念的单处理器任务调度和受保护对象锁定的简单实现模型,目标是根据该模型重新实现 GNAT 任务原语。该模型基于以下几个简单原则:
1.
调度原则
:调度严格按照任务的活动优先级进行抢占式调度。只有当任务是最高优先级就绪任务列表的头部任务时才能执行。
2.
锁持有原则
:只有就绪任务才能持有锁。持有锁的任务在进入睡眠状态之前必须释放锁。
3.
锁获取限制
:如果任务的活动优先级高于锁的上限,则不允许获取该锁。
4.
优先级继承
:持有锁的任务在持有锁期间继承锁的优先级上限。
基于这些原则,当任务执行获取锁的操作时,不会有其他任务持有该锁。因此,对于 GNAT 任务原语 Write_Lock 的实现,只需将当前任务的活动优先级更新为作为参数引用的锁对象的上限优先级;对于原语 Unlock 的实现,需要恢复当前任务的活动优先级,并检查是否有其他任务应抢占当前任务。
(四)任务控制块
在 GNARL 中,每个任务由一个称为任务控制块(ATCB)的记录对象表示。ATCB 包含一个依赖目标的组件 LL,它是一个包含 GNULLI 实现使用的私有数据的记录。当运行时系统基于线程库时,该目标特定数据包括线程 ID,线程 ID 相当于对底层线程实现中另一个记录对象(可能称为线程控制块)的引用。在裸机实现中,任务直接实现,因此没有单独的线程 ID 和线程控制块,所有关于任务的必要信息都直接存储在 ATCB 中。
(五)锁操作
ATCB 和其他需要互斥的 GNARL 对象由锁保护。当运行时系统基于线程库时,锁使用线程库的互斥对象实现。例如,使用 POSIX 线程操作的锁定代码如下:
procedure Write_Lock
(L : access Lock; Ceiling_Violation : out Boolean) is
Result : Interfaces.C.int;
begin
Result := pthread_mutex_lock (L.L’Access);
Ceiling_Violation := Result /= 0;
end Write_Lock;
procedure Unlock (L : access Lock) is
Result : Interfaces.C.int;
begin
Result := pthread_mutex_unlock (L.L’Access);
end Unlock;
在裸机运行时系统中,使用以下代码直接实现这些操作:
procedure Write_Lock
(L : access Lock; Ceiling_Violation : out Boolean) is
Prio : constant System.Any_Priority :=
Current_Task.LL.Active_Priority;
begin
Ceiling_Violation := False;
if Prio > L.Ceiling_Priority then
Ceiling_Violation := True;
return;
end if;
L.Pre_Locking_Priority := Prio;
Current_Task.LL.Active_Priority := L.Ceiling_Priority;
if Current_Task.LL.Outer_Lock = null then
-- If this lock is not nested, record a pointer to it.
Current_Task.LL.Outer_Lock := L.all’Unchecked_Access;
end if;
end Write_Lock;
procedure Unlock (L : access Lock) is
Flags : Integer;
begin
-- Now that the lock is released, lower our own priority.
if Current_Task.LL.Outer_Lock = L.all’Unchecked_Access then
-- This lock is the outer-most,
-- so reset our priority to Current_Prio.
Current_Task.LL.Active_Priority :=
Current_Task.LL.Current_Priority;
Current_Task.LL.Outer_Lock := null;
else
-- If this lock is nested, pop the old active priority.
Current_Task.LL.Active_Priority := L.Pre_Locking_Priority;
end if;
-- Reschedule the task if necessary.
-- We only need to reschedule the task if its Active_Priority
-- is less than the one following it.
-- The check depends on the fact that the background task
-- (which is always at the tail of the ready queue)
-- has the lowest Active_Priority.
if Current_Task.LL.Active_Priority
< Current_Task.LL.Succ.LL.Active_Priority then
Save_Flags (Flags); -- Saves interrupt mask.
Cli; -- Masks interrupts
Delete_From_Ready_Queue (Current_Task);
Insert_In_Ready_Queue (Current_Task);
Restore_Flags (Flags);
Call_Scheduler;
end if;
end Unlock;
由于优先级上限策略,这些操作比 POSIX 线程互斥操作更简单,无需记录锁的所有者,也无需使用循环或特殊的原子硬件操作(如测试并设置)来获取锁。
(六)睡眠和唤醒操作
GNAT 运行时系统使用原语 Sleep 和 Wakeup 分别阻塞和解除阻塞任务。Sleep 只能在当前任务持有其自身 ATCB 的锁时调用。该操作的效果是释放锁并使当前任务进入睡眠状态,直到另一个任务调用 Wakeup 唤醒它。任务唤醒后,在从 Sleep 返回之前重新获取锁。
当运行时系统基于线程库时,这些操作实现如下:
procedure Sleep (Self_ID : Task_ID; Reason : Task_States) is
Result : Interfaces.C.int;
begin
if Self_ID.Pending_Priority_Change then
Self_ID.Pending_Priority_Change := False;
Self_ID.Base_Priority := Self_ID.New_Base_Priority;
Set_Priority (Self_ID, Self_ID.Base_Priority);
end if;
Result := pthread_cond_wait
(Self_ID.LL.CV’Access, Self_ID.LL.L.L’Access);
pragma Assert (Result = 0 or else Result = EINTR);
end Sleep;
procedure Wakeup (T : Task_ID; Reason : Task_States) is
Result : Interfaces.C.int;
begin
Result := pthread_cond_signal (T.LL.CV’Access);
pragma Assert (Result = 0);
end Wakeup;
在裸机运行时系统中,这些操作直接实现如下:
procedure Sleep
(Self_ID : Task_ID; Reason : System.Tasking.Task_States) is
Flags : Integer;
begin
-- Self_ID is actually Current_Task, that is, only the
-- task that is running can put itself into sleep. To preserve
-- consistency, we use Self_ID throughout the code here.
Self_ID.State := Reason;
Save_Flags (Flags);
Cli;
Delete_From_Ready_Queue (Self_ID);
-- Arrange to unlock Self_ID’s ATCB lock.
if Self_ID.LL.Outer_Lock = Self_ID.LL.L’Access then
Self_ID.LL.Active_Priority := Self_ID.LL.Current_Priority;
Self_ID.LL.Outer_Lock := null;
else
Self_ID.LL.Active_Priority := Self_ID.LL.L.Pre_Locking_Priority;
end if;
Restore_Flags (Flags);
Call_Scheduler;
-- Before leaving, regain the lock.
Write_Lock (Self_ID);
end Sleep;
procedure Wakeup (T : Task_ID; Reason : System.Tasking.Task_States) is
Flags : Integer;
begin
T.State := Reason;
Save_Flags (Flags);
Cli; -- Disable interrupts.
if Timer_Queue.LL.Succ = T then
-- T is the first task in Timer_Queue, further check.
if T.LL.Succ = Timer_Queue then
-- T is the only task in Timer_Queue, so deactivate timer.
No_Timer;
else
-- T is the first task in Timer_Queue, so set timer to T’s
-- successor’s Resume_Time.
Set_Timer (T.LL.Succ.LL.Resume_Time);
end if;
end if;
Delete_From_Timer_Queue (T);
-- If T is in Timer_Queue, T is removed. If not, nothing happened.
Insert_In_Ready_Queue (T);
Restore_Flags (Flags);
Call_Scheduler;
end Wakeup;
Wakeup 操作比 Sleep 操作稍微复杂一些,因为被唤醒的任务可能处于定时睡眠调用(类似于 Sleep 调用,但有超时)中。带有超时的睡眠任务会被放入按唤醒时间排序的定时器队列。当超时到期时,定时器中断处理程序将超时任务从定时器队列移动到就绪队列。如果任务在超时到期之前被唤醒,Wakeup 负责将其从定时器队列中移除并可能停用定时器。
(七)动态优先级
当 GNULLI 基于线程库时,优先级更改通过调用线程库完成。对于 POSIX 线程,这些调用非常通用,因此必然比较重量级,因为可能需要更改线程在就绪队列中的位置并调用调度器。然而,在 GNAT 运行时中的使用方式下,正常情况可以更轻量级。
GNAT 运行时系统的策略是,每当更改任务的优先级时,当前任务必须持有受影响任务的 ATCB 锁。
在 Linux 内核下实现 Ada 多任务的 “裸机” 方案
四、性能分析与优势
(一)性能提升
将 GNAT 多任务运行时系统(GNARL)直接在硬件上实现,绕过了传统线程库和操作系统的限制,带来了显著的性能提升。在传统实现中,由于操作系统活动与应用任务的竞争,任务执行时间存在不可预测的变化。而裸机实现消除了这些干扰因素,使得硬实时任务能够获得更高效、更可预测的执行时间。
例如,在 RT - Linux 环境下,前台任务完全在内核地址空间中执行,避免了与后台任务的资源竞争,并且 RT - Linux 提供的细粒度时钟和定时器服务,能够精确安排任务的时间延迟和周期性执行,进一步提高了任务执行的可预测性。
(二)安全认证优势
简单的裸机实现为受限 Ada 任务子集的安全认证奠定了基础。由于完整的现成操作系统通常具有较高的复杂性,这会成为认证过程中的障碍。而裸机实现减少了不必要的复杂性,使得系统更容易满足安全认证的要求,为安全关键型应用提供了可靠的保障。
五、总结与展望
(一)总结
本文介绍了将 GNAT 多任务运行时系统在 Linux 内核下进行裸机实现的方法和技术细节。通过基于 RT - Linux 并对关键组件进行重写,实现了一个支持 Ada 任务调度和同步的简单内核。该内核具有以下特点:
-
高效性和可预测性
:消除了传统线程库和操作系统的干扰,提供了硬实时任务所需的高效执行和可预测的时间控制。
-
可重用性
:尽可能重用了 RT - Linux 的代码,减少了开发工作量。
-
适合安全认证
:简单的设计为受限 Ada 任务子集的安全认证提供了便利。
(二)展望
尽管本文实现的内核已经取得了一定的成果,但仍有一些方面可以进一步改进和扩展:
-
多处理器支持
:目前的实现仅关注单处理器系统,未来可以考虑扩展到多处理器环境,以充分利用多核处理器的性能优势。
-
设备驱动优化
:虽然重用了现有操作系统的设备驱动,但可以进一步优化设备驱动的性能,以更好地满足实时应用的需求。
-
功能扩展
:可以添加更多的功能,如任务间通信机制的优化、动态资源分配等,以提高系统的灵活性和适应性。
六、相关代码和流程总结
(一)代码总结
以下是本文中涉及的主要代码块总结:
| 操作类型 | 线程库实现代码 | 裸机实现代码 |
| — | — | — |
| 锁操作 - Write_Lock |
procedure Write_Lock
(L : access Lock; Ceiling_Violation : out Boolean) is
Result : Interfaces.C.int;
begin
Result := pthread_mutex_lock (L.L’Access);
Ceiling_Violation := Result /= 0;
end Write_Lock;
``` |
```ada
procedure Write_Lock
(L : access Lock; Ceiling_Violation : out Boolean) is
Prio : constant System.Any_Priority :=
Current_Task.LL.Active_Priority;
begin
Ceiling_Violation := False;
if Prio > L.Ceiling_Priority then
Ceiling_Violation := True;
return;
end if;
L.Pre_Locking_Priority := Prio;
Current_Task.LL.Active_Priority := L.Ceiling_Priority;
if Current_Task.LL.Outer_Lock = null then
-- If this lock is not nested, record a pointer to it.
Current_Task.LL.Outer_Lock := L.all’Unchecked_Access;
end if;
end Write_Lock;
``` |
| 锁操作 - Unlock |
```ada
procedure Unlock (L : access Lock) is
Result : Interfaces.C.int;
begin
Result := pthread_mutex_unlock (L.L’Access);
end Unlock;
``` |
```ada
procedure Unlock (L : access Lock) is
Flags : Integer;
begin
-- Now that the lock is released, lower our own priority.
if Current_Task.LL.Outer_Lock = L.all’Unchecked_Access then
-- This lock is the outer-most,
-- so reset our priority to Current_Prio.
Current_Task.LL.Active_Priority :=
Current_Task.LL.Current_Priority;
Current_Task.LL.Outer_Lock := null;
else
-- If this lock is nested, pop the old active priority.
Current_Task.LL.Active_Priority := L.Pre_Locking_Priority;
end if;
-- Reschedule the task if necessary.
-- We only need to reschedule the task if its Active_Priority
-- is less than the one following it.
-- The check depends on the fact that the background task
-- (which is always at the tail of the ready queue)
-- has the lowest Active_Priority.
if Current_Task.LL.Active_Priority
< Current_Task.LL.Succ.LL.Active_Priority then
Save_Flags (Flags); -- Saves interrupt mask.
Cli; -- Masks interrupts
Delete_From_Ready_Queue (Current_Task);
Insert_In_Ready_Queue (Current_Task);
Restore_Flags (Flags);
Call_Scheduler;
end if;
end Unlock;
``` |
| 睡眠和唤醒 - Sleep |
```ada
procedure Sleep (Self_ID : Task_ID; Reason : Task_States) is
Result : Interfaces.C.int;
begin
if Self_ID.Pending_Priority_Change then
Self_ID.Pending_Priority_Change := False;
Self_ID.Base_Priority := Self_ID.New_Base_Priority;
Set_Priority (Self_ID, Self_ID.Base_Priority);
end if;
Result := pthread_cond_wait
(Self_ID.LL.CV’Access, Self_ID.LL.L.L’Access);
pragma Assert (Result = 0 or else Result = EINTR);
end Sleep;
``` |
```ada
procedure Sleep
(Self_ID : Task_ID; Reason : System.Tasking.Task_States) is
Flags : Integer;
begin
-- Self_ID is actually Current_Task, that is, only the
-- task that is running can put itself into sleep. To preserve
-- consistency, we use Self_ID throughout the code here.
Self_ID.State := Reason;
Save_Flags (Flags);
Cli;
Delete_From_Ready_Queue (Self_ID);
-- Arrange to unlock Self_ID’s ATCB lock.
if Self_ID.LL.Outer_Lock = Self_ID.LL.L’Access then
Self_ID.LL.Active_Priority := Self_ID.LL.Current_Priority;
Self_ID.LL.Outer_Lock := null;
else
Self_ID.LL.Active_Priority := Self_ID.LL.L.Pre_Locking_Priority;
end if;
Restore_Flags (Flags);
Call_Scheduler;
-- Before leaving, regain the lock.
Write_Lock (Self_ID);
end Sleep;
``` |
| 睡眠和唤醒 - Wakeup |
```ada
procedure Wakeup (T : Task_ID; Reason : Task_States) is
Result : Interfaces.C.int;
begin
Result := pthread_cond_signal (T.LL.CV’Access);
pragma Assert (Result = 0);
end Wakeup;
``` |
```ada
procedure Wakeup (T : Task_ID; Reason : System.Tasking.Task_States) is
Flags : Integer;
begin
T.State := Reason;
Save_Flags (Flags);
Cli; -- Disable interrupts.
if Timer_Queue.LL.Succ = T then
-- T is the first task in Timer_Queue, further check.
if T.LL.Succ = Timer_Queue then
-- T is the only task in Timer_Queue, so deactivate timer.
No_Timer;
else
-- T is the first task in Timer_Queue, so set timer to T’s
-- successor’s Resume_Time.
Set_Timer (T.LL.Succ.LL.Resume_Time);
end if;
end if;
Delete_From_Timer_Queue (T);
-- If T is in Timer_Queue, T is removed. If not, nothing happened.
Insert_In_Ready_Queue (T);
Restore_Flags (Flags);
Call_Scheduler;
end Wakeup;
``` |
#### (二)流程总结
下面是任务睡眠和唤醒操作的 mermaid 流程图:
```mermaid
graph TD;
A[任务运行] --> B{调用 Sleep};
B --> C[保存标志位];
C --> D[禁用中断];
D --> E[从就绪队列删除任务];
E --> F{是否持有 ATCB 锁};
F -- 是 --> G[恢复任务优先级];
F -- 否 --> H[恢复旧的活动优先级];
G --> I[恢复标志位];
H --> I;
I --> J[调用调度器];
J --> K[重新获取锁];
K --> L[任务睡眠];
M{调用 Wakeup} --> N[保存标志位];
N --> O[禁用中断];
O --> P{任务是否在定时器队列};
P -- 是 --> Q{任务是否是定时器队列第一个};
Q -- 是 --> R{任务是否是定时器队列唯一任务};
R -- 是 --> S[停用定时器];
R -- 否 --> T[设置定时器到下一个任务唤醒时间];
Q -- 否 --> U[无操作];
S --> V[从定时器队列删除任务];
T --> V;
U --> V;
P -- 否 --> V;
V --> W[将任务插入就绪队列];
W --> X[恢复标志位];
X --> Y[调用调度器];
Y --> Z[任务唤醒];
通过以上的总结和展望,我们可以看到在 Linux 内核下实现 Ada 多任务的裸机方案具有很大的潜力和发展空间,未来可以进一步探索和优化,以满足更多复杂实时应用的需求。