导图社区 深入理解Linux内核-绪论
<深入理解Linux内核>书的绪论部分导图
编辑于2021-09-30 00:03:44深入理解Linux内核
绪论
Linux与其他类UNIX内核的比较
Linux与商用UNIX内核如何竞争
单块结构的内核(Monolithic kernel)
编译并静态连接传统的UNIX内核
内核线程
多线程应用程序支持
抢占式(preemptive)内核
多处理器支持
文件系统
STREAMS
Linux优势
Linux是免费的
Linux的所有成分都可以充分的定制
Linux可以运行在低档、便宜的硬件平台上
Linux是强大的
Linux的开发者都是非常出色的程序员
Linux内核非常小、紧凑
Linux与很多通用操作系统高度兼容
Linux有很好的技术支持
硬件依赖性
alpha
arm,arm26
cris
frv
h8300
i386
ia64
m32r
m68k,m68knommu
mips
parisc
ppc,ppc64
s390
sh,sh64
sparc,sparc64
um
v850
x86_64
Linux版本
操作系统基本概念
主要目标
与硬件部分交互,为包含在硬件平台上的所有低层可编程部件提供服务
为运行在计算机系统上的应用程序提供执行环境
多用户系统
特点
核实用户身份的认证机制
防止有错误的用户程序妨碍其他应用程序在系统中运行的保护机制
防止有恶意的用户程序干涉或窥视其他用户活动的保护机制
限制分配给每个用户资源数的计账机制
用户和组
进程
通常定义为“程序执行时的一个实例”或者一个运行程序的“执行上下文”
内核体系结构
使用模块的优点
模块化方法
平台无关性
节省内存使用
无性能损失
UNIX文件系统概述
文件
目录树
“..”
“.”
“/”
硬链接和软连接
硬链接
UNIX命令
$ ln P1 P2
限制
不允许用户给目录创建硬链接。因为这可能把目录树变成环形树,从而就不可能通过名字定位一个文件。
只有在同一文件系统中的文件之间才能创建连接。这带来比较大的限制,因为现代UNIX系统可能包含了多种文件系统,这些文件系统位于不同的磁盘和/或分区,用户也许无法知道他们之间的物理划分
软连接,也称符号连接
目的
为了克服硬链接限制
是什么
短文件,包含另一个文件的任意一个路径名,可以指向任意文件系统中的任意文件或目录,甚至可以指向不存在的文件
UNIX命令
$ ln -s P1 P2
文件类型
普通文件
目录
符号连接
面向块的设备文件
面向字符的设备文件
管道和命名管道
套接字
文件描述符与索引节点
属性
文件类型
与文件相关的硬链接个数
以字节为单位的文件长度
设备标识符
文件系统中标识文件的索引节点号
文件拥有者UID
文件用户组ID
几个时间戳,表叔索引结点状态改变的时间,最后访问时间以及最后修改时间
访问权限和文件模式
访问权限和文件模式
文件的潜在用户类型
作为文件所有者的用户
同组用户,不包括所有者
所有剩下的其他用户
权限
读
写
执行
9种不同的二进制来标记状态
三种附加标记
suid(Set User ID)
进程执行一个文件时通常保持进程拥有者的UID。然而,如果设置了可执行文件的suid标志位,进程就获得了该文件拥有者的UID
sgid(Set Group ID)
进程执行一个文件时保持进程组的用户组ID。然而,如果设置了可执行文件sgid的标志位,进程就获得了该文件用户组的UID
sticky
设置了sticky标志位的可执行文件相当于向内核发出一个请求,当程序执行结束以后,依然保留它在内存。
这个标志已经过时,现在使用基于代码页共享的其他方法
文件操作的系统调用
打开文件
进程调用系统调用
fd=open(path, flag, mode);
path
表示被打开文件的(相对或绝对)路径
flag
指定文件打开的方式(例如:读,写,读/写,追加)。它也指定是否应当创建一个不存在的文件
mode
指定新创建文件的访问权限
会创建
“打开文件”的对象
返回
名叫文件描述符(file descriptor)的标识符
一个打开文件对象包括
文件操作的一些数据结构,如指定文件打开方式的一组标志;表示文件当前位置的offset字段,从这个位置开始将进行下一个操作(即所谓的文件指针)等。
进程可以调用的一些内核函数指针。这组允许调用的函数集合由参数flag的值决定。
POSIX语义所指定的一般特性
文件描述符表示进程与打开文件之间的交互,而打开文件对象包含了与这种交互相关的数据,同一打开文件对象也许由同一个进程中的几个文件描述符标识
几个进程也许同时打开同一文件,在这种情况下,文件系统给每个文件分配一个单独的打开文件对象以及单独的文件描述符。当这种情况发生时,UNIX文件系统对进程在同一文件上发出的I/O操作之间不提供任何形式的同步机制。然而,有几个系统调用,如flock()。可用来让进程在整个文件或部分文件上对I/O操作实施同步
为创建一个新文件,进程也可以调用create()系统调用,它与open()非常相似,都是由内核来处理
访问打开的文件
不同文件的访问方式
普通UNIX文件,可以顺序访问,也可以随机访问
设备文件和命名管道文件,通常只能顺序访问
内核把文件指针存放在打开文件对象中,也就是说,当前位置就是下一次进行读或写操作的位置
顺序访问
默认访问方式
read()系统调用
write()系统调用
系统调用总是从文件指针的当前位置开始读或写。为了修改文件指针的值,必须在程序中显式调用lseek()系统调用。当打开文件时,内核让文件指针指向文件的第一字节(偏移量为0)
lseek()系统调用
newoffset=lseek(fd,offset,whence);
fd
表示打开文件的文件描述符
offset
指定一个有符号的整数值,用来计算文件指针的新位置
whence
指定文件指针新位置的计算方式:可以是offset加0,表示文件指针从文件头移动;也可以是offset加文件指针的当前位置,表示文件指针从当前位置移动;还可以是offset加文件最后一个字节的位置,表示文件指针从文件末尾开始移动
read()系统调用
nread=read(fd, buf, count);
fd
表示打开的文件描述符
buf
指定在进程地址空间中缓冲区的地址,所读的数据就放在这个缓冲区
count
表示所读的字节数
行为
当处理这样的系统调用时,内核会尝试从拥有文件描述符fd的文件中读count个字节,其起始位置为打开文件的offset字段当前值。在某些情况下可能遇到文件结束、空管道等,因此内核无法成功读出全部的count个字节。返回的nread值就是实际所读的字节数。给原来的值加上nread就会更新文件指针。write()参数与read()相似。
关闭文件
close()系统调用
res=close(fd);
释放与文件描述符fd相对应的打开文件对象,当一个进程终止时,内核就会关闭其所有仍打开着的文件。
更名及删除文件
要重命名或者删除一个文件时,进程不需要打开它,实际上,遮阳的操作并没有对这个文件的内容起作用,而是对一个或多个目录的内容起作用
rename()系统调用
res= rename(oldpath,newpath);
改变了文件链接的名字
unlink()系统调用
res= unlink(pathname);
减少了文件链接数,删除了相应的目录项。只有当链接数为0时,文件才被真正删除
UNIX内核概述
概述
Unix内核提供了应用程序可以运行的执行环境,因此,内核必须实现一组服务以及相应的接口,应用程序使用这些接口,而且通常不会与硬件资源直接交互
进程/内核模式
综述
CPU既可以运行在用户态下,也可以运行在内核态下。实际上,一些CPU可以有两种以上的执行状态,但是,所有标准的UNIX内核都仅仅利用了用户态和内核态。
用户态
程序在用户态执行的时候,不能直接访问内核数据结构或内核程序
内核态
程序在内核态执行的时候,可以直接访问内核数据结构或内核程序
一般情况
每种CPU模型都为从用户态到内核态转换提供了特殊的指令,反之亦然。程序执行时,大部分时间都处在用户态下,只有需要内核所提供的服务时才会切换到内核态运行。当内核满足了程序的请求后,它又让程序回到用户态下。
进程
进程是动态的实体,系统内通常只有有限的生存期。创建、撤销和同步现有进程的任务都委托给内核中的一组例程来完成。
内核
内核本身并不是一个进程,而是进程的管理者。
内核/进程模式假定
请求内核服务的进程使用所谓系统调用(system call)的特殊机制。每个系统调用都设置了一组识别请求的参数,然后执行与硬件相关的CPU指令完成用户态到内核态的转换
UNIX系统包括的几个所谓内核线程(kernel thread)的特权进程(被赋予特殊权限的进程),他们的特点
它们以内核态运行与内核地址空间
他们不与用户直接交互,因此不需要终端设备
他们通常在系统启动时创建,然后一直处于活跃状态直到系统关闭
单处理器系统,任何时候只有一个进程在执行,它要么处于用户态要么处于内核态。如果进程运行在内核态,处理器就执行一些内核例程,下图举例说明内核态和用户态之间的相互转换。
处于用户态的进程1发出系统调用之后,进程切换到内核态,系统调用被执行,然后,直到发生定时中断且调度程序在内核态被激活,进程1才恢复在用户态下执行。进程切换发生,进程2在用户态下开始执行,直到硬件设备发出中断请求。中断的结果是,进程2切换到内核态并处理中断
UNIX有几种方式可以激活内核例程
进程调用系统调用
正在执行进程的CPU发出一个异常(exception)的信号,异常是一些反常情况,例如一个无效指令,内核代表产生异常的进程处理异常。
外围设备向CPU发出一个中断(interrupt)信号以通知一个事件的发生,如一个要求注意的请求,一个状态变化或一个I/O操作已经完成等。每个中断信号都是由内核中的中断处理程序(interrupt handler)来处理的。因为外围设备与CPU异步操作,因此,中断在不可预知的时间发生
内核线程被执行。因为内核线程运行在内核态,因此必须认为相应的程序是内核的一部分
进程实现
概述
为了让内核管理进程,每个进程都由一个进程描述符(process descriptor)表示,这个描述符包含有关进程当前状态的详细信息
内核暂停进程执行后,保存在进程描述符里的寄存器内容
程序计数器(PC)和栈指针(SP)寄存器
通用寄存器
浮点寄存器
包含CPU状态信息的处理器控制寄存器(处理器状态字,Process Status Word)
用来跟踪进程对RAM访问的内存管理寄存器
恢复执行
内核用进程描述符中合适的字段来装载CPU寄存器。因为程序计数器中所存的值指向下一条将要执行的指令,所以进程从它停止的地方恢复执行。
不在CPU上执行
进程正等待某一事件,UNIX内核可以区分很多等待状态,这些等待状态通常由进程描述符队列实现,每个(可能为空)的队列对应一组等待特定事件的进程
可重入内核
所有内核都是可重入的(reentrant),这意味着若干个进程可以同时在内核态下执行。当然,单处理器系统上只有一个进程在真正的运行,但是有许多进程可能在等待CPU或某一I/O操作完成时在内核态下被阻塞
提供可重入的一种方式是编写函数,以便这些函数只能修改全局变量,而不能改变局部数据结构,这样的函数叫可重入函数。但是可重入内核不仅仅局限于可重入函数(尽管一些实时内核就是这样实现的)。相反,可重入内核可以包含非可重入函数,并且利用锁机制保证一次只有一个进程执行一个非重入函数
硬件中断发生,可重入内核能挂起当前正在执行的进程,即使这个进程处于内核态。
内核控制路径(kernel control path)表示内核处理系统调用、异常或中断所执行的指令序列
最简单的情况下,CPU从第一台指令到最后一条指令顺序地执行内核控制路径。然而,当以下事件发生时,CPU 交错的执行内核控制路径
运行在用户态下的进程调用一个系统调用,而相应的内核控制路径证实这个请求无法立即得到满足;然后,内核控制路径调用调度程序选择一个新的进程投入运行,结果,进程切换发生,第一个内核控制路径还没完成,而CPU又重新开始执行其他的内核控制路径,在这种情况下,两条控制路径代表两个不同的进程在执行
当运行一个内核控制路径时,CPU检测到一个异常(例如,访问一个不在RAM中的页)第一个控制路径被挂起,而CPU开始执行合适的过程,当这个过程结束时,第一个控制路径可以恢复执行。这种情况下,两个控制路径代表同一个进程在执行
当CPU 正在运行一个启用了中断的内核控制路径时,一个硬件中断发生,第一个内核控制路径还没执行玩,CPU开始执行另一个内核控制路径来处理这个中断。当这个中断处理程序终止时,第一个内核控制路径恢复。在这种情况下,两个内核控制路径运行在同一进程的可执行上下文中,所花费的系统CPU时间都算给这个进程。然而,中断处理程序无需代表这个进程运行。
在支持抢占式调度的内核中,CPU正在运行,而一个更高优先级的进程加入就绪队列,则中断发生,这种情况下,第一个内核控制路径还没有执行完,CPU代表高优先级进程又开始执行另一个内核控制路径。只有把内核编译成支持抢占式调度之后,才可能出现这个情况
例子
考虑三种不同的CPU状态: ·用户态下运行一个进程(User) ·运行一个异常处理程序或者系统调用处理程序(Excp) ·运行一个中断处理程序(Intr)
进程地址空间
每个进程运行在它的私有地址空间
用户态
私有栈
数据区
代码区
内核态
内核的数据区
内核的代码区
另外的私有栈
几个内核控制路径(每个都与不同进程相关)可以轮流执行,每个内核控制路径都引用它自己的私有栈
有时进程共享部分地址空间
一些情况下,这种共享由进程显示的提出
另一些情况下,由内核自动完成共享以节约内存
同一程序由几个用户同时使用,则这个程序只能被装入内存一次,其指令由所有需要它的用户共享,数据不被共享,因为每个用户拥有独立的数据,这样的共享内存空间由系统自动完成
进程间也能共享部分地址空间,以实现一种进程间通信,这就是由System V 引入并且已经被Linux支持的“共享内存”技术
Linux支持mmap()系统调用,该系统调用允许放在块设备上的文件或信息的一部分映射到进程的部分地址空间。内存映射为正常的读写传送数据方式提供了另一种选择。如果同一文件由几个进程共享,那么共享它的每个进程地址空间都包含有它的内存映射
同步和临界区
概述
同步机制
如果内核控制路径对某个内核数据结构进行操作时被挂起,那么其他的控制内核路径就不应当再对该数据结构进行操作,除非它已被设置成一致性(consistent)状态,否则两个控制路径的交互作用将破坏所存储的信息
竞争条件
当某个计算结果取决于如何调度两个或多个进程时,相关代码就是不正确的,我们说存在一种竞争条件(race condition)
问题
对全局变量的安全访问通过原子操作(atomic operation)来保证,然而,内核包含的很多数据结构是无法用单一操作来访问的,例如,用单一的操作从链表中删除一个元素是不可能的,因为内核一次至少要访问两个指针。临界区(critical region)是这样一段代码,进入这段代码的进程必须完成,之后另一个进程才能进入
非抢占式内核
大多数传统UNIX内核都是非抢占式的
在进程在内核态执行时,它不能被任意挂起,也不能被另一个进程代替,因此,在单处理器系统上,中断或异常处理程序不能修改的所有内核数据结构,内核对它们的访问都是安全的
自愿放弃CPU
内核态进程自愿放弃CPU必须确保所有数据结构都处于一致性状态,恢复执行时,也必须重新检查以前访问过的数据结构的值
支持抢占的内核
应用同步机制时,确保进入临界区前禁止抢占,退出临界区时启用抢占
非抢占能力
低效,因为运行在不同CPU上的两个内核控制路径本可以并发的访问相同的数据结构
禁止中断
另一种同步机制
在进入一个临界区之前禁止所有的硬件中断,离开时再启用中断
评价
尽管简单,但远不是最佳的,如果临界区较大,那么在一个相对较长的时间内持续禁止中断就可能使所有的硬件活动处于冻结状态
信号量
广泛使用的机制
信号量(semaphore),它在单处理器系统和多处理器系统上都有效。
是什么
是一个数据结构相关的计数器,所有内核线程在识图访问这个数据结构之前,都要检查这个信号量,可以把每个信号量看成一个对象
组成
· 一个整数变量 · 一个等待的进程链表 · 两个原子方法down()和up()
down()
对信号量的值减1,如果这个新值小于0,该方法就把正在运行的进程加入到这个信号量链表,然后阻塞该进程(即调用调度程序)
up()
对信号量的值加1,如果这个新值大于或等于0,则激活这个信号量链表中的一个或多个进程
内核控制路径访问要保护的数据结构
要保护的数据结构拥有的信号量初始值为1
希望访问时,在相应信号量上执行down()方法,如果信号量当前值非负,则允许访问这个数据结构,否则,把执行内核控制路径的进程加入到这个信号量的阻塞链表并阻塞该进程。当另一个进程在那个信号量上执行up()方法时,允许信号量链表上的一个进程继续执行
自旋锁
多处理器系统使用信号量的问题
信号量并不总是解决同步问题的最佳方案,系统不允许在不同CPU上运行的内核控制路径同时访问某些内核数据结构,在这种情况下,如果修改数据结构所需时间短,那么信号量很可能是低效的,为检查信号量,内核必须把进程插入到信号量链表中,然后挂起它,因为这两种操作比较费时,完成这些操作时,其他的内核控制路径很可能已经释放了信号量
自旋锁
多处理器操作系统使用自旋锁(spin lock),其与信号量非常相似,但没有进程链表,当一个进程发现锁被另一个进程锁着时,它就不停“旋转”,执行一个紧凑的循环指令直到锁打开
当然自旋锁在单处理器环境下是无效的。当内核控制路径试图访问一个 上锁的数据结构时,它开始无休止的循环,因此,内核控制路径可能因为正在修改受保护的数据结构而没有机会继续执行,也没有机会释放这个自旋锁。最后的结果可能是系统挂起
避免死锁
死锁
与其他控制路径同步的进程或内核控制路径很容易进入死锁(deadlock)状态
死锁情形会导致受影响进程或内核控制路径完全处于冻结状态
避免方式
当所用信号量数量较多时,死锁就成为一个突出问题,在这种情况下,很难保证内核路径在各种可能方式下的交错执行不出现死锁状态,有几种操作系统(包括Linux)通过按规定的顺序请求信号量来避免死锁
信号和进程间通信
概述
UNIX信号(signal)提供了把系统时间报告给进程的一种机制,每种时间都有自己的信号编号,通常用一个符号常量来表示
信号编号举例
STGTERM
异步通告
例如,当用户在终端按下中断键(通常为CTRL+C)时,即向前台进程发出中断信号SIGINT
同步错误或异常
例如,当进程访问内存非法地址时,内核向这个进程发送一个SIGSEGV信号
POSIX标准
定义信号数量
大约20种不同的信号
两种用户自定义
可当作用户态下进程通信和同步的原语机制
进程对收到信号作出反映的方式
忽略该信号
异步地执行一个指定的过程(信号处理程序)
如果进程不指定选择何种方式
内核就根据信号编号执行一个默认操作
终止进程
将执行上下文和进程地址空间的内容写入一个文件(核心转储,core dump),并终止进程
忽略信号
挂起进程
如果进程被暂停,则恢复它的执行
因为POSIX语义允许进程暂时阻塞信号,因此内核信号的处理相当精细,此外,SIGKILL和SIGSTOP信号不能直接由进程处理,也不能由进程忽略
AT&T的Unix System V
引入了用户态下其他种类的进程间通信机制,很多Unix内核也采用这些机制
信号量
消息队列
共享内存
统称为System V IPC
内核把它们作为UPC资源来实现
进程要获得一个资源,可以调用
shmget()
semget()
msgget()
与文件一样,IPC资源是持久不变的,进程创建者、进程拥有者或超级用户进程必须显式地释放这些资源
消息队列允许进程利用msgsnd()及msgget()系统调用交换消息,msgsnd()表示把消息插入指定的队列中。msgget()表示从队列中提取消息
IEEE Std 1003.1-2001
定义了一种基于消息队列的IPC机制
即所谓的POSIX消息队列
提供了一个更简单的基于文件的接口
共享内存
进程间交换和共享数据最快的方式
通过调用shmget()系统调用来创建一个新的,大小按需设置
获得IPC资源标识符后,进程调用shmat()系统调用,其返回值是进程的地址空间中新区域的其实地址
希望把共享内存从其地址空间中分离出去时,调用shmdt()系统调用
实现依赖于内核对进程地址空间的实现
进程管理
概述
UNIX在进程和它正在执行的程序之间作出了一个清晰的划分
fork()和_exit()系统调用分别用来创建一个新进程和终止一个进程
调用fork()的进程是父进程,而新进程是它的子进程
父子进程之间能互相找到对方,因为描述每个进程的数据结构都包含有两个指针
一个直接指向它的父进程
另一个直接指向它的子进程
实现fork()一种天真的方式就是将父进程的数据与代码都复制,并把这个拷贝赋予子进程,这会相当费时。
当前依赖硬件分页的内核采用写时复制(Copy-On-Write)技术
即把页复制延迟到最后一刻(也就是说,直到父或子进程需要写一个页时才复制这个页)
_exit()系统调用终止一个进程,内核对这个系统调用的处理是通过释放进程所拥有的资源并向父进程发送SIGCHLD信号(默认操作为忽略)来实现的
而调用exec()类系统调用则是装入一个新程序
当这样一个系统调用执行后,进程就在所装入程序的全新地址空间恢复运行
僵死进程(zombie process)
父进程查询子进程终止
wait4()系统调用允许进程等待,直到其中的一个子进程结束,它返回已终止子进程的进程标识符(Process ID,PID)
内核在执行wait4()系统调用时,检查子进程是否已终止,引入僵死进程的特殊状态是为了表示终止进程
父进程执行完wait4()系统调用之前,进程就一直停留在那种状态,系统调用处理程序从进程描述符字段中获取有关资源使用的一些数据
一旦得到数据,就可以释放进程描述符,当进程执行wait4()系统调用时如果没有子进程结束,内核就通常把该进程设为等待状态,直到子进程结束
很多内核也实现了waitpid()系统调用,它允许进程执行一个特殊的子进程,其他wait4()系统调用的变体也是相当通用的
父进程发出wait4()调用之前,让内核保存子进程的有关信息是一个良好的习惯,假设父进程终止而没有发出wait4()调用,这些信息占用了一些内存中非常有用的位置,而这些位置本来可以用来为活动着的进程提供服务
解决办法是使用一个名为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或SIGTTOU信号。在很多shell命令中,用内部命令bg和fg把一个进程组放在后台或前台
内存管理概述
虚拟内存
所有新近的UNIX系统提供的一种有用的抽象
作为一种罗技层,处于应用程序内存请求与硬件内存管理单元(Memory Management Unit,MMU)之间
用途和优点
若干进程可以并发执行
应用程序所需内存大于可用物理内存时也可以运行
程序只有部分代码装入内存时进程可以执行它
允许每个进程访问可用物理内存的子集
进程可以共享库函数或程序的一个单独内存映像
程序是可重定位的,也就是说,可以把程序放在物理内存的任何地方
程序员可以编写与机器无关的代码,因为他们不必关心有关物理内存的组织结构
虚拟内存子系统
主要成分是虚拟地址空间(virtual address space)的概念。
进程所用的一组内存地址不同于物理内存地址,当进程使用一个虚拟地址时,内核和MMU协同定位其在内存中的实际物理位置
页框
现代CPU包含能自动把虚拟地址转换为物理地址的硬件电路。为了达到这个目标,把可用的RAM划分成长度为4KB或8KB的页框(page frame),并且引入一组页表来指定虚拟地址与物理地址之间的对应关系。一块连续的虚拟地址请求可以通过分配一组非连续的物理地址页框而得到满足
待解决的问题
虚拟内存系统必须解决的一个主要问题就是内存碎片
理想情况下,只有空闲页框数太少时,内存请求才失败
但是我们通常要求内核使用内核上物理连续的内存区域,因此,即使有足够的可用内存,但它不能作为一个连续的大块使用时,内存请求也会失败
随机访问存储器(RAM)的使用
UNIX的RAM划分
一部分专用于存放内核映像(也就是内核代码和内核静态数据结构)
其余部分通常由虚拟内存系统来处理,用于以下方面
满足内核对缓冲区、描述符及其他动态内核数据结构的请求
满足进程对一般内存区的请求及对文件内存映射的请求
借助于高速缓存从磁盘及其他缓冲设备获得较好的性能
每种请求类型都是重要的,但另一方面,因为可用的RAM有限,所以必须在请求类型间作出平衡,尤其是当可用内存所剩无几的时候,因此,当可用内存达到临界阀值的时候,可以调用页框回收(page-frame-reclaiming)算法释放其他内存,对于哪些页框最适合回收的问题没有简单的答案,也没有理论支持,唯一可用的方法是开发经过仔细调节的经验算法
内核内存分配器
概述
内核内存分配器(Kernel Memery 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相比太慢了
通常影响系统性能的瓶颈
硬盘
解决策略
尽可能推迟写硬盘时间
因此
从硬盘读入内存的数据即使任何进程都不再使用它们,它们也继续留在内存中
前提
有好机会摆在面前
新进程请求从硬盘读或写数据,就是被撤销进程曾拥有的数据
当一个进程请求访问磁盘时,内核可以首先检查请求数据是否在缓存中,如果在(把这种情况叫做缓存命中),内核就可以为进程请求提供服务而不用访问磁盘。
sync() 系统调用
把所有“脏”的缓冲区(即缓冲区内容与对应磁盘块的内容不一样)写入磁盘来强制磁盘同步
为避免数据丢失,所有操作系统都会注意周期性的把脏缓冲区写回磁盘
设备驱动程序
功能
内核通过设备驱动程序(device driver)与 I/O 设备交互
构成
包含于内核,由控制一个或多个设备的数据结构和函数组成,这些设备包括硬盘、键盘、鼠标、监视器、网络接口以及连接到SCSI总线上的设备。
这样的方式的优点
可以把特定的设备代码封装在特定的模块中
厂商可以在不了解内核源代码而只知道接口规范的情况下,就能增加新设备
内核以统一的方式对待所有设备,并通过相同的接口访问这些设备
可以把设备驱动程序写成模块,并动态的把它们装进内核而不需要重启系统。不再需要时,也可以动态的卸下模块,以减少存储在RAM中的内核映像大小
接口
说明了驱动程序与内核其他部分及进程之间的接口
一些用户程序(P)希望操作硬件设备。这些程序就利用常用的、与文件相关的系统调用及在/dev 目录下能找到的设备文件向内核发出请求。实际上,设备文件是设备驱动程序接口中用户可见的部分,每个设备文件都有专门的设备驱动程序,它们由内核调用以执行对硬件设备的请求操作