为了便于后续内容的讲解,先简单介绍与操作系统相关的几个概念。
1.微内核与大内核
微内核与大内核是操作系统设计中不同的两种思想,这与CPU的RISC(精简指令集)和CISC(复杂指令集)架构类似。其中,微内核的思想是,把尽量少的操作系统机制放到内核模块中进行实现,而把尽量多的操作系统功能以单独进程或线程的方式实现,这样便于操作系统体系结构的扩展。比如,一个常见的设计思路就是,把进程(或线程)调度、进程间通信机制(IPC)与同步、定时、内存管理、中断调度等功能放到内核中实现,由于这些功能需要的代码量不是很大,所以可使得内核的尺寸很小。另外,把操作系统必须实现的文件系统、设备驱动程序、网络协议栈、IO管理器等功能作为单独的进程或任务来实现,用户应用程序在需要这些功能的时候,通过核心提供的IPC机制(比如消息机制)向这些服务进程发出请求,即一种典型的客户-服务器机制。
这种微内核的实现思路有很明显的优势,比如体系结构更加清晰,扩展性强等,而且由于内核保持很小,移植性(不同CPU之间的移植)也很强。但此思路也有很大的弊端,其中最大的一个弊端就是效率相对低下,因为系统调用等服务都是通过IPC机制来间接实现的,若服务器进程繁忙,对于客户的请求可能无法及时响应。由于效率是嵌入式操作系统追求的最主要目标,因此这种微内核的设计方式不太适合嵌入式操作系统的设计。
图1-1示意了微内核操作系统的设计思路。
图1-1 微内核操作系统的设计思路
大内核的思路相反,是把尽可能多的操作系统功能拿到内核模块中实现,在操作系统加载的时候,把这些内核模块加载到系统空间中。由于这些系统功能是静态的代码,不像微内核那样作为进程实现,而且这些代码直接在调用进程的空间中运行,不存在发送消息、等待消息处理、消息处理结果返回等延迟,因此调用这些功能代码的时候,效率特别高。所以在追求效率的嵌入式操作系统开发中,这种大内核模型是合适的。
但大内核也有一些弊端,最明显的问题就是内核过于庞大,有时候会使得它的扩展性不好(这可以通过可动态加载模块来部分解决)。但在嵌入式操作系统开发中,这种弊端表现得不是很明显。图1-2示意了大内核的开发思路。
图1-2 大内核操作系统的设计思路
2.进程、线程与任务
一般情况下,描述操作系统的任务管理机制时存在三个说法。
●进程:所谓进程,是一个动态的概念。一个可执行模块(可执行文件)被操作系统加载到内存,分配资源,并加入到就绪队列后,就形成了一个进程。一般情况下,进程有独立的内存空间(比如在典型的PC操作系统中,如果目标CPU是32位,则一个进程就有独立的4GB的虚拟内存空间),如果不通过IPC机制,进程之间是无法交互任何信息的,因为进程之间的地址空间是独立的,不存在重叠的部分。
●线程:一般情况下,线程是CPU可感知的最小的执行单元,一个进程往往包含多个线程,这些线程共享进程的内存空间,线程之间可以直接通过内存访问的方式进行通信,线程之间共享同一进程的全局变量,但每个线程都有自己的堆栈和硬件寄存器。
●任务:概念同线程类似,但与线程不同的是,任务往往是针对没有进程概念的操作系统来说的,比如嵌入式操作系统。这些操作系统没有进程的概念,或者说整个操作系统就是一个进程,这种情况下,任务便成了操作系统中最直接的执行单元。另外一个说法就是,任务往往是一个无限循环,操作系统启动时,任务随之启动,然后一直运行到操作系统结束为止。
目前情况下在Hello China的实现中是没有进程概念的,但存在多个执行线索,因此,用任务来描述这些执行线索是最合适的。但考虑到这些执行线索并不一定是无限循环,而是与通用操作系统的线程概念一致,可以根据需要创建,运行结束后自动销毁,因此也用“线程”来称呼系统中的执行线索。为了区别通用操作系统中普通的用户线程(用户进程中的线程),我们把Hello China中的执行线索叫做“核心线程”(Kernel Thread),或简单称为“线程”。
3.可抢占与不可抢占
在操作系统对进程(或线程)的调度策略中存在两种调度方式:可抢占方式和不可抢占方式。在可抢占方式下,操作系统以时间片(Time Slice)为单位来完成进程调度。针对每个进程,一次只能运行一个或几个时间片,一旦时间片消耗完毕,操作系统就会强行暂停其运行,而选择其他重新获得时间片的进程投入运行。在不可抢占方式下,进程会一直运行,操作系统不会强行剥夺其运行权,而是等待其自行放弃运行为止(或者是发生系统调用)。一般情况下,系统调用时,操作系统才进行新一轮调度。(www.xing528.com)
这两种方式在嵌入式操作系统中都被大规模地应用。但很显然,可抢占调度机制具备更好的实时性,因此在一些对实时性要求很高的场合,一般采用可抢占调度方式。但可抢占调度方式会引发另外一个问题,即多个进程之间对共享资源访问的同步问题。因此,在实现可抢占调度的同时,必须实现互斥机制、同步机制等操作系统机制来解决可抢占性带来的问题。不可抢占操作系统不存在“资源竞争”的问题,但也存在同步问题。Hello China操作系统的调度机制是基于可抢占方式进行的。
4.同步机制
有的情况下,线程之间的运行是相互影响的,比如对共享资源的访问就需要访问共享资源的线程相互同步,以免破坏共享资源的连续性。下列线程同步机制可被操作系统实现,用来完成线程的同步和共享资源的互斥访问。
(1)事件(Event)
事件对象是一个最基础的同步对象,事件对象一般处于两种状态:空闲状态和占用状态。一个内核线程可以等待一个事件对象,如果一个事件对象处于空闲状态,那么任何等待该事件对象的线程都不会阻塞。相反,如果一个事件对象处于占用状态,那么任何等待该对象的线程都进入阻塞状态(Blocked)。
一旦事件对象的状态由占用变为空闲,那么所有等待该事件对象的线程都被激活(状态由Blocked变为Ready,并被插入Ready队列),这一点与下面讲述的互斥体不同。
(2)信号量(Semaphore)
信号量也是最基础的同步对象之一,一般情况下,信号量维护一个计数器,假设为N,每当一个内核线程调用WaitForThisObject等待该信号量对象时,N就减1,如果N小于0,那么等待的线程将被阻塞,否则继续执行。
(3)互斥体(Mutex)
互斥体是一个二元信号量,即N的值为1,这样最多只有一个内核线程占有该互斥体对象,当这个占有该互斥体对象的线程释放该对象时,只能唤醒另外一个内核线程,其他的内核线程将继续等待。
注意互斥体对象与Event对象的不同,在Event对象中,当一个占有Event对象的线程释放该对象时,所有等待该Event对象的线程都将被激活,而对于Mutex对象,则只有一个内核线程被激活。
(4)内核线程对象(KernelThreadObject)
内核线程对象本身也是一个互斥对象,即其他内核线程可以等待该对象,从而实现线程执行的同步。但与普通的互斥对象不同的是,内核线程对象只有当状态是Terminal时才是空闲状态,即如果任何一个线程等待一个非Terminal状态的内核线程对象,那么将一直阻塞,直到等待的线程运行结束(状态修改为Terminal)。
(5)睡眠
一个运行的线程可以调用Sleep函数而进入睡眠状态(Sleeping),进入睡眠状态的线程将被加入Sleeping队列。
当睡眠时间(由Sleep函数指定)到达时,系统将唤醒该睡眠线程(修改状态为Ready,并插入Ready队列)。
(6)定时器
另外一个内核线程同步对象是定时器。定时器是操作系统提供的最基础服务之一,比如线程可以调用SetTimer函数设置一个定时器,当设置的定时器到时,系统会给设置定时器的线程发送一个消息。与Sleep函数不同的是,内核线程调用Sleep函数后将进入阻塞状态,而调用SetTimer之后,线程将继续运行。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。