由下到上分别为代码区,字符串常量区,初始化全局变量,未初始化全局变量,堆区,栈区。地址由下到上增大。
栈区的变量按定义顺序向下分布,后定义的变量地址更低,所以栈区向下增长。
堆区向上增长,后定义变量,地址更高。
代码证明
a定义在b前,栈向下增长,next heap的addr大于heap addr,说明堆向上增长
static的变量被编译到全局数据区
如图,g_val_2的地址都是一样的,却有两个值,这必然不可能发生在物理地址上,所以实际上&获得的不是物理地址,指针的地址是虚拟地址或者叫线性地址
进程访问内存时,先访问进程空间上的虚拟地址,虚拟地址经过页表映射出内存的物理地址,物理地址才是数据的真实地址。
当创建子进程时,子进程拷贝了父进程的PCB(当然也有自己的数据如ppid和pid),子进程同时拷贝了父进程的进程地址空间和页表。
当子进程修改父进程的数据时,写实拷贝会在物理空间开辟新空间,并更改虚拟地址映射的物理地址。
所以实际上父子进程的两个相同虚拟地址的变量已经在不同的物理地址,所以才会出现值不同的情况。
进程地址空间是对物理内存空间的映射。
地址空间本质是内核的一个数据结构对象类似于PCB一样,地址空间也是要被操作系统管理的
每个进程运行时都有一份进程地址空间和页表,而物理内存空间是对于所有进程共有的。
32位机器的进程地址空间的大小为4GB:32位机器有32根总线,总线产生低电平记为0,高电平记为1,形成的地址范围位[0,2^32]刚好4GB。
每个进程的进程地址空间都统一为4GB(32位),每个进程都以为自己占有所有的物理内存,实际上根本操作系统不会允许单个进程申请过大空间,只是空间统一范围为4GB。
进程地址空间的区域划分
如图,进程地址空间是线性的,只有要有start和end就可以划分范围 如stack_start,stack_endl划分的栈的空间,划分范围是4GB。划分的数值保存在结构体中,Linux的进程PCB会有一个指针指向这个结构体。
如:每个进程的代码段都在进程地址空间一样的位置。而如果没有进程地址空间,PCB就必须频繁的修改,因为每次进程挂载后又重写运行代码段的位置可能都不一样。 空间在物理内存上可以任意位置申请,地址进程空间可以保证地址的线性关系
如,访问物理内存可以直接修改数据,可能造成数据误改。
如:页表存在读写选项,在写实拷贝发生前,子进程中的页表选项应该为r,防止子进程修改父进程在物理内存中的数据。
cpu中有个cr3寄存器保存页表起始地址,页表属于进程的硬件上下文数据,当cpu切换进程,页表会被保留到进程pcb中。
如页表中标志位表示读写,如char* a="abcde"在内存中应为只读权限
当操作系统发现数据存储在磁盘时,页表会进行缺页中断会把部分数据加载到内存,并补充对应的物理地址。
(读取大数据文件时,由于操作系统的惰性加载,会分批加载数据,进程读取未加载的数据时就需要根据页表的标识符加载数据到内存)
进程=内核数据结构(task_struct&&mm_struct&&页表)+程序的代码和数据
进程独立性:每个进程都有自己的PCB,页表的映射使得内存和进程解耦