分区操作系统

简单介绍

什么是分区

首次提出于ARINC 653标准,它是航空领域的一个标准,定义了多分区操作系统的核心服务。
分区使得操作系统的各个功能模块(即每个分区)看起来好像都被分配了独立的处理器和外设,它们之间通过专线通信。它通常提供资源管理、隔离和分配功能。因此可以这么说:分区是为了尽量把不同模块分开,这也引出了为什么要分区。

为什么要分区

让数据通信更加安全,让错误影响范围尽量小。避免一个模块改变另一个模块的软件或私有数据(无论是在内存还是在传输中),以及控制另一个分区的私有设备。避免一个模块受另外一个模块共享资源的影响,包括性能、速率、延迟等。

“如果各个模块没有分区,则必须保证软件达到适合所有模块功能的级别”。有时这是种冗余的设计,甚至是一种资源浪费。
“分区的目的是尽量避免连带的故障:一个分区中的故障传播到另一个分区中导致故障”1,通常由共享资源造成。

如何分区

最先提出的分区

一是在操作系统上分区,二是在最小内核上,如图所示:

前者依赖大量操作系统层面的软件,后者则仅依赖内核和硬件,也被称为“虚拟机”的方法。

空间分区

但如果是私有数据或设备,即使不分区也访问不到这些非共享的数据啊。除非是缓冲区溢出等错误情况,但分区该如何避免意外错误情况的错误呢(我是否可以称其为metafault)?例如物理上地,让两个分区使用相距甚远的物理空间?

可以放弃MMU(memory management unit)和缓存控制器的虚拟内存和大量查找表,转而使用固定内存分配方案。但由于MMU和CPU一般是高度集成的,因此这个方案并不现实。
另一个方案是软件故障隔离(SFI,software fault isolation),这类似于高级语言中的数组边界检查,不同的是它检查任何对内存的使用。它通过检查软件的机器码来静态地检查内存使用是否安全,然而通过寄存器的间接寻址不能被静态地检查出来,针对这个问题可以做些改进,比如确保寄存器的值与上次检查时没有变化。

时间分区

时间分区类似于时间片调度,在不同分区间使用静态调度例如时间片轮转,而分区内则动态地调度任务。
“一种情况是在单个处理器上支持多个次要功能,例如起落架和天气雷达。在这些功能不需要同时运行时,可以通过让每个功能在活动状态时独占其处理器来实现分区。”
换句话说,就是互斥的功能模块可以通过周期运行(占用资源)来实现分区,即时间分区。
这可以避免分区间的干扰,但不符合实时的要求,因此有人说在RTOS上实现时间分区是不现实的。

但我认为在分区间设置适当的抢占调度策略,不过这样的话在某种程度上就不算是时间分区了,而是将每个分区看做是更高一级的任务。

近年主流的分区

近年来最主流的分区操作系统概念是MILS,即multiple independent levels of security,它是基于内存、CPU隔离(isolation)实现的一种虚拟化技术,这个架构的名字更贴合了分区操作系统的安全概念。
这里middleware的概念,它介于软件和硬件之间,使得上层应用可以调用分区内核里的不同功能,middleware也可以是不同分区自己的不同的操作系统。其结构如下图。
MILS架构
注意这个不是传统的分区操作系统,而是一种虚拟化。这要从虚拟(virtual)的意思说起。如果我们去搜字典会发现,virtual既有“真实的”又有“虚拟的”的含义,而且大部分是在计算机领域用来表达后者的意思,那么到底什么是virtual呢?例如我们经常见到的虚拟内存,实际上就是利用一些非透明的(即对于使用者来说观查不到的)手段,使得内存在使用时感觉好像真的十分大一样。从这里就可以发现,virtual意思是,虽然是假的,但看起来或用起来和真的是一样的。
从这个角度看,分区操作系统就是看起来好像是一个完整的,但不会互相干扰的操作系统。

这样的分布式架构也可以更好地利用当前流行的多核操作系统。可以让几个核心负责一个分区,这样无论是隔离性还是效率都更高一些。注意这里的分布式指的是不同核心做不同的任务,和并行计算那种一个任务拆分给不同核心做是不同的。
不过看到这里我有一个想法,是否可以在执行平时分区的时候正常让不同核心做不同任务,但遇到互斥任务时,再利用起这些多核的并行计算能力?

分区的机制

主要介绍下分区的文件系统分区,通信分区以及设备驱动分区。

分区文件系统2

分区操作系统的文件系统必须提供数据访问请求隔离和时间保证。保证硬实时访问,并为软实时访问和非实时访问公平地共享文件系统和底层存储介质,例如将非实时文件访问调度到所有硬实时请求调度后的空闲调度时段。

为了实现隔离性:

  • 对文件数据访问频率(rate)施加限制(称为带宽分区)
    有助于实现时间隔离的机制。
  • 为每个请求使用专用缓存缓冲区。
  • 对每个任务和每个应用任务分区的磁盘空间施加限制
  • 使用互斥锁,允许独占访问共享数据结构和共享文件区域。
  • 一旦文件访问在底层被成功调度,就保证在一段时间内有足够的资源来确保其成功完成,即减少依赖。
  • 对每个应用任务可能拥有的打开文件数和并发文件访问数施加各种限制,一样是减少依赖。

为了实现可预测性,提供了以下支持:

  • 预分配内存资源
    打开文件的时候就提前分配内存buffer和读取数据的buffer。
  • 为关键文件预分配磁盘区域
    在创建关键文件但还未实际访问时,就根据指定文件的大小限制为其预分配磁盘区域,并且尽量分配到连续空间,以避免寻道延迟。
  • 多级调度:
    在RTFS中使用两级调度。
    对于高级调度,已提交的文件访问请求会被重新排序,以反映其当下的局部优先级。在这一过程中,正在进行的文件访问请求如果尚未开始关键磁盘操作,则可以被中断,以便优先处理优先级更高的请求。
    对于低级调度,在访问磁盘之前,会根据截止日期和磁盘上的磁道地址对磁盘访问请求进行重新排序,这样不仅能满足截止日期的要求,还能最大限度地减少整体磁盘寻道延迟。
    这样做可以提高从磁盘访问数据的速度,这一点非常重要,因为磁盘访问与内存访问和任务计算速度相比要慢得多,因而是性能瓶颈。

应用分区内的任务使用库函数,向RTFS请求各种类型的服务。这些请求通过队列端口(QP)提交,如下图所示。QP 也用于从RTFS向应用分区发送响应。如果需要,一个应用分区可以使用多组QPs向特定RTFS分区发送。
RTFS本身由一组线程实现,它所需的调度和排序是在设计中明确实现的(而非在本地确定优先级,这是为了可移植性)。RTFS为所需的线程调度实现了必要的同步和调度结构。RTFS中使用的主要线程类型如下:

  • 请求队列管理线程(RQMT):
    负责将传入的文件操作请求从QP取出,并将请求的参数复制到RTFS的高级调度队列中。RQMT会为每个请求分配一个全局唯一的标识符,并在RTFS的高级调度队列中执行排序请求所需的任何抢占操作。RQMT还负责通过QP交付对请求的响应。
  • 请求处理线程(RPT):
    RPT用于执行服务请求所需的处理,每个请求都由RQMT分配给一个RPT。对于修改文件元数据(如目录信息、文件分配表等)的文件操作,RPT会将事务记录到非易失性内存(闪存)的日志中,以便于在崩溃时快速恢复文件系统。另外,这也实现对磁盘的异步写入,提高了效率。
    RPT还调用特定于设备的底层调度功能,以减轻磁盘等大容量存储设备寻道延迟的影响。RPT还支持在以网络为中心的环境中远程访问大容量存储设备。
  • 完成线程(CT):
    对于所使用的每种设备,都有一个适当的CT来处理该设备的完成和底层请求调度。CT主要汇集设备状态,以确定待处理的I/O何时完成。设备的I/O请求完成后,该设备的CT会从该设备的低级调度队列头部启动设备的下一个操作。
    RTFS

分区间通信

分区间通信是分区操作系统的重要组成部分,可以极大地影响系统的整体可用性、安全性和可靠性。

最简单的方法当然是在每个分区都有独立的与其他分区通信的专线,但这太昂贵了。因此各个分区之间的通信却必须要依靠共享的介质,如总线网络等,这带来新的问题,即就算分区了,它们还是有一些东西(如通信I/O)分不开。

在《嵌入式系统导论-CPS方法》里看到这么一段描述:

时间触发架构TTA(Time-Triggered Architecture)是一种周期式触发分布式计算的机制。
因为存在一个用于协调计算的全局时钟,TTA为一个计算关联了一个逻辑执行时间。计算的输入是在全局时钟的节拍上提供的,而输出在直到全局时钟的下一个节拍到来前对于其他计算是不可见的。在两个节拍之间,计算之间没有交互,因此也就没有竞态条件。

查查PTIDES(时间可编程),其通过网络时间同步来提供有效的分布式执行。

可以看出来,TTA也是一种实现分布式分区间通信的方式,它通过一个全局时钟(类似于全局管理层)来调度分区通信。可能会问,这只适用于非即时的通信,那即时的怎么办呢?我的理解是,如果两个分区需要即时的通信,那可能是因为它们本不需要分开,即它们在功能上应该是统一的。

由此可以得到,跨分区不能有同步服务(即,调用方阻塞并等待服务提供者回复),因为:(a)一个分区不应该依赖另一个可能有故障的分区来解除其阻塞,以及(b)这会带来很大的性能损失:调用方将至少阻塞直到其下一个时间片。相反,所有分区间通信必须是异步的。

由于分区间的通信不能确保收到信息的时效性,因此需要发出方附上时间信息,但有时仅仅有时间信息还不够,例如飞机降落时的高度信息,在1秒后就不能使用了,但在飞行时候是相同的。解决方法就是将责任交给发送方,它们不止附上时间信息,还告诉接收方这个信息在什么时候之前才是可用的。

总结的说,分区间的通信,可用性责任应该尽量交给发送方。

在MILS架构中,每个任务都有一个安全关键级别。MILS主要依靠2个功能模块来保证安全性:MMR(MILS Message Route)和Guard,前者阻拦未授权的信息,后者降低不符合安全策略的信息安全级别,将过滤后的信息交给MMR。其数据流如下图:
MILS数据流

各个分区间的共享内存是这样组织的:

  • 各分区的内核和hypervisor之间通过共享内存通信。
  • hypervisor再将这个共享内存地址映射到它和主机内核之间的共享内存上。
  • 在hypervisor获得来自主机内核的数据后,将发出中断使得分区进入它们自己的内核态,以访问分区和hypervisor之间的共享内存。

Hypervisor是一个虚拟机管理器,运行在主机内核(host kernel)之上,其上又运行多个虚拟机或我们本次讲的分区,使得多个不同的OS(或我们说的分区)能够共享虚拟化的硬件资源。
hypervisor有两种类型,type-1是直接运行在主机硬件上来控制资源并管理,type-2则作为一个程序运行在主机的操作系统上。

然后再举一个MLIS架构的通信机制为例子3,它主要由通信控制模块和通信信息存储模块组成。
通信控制分为共享文件管理和数据操作管理两部分。
前者主要负责控制对其他分区数据的访问。访问控制决策由主机内核和控制分区中的访问控制策略共同完成,因此可以在一定程度上隐藏分区中的数据访问信息。
后者主要负责分区间的信息传输和修改。
通信信息存储模块主要记录共享文件的发布和订阅行为,并将其行为信息存储在公共分区中。

文章最后说可以在分区间通信数据的动态安全性方面做些工作。

考虑时间分区,分区间通过静态调度如时间片轮转,分区内则动态调度如EDF,此时如何应对分区间的抢占呢?
我有两个想法:

  • 将一个分区视为一个封装后的任务,对分区进行继承优先级等调度。类似于前面RTFS的做法。
  • 设置一个“0分区”,它专门用来完成紧急任务或异常处理,它可以无条件抢占其他分区,其他分区则继续按静态调度。(毕竟紧急情况和异常都是少数)

分区操作系统的设备驱动–以MIL-STD-1553设备驱动程序为例

对于分区操作系统,设备驱动程序、文件系统和网络堆栈等功能都可以被移到RTOS之外。波音公司将MIL-STD-1553设备驱动程序移到了应用程序分区。

将设备驱动程序从内核移植到应用层分区的更改包括4

  • 将设备驱动程序分为内核和I/O分区。
  • 在I/O分区内重新组织代码,将所有对象创建分隔到初始化代码中。
  • 将I/O代码更改为使用ARINC-653接口,而不是直接调用VxWorks函数。
  • 将时序接口和实现从CPU时间(时钟周期)更改为实际(elapsed)时间。
  • 增加读取或写入单个消息而不需要等待的功能。
  • 让用户分区可以获取设备发出的数据帧。
  • 让设备可以在帧结束时通知用户。

在ARINC-653系统中,一旦分区过渡到正常模式,某些功能将变得非法。因此,所有创建元素、打开通信设备等代码必须在初始化模式下执行。

启动过程:

  • 内核初始化
    调用BSP例程来配置内存映射地址,添加一个中断处理程序,用其为每个设备打开一个用于和I/O分区通信的管道
  • I/O分区初始化
    安装驱动程序并创建设备。
  • 时钟总线控制器
    在I/O分区正常工作之前,首先安装一个结束帧handler,它会在一个设备帧结束时给用户软件发出信号。

得到的好处是:

  • 用户分区的的中止(halt)不再导致设备I/O的停止。
  • 消息传递错误不再导致设备I/O中止。

  1. Rushby J. Partitioning in avionics architectures: Requirements, mechanisms, and assurance[R]. 1999. ↩︎

  2. K. Ghose, S. Ray, O. Demir, D. Hogea and J. Imperato, “A time and space partitioned avionics real-time file system,” 24th Digital Avionics Systems Conference, Washington, DC, USA, 2005, pp. 6.C.3-61. ↩︎

  3. Lan Y, Wang Y. An inter-partition communication mechanism in multi-domain isolation operating system[C]//2022 5th International Conference on Data Science and Information Technology (DSIT). IEEE, 2022: 1-6. ↩︎

  4. Mason J F, Luecke K R, Luke J A. Device drivers in time and space partitioned operating systems[C]//2006 ieee/aiaa 25TH Digital Avionics Systems Conference. IEEE, 2006: 1-9. ↩︎

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
动态分区算法是指在内存中分配内存块时,将内存划分为多个大小不同的分区,每个分区可以分配给一个进程使用。当一个进程请求内存时,系统会在分区中选择一个大小合适的分区分配给该进程使用。常见的动态分区算法有以下几种: 1. 首次适应算法(First Fit):从内存的起始位置开始搜索,选取第一个满足条件的分区进行分配。 2. 最佳适应算法(Best Fit):从所有空闲分区中选取最小的满足要求的分区进行分配。 3. 最坏适应算法(Worst Fit):从所有空闲分区中选取最大的满足要求的分区进行分配。 4. 循环首次适应算法(Next Fit):从上次分配结束的位置开始搜索,选取第一个满足条件的分区进行分配。 下面是一个简单的操作系统动态分区算法实验,以首次适应算法为例: 1. 定义一个内存块结构体,包含起始地址、大小和状态等属性。 ``` struct MemoryBlock { int startAddress; int size; bool free; }; ``` 2. 定义一个内存分配表,用于存储内存块信息。 ``` vector<MemoryBlock> memoryTable; ``` 3. 实现内存分配函数,根据首次适应算法从内存分配表中选择一个空闲的分区进行分配。 ``` int allocateMemory(int size) { // 从内存分配表中查找满足条件的空闲分区 for (int i = 0; i < memoryTable.size(); i++) { if (memoryTable[i].free && memoryTable[i].size >= size) { // 找到了合适的分区,进行分配 int startAddress = memoryTable[i].startAddress; memoryTable[i].startAddress += size; memoryTable[i].size -= size; if (memoryTable[i].size == 0) { // 如果分配后分区大小为0,将该分区从内存分配表中删除 memoryTable.erase(memoryTable.begin() + i); } // 返回分配的起始地址 return startAddress; } } // 没有找到合适的分区,返回-1表示分配失败 return -1; } ``` 4. 实现内存释放函数,根据释放的内存块的起始地址和大小,在内存分配表中找到对应的空闲分区并进行合并。 ``` void freeMemory(int startAddress, int size) { // 在内存分配表中查找释放的内存块应该插入的位置 int index = 0; while (index < memoryTable.size() && memoryTable[index].startAddress < startAddress) { index++; } // 将释放的内存块插入到对应的位置 memoryTable.insert(memoryTable.begin() + index, { startAddress, size, true }); // 合并相邻的空闲分区 for (int i = 0; i < memoryTable.size() - 1; i++) { if (memoryTable[i].free && memoryTable[i + 1].free) { memoryTable[i].size += memoryTable[i + 1].size; memoryTable.erase(memoryTable.begin() + i + 1); i--; } } } ``` 5. 测试内存分配和释放函数。 ``` // 初始化内存分配表 memoryTable.push_back({ 0, 100, true }); memoryTable.push_back({ 200, 50, true }); memoryTable.push_back({ 300, 80, true }); // 分配一段大小为30的内存块 int startAddress = allocateMemory(30); if (startAddress == -1) { cout << "内存分配失败!" << endl; } else { cout << "分配了大小为30的内存块,起始地址为:" << startAddress << endl; } // 释放刚刚分配的内存块 freeMemory(startAddress, 30); // 分配一段大小为60的内存块 startAddress = allocateMemory(60); if (startAddress == -1) { cout << "内存分配失败!" << endl; } else { cout << "分配了大小为60的内存块,起始地址为:" << startAddress << endl; } ``` 参考资料: 1. 《操作系统概念》 2. 《计算机操作系统

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值