第一章导论
第一步:程序是如何"诞生"的?
把高级语言变成机器语言:源代码 -> 编译器 -> 汇编器 -> 链接器 -> 可执行文件
编译器:翻译官
做什么 :高级语言源程序 (比如 main.c)翻译成汇编语言。
汇编器:组装工
做什么 :接收编译器输出的汇编代码,把汇编指令"组装"成 CPU 能直接执行的机器码 ,生成一个目标文件 (比如 main.o)
链接器:总装师
把所有"零件包"(目标文件)和"标准件库"(系统库)组装成一个完整的、可以出厂的"成品机器"(可执行文件)
解释器 :它和编译型语言(C/C++)不同。像 Python、JavaScript 这类语言,它们的"搬家"过程是动态的。解释器会一边读取源代码,一边翻译一边执行,不会事先生成一个完整的 .exe 文件。
第二步:程序是如何"搬家"运行的?(装入)
计算机上电时RAM是空的,无法直接执行一个程序。
一个程序包含若干个段(代码段 数据段 堆栈段)。
如果要在内存中运行一个程序,就必须从外存将其引入。
那么哪些段必须从外村引入呢?只读段、初始化读写段、零初始化段
而程序在外村的存储形式是EXE,EXE就是一张蓝图,描述外存中存有哪些程序,存在哪里(.head)
【如何搬运】
可执行文件:程序在外存上的储存方式,保存了程序的逻辑布局的描述。
程序的装入【实例化】:操作系统读取外 存上的可执行文件在内存中生成物理布局实例的过程
复杂程序结构
库的引入
链接器:总装师
把所有"零件包"(目标文件)和"标准件库"(系统库)组装成一个完整的、可以出厂的"成品机器"(可执行文件)
它是 一堆 .obj的大杂烩 。打包进一个大箱子(后缀通常是 .a 或 .lib)。
问题是太大了,而且静态库只是把文件堆在一起,内部的所有符号(变量名、函数名)都是裸露的。
部分链接 :
符号除去:代码以位置无关形式编译成---整个二进制映像,中间符号删去
符号混淆:瞎几把编
【多个程序在内存中的共存】
我们刚刚聊完了程序的"出生证明"(工具链)和"随身行李"(静态库),现在程序已经打包成了一个可执行文件,准备被操作系统"请"进内存里运行了。
当多个程序都想进内存时,该怎么安排它们的座位?
简单分区
怎么做?
硬性规定:在程序编译链接的时候,程序员就必须告诉编译器:"嘿,我这个程序是放在第2区的,你把所有地址都算好。
怎么防越界?------内存保护单元
硬件上引入**内存保护单元,**它手里有一张表,写着每个区域的权限。
致命缺陷:二进制"球"与碎片化
二进制"球"
厂商发给你的不是一个可以随意拼装的零件包(.OBJ),而是一个封死的黑盒子(.BIN或.EXE)这个黑盒子内部写死了:"我就要住在 0x1000!"
碎片化
程序A运行留下的那个坑大小是固定的。如果新来的程序C比这个坑大,就塞不进去。
虚拟地址空间
内存管理单元
程序A发出指令:"我要去虚拟地址 0x0000 取数据。"
MMU查表(页表),发现 0x0000 实际上对应的是物理地址 0x5000。
MMU自动把请求转给物理地址 0x5000
虚拟地址:应用程序认为它自己在访问的地址。也叫逻辑地址。
物理地址:CPU实际送出到内存总线 上的物理存储单元地址 。也叫实地址。
XX地址空间:由XX地址组成的地址集合。
内存管理的进化史------减少内存浪费
分段
程序由代码段、数据段、堆栈段组成,直接按"段"来分配内存
段式内存管理单元
段表:物理地址、段号、段权限。
地址转换 :程序运行时,CPU给出的不再是单纯的地址,而是"段号+段内偏移"
比如程序说:"我要去代码段(段号01)的第100个字节。"
查表找到段号01对应的物理地址,加上偏移量100即可。
分页
"段"太大,容易产生碎片,那我们把内存切成固定大小的"小块",叫页(通常是 4KB)
虚拟页 :虚拟地址空间被切成一页一页。
物理页框:物理内存也被切成同样大小的一页一页。
页式内存管理单元
它记录的是:你的第1号虚拟页,对应物理内存的第5号页框;你的第2号虚拟页,对应物理内存的第99号页框
页表:物理地址、页号、页权限。
地址转换 :
程序说:"我要访问虚拟地址 0x20001072。"
P-MMU把这个地址拆开:高位的 0x20001 是页号,低位的 0x072 是页内偏移。
查表发现:虚拟页 0x20001 对应物理页框 0x08689。
拼接起来:物理地址就是 0x08689 加上偏移 0x072。
【三、多个程序如何共享一个库】
解决"静态链接"的浪费------动态链接
链接器:总装师
把所有"零件包"(目标文件)和"标准件库"(系统库)组装成一个完整的、可以出厂的"成品机器"(可执行文件)
每个exe都要使用输出,那么每次都要在链接obj的时候引入一次库文件。
☆ 考过 问题:
-
磁盘浪费(重复存储) :
如果你电脑里装了10个软件,它们都用到了同一个"窗口界面库"。静态链接会让这10个软件各自带一份库的副本。本来 5MB 的库,现在占用了 50MB 的硬盘空间。
-
内存浪费(重复加载) :
如果你同时运行这10个软件,操作系统得把这10份库代码都加载进内存。明明是一样的代码,却在内存里占了10份位置,简直是暴殄天物。
-
更新困难 :
更改一个库,所有静态链接了它的软件都得重编译。
解决方案:动态链接( Windows 的 .dll 或 Linux 的 .so)
物理地址空间中仅包含库文件(的代码段)的---个副本。程序加载时才链接到程序中。
外存的库共享
可执行文件中不再包含库文件。这样,库文件就只要在外存中存在---份
内存的库共享
物理地址空间中仅包含库文件的---个副本。这样,即便库文件被多个虚拟地址空间中被使用,它也只会占用---份物理内存,减少了内存用量
如何判断动态/静态链接库?
可执行文件中是否包含它的内容。

新问题:
初代动态链接的困境------地址冲突
静态链接是在加载前就确定号位置了,通过虚拟地址映射可以解决。但是动态链接是加载的时候才被引入,代码应该放在那里?
重定位
"搬"到另一个空闲的地方
更严重的问题:无法共享
位置无关代码
代码段
代码段(.text)里所有的跳转和调用,都基于**指令指针(IPSPBP)**的相对偏移。操作系统可以把它加载到任何虚拟地址,而无需修改它。这样,多个程序就能完美共享同一份物理内存中的库代码了

数据段的困境------只读能共享,读写怎么办?
要读写的单独存。
Q:为什么动态链接要用位置无关代码?
由于动态链接库是在载入的时候才引入的代码,会产生与已有地址冲突的问题,为了解决地址冲突问题,重定位法无法实现代码共享,甚至退化为静态链接,因此要用位置无关代码,代码段所有的跳转调用都基于指针IPSPBP的相对偏移,这样操作系统就能将库加载到任何虚拟地址不冲突了。
库调用库
间接地址法
调用另一个库里的函数,不能直接把对方的地址写死在代码里(因为库加载的位置不确定)
因此不存实际地址(直接回填),而是在可读写段中存一个表,当要加载其他库的时候会查表获取引用库的实际地址,此时再回填。操作系统在加载程序时,会把调用库的位置填进你的小本子里。
太多了,加载太慢了
推迟链接(惰性绑定)
一开始先不填,等你真的要用那一行时,我再填进去。
只有静态链接库(.a 或 .lib),但我想要动态链接的效果(代码共享、按需加载),怎么办?
将静态链接器(Linker)做成一个库,静态链接到你的程序里。
运行程序先启动静态连接器,自动把缺少的库搬进来。

大思路:【这个东西解决了什么问题?这个东西还有什么问题?】
如何生成程序?
程序如何从外存载入内存?
如何在内存同时运行多个程序?(简单分区-虚拟地址空间)
如何消除内存碎片?(分段-分页-动态链接)
动态链接遇到的问题与解决方案:
静态链接库三大浪费-动态链接解决(内外存只存一份)
地址冲突-重定位-位置无关-读写段单独存一份
库调用库-间接地址法
间接地址太久了-惰性绑定用的时候再填
如果静态链接库想实现动态链接怎么办?把连接器做成库。
处理器调度原理与涉及
可执行文件和进程的区别:
- 操作系统:操作系统是管理计算机硬件的程序,为应用程序提供基础,充当硬件和用户的中介。用户视角:为了用户使用方便,提高资源利用率 系统视角:资源分配器
- OS三种基本类型:批处理系统、分时系统、实时系统
- 操作系统是控制程序,控制程序管理用户,程序的执行和防止错误和计算机的使用不当。
- 批处理系统:脱机输入系统,批量送入执行,自动运行作业表
优点:节省作业装入时间
缺点:CPU经常偷闲,人机交互性差 - 多道程序设计系统:同时在内存中驻留多个程序,当一个进程等待时,系统会自己切换到另一个进程执行。
优点:通过组织作业使CPU中总有一个作业可以执行,充分利用CPU。
缺点:引起作业调度,CPU调度和内存磁盘管理等问题 - 分时系统是多道程序设计系统的延伸,作业切换频率很高,用户可以在程序运行期间与之交互。
- (1)分时操作系统允许用户共享计算机
(2)采用CPU调度和多道程序设计以提供用户分时计算机的一小部分
(3)在存储期中同时保存几个作业
(4)操作系统保证合理的响应时间
(5)提供文件系统 - 多道程序设计系统和分时是现代计算机操作系统的主题
- 双重模式操作:分为用户模式和内核模式。系统引导时,硬件开始处于内核模式。接着装入操作系统,开始在用户模式下运行。一旦出现陷阱或中断,硬件会从用户模式切换到内核模式。因此只要操作系统获得了对计算机的控制,它就处于内核模式。系统在讲控制交还给用户模式时切换到用户模式。
- 特权指令:将能引起机器损害的指令成为特权指令,硬件仅允许在监督程序模式下使用特权指令
- 系统调用:用户与操作系统进行交互,从而请求系统执行一些只有操作系统才能做到的指令,每个这样的请求都是由用户调用来执行特权指令的,这种请求成为系统调用。
- 硬件保护:
IO:为防止用户执行非法IO,可定义所有IO指令为特权指令
内存保护:通过基址寄存器和界限寄存器来确定程序所能访问的合法地址空间并保护其他内存空间
CPU空间:使用定时器来防止用户程序执行时间过长(作用:防止用户程序无限占用CPU)
- 操作系统结构
1 操作系统的服务:用户:用户界面、程序执行、IO操作、文件系统操作、通信、错误检测 系统:资源分配、统计、保护和安全
2.用户界面:命令行和图形界面
3.API(应用程序接口):一些列适用于程序员的函数 系统调用提供了操作系统提供的有效服务界面
4.区别:程序员调用的是API(API函数),然后通过与系统调用共同完成函数的功能,跟内核无直接关系。系统调用则不与程序员进行交互的,它根据API函数,通过一个软中断机制向内核提交请求,以获取内核服务的接口。
联系:一个API可能会需要一个或多个系统调用来完成特定功能。并不是所有的API函数都一一对应一个系统调用,有时,一个API函数会需要几个系统调用来共同完成函数的功能,甚至还有一些API函数不需要调用相应的系统调用(因此它所完成的不是内核提供的服务)。
5.系统调用类型:进程控制、文件管理、设备管理、信息维护、通信
6.系统程序:最底层是硬件,上面是操作系统,接着是系统程序,最后是应用程序。系统程序提供了一个方便的环境,以便开发程序和执行程序,其中一小部分只是系统调用的简单接口.(文件管理,状态信息,文件修改,程序语言支持,程序装入和执行,通信)
7.操作系统结构:
(1)简单结构:较小,简单且功能有限
(2)微内核:将所有非基本部分从内核移走,并将它们实现为系统程序和应用程序,剩余部分为微内核。
优点:便于扩充操作系统,具有更好的安全性和可靠性,容易从一硬件平台设计移植到另外一个 缺点:要忍受系统功能总开销的增加而导致系统性能下降
(3)分层方法:将操作系统模块化,分成若干层,每层建立在较低层次上,最底层为硬件最高层为用户接口。特点:采用模块化简化了操作系统的设计和实现,每层都是利用较低层次的功能实现。但是对层的仔细认证的定义比较困难,效率较差。
8.虚拟机:单个计算机的硬件抽象为几个不同的执行部件。有的系统程序可以很容易的被应用程序调用,虽然系统程序比其他字程序的层次要高,但是应用程序还是可以将他们的一切下层当成硬件的一部分看做一个整体,这种分成方法自然而然的逻辑延伸为虚拟机的概念。功能:提供与基本硬件相同的接口
9.策略与机制分离:机制如何做,策略做什么。策略可能会随时间或位置有所改变,在最坏情况下,每次策略改变都会造成机制的改变。系统更需要调用机制,这样策略的改变只需重定义一些系统参数。