在c语言中的学习中 我们是这样子描述内存空间的
内存包括栈区 堆区 静态区
栈区是高地址向低地址增长的
堆区是低地址向高地址增长的
其中静态区又分为三部分 分别是初始化数据 未初始化数据 数据段和代码段
可以通过下面的代码来验证上面的这张图
#include
#include
int g_unval;
int g_val = 100;
int main()
{
const char *s ="hello world";
printf("code addr: %p\n", main);
printf("string rdonly addr: %p\n", s);
printf("uninit addr: %p\n", &g_unval);
printf("init addr: %p\n", &g_val);
char *heap = (char*)malloc(10);
char *heap1 = (char*)malloc(10);
char *heap2 = (char*)malloc(10);
char *heap3 = (char*)malloc(10);
char *heap4 = (char*)malloc(10);
printf("heap addr: %p\n", heap1);
printf("heap addr: %p\n", heap2);
printf("heap addr: %p\n", heap3);
printf("heap addr: %p\n", heap4);
printf("stack addr: %p\n", &s);
printf("stack addr: %p\n", &heap);
int a = 10;
int b = 30;
printf("stack addr: %p\n", &a);
printf("stack addr: %p\n", &b);
return 0;
}
我们首先写出下面的一段代码
int g_val = 100;
int main()
{
//数据是各自私有一份(写时拷贝)
if(fork() == 0)
{
//child
int cnt = 5;
while(cnt){
printf("I am child, times: %d, g_val = %d, &g_val = %p\n", cnt, g_val, &g_val);
cnt--;
sleep(1);
if(cnt == 3)
{
printf("##################child更改数据#########################\n");
g_val = 200;
printf("##################child更改数据done#########################\n");
}
}
}
else
{
//parent
while(1)
{
printf("I am father, g_val = %d, &g_val = %p\n", g_val, &g_val);
sleep(1);
}
}
return 0;
}
一个物理地址中对应的值只能有一个 所以说这里的地址肯定不是真正的物理地址
我们在语言层面上打出来的地址都不是真实的物理地址 而是由操作系统分配的虚拟地址
所以说尽管我们看上去父子进程的虚拟地址是一样的 但是它们实际的物理地址却是不一样的 这也就造成一个地址中会出现两个值的情况
进程地址空间是操作系统对于开辟空间的描述 Linux中它就是一个结构体 叫做mm_struct
进程地址空间就类似于一把尺子 尺子的刻度由0x00000000到0xffffffff 尺子按照刻度被划分为各个区域 例如代码区、堆区、栈区等
而在结构体mm_struct当中 便记录了各个边界刻度 例如代码区的开始刻度与结束刻度
在这个结构体中 每一个刻度都代表着一个虚拟地址 这些虚拟地址通过页表 和物理地址建立联系
而由于这些地址是线性增长的 所以说我们也可以将虚拟地址叫做线性地址
操作系统在创建进程的时候会创建PCB和程序地址空间
在进程运行的时候程序地址空间会经过页表映射到真实的物理内存中开辟新的空间
父子进程是共享代码和大部分数据的
但是当修改子进程的数据的时候 就会发生 缺页中断 由于要保持进程的独立性 所以这里会用到一种叫做写时拷贝的技术
我们现在对于进程创建的理解可以认为是
创建 PCB + 代码和数据 + mm_sturct + 页表