在前面我们学习了进程,进程其实就是CPU的抽象,正因为有了进程,操作系统才能更好的支持多道程序运行。然而进程的运行又缺少不了内存的支持,既然操作系统都支持多道程序运行,当然也需要支持多个进程的内存同时存在,这一篇我们就来好好研究研究,内存管理。


文章目录

1.1 内存认识

1.1.1 内存硬件认识

如果我们电脑太卡的话,一般都会觉得是内存不够了,然后就想着扩展一下内存,然后去某宝某东上够买。下面这图就是内存条的样子,大小有8GB,买回来插入到电脑中就可以使用了(笔记本和台式内存条不通用,并且需要注意内存条工作电压)。

内存管理<原理篇>(一、内存认识和无存储器抽象)_内存认识

1.1.2 内存软件认识

就是为了找这个图,下载了好多芯片手册,有stm32mp1,imx6ULL,s5PV210,intel的芯片手册,都浏览了一遍,发现都不满意,最后放弃了,说百度看看运气,结果运气就很好,这个图就很好的描述了我想要的,哈哈哈。

内存管理<原理篇>(一、内存认识和无存储器抽象)_物理地址_02

上一节买的内存条就是这个图的DDR部分,然后左边的框是表示SOC,CPU通过三级cache才能访问到内存,关于Cache等以后学习了计算机组成原理再描述,现在可以当做CPU可以直接操作内存。

在操作系统中会把内存看一个很大的字节数组,比如上面的内存条有8GB,操作系统就会认为是一个8GB的数组,并且每一个地址都有自己的地址。

写了一个测试程序,来看看内存的地址:

#include "stdio.h"

int main(int argc, char ** argv)
{

char a[10] = {0};

for(int i=0; i<20; i++)
{
a[i] = i;
printf("a[%d] = %d addr = %p\n", i, a[i], &a[i]);
}

return 0;
}

char类型是一个字节,我申请了10个字节,但是在赋值的时候,赋值了20个,理论上是越界了,但是因为内存是连续的一个字节数组,所以是可以这样操作的,但是正常代码中,不能这样写,这只是测试代码。我们来看看运行后的结果:

root@ubuntu:~/knowledge_systeam/4.os/3.Memory/1.principle/01# ./test 
a[0] = 0 addr = 0x7fffc552ee90
a[1] = 1 addr = 0x7fffc552ee91
a[2] = 2 addr = 0x7fffc552ee92
a[3] = 3 addr = 0x7fffc552ee93
a[4] = 4 addr = 0x7fffc552ee94
a[5] = 5 addr = 0x7fffc552ee95
a[6] = 6 addr = 0x7fffc552ee96
a[7] = 7 addr = 0x7fffc552ee97
a[8] = 8 addr = 0x7fffc552ee98
a[9] = 9 addr = 0x7fffc552ee99
a[10] = 10 addr = 0x7fffc552ee9a
a[11] = 11 addr = 0x7fffc552ee9b
a[12] = 12 addr = 0x7fffc552ee9c
a[13] = 13 addr = 0x7fffc552ee9d
a[14] = 14 addr = 0x7fffc552ee9e
a[15] = 15 addr = 0x7fffc552ee9f
a[16] = 16 addr = 0x7fffc552eea0
a[17] = 17 addr = 0x7fffc552eea1
a[18] = 18 addr = 0x7fffc552eea2
a[19] = 19 addr = 0x7fffc552eea3

看到的地址都是连续的吧,为啥地址都是这么大呢?我们后面再介绍,如果有看到我前面的编译连接的文章的话,应该也能懂的。

1.1.3 指令执行

一个典型的指令执行周期,首先从内存中读取指令。接着,该指令会被解码,也可能需要从内存中读取操作数。在指令对操作数执行后,它的结果可能存回内存。

想了半天才想到一个例子,也就是我们平时说的i++是不是原子操作。

我们来看看代码:

#include "stdio.h"

char gg = 11;

int main(int argc, char ** argv)
{

gg++;
//printf("gg = %d\n", gg);

return 0;
}

编译之后,我们来看反汇编代码:

00000000004004d6 <main>:
4004d6: 55 push %rbp
4004d7: 48 89 e5 mov %rsp,%rbp
4004da: 89 7d fc mov %edi,-0x4(%rbp)
4004dd: 48 89 75 f0 mov %rsi,-0x10(%rbp)
# 0x200b48 是相对地址,这个后面会介绍
# 第一步:从地址为601030取出数据,存到寄存器%eax寄存器中
4004e1: 0f b6 05 48 0b 20 00 movzbl 0x200b48(%rip),%eax # 601030 <gg>
# 第二步:在寄存器中进行++操作
4004e8: 83 c0 01 add $0x1,%eax
# 第三步:再从累加器%al中,把结果写入到601030这个地址中
4004eb: 88 05 3f 0b 20 00 mov %al,0x200b3f(%rip) # 601030 <gg>
4004f1: b8 00 00 00 00 mov $0x0,%eax
4004f6: 5d pop %rbp
4004f7: c3 retq
4004f8: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)

当代码要执行到gg++;的时候,其中的PC寄存器指向0x4004e1这个内存地址。

0x4004e1内存地址中的值为:0f b6 05 48 0b 20 00。

CPU会解析0f b6 05这个指令,发现这个指令是movzbl,然后在把后面的48 0b 20 00的值,当做地址,去想对应的地址中,取到0x601030这个地址的值,存储到%eax寄存器中。

后面的几步也是这么操作的。

1.2 无存储器抽象

最简单的存储器抽象就是根本没有抽象,早期的大型计算机(20世纪60年代)、小型计算机(20世纪70年代)和个人计算机(20世纪80年代)都没有存储器抽象,每个程序都直接访问物理内存。

比如我们写一个指令:

MOV REGISTER1, 1000

这里的1000就是物理地址,计算机会把物理地址为1000的值移到REGISTER1中。

如果多个程序运行的话,由于没有内存管理,所以其他程序也可能会在1000的物理地址中修改值,这样会操作冲突。

1.2.1 内存布局

即使在无存储器抽象的情况下,也是存在一些可行的选项的。

内存管理<原理篇>(一、内存认识和无存储器抽象)_无存储器抽象_03

图a中,操作系统位于RAM(随机访问存储器)底部。以前被用在大型机和小型计算机上,现在很少使用了。

图b中,操作系统位于内存顶端的ROM(只读存储器)中。用在一些掌上电脑和嵌入式系统中,之前玩过的单片机就是这样的。

图c中,设备驱动程序位于内存顶端的ROM中,而操作系统的其他部分则位于下面的RAM的底部。应用在早期个人计算机中(DOC计算机),在ROM中的系统部分称为BIOS(Basic Input Output System,基本输入输出系统)。

图a和图c的缺点是用户程序出现的错误可能摧毁操作系统,引发灾难性后果。

在没有存储器抽象的系统中是否可以实现并行操作??

答案是可以的,我们可以用多线程来编程,把整个操作系统当做一个进程,在进程中创建多个线程,多个线程共享一个进程的内存,这样完全是可以的,比如单片机中的ucos系统,rtthread系统,都是在一块内存中,实现多任务编程(可以理解成多线程编程)。

那在无存储器抽象中,是否也能支持多个进程运行呢?继续看下面介绍。

1.2.2 运行多程序

即使没有存储器抽象,同时运行多个程序也是可能的。

  1. 交换
    操作系统需要在磁盘中保存多个程序内存的代码(有点像swap),当哪个程序运行的时候,把这个程序从磁盘中读取到内存,把上一个运行程序的内存保存到磁盘中,主要保证同一个时刻内存只有一个程序就可以了。
  2. 分块
    如果不想做这种交换,那还有一种方式:分块。
    IBM 360早期模型是这样解决的:内存被划分为2KB的快,每个快被分配一个4位保护键(我也不知道为啥是4位),保护键存储在CPU特殊寄存器中。PSW(program Status Word,程序状态字)中存有一个4位码,当一个运行程序如果访问保护键与其PSW码不同的时候,cpu硬件会捕获到这一事件,从而防止这种事情发生。(我理解的4位保护键,是因为最大运行16个不同的程序???)

方案2其实是有缺陷的,因为每个程序都是独立编译,造成每个内存的地址都是从0开始,如果都分别装入内存会出现程序崩溃,如图:

内存管理<原理篇>(一、内存认识和无存储器抽象)_内存管理_04

图中灰色和蓝色表示两个程序,分别装载在不同内存中,相互不影响,当程序执行到0地址的时候,发现是JMP 24,这时候CPU就跳到物理地址为24上,这一切正常。当CPU运行第二个程序的时候,在16384的内存中,是一个JMP 28的地址,但是物理地址28是在程序1中,所以这样运行会导致程序崩溃。

这里关键问题是,两个程序都引用了物理地址,从而导致的冲突,要解决也不难,就是两个程序的地址都需要分开,不能混用。IBM 360使用了一套静态重定位的方式解决。

原理是:当一个程序被装载到地址16384的时候,常熟16384就会加载每一个程序的地址上,把地址全部重定位一遍,这样就能保证程序2访问的内存是在16284之后了。但是这种情况也是又缺点的,就是加载的速度比较慢,因为每次加载都需要看看需不需要重定位,如果要还用重定位。

1.3 总结

这一篇就简单介绍了内存认识和在无存储器抽象的时代,内存是怎么工作了,当然也包含现在的简单的单片机系统,不过现在的单片机系统不需要做多进程,也不需要重新装载程序,只需要跑之前烧录好的程序即可。