嵌入式学习(四)嵌入式操作系统

操作系统功能

操作系统是充当计算机用户和计算机硬件之间的一个中介,并用于管理计算机资源和控制应用程序运行的计算机程序。简单的讲,操作系统一般会提供以下服务:

  • 程序运行 一个程序的运行离不开操作系统的配合,其中包括指令和数据载入内存,I/O 设备和文件系统的初始化等等。
  • I/O 设备访问 每种 I/O 设备的管理和使用都有自己的特点。而操作系统接管了这些工作,从而使得用户在使用这些 I/O 设备的过程中会感觉更方便。
  • 文件访问 文件访问不仅需要熟悉相关 I/O 设备(磁盘驱动器等)的特点,而且还要熟悉相关的文件格式。另外,对于多用户操作系统或者网络操作系统,从计算机安全角度考虑,需要对文件的访问权限做出相应的规定和处理。这些都是操作系统所要完成的工作。
  • 系统访问 对于一个多用户或者网络操作系统而言,操作系统需要对用户系统访问权限做出相应的规定和处理。
  • 错误检测和反馈 当操作系统运行时,会出现这样那样的问题。操作系统应当提供相应的机制来检测这些信息,并且能对某些问题给出合理的处理或者报告用户。
  • 系统使用纪录 在一些现代操作系统中,出于系统性能优化或者系统安全角度考虑,操作系统会对用户使用过程纪录相关信息。
  • 程序开发 一般操作系统都会提供丰富的 API 供程序员开发应用程序,并且很多程序编辑工具,集成开发环境等等也都是通过操作系统提供的。
现代操作系统

现代操作系统技术是在综合了以上四种典型的操作系统技术的基础上提出的操作系统实现方式,它适应了现代计算机系统管理和使用的要求。其主要特征是多任务、分时、而且很多系统都开始陆续加入多用户功能。现代操作系统一般包括:

  • 进程及进程管理,
  • 内存及虚拟管理,
  • 信息保护和安全,
  • 调度和资源管理,
  • 模块化系统化设计。

操作系统内核

内存管理
  • 首先,进程调入调出内存存在随机性,我们需要内存管理提供二次定址功能使得任务再次调入内存能够和先前一样正常运行。其次,不同的进程为了自己的正常运行需要有自己的私有空间,内存管理需要提供相应的保护机制以容许这种私有空间的存在。
  • 再次,为了能够协同完成更高级的功能,不同进程之间需要有不同形式的交互,比如说访问对方数据,使用对方进程代码等等,而内存管理所完成的共享功能可以保证顺利实现。
  • 另外,由于计算机采用的是线性存储设备,而计算机程序本身为了完成自身的逻辑任务就有了适合自己特点的逻辑划分,如果操作系统或计算机硬件能够顺利实现这个逻辑划分和线性存储之间的顺利转换对程序本身实现的相对独立性等方面不无裨益。因此内存管理需要提供相应的逻辑组织功能。
  • 最后一点需要指出的就是,计算机存储设备是多样的,如何合理的管理这些存储设备以提高操作系统性能也是内存管理的功能要求的,这就是内存管理的物理组织功能
虚拟内存

虚拟内存机制基于分页技术或者分页与分段两种技术的结合,它是现代操作系统的一个显著特征。虚拟内存技术的实现需要有硬件支持,并得到操作系统配合共同完成,从而能够提供给每个进程一个几乎不受限制的虚拟内存空间。虚拟内存机制的实现不仅需要操作系统方面的软件支持,而且需要有相应的硬件支持,比如地址转换功能支持等。硬件支持主要包括地址转换功能,以及一些为了提高软件支持效率而做出的相应的支持,如 MMU 等。在这一节我们主要介绍虚拟内存机制在操作系统方面的实现。操作系统方面的支持需要考虑三个问题:

  • 系统是否需要虚拟内存支持;
  • 系统对内存分割机制的选择问题;
  • 内存管理算法的选择问题。

Linux 的内存管理机制

嵌入式学习(四)嵌入式操作系统

地址转换与进程分区表

逻辑地址是存在于进程虚拟地址空间的东西,计算机硬件只认识物理地址。因此在程序执行过程中需要完成一个逻辑地址向物理地址转换的过程。在基于分页机制的虚拟内存管理模式中,逻辑地址向物理地址的一般转换过程如下:
首先,系统取出进程要访问的逻辑地址所包含的信息,即分页编码和页内偏移;然后根据前者,在进程分页表中查出对应的物理内存中的页帧编码,而由于页帧和虚拟分页大小完全相同,页内偏移原封不动地下传;最后根据所取得的页帧编码和下传来的页内偏移就得到该逻辑地址在物理内存中的物理地址。

分页加载请求(Page Demanding)

当进程某个分页被加载到物理内存时,其分页表中就会增加一项,用于实现在该分页内的所有虚拟地址到对应的物理地址的转换过程。当某个虚拟地址在该进程所有装载入的分页所包括的范围以外的时候,该虚拟地址就没有对应的物理地址存在,如图 2.1 所示的情况,对进程1而言,当虚拟地址位于分页 v*n2 所包括的地址范围内时,虚拟内存管理就会产生一个寻页错误中断,要求操作系统将该分页载入内存中,同时该进程进入阻塞状态。
当操作系统响应请求将该页载入物理内存之后,如图 2.1 所示, v*n2 被载入物理内存页帧 PFN3,同时进程1的分页表增加一个新条目(如虚线框所示)该进程将被重新**,并继续原来的操作。

分页替换(Page Replacement)

在 Linux 中,分页替换操作也称为页交换(Page Swapping)。在执行这个操作之前,首先需要判断该替换的分页。在 Linux 中采用 LRU 分页替换算法。根据该算法选择准备被替换的分页,而后判断该分页在载入内存之后是否有改动,如果没有的话,可以直接被替换,如果有改动,则需要该分页在外围存储设备中作永久保存之后才可以被替换。前面讲过,LRU 替换算法的效率非常接近最优替换算法。

LRU (Least-Recently-Used)算法是将内存中在过去时间里最早访问到的那个分页替换掉。这种方法在实践中证明通常是很有效的,并且非常接近最优算法的性能。但是由于需要对每个分页的访问时间进行纪录,在实现起来存在一定的困难。

进程控制

进程控制不仅包括对进程创建过程的控制而且还包括对进程状态切换的控制。另外,出于对操作系统某些关键数据如进程控制模块等的保护,进程的执行模式分为两种,即拥有更高权限的内核执行模式和拥有较低权限的用户执行模式。从而进程控制就增加了对进程执行模式切换的控制。
嵌入式学习(四)嵌入式操作系统

并发控制:互斥与同步

并发控制实现的最基本方法就是进程间互斥,也就是说,当某个进程正在执行某个动作的时候,其它进程应当避免获得同样的权限。互斥的基本实现方法有两类,即软件方法和硬件方法。基本软件实现方法采用的是所谓的“忙碌-等待”(Busy Waiting)技术。基本硬件方法的指导思想就是通过硬件方法使得进程在执行某个需要互斥的动作的时候不会被中断。另外还有第三类方法,它们常常为操作系统或程序编译器所采用,即信号量(Semaphore)、管程(Monitor)和消息传递(Message Passing)。

并发控制:死锁处理

考察各种死锁现象我们不难看出死锁现象发生的条件:

  • 进程间互斥:每次只能有一个进程使用某个资源;
  • 占用并等待:进程已经占用了某个资源并且同时等待其它资源空闲以供占用;
  • 非抢占:进程所占用的资源只能由进程自己释放,而不能由外力强迫它释放。
  • 循环等待:由于相互需要对方占有的资源,从而形成了一个闭合的等待进程链。
死锁的预防

一种是限制进程启动法,即如果某个进程启动后会导致死锁,那么就不要启动该进程;
另一种是限制资源分配法,即如果某个资源分配给进程后会导致死锁的话,那就不要分配

中断

嵌入式学习(四)嵌入式操作系统

Linux 的进程与中断管理机制

嵌入式学习(四)嵌入式操作系统

Linux 进程控制块

  • (1) 进程状态:即上面所提到的五种状态的其中一种;
  • (2) 调度信息:操作系统调度时需要的信息以便对进程实施公平调度;
  • (3) 进程标识:需要特别指出的是 Linux 从其安全角度出发,它的进程标识多达八个,不过可分为用户标识和用户组标识两大类;
  • (4) 进程间通讯: Linux 支持的进程间通讯机制不仅包括传统 Unix 系统中的信号、管道和信号量机制,也支持 Unix System V 进程间通讯的共享内存、信号量和消息队列机制;
  • (5) 进程间关联:从进程派生角度看,进程间有父子关系,兄弟关系;而从进程管理角度看,进程往往处于一个进程的双向链表中,进程间有前后关系。进程需要维护相应的指针来阐明这些关系。
  • (6) 时间和定时器:进程需要维护其创建时间,从而决定分配给它的处理器时间片的消耗情况;另外,进程在发送信号等方面需要和定时器打交道,因此需要维护与进程相关的内部定时器;
  • (7) 文件系统:进程在运行期间会打开某个文件,因此需要维护它所打开文件的相关信息;
  • (8) 虚拟内存:进程一般都要用到虚拟内存,Linux 内核需要相关信息来跟踪进程对内存的使用状况;
  • (9) 处理器相关信息:各个寄存器的内容等。

Linux 内核同步机制

  1. 非抢占
    Linux 内核是非抢占式的,具体表现在三个方面:
    首先,任何进程如果以内核模式运行的话,除非它自愿交出处理器的控制权,否则,任何其它进程都没法打断它的运行;
    其次,一个进程正在内核模式下运行,这个时候,中断或异常处理可以中断它的正常运行,但是当处理完中断后,该进程将重获处理器控制权;
    第三,中断或异常处理过程只能被中断或异常处理中断。
  2. 原子操作
    防止某个操作被中断的最简单的方法就是将这个操作通过硬件技术用一个指令来完成。在执行过程中不能被中断的操作就被称作原子操作,它是实现很多更复杂更灵活的互斥控制机制的基础。
  3. 中断禁止
    当某段可执行代码过于冗长,而不能用原子操作来实现的时候,我们就需要考虑更好的互斥控制算法。中断禁止方法是互斥控制的一个主要方法。它能够确保硬件中断不会对内核模式运行下的进程造成干扰,从而实现互斥。当然仅仅有中断禁止,并不一定就能保证这样的进程运行不受影响。比如说,一个“缺页”异常就可以挂起该进程。
  4. 内核信号量
    如果想要保护互斥资源的话,一个想当然的方法就是给这个互斥资源加上一把“锁”,这样其它进程就没法访问,而等互斥资源访问完毕就解除这把“锁”,这样其它进程就可以访问该资源。在 Linux 中,这样的“锁”有两种,一种是内核信号量;一种是自旋锁。前者在单处理器系统和多处理器系统都能使用,而后者则只能用在多处理器系统中。
  5. 自旋锁
    自旋锁的思想就是在不断循环中坚持反复尝试获取一个资源(一把“锁”),直到成功为止。这通常是通过类似 TSI 机器指令的操作进行循环来实现的。自旋锁最重要的特点就是进程在等待“锁”被释放时一直占据着 CPU。一般而言,只能在极短的操作过程中才使用自旋锁。特别是决不能在阻塞操作中持有锁。甚至为了保证尽量短时间地占有锁,可在取得自旋锁以前阻塞当前处理器的中断。自旋锁的基本前提是进程在某个处理器上忙等(busy and wait)一个资源,而另外一个进程在不同的处理器上正使用这个资源,这只有在多处理器系统中才可能。在单处理器系统中,如果一个系统试图获取一个已被占用的自旋锁的话,就会陷入死循环。而多处理器算法对于任意数目的处理器都应该适用。这要求进程必须严格遵守一个规则,那就是当它持有自旋锁时决不能放弃对处理器的控制权(对于 Linux 而言,就是决不能在释放自旋锁之前调用系统的进程调度调度函数)。在单处理器系统中,这样就可以保证进程不会陷入死循环。

在 Linux 中定义在信号量上的两个原子操作分别为:
a 减一操作 down():当进程希望访问互斥资源的话,它调用该操作,信号量的值减一;
b加一操作 up():当进程访问互斥资源完毕,它调用该操作,信号量的值加一。在 Linux 中信号量是由一个结构来表示,即 semaphore,在初始化时,它的值初始化为1,当然也可以初始化为其它的正整数,那样能允许多个进程同时访问互斥资源。

这个结构主要有两个成员:
a一个整型变量 count:也就是信号量的值,如果该变量值非负的话,进程可以访问互斥资源,该变量值的改变只能由上面的两个原子操作来完成;
b 一个等待进程链表指针 wait:如果某个希望访问互斥资源的进程在执行完减一操作之后发现信号量的值为负值的话,进程将会挂起,并进入该链表。

Linux 进程间通讯

前面已经提到过 Linux 支持多种进程间通讯机制。我们这里只看其中比较重要的五种,
信号(Signal)
管道(Pipe and Named Pipe)
信号量(Semaphore)
消息队列(MessageQueue)
共享内存(Shared Memory)