进程地址空间
进程地址空间,本质是一个描述进程可视范围的大小,
地址空间内一定要存在各种区域划分,对线性地址进行start,和end即可
在每一个区的_start 到_end 范围内,这段连续的空间中,每一个最小单位都可以有地址,这个地址可以被进程直接使用
进程地址空间本质上是内存中的一种内核数据结构,在Linux当中进程地址空间具体由结构体mm_struct实现
struct mm_struct
{
unsigned int code_start; //代码区
unsigned int code_end;
unsigned int init_start;//初始化数据
unsigned int init_end
;unsigned int uninit_start//未初始化数据
;unsigned int uninit_end
;unsigned int heap _start//堆区
;unsigned int heap_end
;unsigned int stack _start//栈区
;unsigned int stack_end;
};
#include<stdio.h>
#include<stdlib.h>
int g_val_1; // 未初始化数据
int g_val_2 = 100; // 已初始化数据
int main()
{
printf( "code : %p\n", main );
const char *str = "hello bit";
printf("string: %p\n", str);
printf("init global value: %p\n", &g_val_2);
printf("uninit global value: %p\n", &g_val_1);
char *mem = (char *)malloc(100);
printf("heap: %p\n", mem);
printf("stack: %p\n", &str); // 局部变量存放在栈区, str是局部变量
return 0;
}
栈 是向下增长的 , 堆区是向上增长的
原因: 先定义的变量先入栈 ,后定义的变量后入栈 ,栈向地址减少的方向增长
从图中可以看出堆区是向上增长的
如果有一个局部变量前面加了static ,生命周期就会变成全局变量
原因:static修饰的局部变量,编译的时候已经被编译到全局数据区
同一个变量,同一个地址,同时读取,读到了不同的内容
结论:如果变量的地址,是物理地址,不可能存在上面的现象,也就是说变量的地址是虚拟地址(线性地址)
操作系统在创建父进程的同时,也创建了父进程的进程地址空间,父进程的PCB是有对应的指针指向该进程地址空间
系统还会为父进程创建页表 ,页表是一种kv式的映射关系 ,可以在页表通过虚拟地址来找到物理地址,通过物理地址找到物理内存
进程在访问虚拟地址时,操作系统会查页表,将虚拟地址转换为物理地址,就可以访问到物理地址里面的内容
当父进程开始创建子进程时,系统中创建一个子进程一定是系统中多了一个子进程,多的一个进程也需要被操作系统管理起来,所以操作系统也需要给子进程创建PCB结构,也会给子进程创建进程地址空间,子进程会将父进程的进程地址空间拷贝一份,子进程将父进程的页表结构也拷贝一份
只要父进程和子进程的页表内容一样,父进程和子进程的地址空间也是一样的,虚拟地址到物理地址之间的转换也是一样的,所以父进程和子进程可以共享代码
如果需要将g_val从100修改为200,操作系统会将0x60105c进行写入,在写入之前系统发现g_val这个变量是和父进程共享的,所以在写入之前,操作系统会在物理内存重新开辟一段空间,将0x60105c所对应的物理地址修改,假设修改成0x44332211(写时拷贝)
并将100改成200
为什么要有进程地址空间?
让进程以统一的视角看待内存
增加虚拟地址:
增加进程虚拟地址空间可以我们访问内存的时候,增加一个转换的过程,在这个转化的过程中,可以对我们的寻址请求进行审查,一旦异常访问,直接拦截,该请求不会到达物理内存,保护物理内存
补充页表:
cr3寄存器保存的是当前进程的页表的起始地址,也叫做当前进程正在运行的临时数据
,本质上属于进程的硬件上下文
如果此时进程被切换走了,操作系统会将cr3寄存器中的内容带走,当进程继续运行时,会将cr3寄存器中的页表地址恢复
字符常量区是只能读不能写,进程如何知道?
当操作系统需要访问字符常量区时,CPU读取到虚拟地址,CPU通过cr3寄存器找到页表,并查询页表,发现权限是只读不能写,如果你向该位置写入,操作系统会直接拦截
进程是可以被挂起的 ,但是Linux并没有挂起状态 ,操作系统如何知道进程的代码和数据在不在内存?
操作系统通过页表的标志位来判断对应的代码和数据是否已经被加载到内存
如果操作系统发现对应的代码和数据并没有加载到内存中,此时操作系统会出发缺页中断
然后操作系统就会找到可执行程序的代码,并在物理内存中重新申请一份空间,将可执行程序的代码和数据加载到刚刚重新申请一份空间中,再把这一份空间的地址填到页表当中,如果再访问就可以对应的代码和数据
进程在被创建的时候,是先创建内核数据结构?还是先加载对应的可执行程序呢?
先创建内核数据结构再加载对应的可执行程序
进程=内核数据结构(task_struct && mm_struct &&页表)+程序的代码和数据
因为有地址空间和页表的存在,将进程管理模块,和内存管理模块进行解耦合