Understanding linux kernel第一章
发现逐句翻译实在是太花时间,况且我的本意也不是出版一本书。改写成摘要吧。顺便解释一些专有名字,以防日后遗忘。
1. 介绍
Linux最初是在1991年被Linus Torvalds开发出来的,当时运行在试用了Intel 80386处理器的IBM个人电脑上。那年代能买得起这电脑,看来家里是不差钱的。
Linux是完全兼容Unix的,也就是说很多Unix程序可以直接搬到Linux上执行。
一般来说不同的Linux发行版会吧Linux内核代码放在/usr/src/linux, 此书之后的路径都在此目录内。存疑
1.1 Linux和其他“类Unix”内核比较
类Unix简而言之就是能跑Unix程序的
原书说了一堆屁话,总而言之,说了些Linux,或者说类Unix内核的“卖点”:
- 虚拟内存Virtual memory,简单来说,避免应用程序直接去写内存真实地址,还能征用磁盘空间冒充一下RAM
- 虚拟文件系统Virtual filesystem,VFS。当程序读写磁盘时,不用考虑文件系统是FAT还是NTFS还是exFAT等等。
- 轻量级进程Lightweight processes,我猜应该是fork吧!
- Unix信号signal,进程间通信,典型的例子有SIGSEGV、SIGBUS、SIGILL和SIGFPE
- 线程间通信SVR4 interprocess communications,IPC。例如管道pipe,消息队列message queue
- 支持SMP架构的cpu,应该常见个人cpu都是SMP的吧。(SMP Symmetric Multiprocessor对称多处理,即多核CPU中每个核地位一样,众生平等,与之相对的是多核CPU中有某个核是老大,他能干一些其他小弟不能干的事情)
之后举了些例子,和其他内核比较,太细致,目前来说没意义。我就简单附上他的特点吧,具体意思不解释了。
-
Linux是微内核而不是宏内核。
-
Linux可以动态地加载一些模块module,在写驱动的时候应用得很多,比如一个设备没有显卡,那它就可以不用加载显卡驱动模块了。
-
Kernel threading不是很理解原文,先不写了
-
Multithreaded application support,也不是很理解
-
抢占式内核preemptive kernel,貌似不是默认编译选项。
-
Multiprocessor support即之前说过的SMP支持
-
文件系统,即之前说过的虚拟文件系统特性
-
Streams,不知道原文说些什么
Linux还有些其他优点 -
免费
-
随意修改
-
对硬件性能要求不高
-
性能很屌
-
写linux的人都很屌
-
内核很小
-
兼容性好
-
支持好,意思是你有问题报告给他们开发小组,他们会迅速回应。这里的问题是指系统bug等问题,如果你不会用还是别问他们了。
原文说了一堆,总而言之就是linux好炸了。
1.2 硬件支持
写得没什么意义,总之支持很多cpu架构。
1.3 Linux版本
写了一堆内核版本号的规则,不是很有意思。
本书的内核是2.6.11
1.4 操作系统基本概念
-
内核既是操作系统。
-
内核有2个基本任务:
- 处理好硬件
- 给用户程序user program一个运行的环境
-
Linux内核不允许用户程序直接执行一些敏感操作(例如开关机,从硬件获取一些信息,比如tcpip的帧是从以太网获得,进一步说是从phy芯片获得),而是提交请求给内核,内核评估之后再决定是否执行。因此对于cpu来说有两种模式,user模式和kernel模式。因为在任意一瞬间,一个cpu的核心只能正在跑一个程序,要么user app,要么由于收到了user的请求正在跑kernel。这种机制也已经有了硬件的支持。
1.4.1 多用户
说简单点,允许一个操作系统同时被多个用户登录,而用户之间不会互相影响。
1.4.2 用户和组
用户是通过UID,User ID来区分的,是一串数字。通过组来分配资源。比如某个文件可以被某一群用户,也就是组,访问,而不在这组里的用户就不能。
用户里面的大哥叫superuser,他可以为所欲为。
1.4.3 进程Process
进程就是一个程序的运行实例。相当于打一个游戏多开,那每一个游戏窗口就是这个游戏程序的一个运行实例,就是进程。原书这里还黑了一下win98,说它不是multiuser操作系统。
还说到Linux是分时操作系统,每个进程在一段时间内都有平等的运行时间。
1.4.4 内核架构
就如之前所说,大多数Unix内核都是宏内核:所有的内核层都是和内核本身交织在一起,而且都是在内核态运行。说人话,宏内核大包大揽了很多本来可以不必它来完成的事情,如驱动,文件系统等等。而微内核只含有一些非常必要的功能,剩余的都剥离出去到user space,有需求的时候再来请求内核就是了。
微内核缺点就是理论上速度会有些慢,因为乱七八糟的操作都要请求内核,而cpu切换user态和kernel态是需要时间的。宏内核把很多活儿都放在内核态,切换的次数自然会少一些,而且同为内核态的进程互相交流效率也会比较高。但是就编程而言,微内核由于倾向于把不同功能的内核层分离,代码相互之间不依赖,会便于理解和开发。而且微内核对于RAM的使用也会更有效率。(强行洗白)
为了让不同内核程序分离,Linux用了module这玩意儿,module可以在kernel运行的时候加载或卸载。
1.5 Unix文件系统概述
Unix操作系统设计其实是围绕它的操作系统进行的,接下来看看主要特点,之后会用到。
1.5.1 文件Files
Unix文件用字节流构成的一个信息容器。(请告诉我哪种文件系统不是这样?)内核不会参与到如何解释一个文件的内容。Many programming libraries implement higher-level abstractions, such as records structured into fields and record addressing based on keys. However, the programs in these libraries must rely on system calls offered by the kernel. 不知道什么意思,他是在说数据库么?)从用户的视角去看,文件系统是一种树形数据结构。

这树里面所有非叶节点,都是文件夹。原文接下来说了一堆屁话。
Unix的进程都必须在某个directory中运行,简单来说,进程是一个程序的实例,这程序总得存在某个文件夹中吧,或者运行这个程序的命令行也总得停留在某个directory中吧。接下来原文解释了相对路径和绝对路径。哎,这书有时候把读者当成编程老司机,有时候又把读者当成电脑开机键也不知道在哪儿的小白。之后又解释了“.”和“. .”,无语……
1.5.2 硬连接和软连接hard and soft links
hard link和soft link就不翻译了,连接,链接?莫名其妙
一个directory中的文件名其本质就是一个hard link,或者就称之为link。同一个文件可能有很多link,这些link可以存在于文件系统中的所有位置。
ln p1 p2
这个命令就创建了新的hard link名字叫p2,指向到一个被p1指向的文件。
hard link有两个局限:
- directory是不能有hard link的,directory有了hard link导致文件系统这个树形结构变成图结构了,包含了环路,用于树的遍历的算法就不管用了。图的搜索太复杂,整个文件系统速度就慢了。
- hard link不能指向另一种文件系统的文件
为了克服这2个局限,就有了soft link,简单点说就类似于windows的快捷方式。soft link又称为symbolic link,反正都是s开头,因此用“-s”来作为参数创建。
ln -s p1 p2
原文竟然没有说hard/soft link区别。
- soft link可以指向directory
- soft link可以指向另一种文件系统的文件
- 如果被指向文件被删了,soft link就坏了,hard link还能用,而且被指向文件内容和被删前所保存的一致
- soft link可以和被指向文件的权限不一致,说到底soft link是一个文件,一个快捷方式,而hard link就是这个文件本身
1.5.3 文件类型
Unix有一下文件类型:
-
常规文件
-
文件夹directory,没错文件夹也是一种文件
-
symbolic link
(以上3种是传统意义上的文件,存在硬盘上的) -
块block
-
字符设备文件character
(以上2种是和IO设备有关的,对这种文件进行读写会直接送到内核,然后内核交给驱动去处理) -
管道pipe/命名管道named pipe/先进先出队列FIFO
-
套接字(呵呵哒什么鬼翻译)socket
(以上2种是给IPC,Inter process communication,进程间通信用的)
1.5.4 文件descriptor和Inode
Unix文件本身只是文件的二进制内容,其文件信息比如文件名存在一个称为inode的结构体中。Unix系统的inode至少包含一下几个属性:
- 文件/directory类型(-,d,l,b,c,s,p)
- 这个文件的hard link数量
- 文件字节数
- 设备ID,来表明这文件存在哪个设备上
- Inode number作为文件的身份标识
- 文件拥有者的User ID,UID
- Inode修改,文件上次被访问,文件上次修改,的时间戳
- 权限
1.5.5权限
没什么好说的
1.5.6 内核的文件处理,不知道怎么翻译file-handling system calls
当一个用户访问某个文件时,其实他是访问了磁盘上的数据,由于磁盘是属于硬件设备,因此需要内核来访问。于是内核需要提供几个systems call来让user space的程序去call,call完了就能访问到磁盘了。
下面介绍这几个system call。
1.5.6.1 打开文件
fd = open(path, flag, mode);
常用函数,不解释参数了。
1.5.6.2 访问文件
read,write,lseek
1.5.6.3 关闭文件
close
1.5.6.4 重命名/删除文件
rename,unlink
1.6 Unix内核概述
Unix为了运行于user mode的程序准备了一个运行环境,这个环境的目的在于在一般情况下不让user mode程序直接操作硬件。
1.6.1 Process/Kernel模型
就如之前所说,CPU有2种mode,user mode和kernel mode。事实上有些CPU拥有大于2种以上的mode。比如80x86微处理器有4种mode。但是所有以Unix为标准的内核只会用到2种。
当一个user mode的程序运行的时候,它不能直接访问内核的结构体,也不能使用内核程序。当一个程序以kernel mode运行的时候,这些限制就没有了。每种CPU都会在指令集中提供2种mode之间切换的指令。一个user mode的程序当在请求一个内核提供的服务时候,它就有可能进入倒kernel mode,当内核完成了请求,于是就会把这个程序放回到user mode。
一般来说一个线程的生命是有限的,这些线程的创建,销毁,以及同其他线程同步都是由内核的一组例程完成的。(is delegated to a group of routines in the kernel)
接下来我简单点总结一下,原文绕来绕去都说在说一些相同的话。
Unix中有些线程称之为kernel threads,有以下特点:
- 他们运行在kernel mode
- 他们不和用户交互,因此不需要终端。
- 他们通常随系统初始化的时候运行起来,知道系统关机。
1.6.2 Process线程
每个线程都有一个process descriptor来描述线程的状态。当kernel让一个线程停止运行,以下信息会保存到这个descriptor:
- program counter (PC),stack pointer (SP)
- general purpose registers
- floating point register
- processor control registers
- memory management registers
1.6.3 可重入性reentrant
可重入性就是可重新进入的意思,一个函数可重入的意思就是这个函数它只修改本地数据而不会改变全局变量。但是一个可重入的内核不止这样。找他原文的意思,可重入内核就是抢占式多任务处理?当一个硬件中断来的时候可以打断当前无论什么任务,中断处理完成之后再切回之前打断的任务?原文举了几个例子,抢占式和可重入的区别在于当一个中断发生时,如果这个中断触发了更高优先级的线程进入运行态,那抢占式内核会优先执行那个高优先级线程,完成了之后再切回原来被打断的线程。
1.6.4 线程的地址空间
每个线程都有自己私有的寻址空间,或者叫地址空间,address space。一个再user mode下的线程有自己的栈,数据,还有代码空间(也就是代码本身占用的空间)。在内核模式下运行时,进程会对内核数据和代码区域进行寻址,并使用另一个私有栈。(原文:When running in Kernel Mode, the process addresses the kernel data and code areas and uses another private stack. 这一句是机翻的!非常高的质量!deepl.com强力推荐)。
因为内核是可重入的,所以几个与不同进程相关的内核控制路径(control path)可以轮流执行。在这种情况下,每个内核控制路径(control path)只会在自己的私有内核栈中寻址。
有些情况下,线程之间可能会共享内存,有些情况下共享是由某个线程特地提出的,而有些情况下是内核来完成以减少内存消耗。
打比方说有一个编辑器程序,同时被好几个用户打开,此时,程序本身只会被载入一次,这个编辑器的指令也会被贡献。当然,它的数据(指的是运行时数据,比如每个用户打开的文件所占用的内存空间)是不会被分享的。这种内存共享就是由内核完成的。
上面所说的线程特地提出的分享内存需求,一般是用于线程间通信。
最后,说一下mmap这个system call(MMP??? ),它可让磁盘中的信息映射到某个线程的寻址空间中。这个函数提供了除read,write之外的另一种方法来处理数据。(这里略有疑问,这种mapping是同步的?还是异步的?多线程之间的共享问题怎么办?)
1.6.5同步和critical regions(临界局域)
一个可重入内核肯定需要一个同步机制。如果一个内核控制路径被挂起,此时它正在处理的内核数据,不允许任何其他内核控制路径访问,直到之前被挂起的内核控制路径完成它的修改(显然这是不可接受的)。(内核控制路径kernel control path,这个翻译始终觉得很奇怪,查了一下意思是内核中回应system call,中断,或者异常的一系列指令,希望之后能有直观的解释)如果不这么做,也就是说那个被挂起的内核控制路径挂起的时候,另一个修改了它所接触的数据,然后等那个被挂起的醒来,它会发现它之前接触的数据变得和它认为的不一样。因此所有这种会引起同步问题的代码都称为临界区域。
要解决这个非同步问题,最简单的办法就是让对数据的操作都是原子操作。但是在内核中有很多复杂的数据结构,不可能都是原子操作。于是有了以下这些措施。
1.6.5.1 关闭kernel的抢占性
有些Unix内核对数据同步问题有着很暴力的措施,其中之一就是关闭kernel的抢占性,也就是说一但某一线程进入内核态,作为执行为了满足“这个线程对内核提出的要求”的内核控制路径,此控制路径不可被其他控制路径挂起。因此,在这种系统上,如果cpu又是单核的,那些不会被中断,异常修改的内核数据,都是不用考虑同步性问题的。
当然在这种系统中,一个线程还可以主动让出CPU,比如它要等一个花时间的操作的结果。在他自己把自己挂起之前,它还是得保证所有数据已经写入,并且在等他重新运行的时候,需要检查这个数据是不是已经被修改了。
这种打开和关闭kernel的抢占性可以活用到:仅仅在进入临界区域之前关闭抢占性,然后在离开临界区域之后重新打开。但是不管怎么说,对于多核CPU来说,都是没用的,因为多核CPU在某一瞬间可能同时有2个控制路径都在运行。
1.6.5.2 关闭中断
原文说了一堆,并没什么卵用。总之关闭中断百害而无一利。
1.6.5.3 信号量semaphore
这是一个很常用的机制,无论是单核还是多核系统都很依赖这种机制。简单来说一个semaphore就是一个计数器加上一个数据;所有想要访问其数据的进程都要对这个semphore检查。每个semaphore可以被看成一个对象,它有以下属性:
- 一个整型
- 一个线程等待队列
- 2个原子方法down和up
down这个方法会让semaphore的计数器减一,如果减了一之后变成负数了,down这个方法就会在等待队列加入调用down方法的线程(谁去做了down谁就会被加进去),并且会被阻塞(挂起)。相反,如果up被执行一次,就会让计数器加一,并且如果计数器加了一之后变成正数或零,就从线程队列中挑一个出来继续执行。
对于每个想要保护的数据,都得分别建立他们自己的semaphore,计数器初始化为一。当一个控制路径想要访问数据,它(这个控制路径)就要执行一次down,如果semaphore由于它执行了down而没有变成负数,那就给这个控制路径它想要的数据,否则就把这个控制路径挂起,并将它加入到这个semaphore的等待队列中。直到有另外一个线程执行了up,再从队列中取出它,给它它想要的数据让它继续执行。
1.6.5.4 自旋锁spin lock神翻译
在多核系统中,semaphore并非永远是最优解决方案。多核系统中,我们需要从不同核上运行的线程手中保护数据。如果对于受保护的数据的操作十分简短,那semaphore将变得十分没有效率。如果CPU其中一个核心执行检查semaphore,发现需要挂起相应线程,在它还没有做完这些事情的时候可能另一个核心已经释放了semaphore。这都是因为把线程加入队列然后挂起很费时间。
在这种情况下就有了自旋锁。它和semaphore差不多,除了它没有等待队列,当一个线程发现自旋锁暂时无法解开,没有挂起,它就自己执行一些等待指令(其实就是循环),直到这个自旋锁可以解开。
当然,在单核系统中自旋锁毫无卵用。由于spin lock没有释放cpu,如果cpu单核,那它就等到死吧,因为没有其他线程有机会给这个锁+1。( 此时我就有疑问了,说好的时间片执行呢? )
1.6.5.5 避免死锁
举个简单例子,有p1 p2两个线程,a b是两个数据,p1先拿a再拿b,p2先拿b再拿a,于是当p1拿了a等b,此时p2拿了b等a,那就喜闻乐见了。
但是当这种锁用high了以后,很难避免死锁。所以,有些操作系统,包括linux,通过以预定义的顺序请求锁来避免这个问题。(???什么预定义的顺序请求??? )
1.6.6 信号和线程间通信
Unix通过信号signal对线程提供了一种通知系统消息的机制。每个系统事件都有一个编号,这些编号又通过一串文字替代方便人们使用,比如SIGTERM是15。系统事件分为以下2种:
-
异步事件通知:比如在命令行环境中,用户可以按ctrl+c来给当前运行的线程发一个SIGINT
-
同步事件通知:当一个线程访问了非法地址(
这个“非法地址”翻译得很迷,非法?非什么法?刑法?张三警告?其实就是访问了个不存在的内存地址,比如内存只有1M它访问了0xFFFFFFFF)内核会立刻发SIGSEGV给它。
POSIX定义了大概20种signal(黑人问号,到底几种? ),其中2种是可以用户自定义的,可以被用作系统原生的线程间通信和同步机制,注意是在user mode中。大致来说,一个线程在收到系统通知后有2条路: -
无视它(666)
-
整一个signal的handler来处理
-
(呵呵我咋觉得这说得放屁一样)
如果这个线程没有明说它要怎么样,那系统就会对每个系统事件做默认操作,默认操作可能是以下:
- 结束线程
- 把线程的上下文(context)还有寻址空间(也就是做一次core dump)卸载一个文件里,然后结束
- 忽略这个事件
- 挂起这个线程
- 继续运行这个线程(如果它之前是被挂起状态的话)
内核信号处理相当复杂,因为POSIX语义 允许进程暂时阻断信号。此外,SIGKILL和SIGSTOP信号不能被进程直接处理或忽略。
AT&T的Unix叫system v(v?是你吗 )引入了其他几种线程间通信,比如信号量semaphore,消息队列message queue,共享内存shared memory。他们就被称为system v ipc。
接下来原文说了这些玩意儿的最基本使用方法,我感觉现在没必要说网上一查一大堆。
1.6.7 线程管理
Unix对于线程,以及这个线程正在执行的程序(就是这个线程是由哪个程序实例化出来的)区分得很清楚。fork用来创建新线程,exit用来结束线程,exec用来载入一个新程序(不是很理解,没用过)。当用了exec之后内核会分配给这个线程新的地址空间,在这空间中已经加载了它要的程序。
当一个线程用了fork,所开的新线程就是原来线程的儿子(打拳警告? )。父子之间能够相认主要靠的是描述线程的结构体中有指针分别指了自己的爸爸和自己的儿子们。
原生的fork需要父亲给的数据和一个代码,用来给儿子,但是这比较花时间。现代的内核可以依赖硬件分页单元的内核遵循Copy-On-Write方式,将页面复制推迟到最后一刻(即直到爸爸或儿子需要写入页面)。之后会详细解说。
exit会结束一个线程,这是一个system call,系统收到这个system call之后就会结束这个线程,释放它的资源,并且发一个SIGCHLD信号给它爸爸,但是默认来说爸爸是忽略这个信号的。(儿子死了老子竟然毫无反应 )
1.6.7.1 僵尸进程
爸爸进程如果查询它儿子的死活呢?有一个system call叫wait4,可以用这个让当前线程等待,直到它儿子被终结,这个call会返回挂掉的儿子的PID(process ID)。
线程在被终结之后,会被维持在一个僵尸状态,而不是直接释放资源。这个僵尸状态会持续直到它爸爸接手了它最后的数据。如果爸爸执行了wait4而没有任一个儿子被终结,那内核会把爸爸放入等待状态。
很多内核也有waitpid,这个可以指定pid等待。还有些其他的wait4异型种就不在这儿讨论了。
那很多操作系统允许用户后台执行一个程序,然后登出之后这个线程还在继续。那当这个线程结束时候,由于之前执行它的命令行界面已经登出,那它就没爸爸了,没有人为它执行wait4,那岂不是资源永远释放不掉?其实当一个线程结束时候,内核会让那些孤儿线程认一个叫init的线程做爸爸,init这个线程实在系统初始化的时候就运行着的,是一个特殊的系统进程。这个进程会自动对它被终结的孩子们做wait4。
1.6.7.2 进程组和登录对话(session)
现代的unix系统引入了一个进程组的概念称之为job,比如执行命令
ls | sort | more
用bash举例,会为了这个命令创建一个job,里面有ls sort more三个进程。这样的话,shell会把这3个线程当做一个实体(这个实体就是job)来处理。每个线程descriptor中都有一个field叫线程组ID,process group ID。每个job中,可能会有一个小组长,小组长的PID就是job的process group ID。新创建出的线程最初总是被放在它父亲所在的job中。
现代的unix内核还引入了叫登录对话的玩意儿login session。大体来说,一个login session就是某人登入后运行的所有线程的爸爸,可能称之为老祖宗更确切些。在一个login session中,会有各种job,但总有一个job运行在前台,这个job占用了终端。其他的job都在后台,当某个后台的想要跑到前台来,它就会收到SIGTTIN或者SIGTTOUT,在很多shell中,又bg和fg来放置线程到后台或者前台。
1.6.8 内存管理
内存管理是迄今为止Unix内核里最复杂的部分。此书的三分之一都是说这个的。目前先大致说一下内存管理遇到的问题。
1.6.8.1 虚拟内存
所有的现代Unix都会提供一个称之为虚拟内存的抽象概念。虚拟内存本质上是一个逻辑层,此逻辑层运作在应用堆内存的请求和硬件的内存管理单元MMU之间。虚拟内存有以下目的和优点:
- 数个线程可以相互竞争地运行
- 允许应用得到比实际内存更大的内存空间
- 线程的代码不必一次性载入到内存中
- 每个线程都可以得到自己的一块物理内存
- 对于数个线程都需要的同一个库,或者程序,只需要为这个库或程序创建一份内存
- 程序可以在物理内存的任意位置载入
- 程序员不用针对不同的物理内存写不同的代码,因为虚拟内存对上层屏蔽了硬件差异
虚拟内存系统最主要的一个概念及时虚拟内存空间,程序访问到的虚拟内存地址和真实物理内存地址隔开。当一个线程对虚拟内存寻址,内核和MMU会协同合作来找出对应的真实物理内存地址。
现代的CPU都有专门的硬件来对虚拟内存地址做翻译。为此,RAM被分成以页page为单位,通常一页在4或8kb,以及一个Page table来记录每页对应的物理地址。
CPU还会尽量保证申请到的内存,在物理上是连续的。
1.6.8.2 RAM使用
所有Unix操作系统都会把RAM分为2块,一个几MB的来存内核,剩余的都交给虚拟内存系统,这些剩余的RAM大致有以下3中用途:
- 满足内核要的buffer descriptors还有内核中的dynamic allocation
- 满足线程需要的内存空间以及它要的文件
- 用作磁盘的cache
尽管每个用途都是有必要的,但是由于RAM不是无限量供应的,当内存不够的时候必须做出取舍。另外当内存剩余到达一个警戒位置时,一个释放内存的算法将会被调用。而且在之后的章节中可以看到,此问题(关于释放哪些内存的决定如何做出)没有一种完美解决方案。
另一个虚拟内存系统要解决的问题便是内存碎片。按照完美情况来说,如果要求的内存大小超过了剩余可以用的页的数量,那就应该不能满足这个请求。但是通常对内核的内存请求包含了一个附加条件,要连续的内存,这样,即便在剩余内存页够的情况下,会因为这些剩余的页不是连续页而导致这个请求还是不能满足。
1.6.8.3 内核内存分发
在内存中有个叫Kernel Memory Allocator KMA的玩意儿来分发内存,满足堆内存的请求。这些请求有的是从内核的其他模块来的,有些是用户空间的应用程序通过system call发来的。一个优秀的KMA应该满足一下特点:
- 快
- 省(不浪费内存)
- 尽量少的制造内存碎片
- 可以配合其他内存管理系统来从他们那儿借取、归还内存页
原书下面列举了些用不同算法的KMA,无所谓了,只需要知道linux用的叫Slab allocator。
1.6.8.4 线程的虚拟内存空间处理
线程的虚拟内存空间说这么深奥其实就是某个线程能访问到的所有虚拟内存的地址的集合。内核会把某个线程能访问的虚拟内存地址记下来,用memory area descriptor的表的方式记下来。当一个程序启动时候,内核就会给他由以下部分组成的虚拟地址空间:
- 程序本身的代码
- 经过初始化过的数据
- 未经初始化的数据
- 栈
- 这程序需要的library的代码
- 堆(给这个程序自己malloc用的)
现代的Unix都使用了一种称之为按需分页的技术。这种技术可以让一个程序启动的时候,他的内存页没有一页又对应的物理地址。当他访问某个页时,此时这个页还是不存在的,MMU才给它分配一页。(其实这技术是针对一个程序的堆的,当他执行malloc时,准确来说,是malloc中的brk时,内核才去给它找一个空白页)。
虚拟内存技术对于之前说过的copy-on-write也是至关重要的。比如,当一个创建好新线程时,内核只是把它爸爸的内存页给它,而且告诉它这页是只读的。当这个线程要去修改这个页的时候,才会去申请新的页,再把爸爸的数据拷贝到新的页,最后把这个页给这个线程。
1.6.8.5 caching
物理内存中的相当一部分都被用作硬盘或者其他读写很慢的设备的cache。其原理是从硬盘读到的数据在读这个数据的线程结束后并不会立刻释放,而是存在ram中,因为很有可能之后的线程还会用到,这样就不用再去从硬盘读取了。有一个system call叫sync,就是让所有还没有写入磁盘的数据写入磁盘。为了防止数据丢失,比如意外关机等,操作系统会定期统一把在cache中数据写入磁盘。
1.6.9 设备驱动
内核和IO设备的交互就只能通过驱动啦,这不是废话嘛。驱动是通过特定的内核interface与其互动,这样的好处
- 某类设备的驱动可以封装在一个module中
- 开发商不用知晓整个内核如何运行,而只需要知道怎么和interface交互
- 内核对所有设备一视同仁
- 可以动态加载和卸载

解释一下这图,P就是那些需要操作IO设备的程序,是user mode中的。他们通过访问/dev/目录下的文件的形式,来和IO互动。对这些文件的操作会导致触发这个驱动中的handler。
