C语言整理 - 内存

声明: 本篇博客的学习途径主要为以下网站和课堂讲解,发博客目的仅为学习使用,在该博客的基础上做了一定程序的简略和修改。
参考博客 :
原文链接:http://c.biancheng.net/c/

程序的运行

在这里插入图片描述
在这里插入图片描述

虚拟内存

【问题】如果我们运行的程序较多,占用的空间就会超过内存(内存条)容量。
解决方法: 操作系统(Operating System,简称 OS)提供方法,当程序运行需要的空间大于内存容量时,会将内存中暂时不用的数据再写回硬盘;需要这些数据时再从硬盘中读取,并将另外一部分不用的数据写入硬盘。

这样,硬盘中就会有一部分空间用来存放内存中暂时不用的数据。这一部分空间就叫做虚拟内存(Virtual Memory)。


虚拟地址

【引例】

#include <stdio.h>
#include <stdlib.h>
int a = 1, b = 255;
int main(){
    int *pa = &a;
    printf("pa = %#X, &b = %#X\n", pa, &b);
    system("pause");
    return 0;
}
在 C-Free 5.0 下运行,结果为:
pa = 0X402000, &b = 0X402004

代码中的 a、b 是全局变量,它们的内存地址在链接时就已经决定了,以后再也不能改变,该程序无论在何时运行,结果都是一样的。


【问题】如果物理内存中的这两个地址被其他程序占用了怎么办,我们的程序岂不是无法运行了?
解决方法:虚拟地址。虚拟地址通过CPU的转换才能对应到物理地址,而且每次程序运行时,操作系统都会重新安排虚拟地址和物理地址的对应关系,哪一段物理内存空闲就使用哪一段。

在这里插入图片描述


【使用虚拟地址的好处】

  • 使不同程序的地址空间相互隔离

如果所有程序都直接使用物理内存,那么程序所使用的地址空间不是相互隔离的。恶意程序可以很容易改写其他程序的内存数据,以达到破坏的目的;有些非恶意、但是有 Bug 的程序也可能会不小心修改其他程序的数据,导致其他程序崩溃。

  • 提高内存使用效率

使用虚拟地址后,操作系统会更多地介入到内存管理工作中,这使得控制内存权限成为可能。例如,我们希望保存数据的内存没有执行权限,保存代码的内存没有修改权限,操作系统占用的内存普通程序没有读取权限等。

具体使用虚拟地址的好处体现在 内存分页机制


内存对齐 :提高寻址效率

寻 址 过 程 寻址过程
计算机内存是以字节(Byte)为单位划分的

CPU 通过地址总线来访问内存,一次能处理几个字节的数据,就命令地址总线读取几个字节的数据。
32 位的 CPU 一次可以处理4个字节的数据,那么每次就从内存读取4个字节的数据;少了浪费主频,多了没有用。
64位的处理器也是这个道理,每次读取8个字节

【例】 以32位的CPU为例,实际寻址的步长为4个字节,也就是只对编号为 4 的倍数的内存寻址
例如 0、4、8、12、1000 等,而不会对编号为 1、3、11、1001 的内存寻址。
在这里插入图片描述
这样做可以以最快的速度寻址:不遗漏一个字节,也不重复对一个字节寻址


∴ 对于程序来说,一个变量最好位于一个寻址步长的范围内,这样一次就可以读取到变量的值;如果跨步长存储,就需要读取两次,然后再拼接数据,效率显然降低了。

【例】例如一个 int 类型的数据

  • 如果地址为 8,那么很好办,对编号为 8 的内存寻址一次就可以。
  • 如果编号为 10,就比较麻烦,CPU需要先对编号为 8 的内存寻址,读取4个字节,得到该数据的前半部分,然后再对编号为 12 的内存寻址,读取4个字节,得到该数据的后半部分,再将这两部分拼接起来,才能取得数据的值。

【结论】将一个数据尽量放在一个步长之内,避免跨步长存储,这称为内存对齐

在32位编译模式下,默认以4字节对齐;
在64位编译模式下,默认以8字节对齐。


结 构 体 内 存 对 齐 结构体内存对齐
【例】

#include <stdio.h>
#include <stdlib.h>
struct{
    int a;
    char b;
    int c;
}t={ 10, 'C', 20 };
int main(){
    printf("length: %d\n", sizeof(t));
    printf("&a: %X\n&b: %X\n&c: %X\n", &t.a, &t.b, &t.c);
    system("pause");
    return 0;
}
length: 12
&a: B69030
&b: B69034
&c: B69038

考虑到内存对齐,虽然成员 b 只占用1个字节,但它所在的寻址步长内还剩下 3 个字节的空间,放不下一个 int 型的变量了,所以要把成员 c 放到下一个寻址步长。剩下的这3个字节,作为内存填充浪费掉了。

在这里插入图片描述
编译器之所以要内存对齐,是为了更加高效的存取成员 c,而代价就是浪费了3个字节的空间。


变 量 内 存 对 齐 变量内存对齐

#include <stdio.h>
#include <stdlib.h>
int m;
char c;
int n;
int main(){
    printf("&m: %X\n&c: %X\n&n: %X\n", &m, &c, &n);
    system("pause");
    return 0;
}

可见它们的地址都是4的整数倍,并相互挨着。

在VS下运行:
&m: DE3384
&c: DE338C
&n: DE3388

【经验】

  • 对于全局变量,GCC在 Debug 和 Release 模式下都会进行内存对齐,而VS只有在 Release 模式下才会进行对齐。
  • 而对于局部变量,GCC和VS都不会进行对齐,不管是Debug模式还是Release模式

虚拟地址与物理地址的映射

一对一 实现虚拟地址的映射

假设以程序为单位,把一段与程序运行所需要的同等大小的虚拟空间映射到某段物理空间。
【例】
例如程序A需要 10MB 内存,虚拟地址的范围是从 0X00000000 到 0X00A00000
假设它被映射到一段同等大小的物理内存,地址范围从 0X00100000 到 0X00B00000
即虚拟空间中的每一个字节对应于物理空间中的每一个字节。
在这里插入图片描述
当程序A需要访问 0X00001000 时,系统会将这个虚拟地址转换成实际的物理地址 0X00101000
访问 0X002E0000 时,转换成 0X003E0000,

这种以整个程序为单位的方法很好地解决了不同程序地址不隔离的问题,同时也能够在程序中使用固定的地址。

【优势】比起直接使用物理地址

  • 地址隔离

如上图所示,程序A和程序B分别被映射到了两块不同的物理内存,它们之间没有任何重叠,如果程序A访问的虚拟地址超出了 0X00A00000 这个范围,系统就会判断这是一个非法的访问,拒绝这个请求,并将这个错误报告给用户,通常的做法就是强制关闭程序。

  • 程序可以使用固定的内存地址

虚拟内存无论被映射到物理内存的哪一个区域,对于程序员来说都是透明的,我们不需要关心物理地址的变化,只需要按照从地址 0X00000000 到 0X00A00000 来编写程序、放置变量即可,程序不再需要重定位。

【存在问题】

  • 内存使用效率问题

以程序为单位对虚拟内存进行映射时,如果物理内存不足,被换入换出到磁盘的是整个程序,这样势必会导致大量的磁盘读写操作,严重影响运行速度,所以这种方法还是显得粗糙,粒度比较大。


内存分页机制 实现虚拟地址的映射

【前景提示】当一个程序运行时,在某个时间段内,它只是频繁地用到了一小部分数据,也就是说,程序的很多数据其实在一个时间段内都不会被用到。

【一对一的缺陷】以整个程序为单位进行映射,不仅会将暂时用不到的数据从磁盘中读取到内存,也会将过多的数据一次性写入磁盘,这会严重降低程序的运行效率。

∴ 现代计算机都使用分页(Paging)的方式对虚拟地址空间和物理地址空间进行分割和映射,以减小换入换出的粒度,提高程序运行效率。


分 页 思 想 分页思想

分页(Paging)的思想是指把地址空间人为地分成大小相等(并且固定)的若干份,这样的一份称为一页,就像一本书由很多页面组成,每个页面的大小相等。
如此,就能够以页为单位对内存进行换入换出:

  • 当程序运行时,只需要将必要的数据从磁盘读取到内存,暂时用不到的数据先留在磁盘中,什么时候用到什么时候读取。
  • 当物理内存不足时,只需要将原来程序的部分数据写入磁盘,腾出足够的空间即可,不用把整个程序都写入磁盘。

页 的 大 小 页的大小
目前几乎所有PC上的操作系统都是用 4KB 大小的页。
假设我们使用的PC机是32位的,那么虚拟地址空间总共有 4GB,按照 4KB 每页分的话,总共有

2^32 / 2^12 = 2^20 = 1M = 1048576 个页;

物理内存也是同样的分法。


根 据 页 进 行 映 射 根据页进行映射

在这里插入图片描述
【基础知识】

  • 虚拟空间的页叫做虚拟页(VP,Virtual Page)
  • 物理内存中的页叫做物理页(PP,Physical Page)
  • 磁盘中的页叫做磁盘页(DP,Disk Page)。

【前景铺垫】程序1和程序2的虚拟空间都有8个页,为了方便说明问题,我们假设每页大小为 1KB,那么虚拟地址空间就是 8KB。
假设计算机有13条地址线,即拥有 2^13 的物理寻址能力,那么理论上物理空间可以多达 8KB。
但是出于种种原因,购买内存的资金不够,只买得起 6KB 的内存,所以物理空间真正有效的只是前 6KB。(pp0-pp5)可用

当我们把程序的虚拟空间按页分隔后,把常用的数据和代码页加载到内存中,把不常用的暂时留在磁盘中,当需要用到的时候再从磁盘中读取。
【实例】上图中,我们假设有两个程序 Program 1 和 Program 2

  • 它们的部分虚拟页面被映射到物理页面,比如 Program 1 的 VP0、VP1 和 VP7 分别被映射到 PP0、PP2 和 PP3;
  • 而有部分却留在磁盘中,比如 VP2、VP3 分别位于磁盘的 DP0、DP1中;
  • 另外还有一些页面如 VP4、VP5、VP6 可能尚未被用到或者访问到,它们暂时处于未使用状态。

【后续操作】Program 1 的 VP2、VP3 不在内存中,但是当进程需要用到这两个页的时候,硬件会捕获到这个消息,就是所谓的页错误(Page Fault),然后操作系统接管进程,负责将 VP2 和 PV3 从磁盘中读取出来并且装入内存,然后将内存中的这两个页与 VP2、VP3 之间建立映射关系。


页表:内存地址转换表

在这里插入图片描述
内存地址的转换是通过一种叫做页表(Page Table)的机制来完成的
【问题】

  • 页表是什么?为什么要采用页表机制,而不采用其他机制?
  • 虚拟地址如何通过页表转换为物理地址?

不 使 用 页 表 , 直 接 使 用 数 组 转 换 不使用页表,直接使用数组转换 使使
最容易想到的映射方案是使用数组:

  • 每个数组元素保存一个物理地址
  • 而把虚拟地址作为数组下标

这样就能够很容易地完成映射
在这里插入图片描述
【缺点】但是这样的数组有 2^32 个元素,每个元素大小为4个字节,总共占用16GB的内存,占的空间太大了


使 用 一 级 页 表 使用一级页表 使

既然内存是分页的,只要我们能够定位到数据所在的页,以及它在页内的偏移(也就是距离页开头的字节数),就能够转换为物理地址。

【前景提示】2^12 为一个页的大小,也就是4K。

【例】例如,一个 int 类型的值保存在第 12 页,页内偏移为 240,那么对应的物理地址就是

 2^12 * 12 + 240 = 49392。

虚拟地址空间大小为 4GB,总共包含

 2^32 / 2^12 = 2^20 = 1K * 1K  = 1M = 1048576 个页面

我们可以定义一个这样的数组:

它包含 2^20 = 1M 个元素,
每个元素的值为页面编号(也就是位于第几个页面),
长度为4字节,
整个数组共占用4MB的内存空间。

这样的数组就称为页表(Page Table),它记录了地址空间中所有页的编号。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值