Linux程序地址空间

引言

以下为示意草图 

下面以代码验证一下: 

  1 #include<stdio.h>                                                                                                                     2 #include<stdlib.h>3                                       4 int un_gval;                          5 int init_gval=666;                    6                                       7 int main()                            8 {                                     9   printf("code addr: %p\n", main);    10   const char *str = "hello Linux";    11                                       12    printf("read only char addr: %p\n", str);13    printf("init global value addr: %p\n", &init_gval);14    printf("uninit global value addr: %p\n", &un_gval);15                                                       16    char *heap1 = (char*)malloc(100);                  17    printf("heap1 addr : %p\n", heap1);18     printf("stack addr : %p\n", &str);19   return 0;                           20 }

其中堆区与栈区之间有一大块镂空,堆区向上增长,栈区向下增长,堆栈相对而生

代码验证堆区向上增长:

 16    char *heap1 = (char*)malloc(100);17    char *heap2 = (char*)malloc(100);18    char *heap3 = (char*)malloc(100);19    char *heap4 = (char*)malloc(100);20    printf("heap1 addr : %p\n", heap1);21    printf("heap2 addr : %p\n", heap2);22    printf("heap3 addr : %p\n", heap3);23    printf("heap4 addr : %p\n", heap4);

 

代码验证栈区向下增长:

25    printf("stack addr : %p\n", &str);26    printf("stack addr : %p\n", &heap1);27    printf("stack addr : %p\n", &heap2);28    printf("stack addr : %p\n", &heap3);29    printf("stack addr : %p\n", &heap4); 

 

在栈区上开辟的变量,整体是向下增长的,但是每个对象的使用是局部向上使用

以该对象的的最低地址作为地址

  7 struct s  8 {  9   int a;  10   int b;  11   int c;  12 }obj;13   printf("%p\n",&obj.a);14   printf("%p\n",&obj.b);15   printf("%p\n",&obj.c);    

 

在栈区之上是命令行参数与环境变量存储区域

代码验证:

int main(int argc, char *argv[], char *env[])
{ 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;
}

 

以下图所示真的是我们认为的内存吗? 

 来看一段代码:

int g_val = 555;int main()
{pid_t id = fork();if(id == 0){//childint 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=888;printf("child change g_val: 555->888\n");}cnt--;}}else{//fatherwhile(1){printf("father, Pid: %d, Ppid: %d, g_val: %d, &g_val=%p\n", getpid(), getppid(), g_val, &g_val);sleep(1);}}sleep(100);return 0;
}

这里有一个问题:对同一个地址进行读取,竟然读取到了不同的内容

变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
但地址值是一样的,说明,该地址绝对不是物理地址
在Linux地址下,这种地址叫做 虚拟地址

结论:C/C++看到的地址,绝对不是物理地址,物理地址,用户一概看不到,由OS统一管理(OS必须负责将 虚拟地址 转化成 物理地址

           我们平时用到的地址,都是虚拟地址/线性地址 

什么是进程地址空间

每一个进程运行之后,都会有一个进程地址空间的存在,也都要在系统层面有自己的页表映射结构 ,进程地址空间不存储任何数据,所有数据都存储在物理内存中

每个进程都有自己的task_struct结构体

 

子进程尾修改数据前与父进程共享代码和数据,它们的虚拟地址是一样的,根据虚拟地址在页表映射出的物理地址也是一样的,所以前期打印出来的g_val的值相同

当子进程要修改g_val,那么发生写时拷贝,子进程页表中的g_val的物理地址改变,虚拟地址不变,所以子进程打印g_val=888 父进程打印g_val=555 ,&g_val父子进程打印出来都相同 

因为我们打印出来的不是真正的物理地址,而是虚拟地址

结论:1 写时拷贝发生在物理内存中  2 这份工作由操作系统来完成 3 知道与否不会影响上层语言

每个进程要被OS管理(先描述再组织),所以每个进程都有各自的task_struct, 每个进程都有的地址空间,所以地址空间也要被OS管理 (先描述再组织)-->地址空间最终一定是一个内核的数据结构对象,就是一个内核结构体

在linux中,这个进程/虚拟地址空间,叫做:mm_struct 

其中每个进程的地址空间都有区域划分,有每个区域的start与end

 挑重点:

每个进程被创建时,既有自己的PCB即task_struct 也有自己的地址空间即mm_struct 

 为什么要有地址空间

1 让进程以统一的视角看待内存,所以任意一个进程,可以通过地址空间+页表将乱序的内存数据,变成有序,分门别类地规划好

2  存在虚拟地址空间,可以有效地进行进程访问内存的安全检查

页表结构中除却虚拟地址与物理地址外,还存在访问权限字段

举个例子:我们常说字符常量区不可被修改,所以以下代码是不被通过的:

char *str = "hello Linux";
*str = 'H';

为什么呢? 那曾经又是怎么被加载的?内存不是可以被读写的吗?

原因:字符串常量区的数据在页表映射时,它的访问权限字段被设置为"r"(只读),所以试图修改字符串常量区数据的写入操作在页表映射阶段就被拦截,所以无法修改。

每个进程有各自的地址空间,各自的页表,那么在众多页表中,每个进程怎么找到属于自己的那一张呢?

原因:进程进行各种虚拟地址到物理地址的转换,各种访问内存,一定是这个进行正在CPU上运行

CPU内有一个叫做CR3的寄存器,会存储该进程页表的物理地址,当该进程在CPU上加载时,会把自己的上下文数据中存有的页表地址加载到CR3中,当该进程要切换走时,会把CR3寄存器中存储的页表地址一并带走,所以每个进程都有自己的页表

3 将进程管理与内存管理进行解耦 

页表结构中还存在一个字段用于:内存是否分配以及是否有内容

此外,通过页表让进程的代码和数据映射到不同的物理内存处,从而实现进程的独立性质

进程=内核数据结构+进程的代码和数据

最后,起始完整的地址空间如下图: