程序是怎么装载到内存并被运行的

在后续所有内容之前,我们需要先达成一个共识,所有的程序都是被装载进内存然后才被使用的。装载器会把对应的指令和数据加载到内存里面来,让 CPU 去执行,而程序,包括操作系统就是一堆指令和数据的集合。

下面开始套娃,BIOS硬件初始化并开始加载主引导扇区(多系统需要选择启动哪个系统的原因),将操作系统加载到内存;移交加载控制权给操作系统,操作系统开始装载非操作系统程序到内存。因为Linux和Windows系统的装载器不同,所以这也是为什么Windows上的一部分程序没法在Linux上跑的原因,如.exe。

装载器需要满足两个要求。

第一,可执行程序加载后占用的内存空间应该是连续的,方便寻址。

第二,我们需要同时加载很多个程序,并且不能让程序自己规定在内存中加载的位置,避免冲突。

于是我们可以在内存里面,找到一段连续的内存空间,然后分配给装载的程序,然后把这段连续的内存空间地址,和整个程序指令里指定的内存地址做一个映射。我们把指令里用到的内存地址叫作虚拟内存地址(Virtual Memory Address),实际在内存硬件里面的空间地址,我们叫物理内存地址(Physical Memory Address)。程序里有指令和各种内存地址,我们只需要关心虚拟内存地址就行了。

我们维护一个虚拟内存到物理内存的映射表,这样实际程序指令执行的时候,会通过虚拟内存地址,找到对应的物理内存地址,然后执行。这种方式称之为分段

 

但是分段遇到了两个问题,第一个是会产生内存碎片,还是大碎片

 

在渲染程序之后我们启动 Chrome 浏览器,占用了 128MB 内存,再启动一个 Python 程序,占用了 256MB 内存。这个时候,我们关掉 Chrome,于是空闲内存还有 1024 - 512 - 256 = 256MB。按理来说,我们有足够的空间再去装载一个 200MB 的程序。但是,这 256MB 的内存空间不是连续的,而是被分成了两段 128MB 的内存。回想装载器的两个原则,我们的程序根本没办法加载进来。

那怎么办?

 

 

 

 

我们查看服务器的内存信息,你会发现有一个专门的模块叫做Swap,这块区域就是我们的解决办法,名为内存交换

我们可以把 Python 程序占用的那 256MB 内存写到硬盘上,然后再从硬盘上读回来到内存里面。不过读回来的时候,我们不再把它加载到原来的位置,而是紧紧跟在那已经被占用了的 512MB 内存后面。这样,我们就有了连续的 256MB 内存空间,就可以去加载一个新的 200MB 的程序。但是一个8G的内存,可能有一半都拿来做交换区了,资源浪费严重。

接下来说第二个问题,内存价格昂贵,一般都不会很大,即使是今天16G、8G仍然是主流,遑论以前了,但是磁盘包括SSD动辄上1T,我如果把程序都加载了,内存装不了啊!

这个问题靠分段已经解决不了了,于是引申出了分页。因为科学家发现,程序运行在一段时间之内请求的地址是连续的,那我不全加载不就行了,我先加载一小块或者几小块,等我真用到了我再去真实的物理地址那拿一块,这样一块一块的为页,也就是分页思想的由来。

和分段这样分配一整段连续的空间给到程序相比,分页是把整个物理内存空间切成一段段固定尺寸的大小,且远比分段小得多,那么内存交换也方便了很多。

在 Linux 下,我们通常只设置成 4KB为一页。由于内存空间都是预先划分好的,也就没有了不能使用的碎片,而只有被释放出来的很多 4KB 的页。即使内存空间不够,需要让现有的、正在运行的其他程序,通过内存交换释放出一些内存的页出来,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,让整个机器被内存交换的过程给卡住。

分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。我们完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。

当要读取特定的页,却发现数据并没有加载到物理内存里的时候,就会触发一个来自于 CPU 的缺页错误(Page Fault)。我们的操作系统会捕捉到这个错误,然后将对应的页,从存放在硬盘上的虚拟内存里读取出来,加载到物理内存里。这种方式,使得我们可以运行那些远大于我们实际物理内存的程序。

通过虚拟内存、内存交换和内存分页这三个技术的组合,我们最终得到了一个让程序不需要考虑实际的物理内存地址、大小和当前分配空间的解决方案。

通过上边整个的描述你一定对一个程序是怎么加载到内存并被使用的有了初步了解,那么你一定也认识到了一点,一个物理地址可能映射到多个虚拟地址(请牢牢记住这句话),那么这些虚拟内存地址究竟是怎么转换成物理内存地址的呢?

想要把虚拟内存地址,映射到物理内存地址,最直观的办法,就是来建一张映射表。这个映射表,能够实现虚拟内存里面的页,到物理内存里面的页的一一映射。这个映射表,在计算机里面,就叫作页表(Page Table)。页表这个地址转换的办法,会把一个内存地址分成页号(Directory)和偏移量(Offset)两个部分。这一部分其实可以以CPU的告诉缓存作为参考,道理是一样的。请直接移步CPU高速缓存原理,这里只简单介绍。

 

  1. 把虚拟内存地址,切分成页号和偏移量的组合;
  2. 从页表里面,查询出虚拟页号,对应的物理页号;
  3. 直接拿物理页号,加上前面的偏移量,就得到了物理内存地址。

那么问题来了,一个32 位的内存地址空间,页表一共需要记录 2^20 个到物理页号的映射关系。这个存储关系,就好比一个 2^20 大小的数组。一个页号是完整的 32 位的 4 字节(Byte),这样一个页表就需要 4MB 的空间。怎么算的?可以移步计算机存储器 简单来说就是2^32/4K*4B = 2^20 * 4B = 4M。

每个进程我们都需要维护这么一个映射表,那么请打开自己的任务管理器看看现在电脑有多少个进程。而我们现在已经普遍都是64位操作系统了。

于是我们又引入了多级页表的方式,实际的程序进程里面,虚拟内存占用的地址空间,通常是两段连续的空间。而不是完全散落的随机的内存地址。

我们以一个 4 级的多级页表为例,同样一个虚拟内存地址,偏移量的部分和上面简单页表一样不变,但是原先的页号部分,我们把它拆成四段,从高到低,分成 4 级到 1 级这样 4 个页表索引。

 

对应的,一个进程会有一个 4 级页表。我们先通过 4 级页表索引,找到 4 级页表里面对应的条目(Entry)。这个条目里存放的是一张 3 级页表所在的位置,以此类推最后根据偏移量找到对应的物理地址,每一级这个索引长度有多长,取决于你条目的多少,每一级如果都用 5 个比特表示。那么每一张某 1 级的页表,只需要 2^5=32 个条目。如果每个条目还是 4 个字节(32bit),那么一共需要 128 个字节。而效果是一样的,只不过从乘法变成了加法,但是因为多级页表的存在,也增加了寻址时间,原本1次,现在可能需要4次。

 

  • 18
    点赞
  • 82
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值