找回密码
 注册
搜索
热搜: 回贴
微赢网络技术论坛 门户 服务器 Linux/BSD 查看内容

《深入理解Linux内核(第三版)》 §1.6 Unix 内核概述

2009-12-20 13:35| 发布者: admin| 查看: 46| 评论: 0|原作者: 慕容紫英


§1.2 硬件的依赖性

支持的硬件平台(略)

§1.3 Linux 版本

一直到2.5版本的内核,Linux 都通过简单的编号来区别内核的稳定版和开发版。每个版本号用三个数字描述,由圆点分隔。前两个数字用来表示版本号,第三个数字表示发布
号。第一位版本号2 从1996 年开始就没有变过。第二位版本号表示内核的类型:如果为
偶数,表示稳定的内核;否则,表示开发中的内核。
正如内核版本名字所表示的,稳定版本的内核由Linux的发布者和内核黑客彻底检查过,一个稳定版的新发布主要用来纠正用户所报告的错误或者增加新的驱动程序。另一方面,开发版的不同版本之间可能有非常明显的差异。内核开发者可以自由地采用不同方案进行实验,但这些实验可能导致内核有很大变化。用开发版运行应用程序的用户,当把内核升级到新版时,也许会遇到一些不那么令人愉快的意外。
然而,在Linux 内核2.6 版的开发过程中,内核版本的编号方式发生了很大的变化。主要变化在于第二个数字已经不再用于表示一个内核是稳定版本还是正在开发的版本。因此,现在内核开发者都在当前的2.6 版本中对内核进行大幅改进。只有在内核开发者必须对内核的重大修改进行测试时,才会采用一个新的内核分支2.7。这种2.7 的分支要么产生一个新的内核版本,要么干脆丢弃所修改的部分而回退到2.6 版。
Linux 这种新的开发模式意味着两种内核具有相同的版本号,但却有不同的发布号,如2.6.10 和2.6.11 内核就可能在核心部件和基本算法上有很大的差别。这样一来,具有新发布号的内核可能潜藏着不稳定性和各种错误。为了解决这个问题,内核开发者可能发布带有补丁程序的内核版本,并且用第四位数字表示带有不同补丁的内核版本。例如,在写本段文字时,最新的稳定内核版本是2.6.11.12。
必须强调的是本书描述的是Linux2.6.11 版的内核。


§1.4 操作系统基本概念
一些操作系统允许所有的用户程序都直接与硬件部分进行交互(典型的例子是MSDOS)。与此相反,类Unix 操作系统把与计算机物理组织相关的所有低层细节都对用户
运行的程序隐藏起来。,硬件为CPU引入了至少两种不同的执行模式:用户程序的非特权模式和内核的特权模式。Unix把它们分别称为用户态(User Mode)和内核态(Kernel Mode)。
Linux
内核提供了模块(module)。模块是一个目标文件,其代码可以在运行时链接到内核或从内核解除链接。这种目标代码通常由一组函数组成,用来实现文件系统、驱动程序或其他内核上层功能。与微内核操作系统的外层不同,模块不是作为一个特殊的进程执行的。相反,与任何其他静态链接的内核函数一样,它代表当前进程在内核态下执行.优点:模块化方法,平台无关性,节省内存使用,无性能损失。

§1.5 Unix 文件系统概述
见其他部分总结
§1.6 Unix 内核概述

Unix内核提供了应用程序可以运行的执行环境。因此,内核必须实现一组服务及相应的接口。应用程序使用这些接口,而且通常不会与硬件资源直接交互。

*进程/ 内核模式
如前所述,CPU既可以运行在用户态下,也可以运行在内核态下。实际上,一些CPU可以有两种以上的执行状态。例如,Intel 80x86 微处理器有四种不同的执行状态。但是,
所有标准的Unix 内核都仅仅利用了内核态和用户态。
当一个程序在用户态下执行时,它不能直接访问内核数据结构或内核的程序。然而,当应用程序在内核态下运行时,这些限制不再有效。每种CPU模型都为从用户态到内核态的转换提供了特殊的指令,反之亦然。一个程序执行时,大部分时间都处在用户态下,只有需要内核所提供的服务时才切换到内核态。当内核满足了用户程序的请求后,它让程序又回到用户态下。
进程是动态的实体,在系统内通常只有有限的生存期。创建、撤消及同步现有进程的任务都委托给内核中的一组例程来完成。
内核本身并不是一个进程,而是进程的管理者。进程/ 内核模式假定:请求内核服务的进程使用所谓系统调用(system call)的特殊编程机制。每个系统调用都设置了一组识别进程请求的参数,然后执行与硬件相关的CPU 指令完成从用户态到内核态的转换。
除用户进程之外,Unix 系统还包括几个所谓内核线程(kernel
thread)的特权进程(被
赋予特殊权限的进程),它们具有以下特点:
• 它们以内核态运行在内核地址空间。
• 它们不与用户直接交互,因此不需要终端设备。
• 它们通常在系统启动时创建,然后一直处于活跃状态直到系统关闭。
在单处理器系统中,任何时候只有一个进程在运行,它要么处于用户态,要么处于内核
态。如果进程运行在内核态,处理器就执行一些内核例程。

Unix 内核做的工作远不止处理系统调用。实际上,可以有几种方式激活内核例程:
• 进程调用系统调用。
• 正在执行进程的CPU 发出一个异常(exception)信号,异常是一些反常情况,例如一个无效的指令。内核代表产生异常的进程处理异常。
•外围设备向CPU发出一个中断(interrupt)信号以通知一个事件的发生,如一个要求注意的请求、一个状态的变化或一个I/O
操作已经完成等。每个中断信号都是由内核中的中断处理程序(interrupt handler)来处理的。因为外围设备与CPU 异步操作,因此,中断在不可预知的时间发生。
• 内核线程被执行。因为内核线程运行在内核态,因此必须认为其相应程序是内核的
一部分

*进程实现
为了让内核管理进程,每个进程由一个进程描述符(process descriptor)表示,这个描述符包含有关进程当前状态的信息。
当内核暂停一个进程的执行时,就把几个相关处理器寄存器的内容保存在进程描述符中。
这些寄存器包括:
• 程序计数器(PC)和栈指针(SP)寄存器
• 通用寄存器
• 浮点寄存器
• 包含CPU 状态信息的处理器控制寄存器(处理器状态字,Processor Status Word)
• 用来跟踪进程对RAM 访问的内存管理寄存器
当内核决定恢复执行一个进程时,它用进程描述符中合适的字段来装载CPU寄存器。因为程序计数器中所存的值指向下一条将要执行的指令,所以进程从它停止的地方恢复执
行。
当一个进程不在CPU 上执行时,它正在等待某一事件。Unix 内核可以区分很多等待状态,这些等待状态通常由进程描述符队列实现。每个(可能为空)队列对应一组等待特定事件的进程。

*可重入内核
所有的Unix 内核都是可重入的(reentrant),这意味着若干个进程可以同时在内核态下执行。当然,在单处理器系统上只有一个进程在真正运行,但是有许多进程可能在等待CPU 或某一I/O 操作完成时在内核态下被阻塞。例如,当内核代表某一进程发出一个读磁盘请求后,就让磁盘控制器处理这个请求并且恢复执行其他进程。当设备满足了读请求时,有一个中断就会通知内核,从而以前的进程可以恢复执行。
提供可重入的一种方式是编写函数,以便这些函数只能修改局部变量,而不能改变全局数据结构,这样的函数叫可重入函数。但是可重入内核不仅仅局限于这样的可重入函数(尽管一些实时内核正是如此实现的)。相反,可重入内核可以包含非重入函数,并且利用锁机制保证一次只有一个进程执行一个非重入函数。
如果一个硬件中断发生,可重入内核能挂起当前正在执行的进程,即使这个进程处于内核态。这种能力是非常重要的,因为这能提高发出中断的设备控制器的吞吐量。一旦设备已发出一个中断,它就一直等待直到CPU应答它为止。如果内核能够快速应答,设备控制器在CPU 处理中断时就能执行其他任务。
现在,让我们看一下内核的可重入性及它对内核组织的影响。内核控制路径(kernel control path)表示内核处理系统调用、异常或中断所执行的指令序列。
在最简单的情况下,CPU 从第一条指令到最后一条指令顺序地执行内核控制路径。然
而,当下述事件之一发生时,CPU
交错执行内核控制路径:
• 运行在用户态下的进程调用一个系统调用,而相应的内核控制路径证实这个请求无法立即得到满足;然后,内核控制路径调用调度程序选择一个新的进程投入运行。结果,进程切换发生。第一个内核控制路径还没完成,而CPU 又重新开始执行其他的内核控制路径。在这种情况下,两条控制路径代表两个不同的进程在执行。
• 当运行一个内核控制路径时,CPU 检测到一个异常(例如,访问一个不在RAM中的页)。第一个控制路径被挂起,而CPU 开始执行合适的过程。在我们的例子中,这种过程能给进程分配一个新页,并从磁盘读它的内容。当这个过程结束时,第一个控制路径可以恢复执行。在这种情况下,两个控制路径代表同一个进程在执行。
• 当CPU 正在运行一个启用了中断的内核控制路径时,一个硬件中断发生。第一个内核控制路径还没执行完,CPU开始执行另一个内核控制路径来处理这个中断。当这个中断处理程序终止时,第一个内核控制路径恢复。在这种情况下,两个内核控制路径运行在同一进程的可执行上下文中,所花费的系统CPU 时间都算给这个进程。然而,中断处理程序无需代表这个进程运行。
• 在支持抢占式调度的内核中,CPU正在运行,而一个更高优先级的进程加入就绪队
列,则中断发生。在这种情况下,第一个内核控制路径还没有执行完,CPU代表高
优先级进程又开始执行另一个内核控制路径。只有把内核编译成支持抢占式调度之
后,才可能出现这种情况。


*进程地址空间
每个进程运行在它的私有地址空间。在用户态下运行的进程涉及到私有栈、数据区和代码区。当在内核态运行时,进程访问内核的数据区和代码区,但使用另外的私有栈。
因为内核是可重入的,因此几个内核控制路径(每个都与不同的进程相关)可以轮流执行。在这种情况下,每个内核控制路径都引用它自己的私有内核栈。
尽管看起来每个进程访问一个私有地址空间,但有时进程之间也共享部分地址空间。在一些情况下,这种共享由进程显式地提出;在另外一些情况下,由内核自动完成共享以节约内存。
如果同一个程序(比如说编辑程序)由几个用户同时使用,则这个程序只被装入内存一次,其指令由所有需要它的用户共享。当然,其数据不被共享,因为每个用户将有独立的数据。这种共享的地址空间由内核自动完成以节省内存。
进程间也能共享部分地址空间,以实现一种进程间通信,这就是由System V 引入并且已经被Linux 支持的“共享内存”技术。
最后,Linux 支持mmap()系统调用,该系统调用允许存放在块设备上的文件或信息的一部分映射到进程的部分地址空间。内存映射为正常的读写传送数据方式提供了另一种选择。如果同一文件由几个进程共享,那么共享它的每个进程地址空间都包含有它的内存映射。
*同步和临界区
实现可重入内核需要利用同步机制:如果内核控制路径对某个内核数据结构进行操作时被挂起,那么,其他的内核控制路径就不应当再对该数据结构进行操作,除非它已被重新设置成一致性(consistent)状态。否则,两个控制路径的交互作用将破坏所存储的信息。
一般来说,对全局变量的安全访问通过原子操作(atomic operation)来保证然而,内核包含的很多数据结构是无法用单一操作访问的。例如,用单一的操作从链表中删除一个元素是不可能的,因为内核一次至少访问两个指针。临界区(critical region)是这样的一段代码,进入这段代码的进程必须完成,之后另一个进程才能进入。
这些问题不仅出现在内核控制路径之间,也出现在共享公共数据的进程之间。几种同步技术已经被采用。以下将集中讨论怎样同步内核控制路径。

-*非抢占式内核
在寻找彻底、简单地解决同步问题的方案中,大多数传统的Unix 内核都是非抢占式的:当进程在内核态执行时,它不能被任意挂起,也不能被另一个进程代替。因此,在单处理器系统上,中断或异常处理程序不能修改的所有内核数据结构,内核对它们的访问都是安全的。
当然,内核态的进程能自愿放弃CPU,但是在这种情况下,它必须确保所有的数据结构都处于一致性状态。此外,当这种进程恢复执行时,它必须重新检查以前访问过的数据结构的值,因为这些数据结构有可能被改变。
如果内核支持抢占,那么在应用同步机制时,确保进入临界区前禁止抢占,退出临界区
时启用抢占。
非抢占能力在多处理器系统上是低效的,因为运行在不同CPU上的两个内核控制路径本可以并发地访问相同的数据结构。
-*禁止中断
单处理器系统上的另一种同步机制是:在进入一个临界区之前禁止所有硬件中断,离开时再重新启用中断。这种机制尽管简单,但远不是最佳的。如果临界区比较大,那么在一个相对较长的时间内持续禁止中断就可能使所有的硬件活动处于冻结状态。
此外,由于在多处理器系统中禁止本地CPU上的中断是不够的,所以必须使用其他的同步技术。

-*信号量
广泛使用的一种机制是信号量(semaphore),它在单处理器系统和多处理器系统上都有效。信号量仅仅是与一个数据结构相关的计数器。所有内核线程在试图访问这个数据结构之前,都要检查这个信号量。可以把每个信号量看成一个对象,其组成如下:
• 一个整数变量
• 一个等待进程的链表
• 两个原子方法:down()和up()
down()方法对信号量的值减1,如果这个新值小于0,该方法就把正在运行的进程加入到这个信号量链表,然后阻塞该进程(即调用调度程序)。up()方法对信号量的值加1,如果这个新值大于或等于0,则激活这个信号量链表中的一个或多个进程。
每个要保护的数据结构都有它自己的信号量,其初始值为1。当内核控制路径希望访问
这个数据结构时,它在相应的信号量上执行down()方法。如果信号量的当前值不是负数,则允许访问这个数据结构。否则,把执行内核控制路径的进程加入到这个信号量的链表并阻塞该进程。当另一个进程在那个信号量上执行up()方法时,允许信号量链表上的一个进程继续执行。
-*自旋锁
在多处理器系统中,信号量并不总是解决同步问题的最佳方案。系统不允许在不同CPU上运行的内核控制路径同时访问某些内核数据结构,在这种情况下,如果修改数据结构所需的时间比较短,那么,信号量可能是很低效的。为了检查信号量,内核必须把进程插入到信号量链表中,然后挂起它。因为这两种操作比较费时,完成这些操作时,其他的内核控制路径可能已经释放了信号量。
在这些情况下,多处理器操作系统使用了自旋锁(spin lock)。自旋锁与信号量非常相似,但没有进程链表;当一个进程发现锁被另一个进程锁着时,它就不停地“旋转”,执行一个紧凑的循环指令直到锁打开。
当然,自旋锁在单处理器环境下是无效的。当内核控制路径试图访问一个上锁的数据结构时,它开始无休止循环。因此,内核控制路径可能因为正在修改受保护的数据结构而没有机会继续执行,也没有机会释放这个自旋锁。最后的结果可能是系统挂起。

-*避免死锁
与其他控制路径同步的进程或内核控制路径很容易进入死锁(deadlock)状态。举一个最简单的死锁的例子,进程p1 获得访问数据结构a 的权限,进程p2 获得访问b 的权限,但是p1 在等待b,而p2 在等待a。进程之间其他更复杂的循环等待的情况也可能发生。显然,死锁情形会导致受影响的进程或内核控制路径完全处于冻结状态。
只要涉及到内核设计,当所用内核信号量的数量较多时,死锁就成为一个突出问题。在这种情况下,很难保证内核控制路径在各种可能方式下的交错执行不出现死锁状态。有几种操作系统(包括Linux)通过按规定的顺序请求信号量来避免死锁。


-*信号和进程间通信
Unix 信号(signal)提供了把系统事件报告给进程的一种机制。每种事件都有自己的信号编号,通常用一个符号常量来表示,例如SIGTERM。有两种系统事件:
异步通告
例如,当用户在终端按下中断键(通常为CTRL-C)时,即向前台进程发出中断信号 SIGINT。
同步错误或异常
例如,当进程访问内存非法地址时,内核向这个进程发送一个SIGSEGV 信号。
POSIX 标准定义了大约20 种不同的信号,其中,有两种是用户自定义的,可以当作用户态下进程通信和同步的原语机制。一般来说,进程可以以两种方式对接收到的信号做出反应:
• 忽略该信号。
• 异步地执行一个指定的过程(信号处理程序)。
如果进程不指定选择何种方式,内核就根据信号的编号执行一个默认操作。五种可能的
默认操作是:
• 终止进程。
• 将执行上下文和进程地址空间的内容写入一个文件(核心转储,core dump),并终止进程。
• 忽略信号。
• 挂起进程。
• 如果进程曾被暂停,则恢复它的执行。

因为POSIX语义允许进程暂时阻塞信号,因此内核信号的处理相当精细。此外, SIGKILL和SIGSTOP 信号不能直接由进程处理,也不能由进程忽略。
AT&T 的Unix System V 引入了在用户态下其他种类的进程间通信机制,很多Unix 内核也采用了这些机制:信号量、消息队列及共享内存。它们被统称为System
V IPC。
内核把它们作为IPC资源来实现:进程要获得一个资源,可以调用shmget()、semget()或 msgget()系统调用。与文件一样,IPC 资源是持久不变的,进程创建者、进程拥有者或超级用户进程必须显式地释放这些资源。
这里的信号量与本章“同步和临界区”一节中所描述的信号量是相似的,只是它们用在用户态下的进程中。消息队列允许进程利用msgsnd()及msgget()系统调用交换消息,msgsnd()表示把消息插入到指定的队列中,msgget()表示从队列中提取消息。
POSIX 标准(IEEE Std 1003.1-2001)定义了一种基于消息队列的IPC 机制, 这就是所谓的POSIX 消息队列。它们和
System V IPC 消息队列是相似的,但是,它们对应用程
序提供一个更简单的基于文件的接口。
共享内存为进程之间交换和共享数据提供了最快的方式。通过调用shmget()系统调用来创建一个新的共享内存,其大小按需设置。在获得IPC 资源标识符后,进程调用shmat()系统调用,其返回值是进程的地址空间中新区域的起始地址。当进程希望把共享内存从其地址空间分离出去时,就调用shmdt()系统调用。共享内存的实现依赖于内核对进程地址空间的实现。

*进程管理
Unix 在进程和它正在执行的程序之间做出一个清晰的划分。fork()和_exit()系统调用分别用来创建一个新进程和终止一个进程,而调用exec()类系统调用则是装入一个新程序。当这样一个系统调用执行以后,进程就在所装入程序的全新地址空间恢复运行。
调用fork()的进程是父进程,而新进程是它的子进程。父子进程能互相找到对方,因为描述每个进程的数据结构都包含有两个指针,一个直接指向它的父进程,另一个直接指向它的子进程。
实现fork()一种天真的方式就是将父进程的数据与代码都复制,并把这个拷贝赋予子进程。这会相当费时。当前依赖硬件分页单元的内核采用写时复制(Copy-On-Write)技术,即把页的复制延迟到最后一刻(也就是说,直到父或子进程需要时才写进页)。我们将在第九章“写时复制”一节中描述Linux 是如何实现这一技术的。
_exit()系统调用终止一个进程。内核对这个系统调用的处理是通过释放进程所拥有的资源并向父进程发送SIGCHILD 信号(默认操作为忽略)来实现的。

-*僵死进程(zombie process)
父进程如何查询其子进程是否终止了呢? wait4()系统调用允许进程等待,直到其中
的一个子进程结束;它返回已终止子进程的进程标识符(Process ID,PID)。
内核在执行这个系统调用时,检查子进程是否已经终止。引入僵死进程的特殊状态是为了表示终止的进程:父进程执行完wait4()系统调用之前,进程就一直停留在那种状态。系统调用处理程序从进程描述符字段中获取有关资源使用的一些数据;一旦得到数据,就可以释放进程描述符。当进程执行wait4()系统调用时如果没有子进程结束,内核就通常把该进程设置成等待状态,一直到子进程结束。
很多内核也实现了waitpid()系统调用,它允许进程等待一个特殊的子进程。其他wait4()
系统调用的变体也是相当通用的。
在父进程发出wait4()调用之前, 让内核保存子进程的有关信息是一个良好的习惯,但
是,假设父进程终止而没有发出wait4()调用呢? 这些信息占用了一些内存中非常有用的位置,而这些位置本来可以用来为活动着的进程提供服务。例如,很多shell 允许用户在后台启动一个命令然后退出。正在运行这个shell命令的进程终止,但它的子进程继续运行。
解决的办法是使用一个名为init 的特殊系统进程,它在系统初始化的时候被创建。当一个进程终止时,内核改变其所有现有子进程的进程描述符指针,使这些子进程成为init的孩子。init 监控所有子进程的执行,并且按常规发布wait4()系统调用,其副作用就是除掉所有僵死的进程。
-*进程组和登录会话
现代Unix 操作系统引入了进程组(process
group)的概念,以表示一种“作业(job)”的抽象。例如,为了执行命令行:
$ ls | sort | more
Shell 支持进程组,例如bash,为三个相应的进程ls、sort 及more 创建了一个新的组。shell
以这种方式作用于这三个进程,就好像它们是一个单独的实体(更准确地说是作业)。每个进程描述符包括一个包含进程组ID 的字段。每一进程组可以有一个领头进程(即其PID 与这个进程组的ID 相同的进程)。新创建的进程最初被插入到其父进程的进程组中。
现代Unix 内核也引入了登录会话(login
session)。非正式地说,一个登录会话包含在指定终端已经开始工作会话的那个进程的所有后代进程
—— 通常情况下,登录会话就是shell进程为用户创建的第一条命令。进程组中的所有进程必须在同一登录会话中。一个登录会话可以让几个进程组同时处于活动状态,其中,只有一个进程组一直处于前台,这意味着该进程组可以访问终端,而其他活动着的进程组在后台。当一个后台进程试图访问终端时,它将收到SIGTTIN 或SIGTTOUT 信号。在很多shell 命令中,用内部命令bg 和fg 把一个进程组放在后台或者前台。

*内存管理
内存管理是迄今为止Unix内核中最复杂的活动。在本书中,我们将用超过三分之一的篇幅来描述Linux 是如何实现它的。本节只说明一些与内存管理相关的主要问题。
-*虚拟内存
所有新近的Unix 系统都提供了一种有用的抽象,叫虚拟内存(virtual memory)。虚拟内存作为一种逻辑层,处于应用程序的内存请求与硬件内存管理单元(M e m o r yManagement Unit, MMU)之间。虚拟内存有很多用途和优点:
• 若干个进程可以并发地执行。
• 应用程序所需内存大于可用物理内存时也可以运行。
• 程序只有部分代码装入内存时进程可以执行它。
• 允许每个进程访问可用物理内存的子集。
• 进程可以共享库函数或程序的一个单独内存映像。
• 程序是可重定位的,也就是说,可以把程序放在物理内存的任何地方。
• 程序员可以编写与机器无关的代码,因为他们不必关心有关物理内存的组织结构。
虚拟内存子系统的主要成分是虚拟地址空间(virtual address space)的概念。进程所用的一组内存地址不同于物理内存地址。当进程使用一个虚拟地址时,内核和MMU 协同定位其在内存中的实际物理位置。
现在的CPU包含了能自动把虚拟地址转换成物理地址的硬件电路。为了达到这个目标,把可用RAM 划分成长度为4KB 或8KB 的页框(page frame),并且引入一组页表来指定虚拟地址与物理地址之间的对应关系。这些电路使内存分配变得简单,因为一块连续的虚拟地址请求可以通过分配一组非连续的物理地址页框而得到满足。

-*随机访问存储器(RAM)的使用
所有的Unix 操作系统都将RAM毫无疑义地划分为两部分,其中若干兆字节专门用于存放内核映像(也就是内核代码和内核静态数据结构)。RAM的其余部分通常由虚拟内存
系统来处理,并且用在以下三种可能的方面:
• 满足内核对缓冲区、描述符及其他动态内核数据结构的请求。
• 满足进程对一般内存区的请求及对文件内存映射的请求。
• 借助于高速缓存从磁盘及其他缓冲设备获得较好的性能。
每种请求类型都是重要的。但从另一方面来说,因为可用RAM 是有限的,所以必须在请求类型之间做出平衡,尤其是当可用内存没有剩下多少时。此外,当可用内存达到临界阈值时,可以调用页框回收(page-frame-reclaiming)算法释放其他内存,那么哪些页框是最适合回收的页框呢?正如我们将在第十七章中看到的一样,对这个问题既没有简单的答案,也没有多少理论的支持,惟一可用的解决方法是开发经过仔细调节的经验算法。
虚拟内存系统必须解决的一个主要问题是内存碎片。理想情况下,只有当空闲页框数太少时,内存请求才失败。然而,通常要求内核使用物理上连续的内存区域,因此,即使有足够的可用内存,但它不能作为一个连续的大块使用时,内存的请求也会失败。

-*内核内存分配器
内核内存分配器(Kernel Memory Allocator,KMA)是一个子系统,它试图满足系统中所有部分对内存的请求。其中一些请求来自内核其他子系统,它们需要一些内核使用的内存,还有一些请求来自于用户程序的系统调用,用来增加用户进程的地址空间。一个好的KMA 应该具有下列特点:
• 必须快。实际上,这是最重要的属性,因为它由所有的内核子系统(包括中断处理
程序)调用。
• 必须把内存的浪费减到最少。
• 必须努力减轻内存的碎片(fragmentation)问题。
• 必须能与其他内存管理子系统合作,以便借用和释放页框。
基于各种不同的算法技术,已经提出了几种KMA,包括:
• 资源图分配算法(allocator)
• 2 的幂次方空闲链表

McKusick-Karels 分配算法
• 伙伴(Buddy)系统
• Mach 的区域(Zone)分配算法
• Dynix 分配算法
• Solaris 的Slab 分配算法
我们将在第八章中看到,
Linux 的 KMA 在伙伴系统之上采用了Slab 分配算法。


-*进程虚拟地址空间处理
进程的虚拟地址空间包括了进程可以引用的所有虚拟内存地址。内核通常用一组内存区描述符描述进程虚拟地址空间。例如,当进程通过exec()类系统调用开始某个程序的执行时,内核分配给进程的虚拟地址空间由以下内存区组成:
• 程序的可执行代码
• 程序的初始化数据
• 程序的未初始化数据
• 初始程序栈(即用户态栈)
• 所需共享库的可执行代码和数据
• 堆(由程序动态请求的内存)
所有现代Unix 操作系统都采用了所谓请求调页(demand paging)的内存分配策略。有了请求调页,进程可以在它的页还没有在内存时就开始执行。当进程访问一个不存在的页时,MMU产生一个异常;异常处理程序找到受影响的内存区,分配一个空闲的页,并用适当的数据把它初始化。同理,当进程通过调用malloc()或brk()(由malloc()在内部调用)系统调用动态地请求内存时,内核仅仅修改进程的堆内存区的大小。只有试图引用进程的虚拟内存地址而产生异常时,才给进程分配页框。
虚拟地址空间也采用其他更有效的策略,如前面提到的写时复制策略。例如,当一个新进程被创建时,内核仅仅把父进程的页框赋给子进程的地址空间,但是把这些页框标记为只读。一旦父或子进程试图修改页中的内容时,一个异常就会产生。异常处理程序把新页框赋给受影响的进程,并用原来页中的内容初始化新页框。
-*高速缓存
物理内存的一大优势就是用作磁盘和其他块设备的高速缓存。这是因为硬盘非常慢:磁盘的访问需要数毫秒,与RAM 的访问时间相比,这太长了。因此,磁盘通常是影响系统性能的瓶颈。通常,在最早的Unix 系统中就已经实现的一个策略是:尽可能地推迟写磁盘的时间,因此,从磁盘读入内存的数据即使任何进程都不再使用它们,它们也继续留在RAM 中。
这一策略的前题是有好机会摆在面前:新进程请求从磁盘读或写的数据,就是被撤消进程曾拥有的数据。当一个进程请求访问磁盘时,内核首先检查进程请求的数据是否在缓存中,如果在(把这种情况叫做缓存命中),内核就可以为进程请求提供服务而不用访问磁盘。
sync()系统调用把所有“脏”的缓冲区 (即缓冲区的内容与对应磁盘块的内容不一样)写入磁盘来强制磁盘同步。为了避免数据丢失,所有的操作系统都会注意周期性地把脏缓冲区写回磁盘。

*设备驱动程序
内核通过设备驱动程序(device driver)与I/O 设备交互。设备驱动程序包含在内核中,由控制一个或多个设备的数据结构和函数组成,这些设备包括硬盘、键盘、鼠标、监视器、网络接口及连接到SCSI 总线上的设备。通过特定的接口,每个驱动程序与内核中的其余部分(甚至与其他驱动程序)相互作用这种方式具有以下优点:
• 可以把特定设备的代码封装在特定的模块中。
• 厂商可以在不了解内核源代码而只知道接口规范的情况下,就能增加新的设备。
• 内核以统一的方式对待所有的设备,并且通过相同的接口访问这些设备。
• 可以把设备驱动程序写成模块,并动态地把它们装进内核而不需要重新启动系统。
不再需要时,也可以动态地卸下模块,以减少存储在RAM 中的内核映像的大小。
图1-4 说明了设备驱动程序与内核其他部分及进程之间的接口。
一些用户程序(P)希望操作硬件设备。这些程序就利用常用的、与文件相关的系统调用及在/dev 目录下能找到的设备文件向内核发出请求。实际上,设备文件是设备驱动程序接口中用户可见的部分。每个设备文件都有专门的设备驱动程序,它们由内核调用以执行对硬件设备的请求操作。
这里值得一提的是,在Unix 刚出现的时候,图形终端是罕见而且昂贵的,因此Unix 内核只直接处理字符终端。当图形终端变得非常普遍时,一些如X Window 系统那样的特别的应用就出现了,它们以标准进程的身份运行,并且能直接访问图形界面的I/O 端口和RAM 的视频区域。一些新近的Unix 内核,例如Linux 2.6,对图形卡的帧缓冲提供了一种抽象,从而允许应用软件无需了解图形界面的I/O
端口的任何知识就能对其进行访问(参见第十三章“内核支持的级别”一节)。







最新评论

QQ|小黑屋|最新主题|手机版|微赢网络技术论坛 ( 苏ICP备08020429号 )

GMT+8, 2024-9-30 13:29 , Processed in 0.279883 second(s), 12 queries , Gzip On, MemCache On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

返回顶部