在纯技术层面上,内核是硬件与软件之间的一个中间层。其作用是将应用程序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址
从应用程序的视角来看,内核可以被认为是一台增强的计算机,将计算机抽象到一个高层次上。例如,在内核寻址硬盘时,它必须确定使用哪个路径来从磁盘向内存复制数据,数据的位置,经由哪个路径向磁盘发送哪一条命令,等等。另一方面,应用程序只需发出传输数据的命令。实际的工作如何完成与应用程序是不相干的,因为内核抽象了相关的细节。应用程序与硬件本身没有联系,只与内核有联系,内核是应用程序所知道的层次结构中的最底层,因此内核是一台增强的计算机
Linux内核的高层次概述以及完整的Linux系统中的各个层次
传统上, UNIX操作系统下运行的应用程序、服务器及其他程序都称为进程
。每个进程都在CPU的虚拟内存中分配地址空间。各个进程的地址空间是完全独立的,因此进程并不会意识到彼此的存在。从进程的角度来看,它会认为自己是系统中唯一的进程。如果进程想要彼此通信(例如交换数据),那么必须使用特定的内核机制
由于Linux是多任务系统,它支持(看上去)并发执行的若干进程。系统中同时真正在运行的进程数目最多不超过CPU数目,因此内核会按照短的时间间隔在不同的进程之间切换(用户是注意不到的),这样就造成了同时处理多进程的假象
进程切换
调度
Linux对进程采用了一种层次系统,每个进程都依赖于一个父进程。内核启动init
程序作为第一个进程,该进程负责进一步的系统初始化操作,并显示登录提示符或图形登录界面
UNIX操作系统中有两种创建新进程的机制,分别是fork
和exec
fork
可以创建当前进程的一个副本,父进程和子进程只有PID(进程ID)不同,父进程内存的内容将被复制,至少从程序的角度来看是这样。 Linux使用了写时复制(copy on write),主要的原理是将内存复制操作延迟到父进程或子进程向某内存页面写入数据之前,在只读访问的情况下父进程和子进程可以共用同一内存页exec
将一个新程序加载到当前进程的内存中并执行。旧程序的内存页将刷出,其内容将替换为新的数据。然后开始执行新程序进程并不是内核支持的唯一一种程序执行形式。除了重量级进程(有时也称为UNIX进程)之外,还有一种形式是线程
(有时也称为轻量级进程)。线程也已经出现相当长的一段时间,本质上一个进程可能由若干线程组成,这些线程共享同样的数据和资源,但可能执行程序中不同的代码路径
由于线程和主程序共享同样的地址空间,主程序自动可以访问接收到的数据。除了为防止线程访问同一内存区而采取的互斥机制外,就不需要什么通信了
由于内存区域是通过指针寻址,因此CPU的字长决定了所能管理的地址空间的最大长度。地址空间的最大长度与实际可用的物理内存数量无关,因此被称为虚拟地址空间。使用该术语的另一个理由是,从系统中每个进程的角度来看,地址空间中只有自身一个进程,而无法感知到其他进程的存在
Linux将虚拟地址空间划分为两个部分,分别称为内核空间和用户空间
这种划分与可用的内存数量无关。由于地址空间虚拟化的结果, 每个用户进程都认为自身有3 GiB
内存。各个系统进程的用户空间是完全彼此分离的,而虚拟地址空间顶部的内核空间总是同样的
内核把虚拟地址空间划分为两个部分,因此能够保护各个系统进程,使之彼此隔离。所有的现代CPU都提供了几种特权级别
,进程可以驻留在某一特权级别
在用户状态禁止访问内核空间。用户进程不能操作或读取内核空间中的数据,也无法执行内核空间中的代码,从用户状态到核心态的切换通过系统调用的特定转换手段完成
除了代表用户程序执行代码之外,内核还可以由异步硬件中断激活,然后在中断上下文
中运行。与在进程上下文中运行的主要区别是,在中断上下文中运行不能访问虚拟地址空间中的用户空间部分。因为中断可能随机发生,中断发生时可能是任一用户进程处于活动状态,由于该进程基本上与中断的原因无关,因此内核无权访问当前用户空间的内容。在中断上下文中运行时,内核必须比正常情况更加谨慎,例如,不能进入睡眠状态
在核心态和用户状态执行。 CPU大多数时间都在执行用户空间中的代码。当应用程序执行系统调用时,则切换到核心态,内核将完成其请求。在此期间,内核可以访问虚拟地址空间的用户部分。在系统调用完成之后, CPU切换回用户状态。硬件中断也会使CPU切换到核心态,这种情况下内核不能访问用户空间
大多数情况下,单个虚拟地址空间就比系统中可用的物理内存要大。在每个进程都有自身的虚拟地址空间时,情况也不会有什么改善。因此内核和CPU必须考虑如何将实际可用的物理内存映射到虚拟地址空间的区域。可取的方法是用页表
来为物理地址分配虚拟地址。虚拟地址关系到进程的用户空间和内核空间,而物理地址则用来寻址实际可用的内存
例如,进程A的虚拟内存页1映射到物理内存页4,而进程B的虚拟内存页1映射到物理内存页5。由此可见,不同进程的同一虚拟地
址实际上具有不同的含义
物理内存页经常称作页帧
。相比之下, 页则专指虚拟地址空间中的页
用来将虚拟地址空间映射到物理地址空间的数据结构称为页表
。实现两个地址空间的关联最容易的方法是使用数组,对虚拟地址空间中的每一页,都分配一个数组项。该数组项指向与之关联的页帧,但有一个问题。例如, IA-32体系结构使用4 KiB
页,在虚拟地址空间为4 GiB
的前提下,则需要包含100万项的数组。在64位体系结构上,情况会更糟糕。每个进程都需要自身的页表,因此系统的所有内存都要用来保存页表,也就是说这个方法是不切实际的
因为虚拟地址空间的大部分区域都没有使用,因而也没有关联到页帧,那么就可以使用功能相同但内存用量少得多的模型:多级分页
PGD
用于索引进程中的一个数组(每个进程有且仅有一个),该数组是所谓的全局页目录或PGD
。 PGD的数组项指向另一些数组的起始地址,这些数组称为中间页目录(Page Middle Directory, PMD)PMD
,在通过PGD中的数组项找到对应的PMD之后,则使用PMD来索引PMD。 PMD
的数组项也是指针,指向下一级数组,称为页表或页目录PTE
(Page Table Entry,页表数组),用作页表的索引。虚拟内存页和页帧之间的映射就此完成,因为页表的数组项是指向页帧的偏移量
。它指定了页内部的一个字节位置。归根结底,每个地址都指向地址空间中唯一定义的某个字节在内核分配内存时,必须记录页帧的已分配或空闲状态,以免两个进程使用同样的内存区域。由于内存分配和释放非常频繁,内核还必须保证相关操作尽快完成。内核可以只分配完整的页帧
内核中很多时候要求分配连续页。为快速检测内存中的连续区域,内核采用了一种古老而历经检验的技术: 伙伴系统
。系统中的空闲内存块总是两两分组,每组中的两个内存块称作伙伴
。伙伴的分配可以是彼此独立的。但如果两个伙伴都是空闲的,内核会将其合并为一个更大的内存块,作为下一层次上某个内存块的伙伴
内核对所有大小相同的伙伴(1、 2、 4、 8、 16或其他数目的页),都放置到同一个列表中管理。各有8页的一对伙伴也在相应的列表中
内核本身经常需要比完整页帧小得多的内存块。由于内核无法使用标准库的函数,因而必须在伙伴系统基础上自行定义额外的内存管理层,将伙伴系统提供的页划分为更小的部分。该方法不仅可以分配内存,还为频繁使用的小对象实现了一个一般性的缓存——slab
缓存
页面交换
通过利用磁盘空间作为扩展内存,从而增大了可用的内存。在内核需要更多内存时,不经常使用的页可以写入硬盘。如果再需要访问相关数据,内核会将相应的页切换回内存。通过缺页异
常机制,这种切换操作对应用程序是透明的
页面回收
用于将内存映射被修改的内容与底层的块设备同步,为此有时也简称为数据回写。数据刷出后,内核即可将页帧用于其他用途(类似于页面交换)
内核必须能够测量时间以及不同时间点的时差,进程调度就会用到该功能。 jiffies
是一个合适的时间坐标。名为jiffies_64和jiffies(分别是64位和32位)的全局变量,会按恒定的时间间隔递增。每种计算机底层体系结构都提供了一些执行周期性操作的手段,通常的形式是定时器中断
计时的周期是可以动态改变的。在没有或无需频繁的周期性操作的情况下,周期性地产生定时器中断是没有意义的,这会阻止处理器降低耗电进入睡眠状态。动态改变计时周期对于供电受限的系统
是很有用的,例如笔记本电脑和嵌入式系统
系统调用是用户进程与内核交互的经典方法
对所有的处理器来说,一个共同点就是:用户进程要从用户状态切换到核心态,并将系统关键任务委派给内核执行,系统调用是必由之路
【未完待续…】