0
点赞
收藏
分享

微信扫一扫

从根上理解操作系统(三)

萨科潘 2022-05-02 阅读 73
linux

目录

内存管理

1、代码重定位

2、分段

 3、内存分区 

4、分页

 5、段页结合

参考:


内存管理

        程序的执行过程就是首先将程序放在内存中,PC指向开始地址,然后取指执行。

1、代码重定位

        我们在前面的文章中说过,分段是保护模式的基础,每个段都有CPL和DPL,只有当前段的CPL<=目标段的DPL,才可以访问,这正是保护模式对内核起保护作用的基础。但是用户态的两个进程,其CPL和DPL都是3,所以是理论上是可以互相访问的。所以为了互相隔离,互不影响,仅仅只有分段是不够的,还需要映射表。

        用户程序被编译之后,其地址是从0地址开始的,而且每个程序都是这样的,如果用编译之后生成的地址作为真实的物理地址,那就没多进程什么事了,因为不同进程之间的地址绝对会发生冲突。所以需要用映射表,编译生成的地址被称作逻辑地址,通过映射表映射出来一个地址被称为物理地址(也可以为线性地址,后续讲解)。这样,通过映射表,就可以把不同的程序放在内存不同的位置。

        那么,重定位发生在什么时候呢?编译时 or 载入时?

        编译时重定位只能放在内存中的固定位置,如果内存中对应位置已经使用了,那么新的程序就会对原有程序进行覆盖,从而造成异常,所以编译时重定位不可取。

        如果是载入期,假如有一段程序的逻辑地址是0-100,而物理内存中1000开始的的一段内存是空闲的,就可以被映射到物理地址从1000开始的内存地址,那么该段程序的真实地址就是1000-1100,另一个进程的程序从2000开始,这样的话,看起来是没问题的,不同的程序放在了不同的位置。但是这里,我们不得不介绍一个概念——内存交换,为了充分使用内存,进程使用的程序并不是从始至终放在内存中,而是被交换到磁盘上,等再需要执行该段代码时,会再从磁盘上读回内存,如下图所示。

         在进程对应的程序换出和换入之后,原来换出之前所在的内存位置不一定是空闲的,也就是说程序被换出再换入,物理地址大概率是会更改的,所以载入时地址重定位不合适。

        综上,重定位最适合的时机是运行时重定位。

         如上图所示,一段程序的逻辑地址在编译的时候就确定下来了,而真实的物理地址是在真实执行代码时计算出来的。也就是说先确定一个基地址base,逻辑地址作为偏移地址,那么基地址+偏移地址就是物理地址。如果程序被换出再换入,那么就会更新基地址,这个基地址就放在该进程对应的PCB中。这样的话,再取指执行时,通过新的基地址+偏移地址就可以获取物理地址了。

2、分段

        是将整个程序放在连续的内存中吗?

        程序是由若干个段组成的,每个段有自己的特点和用途,比如代码段只读、代码/数据段不能动态增长。假如代码和数据是在一起的,由于数据是可写的,那么如果有一点偏差就有可能把代码给修改了,实现起来非常费劲。所以把不同的内容放在不同的段中,更容易理解和实现。

         由于程序被分成了多个段,所以可以对多个段分别载入内存。这样也会有其他的好处,如上图,假设3是堆栈段,是可增长的,假如它增长到连续空间不够用了,就需要找更大的空闲空间进行存储,那么此时就涉及到了对内存的拷贝迁移。假如原来一个进程对应一块连续内存,那么整个程序都要进行拷贝,而且必须要有整个程序这么大的内存空间。如果是分段存储的话,只需要把堆栈段做拷贝迁移就行,其他段无需迁移,这样就大大减小的拷贝的时间和成本,提升了内存效率。

        由于一个程序被分段存储,所以上面说的一个基地址就不够了,需要每个段都有一个基地址,定位过程就需要<段号,段内偏移>。

         每一个段都有一个段号,所以每个进程都需要维护一个进程段表用于记录段号和每个段的基地址之间的映射,这个进程段表被称作LDT。如上图,假如CS=0,那么代码段的基地址就是180K,假如DS=1,那么数据段的基地址就是360K。

         在该系列第一篇文章中,我们提到了GDT表,GDT中存储的就是操作系统各个段的基地址等信息。由于LDT本身同样是一段内存,也是一个段,所以它也有个描述符描述它,这个描述符就存储在GDT中。

        如上图是GDT和LDT的关系图,我们看下寻址一个进程的一个段的一个代码指令的过程:

① 先从GDTR寄存器中获得GDT基址。

② 从LDTR寄存器中获取LDT所在段的位置索引(LDTR高13位)。

③ 以这个位置索引在GDT中得到LDT段描述符从而得到LDT段基址。

④ 用段选择符高13位位置索引值从LDT段中得到段描述符。

⑤ 段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址(程序给出)才得到最后的地址。

 3、内存分区 

        上面讲解了分段的概念,进行就是分成了若干个段,并把每个段放在了一段空闲的内存中。那么,怎么知道哪段内存是使用中的,哪段内存是空闲的呢?

        其实也是通过表记录的,从哪个地方开始,多长的内存空间是空闲或使用中的,记录在表中就可以了,如下图

         假如现在来了一个请求,需要内存100K,那么就从空闲分区割出来一段100K的空闲分区,这时,分区表就会跟着更新。

         假如seg2不再使用,需要释放内存,那么内存表就变成了下面这样。

        但是如果一直这样不断的申请内存资源和释放内存资源,会产生什么样的影响呢?这样,就会导致内存中存在越来越多的内存碎片,从而无法再被其他进程使用,导致资源浪费。 为了解决内存分区导致的这种内存效率低下的问题,就引出了内存分页的概念。这里的分区主要适用于虚拟内存。

4、分页

        为了避免上述说的分段导致的内存碎片的问题,就有了分页的概念。分页的概念就是将内存按4k大小分成多个页,针对每个段的内存请求,系统一页一页的分配给这个段。这样的话,一个段真实占用的物理内存无需连续,因此更加灵活多变。有人会问,这样就不会造成内存浪费了吗?当然会,因为分配内存是按照页为单位分配的,但是由于一页只有4K,所以一个段最多可浪费的内存空间小于4K。

        前面讲分段的时候,每个进程都有一个段表,现在的分页机制,每个进程也会有若干个页表。如上图,假设是某个段的页表,把段按4k进行分页,分出了4个页,每个页号对应一个页框号,这个页框号就是物理内存的页框号。其中,页表的寄存器是CR3。页号是怎么来的呢?其实就是逻辑地址除以4k得到的商,偏移是余数。这个计算的过程在一个叫做MMU的硬件实现的。

 5、段页结合

        内存分段和内存分页并不是对立的,它们是可以组合起来在同一个系统中使用的,那么组合起来后,通常称为段页式内存管理。在段页式存储中,段的概念主要面向于程序员,而页的概念主要是面向有物理内存。

 

        如上图,段页式存储的过程中,通过段表映射出的地址并非是物理内存地址,而是线性地址,或者称作虚拟地址,虚拟地址再通过页表的映射才真正获取到物理地址。 假如找代码段某一条指令,CS寄存器中存的是段号,IP寄存器中存的是偏移地址,通过段号查段表获取代码段的线性地址的基址,基址+偏移可以获取线性地址,线性地址的高位属于页号,根据页号查代码段对应的页表,可以映射出物理内存的页框号,根据物理页号和偏移量就可以获取真正的物理地址。

        用于段页式地址变换的数据结构是每一个程序一张段表,每个段又建立一张页表,段表中的地址是页表的起始地址,而页表中的地址则为某页的物理页号。

参考:

李治军老师《操作系统》

《linux内核完全剖析》

两张图看懂GDT、GDTR、LDT、LDTR的关系_citytwilight的博客-CSDN博客

真棒! 20 张图揭开内存管理的迷雾,瞬间豁然开朗-技术圈

操作系统笔记-7-虚拟内存 | IkanのBolg

举报

相关推荐

0 条评论