433110基本分段存储管理方式
# 3.1_10_基本分段存储管理方式
各位同学大家好,在这个小节中我们会学习另一种离散分配的存储管理方式,叫基本分段存储管理。这种管理方式和咱们之前学习的分页存储最大的区别其实就是离散分配的时候所分配的地址空间的基本单位是不同
这个小节中,我们会首先介绍什么是分段。分段的概念思想其实有点类似于我们分页存储管理当中的分页,而之后我们会介绍什么是段表,段表就有点类似于分页存储管理当中的页表。另外在离散分配存储管理方式当中,咱们避免不了一定要谈的问题是怎么实现地址变化。
最后我们会对分段和分页这两种管理方式进行一个对比,我们会按照从上至下的顺序依次讲解,
# 分段
首先来看一下什么是分段。进程的地址空间会按照程序自身的逻辑关系划分为若干个段,比如说一个进程 a 它的大小是 16KB 那么按照它自身的逻辑关系,它有可能会被分为若干个段,每一个段就代表一个完整的逻辑模块,比如说 0 号段的段名叫 main,然后 0 号段存放的就是 main 函数相关的一些东西,然后一号段存放的是某一个子函数,二号段存放的是进程 a 当中某些局部变量等这些信息,
可以看到每一个段都会有一个段名,这个段名是程序员在编程的时候使用的,另外每个段的地址都是从 0 开始编制的,进程 a 本来是有 16k 的地址空间,分段之后 0 号段,它的空地址空间就是 0~7k-1,总共的大小就是 7KB,然后 1 号段是 0~3k-1,总共的大小是 3KB,2 号段也一样,
那操作系统在为用户进程分配内存空间的时候,是以段为单位进行分配的,每个段在内存当中会占据一些连续的内存空间,并且各段之间可以不相邻。比如说0 号段占据的是从 80k 这个地址开始的连续的 4KB 的内存空间,而一号段占据的是从 120k 这个地址开始,连续的 3KB 的地址空间。由于分段存储管理当中是按照逻辑功能来划分各个段的,所以用户编程会更加方便,并且程序的可读性会更高。
比如说用户可以用低级语言汇编语言写这样两条指令,第一条指令是把分段 d 当中的 a 单元内的值独到寄存器一中。第二个指令是把寄存器一当中的内容存到 x 分段当中的一单元当中。
由于各个分段是按逻辑功能模块来划分的,并且这些段名也是用户自己定义的,所以用户在读这个程序的时候就知道这两句代码做的事情就是把某个全局变量的值赋给 x 这个子函数当中的某一个变量。因此对于用户来说,采用了分段机制之后,程序的可读性还是很高的。
在用户编程的时候使用的是段名来操作各个段,但是在 CPU 具体执行的时候,其实使用的是段号这个参数,所以在编译程序其实会把这些段名转换成与他们各自相对应的这些一个的段号,然后 CPU 在执行这些指令的时候,是根据段号来区分各个段的,
在采用了分段机制之后,逻辑地址结构就变成了这个样子,由段号和段内地址或者叫段内偏移量组成,比如说像这个例子当中,段内地址是占了 0~15,总共 16 位,然后段号是 16~31,总共占了也是 16 位,在考试当中我们需要注意的一个很高频的考点。段号的位数决定了每个进程最多可以分多少个段,而段内地址的位数决定了每个段的最大长度是多少,我们以这个例子为例,来看一下 16 位的段号和 16 位的段内地址最大可以支持几个分段?每个段的最大长度又是多少?
我们假设这个系统是按字节编制的,也就是说一个地址对应的是一个字节的大小,那段号占 16 位,所以在这个系统当中,每个进程最多可以有 2 的 16 次方个段,也就是 64k 个段。因为 16 位的二进制数最多也就能表示这样一个范围的数字
同样的段内地址也是占 16 位,并且这个系统是按字节编制的,所以每个段的最大长度应该是二的 16 次方,也就是 64KB 这样的一个大小。
刚才我们提到的这两句用汇编语言写的指令,在经过编译程序编译之后,段名会被编译成对应的段号,而这里提到的 a 单元,b 单元这样的助记符会被编译程序翻译成段内地址,也就是第二个部分,就像这个样子,每个段名会被翻译成与它们对应的各个段号。另外各个段之间的这些用助记符表示的内存单元,会被最终翻译为这个段当中的段内地址,这就是分段相关的一些最基本的概念。
# 段表
接下来我们再来看下一个问题,既然我们的程序被分为了多个段,并且各个段是离散的存储在内存当中的,为了保证程序能够正常的运行,所以操作系统必须能够保证要能从物理内存当中找到各个逻辑段存放的位置,因此为了记录各个段的存放位置,操作系统会建立一张段映射表,简称段表,就像这个样子,用段表记录了各个逻辑段在内存当中的存放的位置。
这个地方大家会发现,段表的作用其实和咱们之前学习的页表的作用是比较类似的,页表是建立了各个逻辑页面到实际的物理页框之间的映射关系,而段表是记录了各个逻辑段到实际的物理内存存放位置之间的映射关系,每个段表由段号,段长和段基址组成,段基址其实就是段在内存当中的存放的起始位置。
从这个图当中我们也能很直观的看到,每个段会对应一个段表项,相比于页表来说,段表当中多了一个跟不同的信息就是段长,因为每个分段的长度可能是不一样的,而我们在分页存储管理当中,每个页面的长度肯定都是一样的,所以在分页存储管理当中,页场是不需要这样显示的记录的;但是在分段存储管理,当中段的长度是需要这样显示的,记录在段表当中。
第二点我们需要注意的是我们的各个段表项的长度其实是相同的,也就是说这些一行一行的段表项在内存当中所占的空间是大小是相同的,比如说这个系统按照字节选址,并且采用分段存储管理方式,逻辑地址结构,段内地址是 16 位,段的长度不可能超过 2 的 16 次方字节,所以在各个段表项当中,用 16 位就肯定可以表示这个段的最大段长了。假设这个系统的物理内存大小是 4GB,那也就是 2 的 32 次方个字节,那这么大的物理内存的地址空间,可以用 32 位的二进制来表示,所以对于基址也就是内存的某一个地址,这个数据我们只需要用 32 个二进制位就可以表示了。
因此每个段的段表项其实只需要 16+32 位,也就是 48 位,总共 6 个字节就可以表示 1 个段表项。因此在这个系统当中,操作系统可以规定每 1 个段表项的长度就是固定的 6 个字节,前 2 个字节表示的是段长,而后面 4 个字节表示的是这个段存放的在内存当中的起始地址,所以和页表类似,这个地方的页号可以是隐含的,页号并不占存储空间。
我们在查询段表的时候,只要我们能够知道断表在内存当中的起始地址 m,我们想要查询 k 号段对应的段表项,我们只需要用段表的起始地址 m,再加上 k 乘以每个段表项的大小 6 个字节,那就可以得到我们想要找到的那个段对应的段表项在内存当中的什么位置了。所以即使这个段号是隐含的没有显示的给出,但是我们依然可以根据段号来查询这个段表。
接下来我们再来看一下,采用了分段存储管理之后,地址变换的过程是什么样的?那还是以刚才提到的这个指令为例,这个用汇编语言写的指令,经过编译程序编译之后会形成一条等价的机器指令。比如说这条机器指令就是告诉 CPU,从段号为二,段内地址为 1024 的内存单元,当中取出内容放到寄存器一当中。不过在计算机硬件看来,段号,段内地址这些逻辑地址其实是用二进制表示的,比如说是这个样子,前面的红色的这 16 位表示的是段号,而后面的黑色的这 16 位表示的是段内地址,所以 CPU 在执行指令的时候,或者说在访问某一个逻辑地址的时候,需要把这个逻辑地址地址变换为物理地址。
# 怎么实现地址变化
我们看一下具体的变换过程,在内存的系统区当中存放着很多用于管理系统当中的软硬件资源的数据结构,包括进程控制块,PCB 也是存放在系统当中的。当一个进程要上处理机运行之前,进程切换相关的那些内核程序,会把进程的运行环境给恢复,这就包括一个很重要的硬件寄存器当中的数据的恢复:段表寄存器,用于存放进程,对应的段表在内存当中的起始地址,还有进程的段表长度到底是多少,因此段表存放的位置还有段表长度,这两个信息在进程没有上处理机运行的时候是存放在进程的 PCB 当中的。当进程上处理机运行的时候,这两个信息会被放到很快的段表寄存器当中。当知道了段表的起始地址之后,就可以知道段表是存放在内存当中的什么地方。
接下来进程运行的过程当中,避免不了要访问一些逻辑地址,比如说要访问逻辑地址 a,
那么系统会根据逻辑地址得到段号 s 和段内地址 w 这是第一步要做的事。
第二步,知道了段号之后,需要用段号和段表长度进行一个对比,来判段一下段号是否产生了越界。如果段号大于等于段表长度的话,就会产生越界中断,那么接下来就会由中断处理程序来负责处理中断,如果没有产生中断的话就会继续执行下去。这个地方稍微注意一下,段号是从 0 开始的,段表长度至少是 1,所以当 s 等于 m 的时候,其实也是会产生越界中断的
在确定这个段号是合法的,没有越界之后,就会根据段号还要段始址指来查询段表,找到这个段号对应的段表项。之前咱们提过,由于各个段表项的大小是相同的,所以用段表始址再加上段号乘以段表项的长度,就可以找到我们要找的目标段对应的段表项在内存当中的位置了。接下来就可以读出段表项的内容。
第四步在找到了这个段号对应的段表项之后,系统还会对逻辑地址当中的段内地址 w 进行一个检查,看看它是否已经超过了这个段的最大段长。如果段内地址大于等于这个段的段长的话,就会产生一个越界中断,否则继续执行。这一步也是和我们页式管理当中区别最大的一个步骤,因为在页市管理当中,每个页面的页长肯定是一样的,所以系统并不需要检查页内偏移量是否超过了页面的长度,但是在分段存储管理方式当中又不同各个段的长度不一样,所以一定需要对段内地址进行一个越界的检查,所以这一步是需要着重注意的,
我们继续往下,因为我们此时已经找到了目标段的段表项,所以我们就知道目标段存放在内存当中的什么地方。最后我们根据这个段的基址,也就是这个段在内存当中的起始地址,再加上最终要访问的段内地址就可以得到我们最终想要的物理地址了。
我们以之前提到的逻辑地址为例,进行一次完整的分析。如果说此时要访问的逻辑地址的段号是二,然后段内地址是 1024 的话,首先需要用段号二和段表长度 m 进行一个检查,显然此时进程的段表长度应该是三,因为它有三个段,所以段号是小于段表长度的,因此段号合法,所以就可以进行下一步。
用段号和段表始址查到这个段号对应的段表项,这样的话就找到了二号段对应的段表项,接下来需要对段内地址的合法性进行一个检查,段内地址和段长对比发现2 号段的段长是 6k,而段内地址是 1024,也就是 1k,所以段内地址是小于段长的,因此在这个地方并不会产生越界,中断可以继续执行下去。
接下来通过段表项我们知道了,这个段在内存当中存放的起始地址是 40k所以用这个段的起始地址 40k,再加上段内地址 w也就是 1024,这样的话我们就得到了最终想要访问的目标内存单元,也就是 a 变量存放的位置,这样的话就完成了对这个逻辑地址的一个访问。
分段存储管理当中的地址变换的过程,需要和分页存储管理的过程进行一个对比记忆。其实大家着重需要关注的分段和分页最大的区别就在于,在分页当中每个页面的长度是相同的,而分段当中每个段的长度是不同的,所以在分页管理当中并不需要对页内偏移量进行一个越界的检查,但是在分段管理当中,我们一定需要对段内地址也就是段内偏移量和段长进行一个对比检查,这就是分段和分页这两种存储管理方式当中,进行地址变换过程时候最大的一个区别。
# 分段和分页的对比
接下来我们再把分段和分页这两种管理方式进行一个统一的对比。
分页当中的页是信息的物理单位,而分段当中的段是信息的逻辑单位,
在分页的时候只考虑各个信息页面的物理大小,比如说每个页面是 4KB,但是在分段的时候必须考虑到信息的这些逻辑关系,比如说某一个具有完整逻辑功能的模块单独的划分成一个段,
另外分段的主要目的是为了实现离散分配,提高内存利用率,但是分段的主要目的是为了更好的满足用户需求,方便用户编程。
所以分页其实仅仅只是系统管理上的需要,它只是一个系统行为,对用户是不可见的,也就是说用户是并不知道自己的进程到底是分为了几个页面,甚至不知道自己的进程是不是被分页了,但相比之下分段对于用户是可见的,用户在编程的时候就需要显示的给出段名,所以用户其实是知道自己的程序会被分段,甚至知道会被分为几个段,每个段的段名是多少,
另外页的大小是固定的,并且这个页面的大小是由系统决定的,但是段的长度却不固定,取决于用户编写的程序到底是什么样一个结构。
从地址空间的角度来说,分页的用户进程地址空间是一维的,比如说一个用户进程的大小总共是 16k,那么在用户看来,它的整个进程的逻辑地址空间应该是从 0~16k-1,用户在编程的时候只需要用一个记忆符就可以表示一个地址,比如说用一个记忆符 a 来表示某个页面当中的某一个内存单元;但是如果系统采用的是分段存储管理的话,那么用户进程的地址空间是二维的,用户自己也知道自己的进程会被分为 0,1,2 这么几个段,并且每个段的逻辑地址都是从 0 开始的。所以在分段管理的这种系统当中,用户编程的时候既需要给出段名,也需要给出段内地址。比如说咱们之前提到的汇编语言指令,用户需要显示的给出段名,还有段内地址。
因此在分页管理当中,在用户自己看来,自己的进程的地址空间是连续的,但是在分段存储管理当中,用户自己也知道自己的进程地址空间是被分为了一个的段,并且每个段会占据一连串的连续的地址空间。
因此分页当中进程的地址空间是一维的,而分段的时候进程的地址空间是二维的,这个点在选择题当中还是很容易进行考察的
除了之前所说的那些不同之外,分段相比于分页来说,最大的一个优点应该是它更容易实现信息的共享和保护。比如说一个生产者进程总共是 16KB 这么大,那么它可能会被分为这样的三个段,其中一号段是是用来实现判段缓冲区此时是否可以访问这样一个功能。其实除了生产者进程之外,其他的生产者进程消费者进程,他们也需要判段缓冲区此时是否可以访问。因此这个段当中的代码应该允许各个生产者进程,消费者进程共享的访问,怎么实现共享的使用这个段呢?
假设我们的生产者进程它有一个这样的段表,它的一号段也就是,判段缓冲区的段是存放在内存的120k 这个地址开始的这个内存空间当中的。如果说消费者进程想要和他共享的使用一号段的话,那么很简单可以让消费者进程的某一个段表项同样是指向这个段存放的起始地址的。所以如果我们想要实现共享的话,那么我们只需要让各个进程的某一个段表项指向同一个段就可以了。这个地方需要注意的是只有纯代码或者叫可重入代码,也就是不能被修改的代码,可以被共享的访问,这种代码不属于临界资源,各个进程即使并发的访问这一系列的代码也不会因为并发产生问题。
比如说有一个代码段只是简单的输出 hello world 这么一个字符串,那么所有的进程并发的访问这个代码段那显然是不会出问题的,但是对于可修改的代码段来说是不可以共享的。比如说有一个代码段当中它比较复杂,有很多变量,那各个进程如果并发的同时访问的代码段的话,那么就有可能因为并发造成数据不一致的问题,因此对于代码来说只有纯代码,这种不属于临界资源的代码可以被共享的访问,这是在分段存储管理方式当中实现共享的一个很简单的方式。
接下来我们再来看一下为什么分页管理当中不方便实现这种信息的共享。假设我们把消费者进程进行分页的话,那么第一个页是 0 号段当中的前半部分的位置占 4KB,第二个页它会包含 0 号段当中的 3KB和 1 号段当中的 1KB,这两个总共组成了 4KB 的页面,类似的第三个页面也会包含一半一号段的内容,还有另一半是二号段的内容。
所以如果采用分页这种方式的话,那么我们如果让消费者进城的某个页表项也指向生产者进程的分页的话,那么显然是不合理的,因为生产者进程的分页当中,只有绿色部分是允许被消费者进程共享的,但是橙色部分不应该被消费者进程所共享,因此由于页面它并不是按照逻辑模块进行划分的,所以我们就很难实现共享,并不像分段那么方便。
其实对于信息的保护原理也是类似的,比如说在生产者进程当中,一号段应该是允许被其他进程访问的,我们只需要把这个段标记为允许其他进程访问,其他的那些段标记为不允许其他进程访问,这就很简单的就实现了对于各个段的保护。
但是如果采用分页管存储管理的话,一号页和二号页当中只有一部分,也就绿色这些部分是允许其他进程访问的,而其他的橙色和紫色的部分不应该允许被其他竞争访问,所以这样的话我们其实不太方便对各个页面进行标记,到底是否允许被其他进程访问,因此采用分页存储的时候,更不容易实现对信息的保护和共享这两个功能。这是关于信息的共享和保护,通过刚才的讲解相信不难理解,
接下来我们再来探讨我们在分段和分页这两种方式当中,访问一个逻辑地址需要几次访存。如果我们采用的是单级页表的分页存储管理的话,那么第一次访存应该是查询页内存当中的页表,第二次访存才是查询最终的目标内存单元,这个过程咱们在之前已经分析过很多次就不再展开,所以采用单级页表的分页存储管理总共需要两次访存,
如果采用分段的话,第一次访存是查询内存当中的段表,第二次访存是访问目标内存单元,所以采用分段的时候也是总共需要两次访存。在分页存储管理当中,我们知道我们可以引入快表机构来减少在进行地址转换的时候,访问内存的次数,所以其实在分段管理当中也类似,我们也可以引入快表机构,然后可以把近期访问过的段表项放到快表当中,这样的话只要快表能够命中,那么我们就不需要再到内存当中查询段表,我们就可以少一次访存
这就是分段和分页管理的一个对比。
# 小结
在学习了分页存储管理之后,小节的内容其实并不难理解,我们介绍了什么是分段,在分段存储管理当中逻辑地址结构是什么样的,另外我们介绍了和页表和类似的段表,只不过对于段表来说,大家需要着重注意的是,每个段表项当中一定会记录这个段的段长是多少,而在分页存储管理当中,每个页面的长度是不需要在页表当中记录的,因为各个页面的长度一样,而在分段存储当中各个段的长度是不一样的,所以这是他们俩之间的一个最明显的一个区别,
由于各个段的段长不一样,所以在地址变换的时候大家也需要注意,在找到了对应的段表项之后,还需要对段长和段内地址进行一个对比的检查,看一下段内地址是否越界,除了这个步骤之外,其他的那些步骤其实和页式管理当中地址变换的过程也是大同小异的。分段和分页的对比,这些知识点是很容易在选择题当中进行考察的,所以大家还是需要理解这些点。这个小结的内容还需要大家通过课后的集体再进行进一步的实践巩固,也需要能够根据题目当中给出的信息来手动的完成这个地址变换的过程。