内核本身并不是一个进程,而是进程的管理者。进程/内核模式假定:请求内核服务的进程使用所谓系统调(system call)的特殊编程机制。每个系统调用都设置了一个组识别进程请求的参数,然后执行与硬件相关的CPU指令完成从用户态到内核态的转换。
Unix系统还包括几个所谓内核线程(kernel thread)的特权进程(被赋予特殊权限的进程),它们具有以下特点:
它们通常系统启动时创建,然后一直处于活跃状态直到系统关闭。
为了让内核管理进程,每个进程由一个进程描述符(process descriptor)表示,这个描述符包含有关进程当前状态的信息。
当内核暂停一个进程的执行时,就把几个相关处理器寄存器的内容保存在进程描述符中。这些寄存器包括:
所有的Unix内核都是可重入的(reentrant),这意味着若干个进程可以同时在内核状态下执行。
提供可重入的一种方式是编写函数,以便这些函数只能修改局部变量,而不能改变全数据结构,这样的函数叫可重入函数。
(内核控制路径(kernel control path)表示内核处理系统调用、异常或中断所执行的指令序列。)
每个进程运行在它的私有地址空间。在用户态下运行的进程涉及到私有栈、数据区和代码区。当在内核态进行时,进程访问内核的数据区和代码区,但使用另外的私有栈。
尽管看起来看每个进程访问一个私有地址空间,但有时进程之间也共享部分地址空间。在一些情况下,这种共享由进程显式的提出另外一些情况下,由内核自动完成共享以节约内存。
实现可重入内核需要利用同步机制:如果内核控制路径对某个内核数据结构进行操作时被挂起,那么,其他的内核控制路径就不应当再对该数据结构进行操作,除非它已被重新设置成一致性(consistent)状态。否则,两个控制路径的交互作用将破坏所存储的信息。
临界区(critical region)是这样的一段代码:进入这段代码的进程必须完成,之后另一个进程才能进入。
在寻找彻底、简单夺解决同步问题的方案中,大多数传统的Unix内核都是非常抢占有式的:当进程的内核态执行时,它不能被任意挂起,也不能被另一个进程代替。
如果内核支持抢占,那么在应用同步机制时,确保进入临界区前禁止抢占,退出临界区时启用抢占。(不知道windows的临界区有没有这功能)
非抢占能力在多处理器系统上是低效的,因为运行在不同CPU上的两个内核控制路径本可以并发地访问相同的数据结构。(不明白,为什么能并发还会低效。)
单处理器系统上的另一种同步机制是:在进入一个临界区之前禁止所有硬件中断,离开时再重新启用中断。
在多处理器系统中禁 止本地CPU上的中断是不够的,所以必须使用其他的同步技术。
广泛使用的一种机制是信号量(semaphore),它在单处理器系统和多处理器系统上都有效。信号量仅仅是与一个数据结构相关的计数器。
在多处理器系统中,信号量并不总是解决同步问题的最佳方案。系统不允许在不同CPU上运行的内核控制路径同时访问某些内核数据结构,如果修改数据结构所需的时间比较短,那么,信号量可能是很低效的。内核必须把进程插入到信号量链表中,然后挂起它。因为这两种操作比较费时,完成这些操作时,其他的内核控制路径可能已经释放了信号量。
在这些情况下,多处理器操作系统使用了自旋锁(spin lock)。自旋锁与信号量非常相似,但没有进程表;当一个进程发现锁被另一个进程销着时,它就不停地“旋转”,执行一个紧凑的循环指令直到锁打开。
自旋锁在单处理器环境下是无效的。当内核控制路径试图访问一个上锁的数据结构时,它开始无休止的循环。
与其他控制路径同步的进程或内核控制路径很容易进入死锁( deadlock)状态。进程之间复杂的循环等的情况也可能发生。
1.、Unix信号(signal)提供了把系统事件报告给进程的一种机制。每种事件都有自己的信号编号,通常用一个符号常量来表示,例如:SIGTERM。有两种系统事件:
2、进程可以以两种方式对接收到的信号量做出反应:
如果进程不指定选择何种方式,内核就根据信号的编号执行一个默认操作。五种可能的默认操作是:
3、SIGKILL和SIGSTOP信号不能直接由进程处理,也不能由进程忽略。
4、消息队列允许进程利用msgsnd()及msgget()系统调用交换消息。
5、共享内存为进程之间交换和共享数据提供了最快的方式。其大小按需设置。
描述每个进程的数据结构都包含有两个指针,一个直接指向它的父进程,另一个直接指向它的子进程。
_exit()系统调用终止一个进程。内核对这个系统调用的处理是通过释放民拥有的资源并向父进程发送SIGCHILD信号(默认操作忽略)来实现的。
父进程如何查询其子进程是否终止了呢?wait4()系统调用允许进程等待,直到其中的一个子进程结束;它返回已终止子进程的进程标识符(process ID,PID);
内核在执行这个系统调用时,检查子进程是否已经终止。引入僵死进程的特殊姿态是为了表示终止的进程:父进程执行完wait4()系统调用之前,进程就一直停留在那种状态。系统调用处理程序从进程描述符字段中获取有关资源使用的一些数据;一旦得到数据,就可以释放进程描述符。
很多内核也实现了 waitpid()系统调用,它允许进程等待一个特殊的子进程。
init监控所有子进程的执行,并县城按常规发布wait4()系统调用,其副作用就是除掉所有僵死进程。
现代unix操作系统引入了进程组(process group)的概念,以表示一种“作业(job)”的抽象。例如:为了执行命令行:
$ ls | sort | more
shell支持进程组,例如bash,为三个相应的进程ls、sort 及more创建了一个新的组。shell以这种方式作用于这三个进程,就好像它们是一个单独的实体(更准确地说是作业)。每个进程描述符包括一个包含进程组ID的字段。每一进程组可以有一个领头进程(即其PID与这个进程组的ID相同的进程)。新创建的进程最初被插入到其父进程的进程组中。
现代Unix内核也引入了登录会话(login session)。通常情况下,登录会话就是shell进程为用户创建的第一条命令。
内存管理是迄今为止Unix内核中最复杂的活动。
所有新近的Unix系统都提供了一种有用的抽象,叫虚拟内存(virtual memory)。虚拟内存作为一种逻辑层,处理应用程序的内存请求与硬件内存管理单元(Memory Managerment Unit,MMU)之间。虚拟内存有很多用途和优点:
虚拟内存子系统的主要成分是虚拟地址空间(virtual address space)的概念。进程所用的一组内存地址不同于物理内存地址。当进程使用一个虚拟地址时,内核和MMU协同定位其在内存中的实际物理位置。现在的CPU包含了能自动把虚拟地址转换成物理地址的硬件电路。
所有Unix操作系统都毫无疑义地分为两部分,其中若干兆字节专门用于存放内核映像(也就是内核代码和内核静态数据结构)。RAM的其余部分通常由虚拟内存系统来处理,并县城用在以下三种可能的方面:
当可用内存达到临界阈值时,可以调用页框回收(page-frame-reclaiming)算法释放其他内存。
虚拟内存系统必须解决的一个主要问题是内存碎片。理想情况下,只有当空闲页框数太少时,内存请求才失败。然而,通常求内存使用物理上连续的内存区域,因此,即使有足够的可用内存,但它不能作为一个连续的大块使用时,内存的请求也会失败。
内核内存分配器(Knerl Memory Allocator,KMA)是一个子系统,它试图满足系统中所有部分对内存的请求。一个好的KMA应该具有下列特点:
基于各种不同的算法技术,已经提出了几种KMA,包括:
进程的虚拟地址空间包括了进程可以引用的所有虚拟内存地址。内核通常用一组内存区描述符描述进程虚拟地址空间。内核分配给进程的虚拟地址空间由以下内存区组成:
所有现代Unix操作系统都采用了所谓请求调页(demand paging)的内存分配策略。有了请求调页,进程可以在它的页还没有在内存时就开始执行。当进程访问一个不存在的页时,MMU产生一个异常;异常处理程序找到受影响的内存区,分配一个空间的页,并用适当的数据把它初始化。
物理内存的一大优势就是用作磁盘和其他块设备的高速缓存。在最早的Unix系统中就已经实现的一个策略是:尽可能地推迟写磁盘的时间。因此从磁盘读入内存的数据即使任何进程都不再使用它们,它们也继续留在RAM中。
当一个进程请求访问磁盘时,内核首先检查进程请求的数据是否在缓存中,如果在(把这种情况叫做缓存命中),内核就可以为进程请求提供服务而不用访问磁盘。
sync()系统调用把所有“脏”的缓冲区(即缓冲区的内容与对应磁盘块的内容不一样)写入磁盘来强制磁盘同步。为了避免数据丢失,所有的操作系统都会注意周期性地把脏缓冲写回磁盘。
内核通过设备驱动程序(device driver)与I/O设备交互。设备驱动程序包含在内核中,由控制一个或多个设备数据结构和函数组成。通过特定的接口,每个驱动程序与内核中的其余部分(甚至与其他驱动程序)相互作用这种方式具有以下优点:
可以把特定设备的代码封装在特定的模块中。
厂商可以在不了解内核源代码而只知道接品规范的情况下,就能增加新的设备。
内核以统一的方式对待所有的设备,并县城通过相同的接口访问这些设备。
可以把设备驱动程序写成模块,并动态地把它们装进内核而不需要重新启动系统。不再需要时,也可以动态地卸下模块,以减少存储在RAMk的内核映像的大小。