程序地址空间
在学习C、C++时候,我们将我们所写的程序看成如下这种结构:
我们用下面的代码验证上面的地址分布:
#include <iostream>
using namespace std;
int un_gval;
int init_gval=100;
int main(int argc, char *argv[], char *env[])
{
printf("code addr: %p\n", main);//代码区
char *str = "hello Linux";
printf("read only char addr: %p\n", str);//字符常量区
printf("init global value addr: %p\n", &init_gval);//未初始化全局变量区
printf("uninit global value addr: %p\n", &un_gval);//已初始化全局变量区
char *heap = (char*)malloc(100);
printf("heap1 addr : %p\n", heap);//堆区
printf("stack addr : %p\n", &str); //栈区
return 0;
}
我们可以看出来上述地址遵循的就是我们上面画的一种结构。
堆区在我们申请空间的时候堆区是会不断往上走的,而栈区定义变量的时候会依次往下走的
,所以说堆栈相对而生
,我们用下面的代码验证
#include <iostream>
using namespace std;
int un_gval;
int init_gval=100;
int main(int argc, char *argv[], char *env[])
{
printf("code addr: %p\n", main);
char *str = "hello Linux";
printf("read only char addr: %p\n", str);
printf("init global value addr: %p\n", &init_gval);
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);
static int a = 0;
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("stack addr : %p\n", &str);
printf("stack addr : %p\n", &heap1);
printf("stack addr : %p\n", &heap2);
printf("stack addr : %p\n", &heap3);
printf("stack addr : %p\n", &heap4);
printf("a addr : %p\n", &a);
return 0;
}
栈虽然是向下增长的,但是栈中的数组,结构体等结构的地址是向上增长
比如说开辟数组
开辟十个空间,那么数组中第一个元素在空间的最下面,也就是地址最低
处,然后依次往上放后面的元素
int main()
{
int a[10];
for(int i = 0; i < 10; i++)
cout << &a[i] << endl;
system("pause");
return 0;
}
深入探究地址空间与物理内存
现在写上这样一段代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 100;
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
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
{
//father
while(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;
}
我们发现上述代码父进程创建出来的子进程根我们之前说的子进程没有自己的数据和代码,所以跟父进程共享代码和数据,一开始全局变量的值以及全局变量的地址相同这看起来是对的,但是后面当我们对子进程全局变量g_val的值进行修改后,我们发现父、子进程的值不同,那没问题,但是为什么会对应同一个地址。
为什么同一个地址会对应不同的值???
但计算机办法骗我们啊!难道之前学的有问题??
我们大胆猜想我们在学习C、C++时候遇到的地址不是真实的物理地址,那么会是什么呢?答案是:我们之前遇到的都是虚拟地址、线性地址
我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由操作系统统一管理操作系统必须负责将虚拟地址转化成物理地址 。
进程地址空间
当子进程修改全局变量之前,由于进程具有独立性
所以各自有各自的进程地址空间,我们其实是通过一种叫页表的结构将我们的虚拟地址与物理地址进行映射
,当父、子进程没有对我们的数据进行写入修改时,我们的子进程与父进程的数据的物理地址都是相同的,而我们程序打印的地址是我们的进程地址空间的地址,所以我们都看到上述代码中我们这些进程的数据的地址会是一模一样的。
但子进程修改全局变量之后,操作系统会对g_val进行写时拷贝
,会在物理内存中找到一处地址将修改后的值拷贝过去。
页表的结构
页表的结构可以看成一个KV结构(unordered_map),而其中的虚拟地址对应Key、物理地址对应Value.
其实页表不仅仅只有映射关系它还有物理内存中每一个区域的读写权限:
这样我们就似乎可以理解下面这段代码
int main()
{
char *str = "hello Linux";
*str = 'H'; //字符常量区不可被修改 //error
return 0;
}
很明显,常量字符串对应的物理地址区域的权限是只可读。
除此之外,页表还有另外的一个功能
虚拟地址在映射物理内存时。在页表映射时,会将不同的数据类型进行划分
使得映射到物理内存后是比较有序的一种状态!
为什么要存在地址空间?
假如直接将数据存储在物理空间上,那么这意味每个进程不再是独立的,不同进程之间有可能读取到对方的数据,这是很不安全的!
- 有效的保护了物理内存
存在虚拟地址空间,可以有效的进行进程访问内存的安全性检查!
- 保证进程的独立性
页表映射到不同的区域,实现进程的独立性,各个之间进程不知道其他进程的存在
- 使操作系统的耦合度更低
因为有地址空间和页表的存在,物理内存可以不关心未来数据的类型,直接对其进行加载,物理内存的分配和进程的管理分开,使其直接没有关系,所以内存管理模块和进程管理模块就完成了解耦合。