W...Y的主页 😊
代码仓库分享💕
程序地址空间回顾
我们在讲C语言的时候,大家应该都见过这样的空间布局图:
为了更好的验证不同的数据在内存中的存储位置,下面这段代码我们可以去实验一下:
#include<stdio.h>
#include<stdlib.h>
int g_val = 100;
int g_unval;
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;
}
这就是我们所打印出来的结果,我们可以知道:
1.堆与栈是相对而生的。
2.先有命令行操作符地址再有环境变量的地址
如果我们想看一下命令行与环境变量存的字符串所在的地址,我们需要稍微修改一下代码:
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]);
}
这样我们就能看出无论是表还是表指向的项目,都是在栈空间的上面保存着!!!
而我们的static变量的存储位置在未初始变量与初始化数据直接,说明操作系统已经默认把static变量默认成全局变量了,所以他会在我们进程运行期间一直存在不会被销毁。
那上面的结构图中的地址,是内存的地址吗?我们再来看一段代码:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int g_val = 100;
int g_unval;
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 0;
//子进程
while (1)
{
printf("child, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
cnt++;
if (cnt == 5)
{
g_val = 200;
printf("child change g_val: 100->200\n");
}
}
}
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 等于100,当我们让程序运行5秒后在子进程中修改g_val的内容,将100改到200后,我们在进行观察发现子进程中的g_val被修改成200,而父进程的g_val不变。但是父子进程中的g_val的地址却是相同的!!!
内容不一样地址却一样???综上所述:这个地址绝对不是内存物理地址!!!
这个地址被叫做虚拟地址或者线性地址。 所以我们所使用的地址全都不是物理地址。我们所打印的地址看到的空间排布不是物理内存。
进程地址空间
地址空间(address space)表示任何一个计算机实体所占用的内存大小。比如外设、文件、服务器或者一个网络计算机。地址空间包括物理空间以及虚拟空间。
物理地址 (physical address): 放在寻址总线上的地址。放在寻址总线上,如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。如果是写,电路根据这个地址每位的值就将相应地址的物理内存中放入数据总线上的内容。物理内存是以字节(8位)为单位编址的。
虚拟地址 (virtual address): CPU启动保护模式后,程序运行在虚拟地址空间中。注意,并不是所有的“程序”都是运行在虚拟地址中。CPU在启动的时候是运行在实模式的,内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的。
所以之前说‘程序的地址空间’是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?看图:
这就说明在我们的g_val中子进程先将我们父进程的地址空间进行拷贝,而我们的页表如同我们的hash映射一样,将我们的虚拟地址与物理地址进行映射,所以父子进程的g_val的虚拟地址是相同的。
当我们对子进程的值进行修改时,我们操作系统会在物理内存中开辟另一块空间进行储存,而我们的页表的映射关系也会发生改变。即key的值不会改变但value的值会发生改变。
进程地址空间在每一个进程中都会存在一个进程地址空间,在32位操作系统下的范围是[0,4GB]。
在操作系统下管理地址空间就如同管理硬件或PCB,都是创建一个结构体进行管理的其对应的都是数据结构。
那管理进程空间的结构体里面都有说明数据呢?PCB结构体指令中肯定有一个指针指向管理进程空间的结构体中:
上图我只截取了一小部分源码,从中我们可以看出有许多划分空间的指针,这些都是为了划分物理内存的虚拟内存指针,还有记录代码、数据、堆、栈等等开始结束的位置。而区域划分的本质就是区域内的各个地址都可以使用 。
我们的地址空间不具备对我们的代码和数据的保存能力,他们都存在物理空间。而给我们一张进程映射表——页表可以将地址空间转换到物理空间中去。
为什么要有地址空间呢?
1.将物理内存从无序变有序,让进程以统一的视角进行管理
2.将进程管理和内存管理进行解耦合
3.内存地址空间+页表是保证内存安全的重要收段
malloc/new是如何申请空间的呢?
首先我们就要理解new/malloc的堆空间申请开辟后我们不一定是直接使用的。可能是过去1ms甚至1s后我们才去使用的。但是操作系统的主旨是一定要为效率和资源使用率负责的!!!所以我们经常使用的new/malloc本质先不会直接在物理内存中申请开辟空间,而是在虚拟地址中先进行开辟。但是页表中却没有对应物理地址的映射,只有当我们准备进行写入时才会有对应的物理地址和映射关系的出现!!!
这样做的好处是:
1.充分保证内存的使用率。
2.提升new和malloc的性能
以上就是本次的内容,我们已经学习了进程的基本概念,下篇博客我们将学习如何操作进程,请尽情期待!!!