1.程序地址空间(虚拟地址)结果图

- 地址按照从正文代码到环境变量依次增大。
- 栈的增长相反
- 程序地址空间不是内存
- 程序地址空间(虚拟空间 / 进程地址空间)不是语言层的概念,是系统的概念
证明程序地址空间不是内存
cpp
#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 0;
}
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;
}

我们发现,输出出来的变量值和地址是⼀模⼀样的,很好理解呀,因为⼦进程按照⽗进程为模版,⽗ ⼦并没有对变量进⾏进⾏任何修改。
cpp
#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 0;
}
else if(id == 0){ //child,⼦进程肯定先跑完,也就是⼦进程先修改,完成之后,⽗进程
再读取
g_val=100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}else{ //parent
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}

我们发现,⽗⼦进程,输出地址是⼀致的,但是变量内容不⼀样!能得出如下结论:
• 变量内容不⼀样,所以⽗⼦进程输出的变量绝对不是同⼀个变量
• 但地址值是⼀样的,说明,该地址绝对不是物理地址!
• 在Linux地址下,这种地址叫做 虚拟地址
• 我们在⽤C/C++语⾔所看到的地址,全部都是虚拟地址!物理地址,⽤⼾⼀概看不到,由OS统⼀ 管理
OS必须负责将 虚拟地址 转化成 物理地址。
2.引入新概念


2-1 一个进程对应一个虚拟地址空间
(1)虚拟地址空间 != 物理内存,它和物理内存之间的内在联系是页表,通过页表构成一个映射关系
(2)task_struct描述一个进程,对应一个虚拟地址空间。
(3)分为32位机器和64位机器:
例如:
32位机器-2^32个地址 = 4GB,一个地址表示一个字节。
2-2 一个进程对应一套页表
用户空间可以直接拿到地址直接访问带目标代码。
解释:
页表两边分别代表的是:虚拟地址+物理地址。
页表是用来做虚拟地址和物理地址映射的
2-3 子进程写时拷贝

父进程有自己的虚拟地址空间,子进程也要有自己的虚拟空间和页表。
子进程拷贝父进程PCB,地址空间,页表拷贝自父进程。
当子进程对变量进行修改时。
由于进程具有独立性!!!子进程g_val++对父进程g_val进行写时拷贝,新开辟地址空间,物理空间改变,虚拟地址空间不变。
所以这就可以解释为什么输出地址一致,但是变量不同,因为那对应的是虚拟地址,而真实的数据存储在真实的物理内存中。
3.虚拟地址与进程地址空间
3-1 大富翁例子

一个富翁给他的每一个私生子都画一个大饼,跟他们说一旦他完成了一个什么什么目标,就把自己全部的财产都给他。
总体:让任意一个进程都以为自己独占整个物理内存。
如何管理饼(虚拟地址空间)?先描述,再组织。
本质是通过一个数据结构来组织:struct mm_struct 。
区域划分:来规定虚拟地址空间各个区域的开始和结尾:
桌子:地址空间 小朋友:每个的空间规定开始和结束 刻度:就是地址。
4.具体是如何实现的?

虚拟地址空间范围:00000....~fffff.....
什么叫做区域划分?
标识确认区域的开始和结束即可,也可以添加区域的大小。
例如:
桌子上的每一个刻度叫做地址。
知道桌子的大小,那么就对桌子进行统一编址。
内核管理下的区域划分

总地址空间,开始,结束, 开始, 结束
综上所述

物理地址转化成虚拟地址的过程:
(1)通过mm_struct申请开辟空间和初始化数据:
1.开辟空间
2.初始化从哪来?
程序加载的过程中,进行初始化。
(2) 一个程序加载到内存后:
1.虚拟地址空间中申请指定大小的空间 --- 调整区域划分2.加载程序,申请物理空间
3.页表进行虚拟地址空间和物理地址空间的映射,完毕
5.为什么我们需要地址空间

(1)代码的地址一定是连续的,地排布是规律的。
页表的映射->使得无序变得有序
(2)在地址转换过程中,对地址的操作进行合法性判定 -- 保护物理内存
访问某个代码或者变量的时候,需要把虚拟地址转化成为物理地址。页表还有读写权限,如果要对代码区进行写入,但只有读权限,那么就会实现对物理内存的保护"转化过程中,也可以对你的地址和操作进行合法性判定",实现对物理内存的保护
a.什么是野指针?
对一个已经释放的空间进行访问,就会造成野指针
但不一定会造成程序崩溃。
b.char *str = "helloworld";*str = 'H';
能编译过,但运行时不行
字符串是字符常量区,只读,不可以写或者修改。强行进程写入会导致崩溃。
为什么在字符常量区写入会崩溃?
答:查找页表的时候没权限被拦截了。
(3).解耦合, 不让虚拟地址空间直接指向物理内存
(4).缺页中断:
虚拟地址找不到对应的物理地址,那么就把数据代码重新加载,新生成一个物理内存,再次形成对应。
澄清一些问题:
1.我们可以不加载对应的代码和数据,只有task_struct,mm_struct,页表即可, 因为缺页保护会自动把数据和代码进行加载
2.创建进程,先有task_stcurt,mm_struct等,还是先加载代码和数据?
现有内核数据结构 --- task_stcurt,mm_struct等。
3.如何理解进程挂起?
只保留内核数据结构(task_stcurt,mm_struct),页表清空, 代码数据等换出磁盘,省出空间。
堆区,不止一个吧?brk,不止一个起始虚拟地址吧?
栈有栈顶和栈底,移动指针即可,堆的时候可能new malloc10几次,说不通。
vm_area_struct(list链表链接起来的),每一个子区域的有start和end,一个区域一个vm_area_struct
形成链表把多个堆区管理起来
mm_struct对整个堆区的整体描述,指向vm_struct的链表,方便对vm_area_struct进行管理
vm_area_struct包含起始地址和结束地址,已经前后区域的指针
vm_area_struct:局部代码区域
mm_area_struct:整体代码区域
进程具有独立性
1.内核数据结构独立
2.加载进的内存的代码和数据独立


每个区域都有vm
为什么需要地址空间?
1.保护物理内存的所有合法数据
2.进程管理和内存管理完成解耦合
3.进程视角下内存分布都可以是有序的







