内存管理<原理篇>(四、分段和分页)

4.1 分段

在上一篇已经介绍了交换和分区,不管是交换还是分区,都有提到,我们需要把进程加载到内存中,然后再运行,那进程是怎么加载进内存的呢?是整个进程都一起加载么?

看过上一篇的都了解,按照上一篇的技术是很有可能整个进程一起加载,不过现在操作系统应该不这样做了,现在操作系统引入的是分段,如果看过编译链接的朋友,应该就会恍然大悟,原来我们说的各个程序段是这个意思。

4.1.1 程序段介绍

我们写程序时,我们会认为程序由主程序加上一组方法、函数所构成并且程序还包含各种数据结构:对象、数组、堆栈等。每个模块或函数之间都是通过名字来引用的。

我们经常会说堆栈、变量、C库等,但我们并不知道这些东西存在哪里,也不知道这些东西是怎么存储的。

正因为我们程序有这么多元素,并且各种元素属性不一样,有的只读,有的可读可写,有的可以增长,如果像上一节课一个进程就分配一块内存,这样做缺点很大,所以引进了我们今天的主题,分段。

分段就是支持这种用户视图的内存管理方案。逻辑地址空间是由一组段构成。每个段都有名称和长度。

怎么确定段内的地址?其实只需要<段号, 偏移>就能确定位置,这个下一节详细介绍。

下面我们就来先认识一下,现代计算机代码编译后的各个段,当然现代计算用的是虚拟内存,这个我们后面介绍,先忽略,我们只是来看看各个段的介绍:

root@ubuntu:~/knowledge_systeam/4.os/3.Memory/1.principle/01# objdump -h test

test:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .interp       0000001c  0000000000400238  0000000000400238  00000238  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .note.ABI-tag 00000020  0000000000400254  0000000000400254  00000254  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .note.gnu.build-id 00000024  0000000000400274  0000000000400274  00000274  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .gnu.hash     0000001c  0000000000400298  0000000000400298  00000298  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .dynsym       00000048  00000000004002b8  00000000004002b8  000002b8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 .dynstr       00000038  0000000000400300  0000000000400300  00000300  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 .gnu.version  00000006  0000000000400338  0000000000400338  00000338  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  7 .gnu.version_r 00000020  0000000000400340  0000000000400340  00000340  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  8 .rela.dyn     00000018  0000000000400360  0000000000400360  00000360  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  9 .rela.plt     00000018  0000000000400378  0000000000400378  00000378  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 10 .init         0000001a  0000000000400390  0000000000400390  00000390  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 11 .plt          00000020  00000000004003b0  00000000004003b0  000003b0  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 12 .plt.got      00000008  00000000004003d0  00000000004003d0  000003d0  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 13 .text         00000192  00000000004003e0  00000000004003e0  000003e0  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 14 .fini         00000009  0000000000400574  0000000000400574  00000574  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 15 .rodata       00000004  0000000000400580  0000000000400580  00000580  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 16 .eh_frame_hdr 00000034  0000000000400584  0000000000400584  00000584  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 17 .eh_frame     000000f4  00000000004005b8  00000000004005b8  000005b8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 18 .init_array   00000008  0000000000600e10  0000000000600e10  00000e10  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 19 .fini_array   00000008  0000000000600e18  0000000000600e18  00000e18  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 20 .jcr          00000008  0000000000600e20  0000000000600e20  00000e20  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 21 .dynamic      000001d0  0000000000600e28  0000000000600e28  00000e28  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 22 .got          00000008  0000000000600ff8  0000000000600ff8  00000ff8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 23 .got.plt      00000020  0000000000601000  0000000000601000  00001000  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 24 .data         00000011  0000000000601020  0000000000601020  00001020  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 25 .bss          00000007  0000000000601031  0000000000601031  00001031  2**0
                  ALLOC
 26 .comment      00000035  0000000000000000  0000000000000000  00001031  2**0
                  CONTENTS, READONLY

现代操作系统也是把程序分了这么多个段,前面是段号,Size是段的大小。

其中我们最熟悉的13 .text 代码段;24 .data 数据段。

这两个段,以后讲虚拟内存还会回来介绍的。

4.1.2 各段放入内存信息

上面我们见识到了,一个程序分了这么多个段,每个段都各自加载到内存中,内存是怎么申请的呢?

就是上一篇写的分区,通过连续内存分区分配的方式给程序中各个段分配内存。(虽然我们上一节课说了分区分配很多缺点,但是也是需要了解一下,并且在下面会提出解决分区分配缺点的方案)

在这里插入图片描述

这个图就是分段后,进程加载进内存的部署,当然现在操作系统使用的虚拟内存并不是这样(不过也差不多了),不过我们暂时先看看之前是怎么处理的,怎么演变过来的。

我们也随便看看现在操作系统进程加载进内存的分布:

root@ubuntu:/proc/1604# cat maps 
00400000-00401000 r-xp 00000000 08:01 12460229                           /root/knowledge_systeam/4.os/3.Memory/1.principle/01/test
00600000-00601000 r--p 00000000 08:01 12460229                           /root/knowledge_systeam/4.os/3.Memory/1.principle/01/test
00601000-00602000 rw-p 00001000 08:01 12460229                           /root/knowledge_systeam/4.os/3.Memory/1.principle/01/test
7f9c3cb34000-7f9c3ccf4000 r-xp 00000000 08:01 791097                     /lib/x86_64-linux-gnu/libc-2.23.so
7f9c3ccf4000-7f9c3cef4000 ---p 001c0000 08:01 791097                     /lib/x86_64-linux-gnu/libc-2.23.so
7f9c3cef4000-7f9c3cef8000 r--p 001c0000 08:01 791097                     /lib/x86_64-linux-gnu/libc-2.23.so
7f9c3cef8000-7f9c3cefa000 rw-p 001c4000 08:01 791097                     /lib/x86_64-linux-gnu/libc-2.23.so
7f9c3cefa000-7f9c3cefe000 rw-p 00000000 00:00 0 
7f9c3cefe000-7f9c3cf24000 r-xp 00000000 08:01 791108                     /lib/x86_64-linux-gnu/ld-2.23.so
7f9c3d116000-7f9c3d119000 rw-p 00000000 00:00 0 
7f9c3d123000-7f9c3d124000 r--p 00025000 08:01 791108                     /lib/x86_64-linux-gnu/ld-2.23.so
7f9c3d124000-7f9c3d125000 rw-p 00026000 08:01 791108                     /lib/x86_64-linux-gnu/ld-2.23.so
7f9c3d125000-7f9c3d126000 rw-p 00000000 00:00 0 
7ffe32b26000-7ffe32b47000 rw-p 00000000 00:00 0                          [stack]
7ffe32ba9000-7ffe32bac000 r--p 00000000 00:00 0                          [vvar]
7ffe32bac000-7ffe32bae000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

也是一个段占一块内存,不过因为有虚拟内存的存在,每个段的地址都知道了,不过之前的系统在没有虚拟内存的情况下,每个段的地址和段号是根据段表来决定的,并且如果程序的运行也离不开段表。

下面我们就来了解了解段表。

4.1.3 段表

因为我们分了很多个段,并且每个段都有不同的基地址,这样的结构让我们想起了表。

计算机也引入了一张这样的表,叫段表。段表的每个条目都有段号、段基地址、段界限

段基地址包含该段在内存中开始物理地址,逻辑地址映射成物理地址有用。

段界限指该段的长度,一般做保护处理,怕访问溢出。(这个可以看前面章节)

接下来我们就按哈工大老师说的例子,我们来过一边重定位:

mov [DS:100], %eax
jmpi 100, CS
段号基地址长度保护
0180K150KR
1360K60KR/W
270K110KR/W
3460K40KR

问题:假设DS=1,CS=0,上面两条指令运行时重定位成什么?

DS=1,说明这个数据段在1号段,1号段的基地址为360K,段内偏移是100,所以我们要把这个内存里的数据:360K+100,赋值到eax寄存器中。

CS=0,说明这个指令段在0号段,0号段的基地址为180K,段内便宜也是100,所以我们要跳到180K+100的指令上,然后继续执行。

4.1.4 总结

我们引入分段之后,跟之前执行程序的最大区别是,之前只有一个重定位寄存器即可,因为都是一个段,只有一个基地址加偏移就可以了,现在分了很多个段,自然就需要段表了,所以执行程序的时候多了一步查找过程。

现代操作系统其实有一个GDT和LDT表,跟上面的段表比较相似了,GDT表示操作系统标记每个进程的地址的,LDT表示每个进程中各个段的地址,有点跟段表相似了,之后再分析这两个。

4.2 分页

4.1介绍的分段是对程序的划分,并且也是对逻辑地址的划分,并且通过分区分配的方式加载进程的各个段,上一篇也介绍了分区方式的缺点,所以分区的方式是操作系统早期对物理内存的划分。那现在操作系统对内存是如何划分的???

没错,现代操作系统对物理内存的划分是分页。那啥是分页呢???

上一篇我们用了分面包来介绍分区,那大家想想现在分面包是怎么分的???

现在的面包是分片,每个人的胃口都不一样,胃口大的就吃多几片,胃口小的就吃小就吃一片,这样浪费就不会那么多,内存的分页也是这种思想,没页大小是4KB,当然也有其他的大小。

下面我们就来看看是怎么使用分页的。

4.2.1 概念介绍

分页是将物理内存分为固定大小的块,称为帧、页帧或页框。每个页框(帧、页帧)都有一个编号,叫做页框号(帧号、页帧号),也框号是从0开始的。

将逻辑内存(是否想起之前说的地址空间的抽象)也分为跟页框大小一样的块,称为页或页面。每个页面也有一个编号,叫页号,页号也是从0开始。

4.2.2 各段加载进内存

前面感受了分段的时候,一整块加入内存,那现在物理内存引入了分页,我们就来感受一下分页吧。

在这里插入图片描述

这样为了画少一点,我把一页设为512K,其中代码段为1M,刚好占了两页,数据段512k也刚好一页,然后堆段只有612k,但是也需要占两也,其中剩下的就是内部碎片,因为我这里分的页大,所以内部碎片也很大,其实实际系统大部分都只分4k作为一页,即使有内部碎片也影响不大,栈段也是1M,刚好2页。

最后存在物理内存中,也是离散的,这就是分页的好处,不用一起申请那么大的内存。所以可以离散存储。但是也因为这种离散存储,导致我们指令寻找不到物理地址,那这里也再次引入页表的概念。

页表其实跟上面说的段表差不多,也是通过页号了找到对应的物理地址。

4.2.3 例子分析

那逻辑地址是怎么找到物理地址的,我们来研究一下。

例子还是看哈工大老师举的例子:

mov [0x2240], %eax  // 逻辑地址
页号页框号保护
05R
11R/W
23R/W
36R

我们的页就按4K来计算。

逻辑地址为0x2240,那它的

页号= 0x2240 / (4 *1024) = 0x02

页偏移 = 0x2240 % (4 *1024) = 0x240

通过查找页表,页号为2的,页框号为3,页框号为3的话,基地址为0x03 * 4 * 1024 = 0x3000

最后再加上偏移,最终物理地址 = 0x3000 + 0x240 = 0x3240

4.2.4 总结

我们这一篇学习了程序分段,物理内存分页,也就是段页式初步学习,一个程序的执行,需要先查段表,查出段表中的基地址,然后计算出当初指令或数据的逻辑地址,然后再根据逻辑地址计算出在哪一页,然后通过页号去查页表,最后才找到具体的物理地址,这样确实麻烦了不少。不过后期虚拟内存的引进就少了一层,下一篇我们主要学习页表,快接近真想了,加油。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值