操作系统——内存使用与分段

目录

一、内存使用与程序重定位

1.从取值——执行到内存使用

2.程序重定位

二、分段

1.段的概念

2.分段机制下的地址转换 

一、内存使用与程序重定位

1.从取值——执行到内存使用

计算机运行的过程就是不断地取出并执行指令。指令是存放在内存中的,执行指令的过程中也需要用到内存。

《操作系统——内存使用与分段》

如图的例子中,一个C语言程序被编译为带地址的机器指令序列,这个序列放在磁盘上。将这段序列从磁盘读入到内存中,设置PC指针。将PC初始化为这段程序的入口地址,即图中的标号entry标定的地址,取出的第一条指令是“call 40”。

第一条指令将PC设置为40,那么去地址40处取出指令、执行指令。地址40处的指令应当是”mov 1, [300],”,如果的确是这条指令,那么内存就得到了正确地使用。

要想确保内存中地址40处为“mov 1, [300]”,那么就要保证该程序段是在内存的0位置处,这样PC=40的时候取出的就是这条指令。但我们知道,内存的0位置处已经在载入操作系统的时候被占用了,就算没有被占用,如果我们执行并发的程序,那么0位置处也不能同时放置两个程序。那么为了正确地取指令、执行指令,我们可以在内存中找一段空闲区域,比如从地址1000开始的一段内存,将可执行文件读入到从地址1000开始的这段物理内存区域,设置PC=1000开始执行,,那么当执行“call 40”时实际执行的指令是“call 1040”。这就是程序重定位

2.程序重定位

《操作系统——内存使用与分段》

在编译形成可执行程序时,用到的地址都是从0开始的相对地址,如图例子中的40,通常被称为逻辑地址。当程序载入物理内存中时,可能载入到任一空闲内存段,比如上例就载入到1000开始的物理内存处,为了使程序正确的执行,就需要进行程序重定位,即将程序中的逻辑地址对应到实际物理内存地址。

可以发现重定位的过程很简单,就是给偏移地址加上一个基值,比如给40加上基值1000,就变成了实际物理内存地址1040。关键在于什么时候进行重定位,如果在程序编译的时候进行重定位,即在编译产生可执行代码时,要将程序中出现的逻辑地址全部加上1000再写入可执行文件。显然编译时重定位的可执行程序只能载入到从1000开始处的物理内存,只有那些执行固定任务的计算机系统在编译时就预先已知将来的执行位置。

但对于我们日常使用的PC机来说,物理内存中的空闲空间是不定的,程序第一次执行时可能是在物理内存1000的位置,执行完毕退出后,第二次重启时可能在物理内存的2000位置,因此,在编译时进行重定位是行不通的。

第二种方法是在载入时重定位,就是在载入程序时,根据载入区域的地址来修改程序中的逻辑地址。如果程序被载入到物理内存为1000开始的物理内存区域,地址40就被修改为1000+40=1040。

这种重定位方法比编译时重定位要灵活很多,但当程序被载入后,就不可以改变在内存中的位置了,如果程序代码从1000挪到2000位置后,指令“call 1040”就失效了。

《操作系统——内存使用与分段》

进程在执行过程中,进程的切入和切出是很有必要的。如图(a),进程1在执行过程中出现了长时间的阻塞等待,如果进程1一直待在内存中,对内存资源的占用造成了很大的浪费,此时就需要将进程1换出到磁盘中,将进程3换入到原先进程1的位置。而当进程1又可以执行了,将其换入到内存中,但位置已经发生了改变,这种情况下该如何重定位?

这时就引出了第三种重定位方法——在程序执行时重定位。程序载入内存执行时,记录下程序载入处内存的基址,每当一条指令执行时,就将指令中的逻辑地址加上基址得到实际地址。

由于每条指令都需要进行如上操作,为了提高指令执行的效率,可设计硬件来快速完成这个过程,这就是著名的存储管理部件(MMU)

《操作系统——内存使用与分段》

在执行进程1时,PCB1中保存的基址1000赋给CPU基址寄存器,此时执行指令“call 40”,经过地址变换后即执行“call 1040”,正好跳转到进程1中main()函数执行。

《操作系统——内存使用与分段》

现在从进程1切换到进程2,首先要找到进程2的PCB2,根据其中的信息进行内核栈切换、用户栈切换、PC指针切换等。除此之外,我们还要进行内存地址的切换,就是重定位基址寄存器的切换。如图会将PCB2中的2000赋给重定位基址寄存器,那么执行“call 40”指令时实际执行的就是“call 2040”。

通过切换PC让多个进程中的指令交替执行,通过切换基址寄存器实现进程工作内存空间的切换。两个部分都切换完成后,MMU执行重定位让所有进程的指令都能正确执行。

二、分段

1.段的概念

《操作系统——内存使用与分段》

 程序由若干段组成,如代码段、数据段、栈段等。代码段是程序指令形成的段,只读;数据段存放存放程序使用的数据,可读可写;栈段用于实现函数调用,一般只能向下生长。

将每个段都分开来,单独从0开始编址,就出现了如上图所示的结构。

《操作系统——内存使用与分段》

 程序分段以后,不能将程序作为一个整体载入到内存中去,而是将各个分段分别载入内存。这种情况下,每个段的基址是不同的,此时我们就需要记录每个段的基址,多个基址会形成一个表,这就是段表

《操作系统——内存使用与分段》

2.分段机制下的地址转换 

采用了分段机制后,逻辑地址就变成了“段号:段内偏移量”的形式,我们熟悉的CS:EIP就是这种形式。

《操作系统——内存使用与分段》

以“call 40”指令为例,其段号CS=0,根据段号去短表中查询到该段的基址是180KB,其段内偏移量IP=40,因此这条指令的物理内存地址为180KB+40。

《操作系统——内存使用与分段》

 

此处的段表描述的是一个进程的各段在物理内存中的位置,每个进程都有自己的段表,即局部描述符表(LDT)。而GDT(全局描述符表)表述的是操作系统的代码段、数据段等,表中有指向各个进程LDT的表项。

《操作系统——内存使用与分段》

 现在用一个例子将整个故事串联起来:系统启动时将操作系统代码载入到内存中,系统启动以后通过shell创建出进程1、进程2,现在屋里内存中有操作系统代码、进程1、进程2,假设它们都要执行“call 40”。

操作系统的代码段被载入到物理内存的0地址处,所以操作系统代码对应的那一项,此处假定为GDT表中的第一项,即第八个字节,GDT表项中存放的是0。进程1的代码段被加载到物理内存的1000地址处,所以进程1的LDT的第一项存放的是1000,对于进程2同理。

 现在开始执行进程1的“call 40”,此处的逻辑地址40并没有给出相应的段,但在执行时会默认加上CS寄存器。系统启动的最后会调用move_to_user_mode进入用户态,而这个过程主要就是将CS、DS等寄存器的值设置为0x0f。

0x0f对应的二进制位00001111B,CS的最后两位数用来表示CPL,那么CPL就为3,表示在执行用户态,CS的倒数第三位就是TI位,此处TI=1表示要查找LDT表以找到CS对应的段。剩下的内容00001000B就是该段在LDT中的表项位置,正是第8个字节。MMU根据CS寄存器的值去查LDT表,首先从寄存器LDTR中找到一个LDT表的基址,假设LDTR指向的刚好是进程1的LDT表的开始位置。MMU根据CS寄存器中存放的表项位置8和LDTR配合会找到对应的段表项,取出段的基址1000再加上偏移地址40得到物理地址1040,发送到地址总线上跳转到进程1中的某条指令。

在进程1执行的过程中出现了一个中断,比如说是系统调用中断“int 0x80”。操作系统通过中断描述符表(IDT)找到相应的中断处理函数的入口,即从IDT表项中取出段号信息赋给CS,从IDT表中取出段内偏移量赋给EIP。而此时IDT表中存放的段号信息就是0x08,所以“int 0x80”中断进入内核以后,CS寄存器的值就一直是0x08了。

现在需要在操作系统内核中取值执行了,此时执行指令“call 40”,逻辑地址任然是40,而CS寄存器的值为00000100B,CPL=0,TI=0,表示要查找GDT表。系统启动以后,GDTR一直指向这个GDT表的开始地址,MMU根据GDTR和8查找GDT表,从GDT表中第八个字节对应的表项取出段基址0,和段内偏移地址40相加得到40,相加以后得到物理地址40,放到地址总线上取出内核中的某条指令。

在操作系统内核代码执行完成以后,要从系统内核返回用户态,此时操作系统会决定是否要重新调度,现在假定要调度到进程2去执行。操作系统找到进程2的PCB,找到进程2的LDT表,让LDTR指向进程2的LDT表。

还要完成内核栈的切换,用iret指令从进程2的内核栈中取出CS:ESP和SS:ESP赋给CPU寄存器。进程2内核栈中存放的CS信息一定是0x0f,所以完成切换后CS=0x0f。现在进程2执行指令“call 40”,逻辑地址是40,MMU用LDTR指向的段表起始地址加上段表项位置8取出段的基址2000, 和段内偏移量40相加输出到地址总线上。

    原文作者:花生酱拌面
    原文地址: https://blog.csdn.net/m0_56561130/article/details/120525000
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞