目录
1.程序地址空间
2.进程地址空间
1.程序地址空间
我们在讲C/C++语言的时候,32位平台下,我们见过这样的空间布局图
我们来验证一下这张图的正确性:
int un_gval;
int init_gval=100;
int main(int argc, char* argv[],char* env[])
{
//代码区
printf("code addr: %p\n",main);
//字符常量区
const char *str = "hello Linux";
//*str = 'h';//不能修改因为字符常量区是被写入到代码区的,而代码区不能被修改
printf("read only char addr: %p\n",str);
//已初始化全局变量区
printf("init global value addr: %p\n",&init_gval);
//所谓的静态区就是已初始化全局变量区
static int a ;
printf("stack addr: %p\n",&a);
//已初始化全局变量区
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);
char *heap5 = (char*)malloc(100);
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("heap5 addr: %p\n",heap5);
//栈区
printf("stack1 addr: %p\n",&heap1);
printf("stack2 addr: %p\n",&heap2);
printf("stack3 addr: %p\n",&heap3);
printf("stack4 addr: %p\n",&heap4);
printf("stack5 addr: %p\n",&heap5);
//命令行参数
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;
}
运行结果:
通过观察静态变量的位置,可以认为静态变量就是全局变量,只是静态变量只初始化一次,有作用域的限制。
这里栈区还有一个特点:我们平时定义结构体对象时,我们取地址都是返回整个结构体最低的地址,内部是使用低地址向高地址排列,使用的是起始地址加偏移量的访问方式,但是栈区整体还是先使用高地址在使用低地址。
那么这里就有一个问题了,这张图是真实的物理内存吗?
我们再来验证一下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int g_val = 100;
int main()
{
pid_t id = fork();
if(id==0)
{
//子进程
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
{
//父进程
while(1)
{
printf("father, pid:%d, ppid:%d, g_val:%d ,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
sleep(1);
}
}
return 0;
}
运行上面代码的结果:
什么意思呢?就是我们定义了一个全局变量 g_val,然后我们通过 fork() 创建了一个子进程,让子进程修改了全局变量。我们之前文章中提到过,因为进程之间要保证数据的独立性,父进程的数据子进程也要有一份,而Linux采用写时拷贝,所以在子进程没有修改全局变量值时,父进程和子进程的全局变量地址相同可以理解。但是子进程对全局变量做修改后,写时拷贝应该重新申请一块空间来存放修改后的值,但是根据运行结果我们发现地址还是相同的,子进程全局变量的地址并没有改变,同一个地址竟然读出不同的值?所以我们可以大胆推测我们看到的地址并不是真正的物理地址。
得出结论:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
- 但地址值是一样的,说明,该地址绝对不是物理地址!
- 在Linux地址下,这种地址叫做 虚拟地址
- 我们在用C/C++语言所看到的地址,全部都是虚拟地址/线性地址!物理地址,用户一概看不到,由OS统一管理,OS必须负责将 虚拟地址 转化成 物理地址。
2.进程地址空间
2.1 操作系统如和将虚拟地址转换为物理地址
所以之前说“程序的地址空间”是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?
每一个进程运行之后,都会有一个进程地址空间的存在!都要在系统层面都要有自己的页表映射结构
在C/C++中,变量在编译形成可执行程序后,就没有变量名的概念了,都是地址。
2. 什么是地址空间?什么是区域划分?
地址空间也要被OS管理起来!!每一个进程都要有地址空间,系统中,一定要对地址空间做管理。如何管理地址空间呢? 也是通过之前文章提过的先描述,在组织。所以地址空间最终一定是内核的数据结构对象,就是一个内核结构体。
在这个结构体中,分别有每个空间如栈区,堆区的开始和结束位置。
在Linux中,这个进程/虚拟地址空间的东西,叫做:struct mm_struct
struct mm_struct
{
long code start;
long code_end;
long data_start;
long data_end;
long heap_start;
long heap_end;
long stack_start;
long stack_end;
// ...
}
进程PCB Linux 中的struct task_struct 中也是有指针指向mm_struct的。
3.为什么要有地址空间
- 让进程以统一的视角看待内存,所以任意一个进程,可以通过地址空间+页表可以将乱序的内存数据,变成有序,分门别类的规划好,使得无序边有序。
- 存在虚拟地址空间,因为页表中有访问权限字段,可以有效的进行进程访问内存的安全检查,比如我们无法修改字符常量的内容,是因为页表访问权限是只读。
- 将进程管理和内存管理进行解耦。
- 通过页表让进程映射到不同的物理内存处,从而实现进程的独立性!所以每一个进程都认为自己可以使用4GB的空间,但是真实的物理空间只有4GB,一个进程并不知道其他进程的存在。
- CPU中也有一个CR3寄存器来保存页表的地址,这个地址是真实的物理地址。
扩展问题
我们如果在玩一些大型游戏时,游戏所需要的内存非常大,我们之前学习过 进程 = 内核数据结构体PCB+程序的代码和数据,我们把游戏加载到内存中时,是把所有的代码和数据都拷贝过来吗?根据我们呢的常识,显然不是这样的,因为我们得内存很小,为什么游戏还是可以运行的呢?因为页表中还有是否分配空间和是否有内容的字段,00,表示既没有分配空间也没有内容,我们游戏一次只加载一部分代码和数据,当CPU执行完这段代码时,要执行下面代码,操作系统就会将上面字段改为00,出现缺页中断,然后再去磁盘中拷贝接下来的代码和数据,释放执行完的代码和数据,这样就可以使得我们得游戏可以正常运行。
本篇结束!