第1章 温故而知新
1.3 站得高,望得远:
计算机系统软件体系结构采用一种层的结构,有人说过一句名言:
“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”
“Any problem in computer science can be solved by another layer of indirection.”
这句话几乎概括了计算机系统软件体系结构的设计要点,整个体系结构从上到下都是按照严格的层次结构设计的。
1.4 操作系统做什么:
操作系统的一个功能是 提供抽象的接口,另一个主要功能是 管理硬件资源。
从硬件的角度来说,不允许应用程序直接操作硬件;
从应用程序的角度来说,不希望在开发应用程序时直接读写硬件端口、处理硬件中断等这些琐碎事情。
1.4.2 设备驱动:
因为PC的硬件多如牛毛,操作系统开发者不可能为每个硬件开发一个驱动程序,
这些驱动程序的开发工作通常由硬件厂商完成。
操作系统开发者为硬件生产厂商提供了一系列接口和框架,凡是按照这个接口和框架开发的驱动程序都可以在该操作系统上使用。
(这就是为什么每当你插上一个新鼠标,就会提示“正在安装驱动程序”的原因,鼠标的驱动程序是由硬件厂商开发好的)
1.5 内存不够怎么办:
程序直接使用内存的物理地址存在的问题:
- 地址空间不隔离
- 内存使用效率低
- 程序运行的地址不确定
解决这几个问题的思路就是使用我们前文提到过的法宝:增加中间层,即使用一种间接的地址访问方法。
1.5.2 分段:(Segmention)
最开始人们使用的是一种叫做“分段”(Segmention)的方法,基本思路是把一段与程序所需要的内存空间大小的虚拟空间映射到某个地址空间。
但分段的方法并没有解决第二个“内存使用效率低”的问题。如果内存不足,被换入换出到磁盘的都是整个程序,这样势必会造成大量的磁盘访问操作(程序从内存换出到磁盘,运行时再从磁盘换入到内存)。
事实上,根据程序的局部性原理,当一个程序在运行时,在某个时间段内,它只是频繁的用到了一小部分数据,也就是说,程序的很多数据其实在一个时间段内都是不会被用到的。
人们很自然的想到了更小粒度的内存分割和映射的方法,使得程序的局部性原理得到充分的利用,大大提高了内存的使用率。这种方法就是“分页”(Paging)。
1.5.3 分页
分页的基本方法是把地址空间人为的等分成固定大小的页,每一页的大小由硬件决定,或硬件支持多种大小的页,由操作系统选择决定页的大小。
比如Intel Pentium 系列处理器支持 4KB 或 4MB 的页大小,那么操作系统可以选择每页大小为4KB,也可以选择每页大小为4MB,但是在同一时刻只能选择一种大小,所以对整个系统来说,页就是固定大小的。
目前几乎所有的PC上的操作系统都是用 4 KB 大小的页。
虚拟地址空间 和 物理地址空间 都是一样的分法。
那么,当我们把进程的虚拟地址空间按页分割,把常用的数据和代码页装载到内存中,把不常用的代码和数据保存在磁盘里,当需要用到的时候再把它从磁盘里取出来即可。
页错误(Page Fault):
图1-6中 Process1 的 VP2 和 VP3 不在内存中,但是当进程需要用到这两个页的时候,硬件会捕获到这个消息,就是所谓的“页错误”(Page Fault),然后操作系统接管进程,负责将 VP2 和 VP3 从磁盘中读出来并装入到内存,然后将内存中的这两个页与 VP2 和 VP3 之间建立映射关系。
保护也是页映射的目的之一,简单的说就是每个也可以设置权限属性,谁可以修改,谁可以访问等,而只有操作系统有权限修改这些属性,那么操作系统就可以做到保护自己和保护进程。
虚拟存储的实现需要依靠硬件的支持,对不不同的CPU来说是不同的。但是几乎所有的硬件都采用一个叫 MMU(Memory Management Unit)的部件来进行页映射。
一般 MMU 都集成在CPU内部,不会以单独的部件存在。
1.6 众人拾柴火焰高:
1.6.1 线程基础:
在Linux下,用以下方法可以创建一个新的任务:
- fork:复制当前进程
- exec:使用新的可执行映像覆盖当前可执行映像
- clone:创建子进程并从指定位置开始执行
fork函数产生一个和当前进程完全一样的新进程。
fork函数产生新任务的速度非常快,因为fork并不复制原任务的内存空间,而是和原任务一起共享一个 “写时复制”(Copy On Write,COW) 的内存空间。
所谓“写时复制”,指的是两个任务可以同时*的读取内存,但任意一个任务试图对内存进行修改时,内存就会复制一份提供给修改方单独使用,以免影响到其他的任务使用。
fork只能够产生本任务的镜像,因此须要使用 exec 配合才能够启动别的新任务。
exec可以用新的可执行映像替换当前的可执行映像,因此在fork产生了一个新任务之后,新任务可以调用exec来执行新的可执行文件。
fork和exec通常用于产生新任务,而如果要产生新线程,则可以使用clone。