程序的装入和链接 | ||
用户程序要在系统中运行,必须先将用户程序装入内存,然后再将装入内存的用户程序变为一个可以执行的程序; | ||
从用户源代码到可执行程序,通常要经过以下几个步骤: | ||
1 | 编译 ,编译程序(Compiler)对用户源程序进行编译,形成若干个目标模块(Object Module) | |
2 | 链接 ,链接程序(Linker)将编译后形成的一组目标模块以及这些目标模块所需的函数库(lib),链接在一起,形成一个完整的装入模块(Load Module) | |
3 | 装入 ,装入程序(Loader)将装入模块,装入内存 |
先介绍一个无需进行链接的单个目标模块的装入过程,该目标模块也就是装入模块;
将一个装入模块装入内存时,可以有以下三种装入方式:
1. 绝对装入方式(Absolute Loading Mode)
背景:当计算机系统较小,且仅能运行单道程序时,完全可以知道程序将驻留在内存的什么位置;此时可以采用绝对装入方式
绝对装入方式的含义:用户程序经过编译后,将产生绝对地址(物理地址)的目标模块
流程说明:事先已知用户程序(进程)驻留在从 R 处开始的位置,则编译程序所产生的目标模块(装入模块),便可从 R 处开始向上扩展;绝对装入程序便可按照装入模块中的地址,将程序和数据装入内存;装入模块被装入内存后,由于程序中的相对地址(逻辑地址)与实际内存地址完全相同,故不需要对程序和数据的地址进行修改
特点:程序中所使用的绝对地址既可在编译或汇编时给出,也可由程序员直接赋予;
缺点:由程序员直接给出绝对地址时,不仅要求程序员熟悉内存的使用情况,而且一旦程序或数据被修改后,可能要改变程序中的所有地址
改进措施:在程序中采用符号地址,然后在编译或汇编时,再将这些符号地址转换为绝对地址
2. 可重定位装入方式(Relocation Loading Mode)
绝对装入方式不足:绝对装入方式只能将目标模块装入到内存中事先指定的位置,这只适用于单道程序环境
新问题:在多道程序环境下,编译程序无法预知编译后所得到的目标模块应放在内存的何处
新问题措施:用户程序编译后所形成的若干个目标模块,起始地址通常都是从 0 开始的,程序(目标模块)中用到的所有地址也都是相对于所在目标模块的起始地址计算得到的;此时,不能再用绝对装入方式,而应采用可重定位装入方式,根据内存的具体情况将装入模块装入到内存的合适为止
可重定位装入方式的特点:可重定位装入程序将装入模块装入内存后,会使得装入模块中的所有逻辑地址与实际装入内存后的物理地址不同
举例说明:在用户程序的 1000 号单元处有一条指令 "LOAD 1,2500" ,该指令的功能是将 2500 单元中的整数 X 取至寄存器 1 ;但若将该用户程序装入到内存的 10000 ~ 15000 号单元而不进行地址变换,则在执行 11000 号单元中的指令时,它将仍从 2500 号单元中把数据取至寄存器 1 ,而导致数据错误 。正确的方法应该是,将取数指令中的地址 2500 修改成 12500 ,即把指令中的逻辑地址 2500 与本程序在内存中的起始地址 10000 相加,才得到正确的物理地址 12500 。除了数据地址应修改外,指令地址也须做同样的修改,即将指令的逻辑地址 1000 与起始地址 10000 相加,得到绝对地址 11000 。
通常,把在装入时对目标程序中指令地址和数据地址的修改过程,成为 "重定位" 。
因为地址变换通常是在进程装入时一次性完成的,以后不再改变,故称为 "静态重定位"
总结:
3. 动态运行时装入方式(Dynamic Run-time Loading)
可重定位装入方式优点:可将装入模块装入到内存中任何允许的位置,故可用于多道程序环境
可重定位装入方式缺点:不允许程序运行时在内存中移动位置
可重定位装入方式缺点说明:程序在内存中移动,意味着程序的物理位置发生了变化,这时必须对程序和数据的地址(绝对地址)进行修改后才能运行;实际情况是,在运行过程中,程序在内存中的位置经常会改变。例如,在具有对换功能的系统中,一个进程可能会被多次换出,又多次被换入,每次被换入后在内存中的位置通常是不同的。
解决可重定位装入方式缺点的措施:采用动态运行时装入方式
动态运行时装入方式说明:动态运行时的装入程序在把装入模块装入内存后,并不立即把装入模块中的逻辑地址转换为物理地址,而是把地址转换操作推迟到程序真正执行时才进行;因此,装入内存后的所有地址还是逻辑地址 。为了使地址转换不影响指令的执行速度,动态运行时装入方式需要一个重定位寄存器来支持地址转换
源程序经过编译后,可得到一组目标模块;链接程序的功能,就是将这组目标模块以及它们所需要的库函数装配成一个完整的装入模块;在对目标模块进行链接时,根据进行链接的时间不同,可把链接分类为以下三种:
1. 静态链接(Static Linking)方式
静态链接方式的含义:在程序运行之前,先将各目标模块以及这些目标模块各自所需的库函数链接成一个完整的装配模块,以后不再拆开;把这种事先进行链接的方式称为 "静态链接方式"
示例:
在经过编译之后得到三个目标模块 A 、B 、C ,它们的长度分别为 len_a ,len_b ,len_c ;
在模块 A 中有一条语句 CALL B 用于调用模块 B ;在模块 B 中有一条语句 CALL C 用于调用模块 C ;对于模块 A 来说,B 是外部调用符号;对于模块 B 来说,C 是外部调用符号;将这几个目标模块装配成一个装入模块时,必须解决以下两个问题:
(1). 对相对地址进行修改:
由编译程序所产生的所有目标模块中,使用的都是相对地址,每个目标模块的起始地址都是 0,每个目标模块中的地址都是相对于起始地址计算得到的;在链接成一整个装入模块后,原目标模块 B 和 C 在装入模块中的起始地址不再为 0 ,而分别是 Len_a 和 Len_a + Len_b ,所以必须修改目标模块 B 和 C 中所有的地址,即把原目标模块 B 中的所有地址都加上 Len_a ,把原目标模块 C 中的所有地址都加上 Len_a + Len_b
(2). 变换外部调用符号:
将每个模块中所用的外部调用符号也都变换为相对地址,如把 B 的起始地址变换为 Len_a ,把 C 的起始地址变换为 Len_a + Len_b
小结:这种先进行链接所形成的一个完整的装入模块,又称为 "可执行文件" ;通常都不再把它拆开,要运行时直接将该可执行文件装入内存
这种事先进行链接,以后不再拆开的链接方式,称为 "静态链接方式"
2. 装入时动态链接(Load-time Dynamic Linking)
这是指,将用户源程序编译后所得到的一组目标模块,在装入内存时,采用边装入边链接的链接方式 。
即在装入一个目标模块 A 时,若发生一个外部模块 B 调用事件,装入程序会去找相应的外部目标模块 B ,并将该外部目标模块 B 装入内存,还要按照静态链接方式中修改目标模块中的相对地址
装入时动态链接的优点
(1). 便于修改和更新
由静态链接方式装配在一起的装入模块,如果要修改或更新其中某个目标模块,则需要重新打开整个装入模块;这不仅效率低,有时候还无法实现
采用装入时动态链接方式,由于各目标模块是分开存放的,所以要修改或更新各目标模块则非常容易
(2). 便于实现对目标模块的共享
采用静态链接方式,每个应用模块都必须含有其目标模块的拷贝,无法实现对目标模块的共享;
采用装入时动态链接方式,OS 很容易将一个目标模块链接到多个应用模块上,实现多个应用程序对该目标模块的共享
3. 运行时动态链接(Run-time Dynamic Linking)
在许多情况下,应用程序在运行时,每次要运行的模块可能都不相同;由于事先无法知道本次要运行哪些目标模块,所以只能将所有可能要运行到的目标模块全都装入内存,并在装入时全部链接在一起;这种方式效率很低,有些目标模块根本不会被调用
运行时动态链接方式,是对装入时动态链接方式的改进
运行时动态链接:将对某些模块的链接推迟到程序执行时才进行;在执行过程中,当发现一个被调用的目标模块尚未装入内存时,OS 立即找到该目标模块,并将该目标模块装入内存,将其链接到调用者模块上(修改相对地址,变换调用符号);在执行过程中未被用到的目标模块,都不会被装入内存以及被链接到装入模块上;这样的链接方式不仅能加快程序的装入过程,而且能节省大量内存空间
为了能将用户程序装入内存,必须为用户程序分配一定大小的内存空间
连续分配方式,是最早出现的一种存储器分配方式;连续分配方式,为一个用户程序分配一个连续的内存空间,"连续" 即程序中代码或数据的逻辑地址相邻,也就是分配的内存空间的物理地址也是相邻的
连续分配方式可分为 4 种:单一连续分配 、固定分区分配 、动态分区分配 、动态可重定位分区分配
(1). 在单道程序环境下,当时的存储器管理方式是把内存分为 "系统区" 和 "用户区" 两部分
(2). 系统区内存尽提供给 OS 使用,系统区通常位于内存的低地址部分;在用户区内存中,仅装有一道用户程序,即整个内存的用户空间由该用户程序独占
(3). 这样的存储器分配方式,称为 "单一连续分配方式"
说明:
很多早期的单用户 、单任务操作系统都配置了存储器保护机构,用于防止用户程序对操作系统的破坏;但后来多款单用户操作系统都未采取存储器保护措施,这样既可以节省硬件,同时在单用户环境下,机器被独占,不可能被其他用户程序干扰;即使出现破坏行为,也是用户程序自己破坏操作系统,只会影响该用户程序自己的运行,而操作系统可通过系统重新启动而再次装入内存
背景
为了能在内存中载入多道程序,且使这些程序之间不会相互干扰,于是将 "整个用户空间" 划分为若干个固定大小的区域,在每个分区中只装入一道作业,从而形成了最早的 、最简单的一种可运行多道程序的分区式存储管理方式;
当有一个空闲分区时,便可以再从 "外存的后备作业队列" 中选择一个适当大小的作业,装入该空闲分区;当该作业结束时,可再从后备作业队列中找出另一合适作业调入该分区
1. 划分分区的方法
有下面两种方法,将内存的用户空间划分为若干个固定大小的分区:
(1). 分区大小相等(指,用户空间划分的分区,大小都一样)
缺点:
缺乏灵活性,当程序太小时会造成内存空间浪费;
当程序太大,一个分区又无法装入该程序,致使该程序无法运行
优点:
当利用一台计算机同时控制多个相同对象时,因为这些对象所需的内存空间大小相同,这种划分方式更方便 、实用
(2). 分区大小不等
为了增加存储器分配灵活性,故将用户空间划分为若干个大小不等的分区;
最好能调查常在该系统中运行的作业大小,根据用户需要来划分分区;
通常,把内存的用户空间分成多个较小的分区 、适量的中等分区 、少量的大分区;这样便可以根据程序大小,为程序分配适当的分区
2. 内存分配
(1). 为了便于内存分配,通常将分区按大小进行排队(分区大小不等)
(2). 为这些排队的分区,建立一张分区使用(状态)表,其中各表项的内容包括:每个分区的起始地址 、分区大小 、分区状态(是否已被分配)
(3). 当有用户程序要装入时,由 "内存分配程序" 根据用户程序的大小,来检索该 "分区使用表" ,从中找出一个能满足要求的 、尚未分配的分区 ,将该分区分配给该用户程序 ,然后将该分区对应的表项(分区状态)置为 "已分配" ;若没有找到大小足够的分区,则拒绝为该用户程序分配内存(分区)
说明:固定分区分配,是最早出现的 、可用于多道程序系统的存储管理方式;由于每个分区大小固定,必然造成存储空间浪费,因而现在已经很少用于通用的 OS ;但在某些用于控制多个相同对象的控制系统中,由于每个对象的控制程序大小相同,且程序事先已编写好,所需数据数量也是固定的,所以仍采用固定分区存储管理方式
动态分区分配,也称为 "可变分区分配" ,是根据进程的实际需要,动态地为进程分配内存空间;
实现动态分区分配时,涉及到分区分配中所用的数据结构 、分区分配算法 、分区的分配操作和回收操作,这三个问题
1. 动态分区分配中的数据结构
为了实现动态分区分配,系统中必须配置相应的数据结构,用来描述空闲分区 、已分配分区的情况,为分配提供依据
常用的数据结构有以下两种:
(1). 空闲分区表
在系统中设置一张空闲分区表,用于记录每个空闲分区的情况;
每个空闲分区的信息映射为表中的一个表项,表项中的字段包括:分区号 、分区大小 、分区起始地址等信息
(2). 空闲分区链
为了实现对空闲分区的分配和链接,在每个分区的起始部分设置一些用于控制分区分配的信息;
在分区头部设置用于链接各分区的前向指针,在分区尾部设置后向指针;通过前 、后向链接指针,可将所有的空闲分区链接成一个双向链表
为了检索方便,在分区尾部 "重复" 设置状态位和分区大小字段;当分区被分配出去后,把状态位从 "0" 改为 "1" ,此时,前 、后向指针将失去意义
2. 动态分区分配算法
为了把一个新作业装入内存(用户空间的某个分区),必须按照一定的分配算法,从空闲分区表或空闲分区链中选出一个分区,分配给该作业;
内存分配算法对系统性能影响很大,人们花了大力气研究内存分配算法,故而产生了许多动态分区分配算法;后面介绍的 4 种动态分区分配算法,都属于 "顺序式搜索算法" ;再后面介绍 3 种 "索引式搜索算法"
3. 分区分配操作
在动态分区存储管理方式中,主要的操作是分配内存 、回收内存
1). 分配内存
系统将利用某种分配算法,从空闲分区表(链)中找到所需(匹配)大小的分区;
设请求的分区大小为 u.size ,表中每个空闲分区的大小可表示为 m.size
若 m.size - u.size <= size(size 是事先规定的不再分割的剩余分区的大小),说明多余部分太小,可不再切割,故可将整个分区分配给请求者
否则(即 m.size - u.size > size 多余部分超过 size),便从该分区中按请求的大小,划出一块内存空间分配给用户,余下的分区内存仍留在空闲分区表(链)中
然后,将分配出来的内存块的首地址,返回给调用者
2). 回收内存
当进程运行完毕释放内存时,系统根据回收区(先前分配出去的内存块)的首地址,从空闲分区表(链)中找到相应的插入点,假设插入点的前一个空闲分区为 F1 ,插入点的后一个空闲分区为 F2 ,此时可能出现以下四种情况之一:
(1). 回收分区与插入点的前一个空闲分区 F1 相邻接;此时应将回收分区放置到插入点的前一分区的尾部,不必为回收分区分配新表项,而只需修改前一分区 F1 的大小
(2). 回收分区与插入点的后一个空闲分区 F2 相邻接;此时应将回收分区放置到插入点的后一分区的头部,形成新的空闲分区,用原回收分区的首地址作为新空闲分区的首地址,新分区大小为回收分区与 F2 之和
(3). 回收分区同时与插入点的前 、后两个分区邻接;此时将三个分区合并,使用 F1 的表项和 F1 的首地址,取消 F2 的表项,此时 F1 分区的大小为原 F1 分区、回收分区 、F2 分区,三者和
(4). 回收分区既不与 F1 邻接,也不与 F2 邻接;此时应为回收分区单独建立一个新表项,填写回收区的首地址和分区大小,并根据其首地址插入到空闲链中的适当位置
为了实现动态分区分配,通常是将系统中的 "空闲分区" 链成一个 "双链表"
所谓 "顺序搜索" ,是指依次搜索(遍历)空闲分区链上的空闲分区(结点),去寻找一个大小能满足进程需求的分区
基于顺序搜索的动态分区分配算法有如下四种:首次适应算法 、循环首次适应算法 、最佳适应算法 、最坏适应算法 (其实我觉得把算法称为 "策略" 更好)
1. 首次适应(First Fit,FF)算法
我们以空闲分区链为例,来说明采用 FF 算法时的分配情况
(1). FF 算法要求空闲分区链以地址递增的次序链接
(2). 在分配内存块时,(总是)从链首开始顺序查找,直至找到一个大小能满足要求的空闲分区为止
(3). 然后再按照作业的大小,从该空闲分区中划出一块内存空间,分配给请求者,该空闲分区中剩下的内存空间仍留在空闲链中
(4). 若遍历整个链表都没有找到一个能满足要求的分区,表明系统中没有足够大的内存分配给该进程,内存分配失败,返回
优点:
FF 算法优先利用内存中低地址部分的空闲分区,从而保留了高地址部分的大空闲区,这为之后到达的大作业,分配大的内存空间创造了条件
缺点:
低地址部分不断被划分,链中会留下许多难以利用的 、很小的空闲分区,这些还在链中的 、小的空闲分区,称为 "碎片" ;每次查找又都是从低地址开始的,增加了可用空闲分区的查找开销
2. 循环首次适应(Next Fit,NF)算法
改进:为了避免低地址部分留下许多很小的空闲分区(碎片),以及减少可用空闲分区的查找开销
循环首次适应算法的步骤:
循环首次适应算法在为进程分配内存空间时,不再是每次都是链首开始查找,而是从上次找到的空闲分区的下一个空闲分区开始查找,直至找到一个能满足进程要求的空闲分区,从该空闲分区中划出一块与请求大小相等的内存空间,分配给作业
算法适配:
应设置一个 "起始查寻指针" ,用于指示下一次起始查寻的空闲分区,并采用循环查找方式,即如果最后一个(链尾)空闲分区的大小仍不能满足要求,则应返回到第一个(链首)空闲分区,比较该空闲分区的大小是否满足要求;找到后,应调整起始查寻指针(的指向);没找到满足要求的空闲分区,则内存分配失败,返回
优点:
能使内存中的空闲分区分布得更加均匀,从而减少了空闲分区的查找开销
缺点:
空闲分区链中,从而缺少大的空闲分区
3. 最佳适应(Best Fit,BF)算法
所谓 "最佳" 是指,每次为作业分配内存时,总是把能满足要求 、又是最小的空闲分区分配给作业
为了加速寻找,该算法要求将所有的空闲分区,按其容量(大小)以从小到大的顺序形成一个空闲分区链;这样,第一次找到的能满足要求的空闲分区必然是最佳的
缺点
每次分配后所切割下来的剩余部分总是最小的,这样,在空闲分区链中会留下许多难以利用的 "碎片"
4. 最坏适应(Worst Fit,WF)算法
最坏适应分配算法选择空闲分区的策略,与最佳适应算法正好相反
最坏适应算法的步骤
扫描整个空闲分区链,总是挑选一个最大的空闲分区,从中分割一部分内存块给作业使用
缺点
空闲分区链中,缺乏大的可用的空闲分区,故称最坏适应算法
优点
可以使链表中剩下的空闲分区不至于太小,产生 "碎片" 的可能性最小,对中 、小作业有利
最坏适应算法的查找效率很高(算法要求,将所有的空闲分区,按其容量从大到小的顺序形成空闲分区链,查找时,只要看第一个分区能否满足作业要求即可)
背景:对换技术也称为交换技术,最早
为什么要引入对换技术? | 在多道程序环境下: (i). 一方面:内存中的某些进程由于某事件尚未发生而被阻塞运行,而这些进程占用了大量的内存空间,甚至有时可能出现内存中所有进程都被阻塞(没有进程正在运行),迫使 CPU 停下来等待 (ii). 另一方面:有许多作业因内存空间不足,一直驻留在外存上,无法进入内存运行 总结:上述情况的发生,对系统资源是一种严重的浪费,导致系统吞吐量下降 |
解决方法 | 为了解决上述问题,在系统中又增设了对换(交换)设施 |
"对换" 的概念 | 所谓 "对换" ,是指 (i). 把内存中暂时不能运行的进程或者暂时不使用的程序及数据,换出到外存上,以便腾出足够的内存空间 (ii). 再把已具备运行条件的进程或进程所需的程序及数据调入内存 |
"对换" 的优点 | "对换" 是改善内存利用率的有效措施,利用 "对换" ,可以直接提高处理机的利用率和系统的吞吐量 |
"对换" 的发展 | "对换" 技术自从 20 世纪 60 年代初期出现便引起了人们的重视 在早期的 UNIX 系统中就引入了对换功能,该功能一直保留至今,各个 UNIX 版本实现对换功能的方法,大体上是一样的; 即在系统中设置一个对换进程,由该对换进程将内存中暂时不能运行的进程调出到磁盘的对换区;同样由该对换进程将磁盘上已具备运行条件的进程调入内存 在 Windows OS 中也具有对换功能 如果一个新进程在装入内存时发现内存不足,可以将已在内存中的老进程换出至磁盘,腾出内存空间 由于对换技术的确能有效地改善内存利用率,故现在已被广泛应用在 OS 中 |
连续分配方式的缺点 | 连续分配方式会形成许多 "碎片" ,虽然可以通过 "紧凑" 方法将许多碎片拼接成可用的大块空间,但开销代价巨大 |
改进想法 | 如果允许将一个进程直接分散地装入到许多不相邻接的分区中,便可充分地利用内存空间,而无须再进行 "紧凑拼接" |
基于这一思想而产生了离散分配方式;根据在离散分配时所分配地址空间的基本单位的不同,又可将离散分配分为以下三种: | |
分页存储管理方式 | 在该方式中,将用户程序的地址空间分为若干个固定大小的区域,称为 "页" 或 "页面" ;典型的页面大小为 1 KB ; 相应地,也将内存空间分为若干个物理块或页框(frame), 页面和页框大小相同;这样可将用户程序的任一页面放入任一页框中,实现了离散分配 |
分段存储管理方式 | 这是为了满足用户要求而形成的一种存储管理方式;把用户程序的地址空间分为若干个大小不同的段,每段可定义一组相对完整的信息;在内存分配时,以段为单位,这些段在内存中可以不相邻接,所以也同样实现了离散分配 |
段页式存储管理方式 | 这是分页和分段两种存储管理方式相结合的产物;它同时具有两者的优点,是目前应用较广泛的一种存储管理方式. |