前言
我们在之前学习过一张表,里面存放着栈,堆,常量区等,我们认为这就是内存。
其实,更具体来讲,应该是进程地址空间,我们一起来了解一下吧!!!
一、程序地址空间
我们在之前学习过这张图
我们可以写一段代码验证一下
#include <stdio.h>
#include <stdlib.h>
int g_unval;
int g_val = 100;
int main(int argc, char *argv[], char *env[])
{
printf("code addr: %p\n", main);
printf("init data addr: %p\n", &g_val);
printf("uninit data addr: %p\n", &g_unval);
char *heap = (char*)malloc(20);
char *heap1 = (char*)malloc(20);
char *heap2 = (char*)malloc(20);
char *heap3 = (char*)malloc(20);
static int c;
printf("heap addr: %p\n", heap);
printf("heap1 addr: %p\n", heap1);
printf("heap2 addr: %p\n", heap2);
printf("heap3 addr: %p\n", heap3);
printf("stack addr: %p\n", &heap);
printf("stack addr: %p\n", &heap1);
printf("stack addr: %p\n", &heap2);
printf("stack addr: %p\n", &heap3);
printf("c addr: %p, c: %d\n", &c, c);
for(int i = 0; argv[i]; i++)
{
printf("argv[%d]=%p\n", i, argv[i]); //?
}
for(int i = 0; env[i]; i++)
{
printf("env[%d]=%p\n", i, env[i]);
}
return 0;
}
我们可以观察到这样的现象
命令函参数环境变量,无论是表,还是表里的内容,都是在栈区上面的。
static静态变量,不初始化默认是0.
我们再来看一下下面这段代码
#include <unistd.h>
#include <sys/types.h>
int g_val = 100;
int main()
{
pid_t id = fork();
if (id < 0)
{
perror("fork");
return 0;
}
//子进程
else if (id == 0)
{
printf("child:id:%d,pid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
}
else
{
printf("parent:id:%d,pid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
}
return 0;
}
运行的结果我们很好理解,子进程创建是以父进程为模板进行创建的,父子并没有对变量进行修改。打印的内容和地址都是相同的,父子代码共享。
发生写时拷贝数据各自开辟一份,私有一份。
如果我们对数据进行修改了呢??
#include <unistd.h>
#include <sys/types.h>
int g_val = 100;
int main()
{
pid_t id = fork();
if (id < 0)
{
perror("fork");
return 0;
}
//子进程
else if (id == 0)
{
g_val=200;
printf("child:id:%d,pid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
}
else
{
printf("parent:id:%d,pid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
}
return 0;
}
子进程对数据进行了修改,发生了写时拷贝,数据被修改成了200,打印的结果我们很好理解。
但是地址为什么还是相同的呢??
变量内容不一样,父子进程输出的变量绝对不是一个变量。
地址值是一样的,这绝对不是物理地址。
我们在Linux下,这种地址称为虚拟地址。
我们在C/C++看到的地址,都是虚拟地址。物理地址是由操作系统进行管理的,用户看不到。
虚拟地址和物理地址之间的转化由操作系统完成。
二、进程地址空间
我们之前学到那张图应该叫做进程地址空间,
本质上是内存中的一种内核数据结构,在Linux中进程地址空间具体由结构体mm_struct实现
每个进程被创建,对应的PCB和mm_struct.也随之被创建。操作系统可以通过进程的PCB找到mm_struct.。因为PCB中有一个结构体指针指向这个mm_struct.
folk之后,父进程创建子进程,子进程也要拥有自己的PCB和地址空间,子进程和父进程代码数据共享。
地址空间和物理内存之间存在一个页表,里面存放的是物理地址和地址空间的映射关系。
我们就可以通过地址空间里面的地址,借助页表的映射关系,在物理内存中找到对应的数据和代码。
当子进程对数据进行修改本质是在物理内存中发生写时拷贝,开辟一块同样大的空间,把数据拷贝过去,再进行修改,同时修改子进程与物理内存的映射关系。
我们为什么要拷贝呢??直接修改不就行了??
我们的对数据的修改可能完成这样的动作:g_val++。
经过上面的解释,我们也可以进一步理解为什么folk之后两个返回值不同了。
操作系统要不要对地址空间组管理呢??
先描述,在组织。
进程地址空间是数据结构,具体到进程中,就是特定数据结构的对象。
我们为什么要进行写时拷贝??
进程之间具有独立性,多进程运行,需要独享各种资源,多进程运行互不干扰,不能让子进程的修改影响父进程。
为什么不在创建子进程时就将数据拷贝
子进程不一定使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要读数据进行拷贝,我们应该按需分配,提高内存资源的利用率。
我们划分为不同的区域,堆,栈等等。
可以判断是否越界,可以扩大或者缩小范围。
进程地址空间就像一把尺子,尺子刻度由0x00000000到0xffffffff,尺子按照刻度被划分在不同区域。而结构体mm——struct中,便记录了各个边界的刻度
每个刻度都代表一个虚拟地址,这些地址通过页表映射与物理内存建立联系。
堆向上增长和栈向下增长就是为了改变mm_struct中堆和栈的边界刻度
区域划分的本质:区域内每个地址都可以使用
CPU中存在一个叫做CR3的寄存器,这个寄存器保存到就是页表的起始地址。
我们为什么要有进程地址空间??
1.有了进程地址空间,就不会有任何系统级别的越界问题存在了。例如进程1不会错误的访问到进程2的物理内存空间,因为你对地址空间进行操作之前需要先通过页表映射到物理内存,而页表只会映射到属于你的物理内存。
2.有了进程地址空间后,每个进程都被认为自己独占内存,这样能更好地完成进程的独立性以及合理使用内存空间,将进程调度和内存管理进行解耦和(两边互不干扰,各干各的)
3.用于保护内存安全
4.有了进程地址空间后,每个进程都认为看的是相同的空间范围。我们在编写程序时只需要关注虚拟地址,而不用关系数据在物理内存中的实际存储位置。
我们之前学过的malloc和new本质是在进程的虚拟地址空间中申请,等用的时候,再到物理内存中开辟相应的空间。
操作系统,一定要为效率和资源利用率负责
总结
以上就是今天要讲的内容,本文仅仅详细介绍了Linux进程地址空间的内容。希望对大家的学习有所帮助,仅供参考 如有错误请大佬指点我会尽快去改正 欢迎大家来评论~~ 😘 😘 😘