目录
前言
早在学C语言阶段, 我们就时常听到栈区, 堆区, 常量区...等这些概念, 那他们到底是什么? 内存不是一大块空间吗? 为什么还要分区域, 我们在正常使用电脑时怎么没有注意过? 带着这些问题, 本文将为大家进行解惑;
1.程序地址空间初始理解
大多数现代编程语言中都存在的内存管理概念。它们是用于描述程序在运行时如何分配和管理内存的区域, 在C/C++中尤其的明显;
1.1 各区域在内存中的划分
根据前边C/C++的基础知识, 我们可以编写一个程序来验证一下:
int un_gval;
int init_gval = 100;
int main(int argc, char *argv[], char *env[])
{
printf("code addr: %p\n", main);
char *str = "hello Linux";
printf("read only char addr: %p\n", str);
printf("init global value addr: %p\n", &init_gval);
printf("uninit global value addr: %p\n", &un_gval);
char *heap1 = (char *)malloc(100);
char *heap2 = (char *)malloc(100);
char *heap3 = (char *)malloc(100);
char *heap4 = (char *)malloc(100);
static int a = 0;
// static修饰前后对比: static修饰后本质变成了全局变量
printf("heap1 addr : %p\n", heap1);
printf("heap2 addr : %p\n", heap2);
printf("heap3 addr : %p\n", heap3);
printf("heap4 addr : %p\n", heap4);
printf("stack addr : %p\n", &str);
printf("stack addr : %p\n", &heap1);
printf("stack addr : %p\n", &heap2);
printf("stack addr : %p\n", &heap3);
printf("a addr : %p\n", &a);
int i = 0;
for (; argv[i]; i++)
{
printf("argv[%d]: %p\n", i, argv[i]); // 命令行参数
}
for (i = 0; env[i]; i++)
{
printf("env[%d]: %p\n", i, env[i]); // 环境变量
}
return 0;
}
这里就不再演示执行结果, 感兴趣的可以去验证一下;
经过验证可以得出以下区域分布:
数组在内存中如何存储?
1.2 Linux中父子进程的进程地址空间
使用fork创建进程, 子进程修改父进程中变量的数据, 根据前边的知识基础, 子进程修改数据时会进行写时拷贝, 我们可以编写一个小程序验证一下:
int g_val = 100;
int main()
{
pid_t id = fork();
if (id == 0)
{
//child
int cnt = 5;
while (1)
{
printf("child, Pid: %d, Ppid: %d, g_val: %d, &g_val=%p\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
if (cnt == 0)
{
g_val = 200;
printf("child change g_val: 100->200\n");
}
cnt--;
}
}
else
{
//father
while (1)
{
printf("father, Pid: %d, Ppid: %d, g_val: %d, &g_val=%p\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
}
}
sleep(100);
return 0;
}
经过测试发现, 子进程修改了值以后父子进程值的地址依然相同, 相同的地址为什么会读取出不同的内容?
至此可以说明: 我们平时在C/C++中看到的地址绝对不是物理地址! 而是虚拟地址;
原理解释:
每个进程都会有自己的进程地址空间, 进程地址空间中的地址全部都是虚拟地址, 虚拟地址并不存储实际的数据, 数据实际存储在物理内存当中, 按照图中所示, 假设我们存储10这个数字, 在物理内存中有它自己的物理地址, 物理地址通过页表与虚拟地址建立连接, 形成映射结构;
父进程在创建子进程时, 会将自己的数据拷贝一份给子进程(包含页表); 父进程与子进程虚拟地址相同, 页表相同, 最终就指向了相同的物理地址;
现在回到最初的问题: 为什么相同的地址会看到不同的值?
当子进程对数据进行修改时:
子进程对数据进行修改, 就会导致写时拷贝: 在物理内存中将数据拷贝一份, 然后将子进程页表中的物理地址修改为新的物理地址;
在子进程修改数据时, 修改的就是新拷贝的数据, 这也就导致了为什么相同的虚拟地址看到的是不同的数据(页表中的虚拟地址与物理地址的映射发生了改变) ;
注意:
这时的子进程与父进程依然共用同一份代码, 只有数据的映射发送变化;
2. OS中的进程地址空间
上述的结构是在单个进程中的结构, 那在OS中又是怎么表现的呢? 进程地址空间与OS的关系又是什么? OS如何管理这些进程的地址空间吗?
可以这样理解, 假设OS就是一个大富翁, 而内存就算他的财富, OS下运行的每个进程可以理解为是他的子女, 他们(进程相互独立)互相都不知道其他人的存在, 每个进程在执行任务时, OS会向进程分配空间;
大富翁(OS)的这些子女(进程)都认为自己将来可以得到所有财富(内存, 但实际上并不会) , 这里涉及到页表映射机制, 本文不进行详细解释; 后边会进行介绍, 可以先这样理解;
多个进程可以在OS下运行, 那么OS是如何管理这些进程地址空间的? 六字真言(先描述,再组织);
所谓的地址空间在OS中最终一定是一个内核数据结构对象:
结构复杂, 简化一下:
而OS再将这些结构化对象组织成链表, 那么对这些进程地址空间的管理就变成了对链表的增删查改;
在进程PCB中(进程控制块), 有着一个struct mm_struct* mm这样的指针, 这个指针会指向struct mm_struct对象, 这样进程就有了进程地址空间;
3. 虚拟地址空间设计的优点
- 更加合理高效的使用内存
任意一个进程, 都可以通过进程地址空间+页表, 将乱序的内存, 变得有序, 进行规划;
- 访问内存的安全检查
页表的结构其实还是比较复杂的, 它不仅可以映射内存, 还可以进行安全检查, 每个地址都有对应的标识字段, 标识该空间的访问权限, 以及这块空间是否分配(已使用) ;
- 将进程管理和内存管理进行解耦
把可执行程序从磁盘加载到内存当中, 可以是任意位置, 代码和数据也可以分开存储, 可以随意的加载, 有页表存在, 建立映射, 进程可以以统一的视角看待内存;
进程在运行时, CPU如何知道当前进程的页表呢?
每个进程都有自己的页表, 进程在被CPU执行时, CPU中有一个叫CR3的寄存器, 它记录着当前进程页表的地址;
注意:
CPU中的CR3寄存器存储的是页表的物理内存;
联系进程的切换: CPU在进行进程切换时, 会把CR3寄存器中进程的页表地址存储到进程的PCB当中(进程上下文);
页表中有一个标记为, 记录内存空间是否分配, 为什么会有这个标志位?
在现实使用中, 比如大一些大型游戏(可执行程序较大), 不可能一次性完全加载到内存当中, 它只会加载一部分, 在内存中先开一小块空间给进程, 然后通过页表映射; 当需要新的数据时, 就会把暂时不用的数据挂起, 加载到磁盘当中, 然后将需要的数据换入;
如何判断数据已经被换出呢? 就需要这个标记位, 数据被换出, 就改为未分配状态; 如果页表中进程地址空间对应的标记位是未分配, 那么我们就可以认为当前进程被挂起;
拓展
进程在运行代码时, 只需要访问虚拟地址, 进程需要内存时, OS就会给进程申请, 然后将数据加载进来;
如果进程访问一块虚拟地址数据中, 该虚拟地址没有分配空间, 这时OS就会暂停进程:
- 在内存中开一块空间
- 把需要的代码和数据加载到内存当中
- 把对应的物理内存填充到进程的页表当中
- 将标志位进行修改(表示已分配)
- 继续进程执行
总结:
虚拟地址没有分配, OS暂停进程, 在内存申请空间, 将数据和代码进行加载, 修改页表, 这个过程就叫做缺页中断;
在每个进程的mm_struct对象里还会有一个, vm_area_struct* mmap的结构体指针;
该结构体对象的主要作用是对mm_struct分配的内存空间, 进行更细致的划分, mm_struct对象中可以包含多个vm_area_struct对象, 每个vm_area_struct对象描述一块内存区域, 这些结构体对象按照地址的顺序连接起来, 形成一个链表结构, 虚拟内存区域链表;
内存不是一大块空间吗? 为什么还要分区域, 我们在正常使用电脑时怎么没有注意过?
总结
回到文章的开始, 在日常使用电脑时,用户通常与高层次的图形用户界面(GUI)交互,而不需要直接接触或管理内存的低层细节。这让许多人忽视了内存区域的划分和管理; 对于开发者和计算机科学学生来说,了解内存的划分和管理则是更为核心的知识,因为这直接影响到编写高效和安全的代码, 以上便是本文的全部内容, 希望对你有所帮助, 感谢阅读!