🔥个人主页: Milestone-里程碑
❄️个人专栏: <<力扣hot100>> <<C++>><<Linux>>
🌟心向往之行必能至
目录
[一. 以往知识回顾](#一. 以往知识回顾)
[3.1 解析地址相同,值不同](#3.1 解析地址相同,值不同)
[3.2 虚拟内存管理](#3.2 虚拟内存管理)
[3.2.1 描述进程](#3.2.1 描述进程)
[3.2.2 调整区域划分](#3.2.2 调整区域划分)
[4.1 将"无序"变"有序"](#4.1 将"无序"变"有序")
[4.2 保护物理内存](#4.2 保护物理内存)
一. 以往知识回顾
想必之前在学C时,我们都见过这张图

可以看到上面即有内核空间,也有用户空间
但这里告示你个结论,这里你取到的所有地址,和以往你使用的指针都是虚拟内存,非物理内存
二.验证虚拟内存
bash
[lcb@hcss-ecs-1cde 3]$ ./test
parent[876]: 0 : 0x601058
child[877]: 1 : 0x601058
parent[876]: 0 : 0x601058
parent[877]: 1 : 0x601058
child[878]: 2 : 0x601058
child[879]: 1 : 0x601058
parent[876]: 0 : 0x601058
parent[877]: 1 : 0x601058
parent[879]: 1 : 0x601058
parent[878]: 2 : 0x601058
child[880]: 1 : 0x601058
child[882]: 2 : 0x601058
child[881]: 2 : 0x601058
child[883]: 3 : 0x601058
parent[876]: 0 : 0x601058
parent[877]: 1 : 0x601058
parent[879]: 1 : 0x601058
parent[878]: 2 : 0x601058
parent[880]: 1 : 0x601058
parent[883]: 3 : 0x601058
parent[882]: 2 : 0x601058
child[885]: 2 : 0x601058
child[884]: 1 : 0x601058
parent[881]: 2 : 0x601058
child[886]: 2 : 0x601058
child[890]: 2 : 0x601058
child[893]: 3 : 0x601058
child[894]: 3 : 0x601058
child[887]: 3 : 0x601058
child[891]: 4 : 0x601058
源代码:
bash
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return ;
else if(id == 0){
//child
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else
{ //parent
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
bash
parent[3767]: 0 : 0x601058
child[3768]: 0 : 0x601058
我们这里可以看到parent与child共享同一空间,其中原因我们在之前讲解fork已经说过,不再赘述
而我们知道,如果其中一个变量发生变化,对于父与子进程就会发生写实拷贝
那么如果他们不共享空间呢
源码
bash
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 int g_val = 0;
5 int main()
6 {
7 while(1)
8 {
9 pid_t id = fork();
10 if(id < 0){
11 perror("fork");
12 return 1;
13 }
14 else if(id == 0){ //child
15 ++ g_val;
16 printf( "child[%d]: %d : %p\n", getpid(), g_val, &g_val);
17 }
18 else{ //parent
19 printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
20 }
21
22 sleep(1);
23 }
24 return 0;
25 }
结果
bash
[lcb@hcss-ecs-1cde 3]$ ./test
parent[876]: 0 : 0x601058
child[877]: 1 : 0x601058
parent[876]: 0 : 0x601058
parent[877]: 1 : 0x601058
child[878]: 2 : 0x601058
child[879]: 1 : 0x601058
parent[876]: 0 : 0x601058
parent[877]: 1 : 0x601058
parent[879]: 1 : 0x601058
parent[878]: 2 : 0x601058
child[880]: 1 : 0x601058
child[882]: 2 : 0x601058
child[881]: 2 : 0x601058
child[883]: 3 : 0x601058
parent[876]: 0 : 0x601058
parent[877]: 1 : 0x601058
parent[879]: 1 : 0x601058
parent[878]: 2 : 0x601058
parent[880]: 1 : 0x601058
parent[883]: 3 : 0x601058
parent[882]: 2 : 0x601058
child[885]: 2 : 0x601058
child[884]: 1 : 0x601058
parent[881]: 2 : 0x601058
child[886]: 2 : 0x601058
child[890]: 2 : 0x601058
child[893]: 3 : 0x601058
child[894]: 3 : 0x601058
child[887]: 3 : 0x601058
child[891]: 4 : 0x601058
我们能够发现每次执行 子进程与父进程的g_val不同,但输出的地址却是一样
如果上面存储的内存还为物理内存的话,又因为内存都有对应的值,那么就会出现问题了
因此:
变量内容不⼀样,所以⽗⼦进程输出的变量绝对不是同⼀个变量
但地址值是⼀样的,说明,该地址绝对不是物理地址!
在Linux地址下,这种地址叫做 虚拟地址
我们在⽤C/C++语⾔所看到的地址,全部都是虚拟地址!物理地址,⽤⼾⼀概看不到,由OS统⼀管理
OS则必须将虚拟地址转换为物理地址
三.进程地址空间
3.1 解析地址相同,值不同
那么该如何理解输出地址相同,但值不同呢?
原因就在于虚拟地址空间有页表

这里页表每行存储两个值,一个是虚拟空间的地址,一个是物理内存的地址,一一对应
在g_val发生变化之前,父子进程共用一个列表,但变化后,为了保证进程的独立性,系统先写实拷贝一份,子进程再进行修改

页表将虚拟地址(逻辑地址)映射到物理地址。每个进程拥有独立的页表,确保进程间的内存隔离,这也是为什么输出地址相同,而值不同
3.2 虚拟内存管理
与之前提到的OS管理一样,数据少时,直接管理也OK,但数据量大时,就效率过低了,此处一样要先描述再组织
描述linux下进程的地址空间的所有的信息的结构体是 mm_struct (内存描述符)。每个进程只有⼀ 个mm_struct结构,在每个进程的 task_struct 结构中,有⼀个指向该进程的mm_struct结构体指针。
先描述各个进程描述,再通过数据结构组织
3.2.1 描述进程
上面的地址是以一个字节为单位的,而每个区都有自己的空间大小(类似你小学与同桌的三八线),而要获取你们使用三八线的桌子空间大小很简单,即每个人的空间大小,就是末位置-起始位置=长度
虚拟内存同样,所有对进程描述,mm_struct会有 int begin int end,这就叫做区域划分了,只需要知道起始与末位置即可,而管理虚拟内存,即将这些描述通过链表组织起来
查看源码也确实如此

3.2.2 调整区域划分
虚拟地址以字节为单位,通过页表,实现了虚拟内存与物理内存的划分,但我们有可能会遇到两种情况
一:每次OS从硬盘读取的数据大小不同,虚拟内存如果分配?
其实OS会先在虚拟内存加载该部分空间(前面提到的预加载),然后再将硬盘的数据大小加载到页表,与虚拟内存一一对应
二:如果出现一个区域的内存不足,如何处理?
解决办法与你和你同桌类型,划定三八线后,你或许会觉得自己的空间太小,此时你就可以把三八线往你同桌那边挪动
此处类似,假设有 x y区域 ,对y进行扩张 ,即将 x_end-n y_start-n即可
问题:
mm-struct的初始化值从何来?
加载的时候,进行初始化
四.为什么要有虚拟内存空间
4.1 将"无序"变"有序"
我们之前提到OS时,说过task_struct中的代码和数据都是绑定在一起的,此处我们思考发现:
经过页表的映射,我们并不需要物理内存连续,绑定了,只需要映射后的虚拟内存连续即可,这样可以灵活我们代码与数据的分布
4.2 保护物理内存
其实页表还有一列权限列,与我们前面提到的文件 目录权限一样,它规定了可以进行的 读 写 执行操作
bash
char * str ="hello";
str = "h";
如果在没学虚拟内存时,我们知道这个错,也只会说str是常量字符串,在创建时,只被给予了r -- 读的权限,因此不可修改
野指针问题:如果使用了野指针,很有可能会导致运行崩溃
那么页表就很好地解决了该问题,但释放了指针后,即将虚拟地址映射的物理空间释放了,找不到对应的值,避免了运行崩溃
让进程管理和内存管理进行一定的解耦合
五.问题
1.我们可以不加载代码和数据,只有task_struct mm_struct,页表
原因:如果读取到虚拟地址时,会发生缺页中断,去硬盘读取
2.创建进程,先有代码和数据,才会加载,才会有task_struct mm_sturct
先有struct
3.如何理解进程挂起?
在前面我们已经提到了进程挂起,即是在内存不足时,将一些优先级低的运行进程放回硬盘中,那么在这里,就是将一些优先级低的运行进程映射出的物理内存地址放回硬盘中,但保留虚拟空间地址,当需要时,通过该地址取出即可
4.通过malloc与new出来的空间在堆区,但不一定连续,一个strat end好像无法解决?
确实如此,但我们搜索可知,其实在定义时,定义了多组start,end,它们通过链表相连

- 上面的虚拟地址使用的地址是以一个字节为单位,那么int如何找呢?
先找到地址最小的位置,再往后找三个地址
进程具有独立性:
1.内核数据结构独立
2.加载进入的代码和数据独立
