基本要求: 一个进程需要一块存储时分配,完成工作后收回
基本结构: 首先分为物理存储和逻辑存储。
- 物理地址(PA, Physical Address) :用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。
- 逻辑地址(LA, Logical Address) :CPU执行机器指令时,用来指定一个操作数或者是一条指令的地址。也是用户编程时使用的地址。
内存管理的目标
- 抽象:逻辑地址空间
- 保护:独立地址空间
- 共享:访问相同内存
- 虚拟化:更大都地址空间
操作系统中的内存管理方式:
- 重定位(relocation)
- 分段(segmentation)
- 分页(paging)
- 虚拟存储(virtual memory/storage)
- .c file 函数位置、变量名即逻辑地址。
- .s file 汇编语言中更加贴近机器语言,但是依然用符号代表变量名字。
- .o file 机器语言中,起始地址都是从0开始的,把变量名转换为地址。
- linker把多个.o file变成一个单一的.exe file。
- .exe file 中,地址已经做了全局的分布,不同的.o file程序的地址在单一程序中已经有各自的定义。
- .exe file 通过loader放到内存中运行,会有一定的偏移量,所以程序依照偏移量来进行正确数据的访问和指令的操作。
- 常用数据放在物理内存中
- 不常用数据放在外存
- 运行的程序直接用虚存地址,不用关注具体放在物理内存还是外存
2. 简化应用编译和加载运行: 每个运行程序具有独立的地址空间,而不管代码和数据在物理内存的实际存放,从而简化:
- 编译的执行程序链接
- 操作系统的执行程序加载
- 共享:动态链接库、共享内存
- 内存分配:物理不连续,虚拟连续
3. 保护数据: 虚拟内存可保护数据
- 独立的地址空间使得区分不同进程各自内存变得容易
- 地址转换机制可以进行可读/可写/可执行的检查
- 地址转换机制可以进行特权级检查
运行应用所占内存按存储数据特 征划分成多个段(Segment)
内存分配方式
- 静态内存分配
- 动态内存分配
- 连续内存分配
- 非连续内存分配
参考博文:linux下,程序各个部分对应的段位置,图说 bss段 text段 data段 rodata段 栈 堆
1.受保护区: 受保护区,是用户不能访问的地址,受系统保护,一般为 0-4k,我们平时写程序的初始化一个指针,
这个NULL就是指向的受保护区,0 地址。
2. 代码段:主要存放用户编写的业务逻辑程序、算法等,这里的地址是绝对地址,因此如果有链接动态库,是放在共享库中的。
3. rodata段: .rodata段 是 只读数据段,比如我们用const修饰的值就是放在这个区域的。
4. data段: .data段 是数据段,存放了已经初始化的全局变量,以及已初始化全局静态变量,已初始化的局部静态变量等。
初始化的全局变量,已经初始化和未初始化的静态变量都在这个段,初始化的局部静态变量。
5.bss段: .bss段 是未初始化的全局变量以及未初始化的静态局部变量。
6.堆: 程序员可以用来分配内存的空间,使用malloc分配。
在创建进程的时候,会为进程分配多大的堆。
7.共享库: 共享库,在调用了共享库的可执行程序里面,共享库放在这个位置。
8.栈: 栈,程序执行过程中,程序用来存放临时变量和函数返回值的区域(局部变量、函数)。由系统分配和回收,在进程创建的时候,每个进程/线程都有自己独立的栈空间。
9.命令行和环境变量: 就是我们linux的环境变量env,命令行参数,比如
这里面argv所带的参数。
动态内存分配是指运行时的内存分配
- 栈(stack)
- 局部变量
- 堆(heap)
- 函数分配内存
- 函数释放内存
1. 堆和栈的内存分配
- 栈由编译器管理:隐式分配
- 堆的分配和释放由程序员管理:显式分配
2. 分配大小
- 栈是由高地址向低地址生长的数据结构,是一块连续的内存,能从栈中获得的内存较小,编译期间确定大小;
- 堆是由低地址向高地址生长的数据结构,是一个不连续的储存空间,内存获取比较灵活,也较大
3. 动态内存分配函数
-
malloc()函数:
- 申请一块size大小的连续堆内存
- 函数返回值是一个指针,指向刚分配的内存首地址
- 如果申请内存失败, 返回一个空指针,即返回值为NULL
-
动态内存的分配和释放必须成对使用
- 如果malloc()比free()多,会造成内存泄漏
- 如果malloc()比free()少,会造成二次删除,破坏内存,导致程序崩溃
4. 动态内存回收函数
- free()函数:
- 释放指针变量在堆区上的内存空间
- 不能释放栈上的内存空间
- free()要与malloc()成对使用
1. 连续内存分配: 给应用分配一块不小于指定大小的连续的内存区域
操作系统需要维护的数据结构
- 所有进程的已分配分区
- 空闲分区(Empty-blocks)
动态分区方式对比表
2. 分配需要寻找一个合适的分区
3. 重分配需要检查是否可以合并相邻空闲分区
在高地址易于产生更大空闲块
分配大块时间较慢
2. 空闲分区列表按照大小排序(从小到大)
3. 重分配需要检查是否可以合并相邻空闲分区
可减小外碎片大小
避免大的空闲分区拆分
大部分是小尺寸时高效
易产生很多无用小碎片
2. 空闲分区列表按由大到小排
3. 重分配需要检查是否可以合并相邻空闲分区
大部分分配是中尺寸时高效
大空闲块易破碎,大分区无法被分配
三种分配方式并无优劣之分,因为我们无法判断内存请求的大小。
4. 碎片整理
可以看到的是,三种分区动态分配的方式都会产生外部碎片,因此我们可以对碎片进行一定的整理来解决碎片问题,通过调整进程占用的分区位置来较少会避免分区随便
(1)紧凑: 通过移动分配给进程的内存分区,以合并外部碎片
紧凑条件: 所有的应用程序可动态重定位
(2)分区对换
伙伴系统比较好的折中了分配和回收过程当中合并和分配块位置的碎片问题。
1. 分区大小
- 可分配分区的大小 2U
- 待分配分区的大小为2(U-1) < s ≤ 2U,把整块分配给应用
- 待分配分区的大小为 s ≤ 2(i-1),
- 将大小为 2i 的当前空闲分区划分成两个大小为 2i-1空闲分区
- 重复划分过程,直到2(i-1) < s ≤ 2i ,把一个空闲分区分配出去
2. 数据结构
- 空闲块按大小和起始地址组织成二维数组
- 初始状态:只有一个大小为 2U的空闲块
- 分配给一个程序的物理内存是连续的
- 内存利用率较低
- 有外碎片 / 内碎片的问题
能否通过新的一些手段来改善这些情况?=> 非连续内存分配
2. 非连续内存分配的优点
- 一个程序的物理地址空间是非连续的
- 更好的内存利用和管理
- 允许共享代码与数据(共享库等…)
- 支持动态加载和动态链接
3. 非连续内存分配的缺点
-
建立虚拟地址和物理地址的地址转换难度大
-
软件方案(开销相当大)
-
硬件方案(采用硬件辅助机制)
➢ 分段(Segmentation)
➢ 分页(Paging)
2. 段访问机制
段的概念:
- 段表示访问方式和存储数据等属性相同的一段地址空间
- 对应一个连续的内存“块”
- 若干个段组成进程逻辑地址空间
1. 页式存储管理概念
页帧(帧、物理页面,Frame, Page Frame)
- 把物理地址空间划分成大小相同的基本分配单位
- 2的n次方,如512,4096,8192
页面(页、逻辑页面,Page)
- 把逻辑地址空间也划分为相同大小的基本分配单位
- 帧和页的大小必须是相同的
页面到页帧:
- 逻辑地址到物理地址的转化
- 页表
- MMU/TLB
2. 页式存储管理的地址转化
页帧(Frame)
内存物理地址的表示:二元组(f,o)
- :帧号(F位,共有 2F 个帧
- :帧内偏移(S位,每帧有 2S 字节)
物理地址计算实例:
- 假定16-bit的地址空间
- 9-bit(512 byte)大小的页帧
- 物理地址表示 = (3,6)
物理地址 = f * 2s + o = 3 * 2 9 + 6 = 1542
- 页内偏移 = 帧内偏移
- 通常:页号大小 ≠ 帧号大小
进程逻辑地址的表示:二元组(p,o)
p —— 页号(p位,2p个页)
o —— 页内偏移(S位,,每页有2S字节)
(2)分页机制的性能问题 : 空间代价、时间开销
-
访问一个内存单元需要2次内存访问
➢ 一次用于获取页表项
➢ 一次用于访问数据 -
页表可能非常大
➢ 64位机器如果每页1024字节,那么一个页表的大小会是多少?(264 / 210 = 254)
➢ 每一个运行的程序都需要有一个页表 -
如何处理?
➢ 缓存(Caching)
➢ 间接(Indirection)访问
4. 反置页表
- 对于大地址空间(64-bits)系统,多级页表变得繁琐
- 页寄存器和反置页面的思路
- 不让页表与逻辑地址空间的大小相对应
- 让页表与物理地址空间的大小相对应
段式存储在内存保护方面有优势,页式存储在内存利用和优化转移到后备存储方面有优势。