上篇文章:Linux基础4-进程4(环境变量,命令行参数详解)-CSDN博客
本章重点:
1 重新理解c/c++地址空间
2 虚拟地址空间
一. c/c++地址空间
地址空间布局图:
运行下列代码,进行观察
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int global_value = 100;
int main()
{
pid_t id = fork();
if(id < 0)
{
printf("fork error!\n");
return -1;
}
else if(id == 0) //子进程
{
int cnt = 0;
while(1)
{
printf("子进程,pid:%d ppid:%d | global_value:%d,&global_value: %p\n",getpid(),getppid(),global_value,&global_value);
sleep(1);
cnt++;
if(cnt == 10)
{
global_value = 1234;
printf("子进程已经修改了全局变量!\n");
}
}
}
else
{
//父进程
while(1)
{
printf("父进程,pid:%d ppid:%d | global_value:%d,&global_value: %p\n",getpid(),getppid(),global_value,&global_value);
sleep(2);
}
}
return 0;
}
我们定义一个全局变量,创建父子进程。让他们打印各自的全局变量值和其地址,然后让子进程修改其全局变量,观察父子进程各自的全局变量和其地址
运行结果如下
可以看到,子进程修改全局变量后,父子进程全局变量的地址相同但是值却不相同!这是为什么呢?
地址没变:说明这里使用的地址不是物理地址,我们C语言所使用的地址不是物理地址而是虚拟地址(逻辑地址)
我们所打印出来的地址空间排布,都是虚拟地址。所以上面的地址分布图是虚拟地址空间
OS访问变量时候,必须要通过虚拟地址找到其物理地址才能访问
二. 理解虚拟地址空间
每一个进程都会认为自己占有所有的系统资源,每当需要的时候就向操作系统申请。
地址空间的本质:是内核中的一种数据结构 mm_struct。与PCB类似
struct mm_struct
{
char* when;
char* who;
char* targrt;
char* sum;
..
..
..
}
我们通过管理mm_struct进而管理每一个进程的虚拟地址空间。使用mm_struct描述每一个进程的虚拟地址,再用链表将其连接起来就能够管理
虚拟地址空间
1 地址空间描述的基本空间是字节
2 以32位操作系统为例,每一个虚拟地址空间具有 2^32 B = 4GB 个字节
3 每一个字节都代表唯一的地址,所以一共能够表示 2^32个地址
000000000 00000000 00000000 00000000
000000000 00000000 00000000 00000001
.
.
11111111 11111111 11111111 11111110
11111111 11111111 11111111 11111111
所以我们只需要用 2^32个数字用于表示每一个字节的区域即可
虚拟地址空间通过类似下列代码进行调整各区域的大小
struct mm_struct
{
unit32_t code_start,code_end; //代码区
unit32_t data_start,data_end; //常量区(数据段)
unit32_t heap_start,heap_end; //堆区
unit32_t stack_start,stack_end; //栈区
// ......
};
//分配地址
*mm = malloc(struct mm_struct)
*mm->code_start=0x1111 1111 //区域的起始地址
*mm->code_end= 0x1211 1111 //区域的结束地址
//起始地址和结束地址的中间部分称为虚拟空间
*mm->data_start=0x1300 0000
*mm->data_end= 0x1400 0000
//堆栈区的调整,本质就是修改各个区域的start和end
//我们使用malloc new 申请堆空间,定义局部变量,函数调用-> 就是扩大堆栈区的空间
//函数调用完毕,free空间->就是缩小栈区,堆区空间
*mm->heap_start=0x1400 0001
*mm->heap_end= 0x1500 0000
*mm->stack_start=0x7ffff 7fff
*mm->stack_end= 0x8fff ffff
我们在PCB中定义一个指针指向 mm_struct,这样就能给进程分配进程地址空间
三. 虚拟地址空间与内存,磁盘之间的关系
如上图,每一个进程都有自己的进程地址空间,进程地址空间通过页表映射到物理内存中。
进程运行时候,将磁盘中的数据加载到内存中,然后通过页表与进程地址空间形成映射关系。
每一个进程都有自己的进程地址空间
每一个进程都有自己独立的进程地址空间,他们都通过页表和内存之间的映射找到对应的代码和数据。
这样就能够保证进程之间的独立性,统一使用相同的虚拟地址空间而不会相互干扰
这也说明了为什么我们最开始的时候,父子进程的全局变量的地址相同而值不同的原因!
尽管他们的虚拟地址空间相同,他们通过页表映射到物理内存是不相同的!
四. 为什么要存在进程地址空间
4.1 进程直接访问物理内存,如果发生非法访问咋办?
如果发生了非法访问,这样是非常不安全的。因为会访问和干扰到其他进程的数据
操作系统通过页表可以拦截进程的非法操作,从而达到保护操作系统的目的!(每一个进程都要遵守这一规则)
4.2 虚拟地址空间的存在,可以保证进程之间的独立性
如上图,最开始父子进程的全局变量其实是一个(即通过页表映射到的物理地址相同)。
然后当子进程修改全局变量的时候,会发生写时拷贝
写实拷贝:父子进程任何一方尝试修改(写入)相同物理内存的数据,OS会先进程数据拷贝,然后更改修改进程的页表映射关系。然后再让进程进行修改
写实拷贝能够保证不同进程的数据进程分离从而达到了进程之间的独立性
进程 = PCB + 进程对应的数据代码
我们知道:每一个进程的PCB都是独立的,现在每一个进程对应的数据代码也是独立的
这说明:进程是独立的
4.3 方便进程和编译器以统一的视角,来看待对应的代码和数据的各个区域
我们知道 进程的 mm_struct(进程地址空间)和 C语言程序中的逻辑地址,二者的规则是一样的!
这样一来,我们只要编译完代码就能够直接使用。
我们编译完的可执行程序,在没有加载到内存中的时候,有没有逻辑地址呢? 有!
虚拟地址空间,操作系统要遵守这个规则,编译器也要遵守这个规则。 编译器编译代码的时候就是通过地址空间的方式进行编址的!