目录
接下来的日子会顺顺利利,万事胜意,生活明朗-----------林辞忧
一:程序地址空间
1.在学习c/c++时,经常会听到堆区,栈区,代码段,常量区等对于空间中内存的划分
接下来将通过在Linux运行代码的结果来再次认识地址空间的划分
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val_1;
int g_val_2 = 100;
int main(int argc, char *argv[], char *env[])
{
printf("code addr: %p\n", main);
const char *str = "hello bit";
printf("read only string addr: %p\n", str);
printf("init global value addr: %p\n", &g_val_2);
printf("uninit global value addr: %p\n", &g_val_1);
char *mem = (char*)malloc(100);
char *mem1 = (char*)malloc(100);
char *mem2 = (char*)malloc(100);
printf("heap addr: %p\n", mem);
printf("heap addr: %p\n", mem1);
printf("heap addr: %p\n", mem2);
printf("stack addr: %p\n", &str);
printf("stack addr: %p\n", &mem);
static int a = 0;
int b;
int c;
printf("a = stack addr: %p\n", &a);
printf("stack addr: %p\n", &b);
printf("stack addr: %p\n", &c);
int i = 0;
for(; argv[i]; i++)
printf("argv[%d]: %p\n", i, argv[i]);
for(i=0; env[i]; i++)
printf("env[%d]: %p\n", i, env[i]);
2.static修饰的局部变量,在编译时会被编译到全局数据区
- 当执行这段代码之后
会发现这里同一个地址保存了两个不同的值,因此这里的地址不是物理地址,因为每个物理地址只能保存一个值,这里的地址则是虚拟地址(线性地址) ,对于平时所用的指针的地址等都是虚拟地址
4.当父进程创建出来后,系统为了让该进程更好的运行,除了创建内核数据结构PCB之外,操作系统还要为进程创建进程地址空间(内核为进程创建的结构体对象),因此每个进程在创建时,操作系统都要为进程创建对应的地址空间,并且对应的PCB中是有指针指向地址空间的
5.当系统要为该变量 在内存当中开辟一段空间时,在对应的物理内存上也会开辟一段空间,并且将虚拟地址和物理地址对应的存放在页表当中,因此每个进程都有自己的代码和数据,对应的进程就可以通过虚拟地址映射出物理地址,找到自己的代码和数据
6.当进程在访问虚拟内存时,操作系统会自动根据页表将虚拟地址转化为物理地址,这样就可以访问到物理内存中的内容
7.刚开始时,父进程有自己的代码和数据,在创建子进程时,子进程会以父进程为模板来初始化自己内部的结构体对象的各种值,当然也有自己的一部分以写时拷贝的方式私有的数据,把父进程的地址空间和页表也会拷贝一份,这样刚开始时,父子进程共用一张页表,也因此指向的空间相同,父子进程共享代码和数据(保存在物理内存中),但因为进程独立性,子进程也有属于自己的页表和进城地址空间
8.当子进程对虚拟地址中内容进行修改时,在系统层面上识别到往虚拟地址中写入,由页表查询到对应被修改内容的物理地址,若这个变量是父子共享的话,操作系统会在写入之前在物理内存中重新划分一块空间,将历史值修改过来,再更新页表中新空间的物理地址,使其指向新空间,将父子共享的数据在物理内存上分开了。即子进程修改了数据,要发生写时拷贝,再重新建立映射关系,在父子访问时,就会分别映射到不同的物理内存,使其指向不同的值
- fork在进行返回的时候,本质也是向pid_t id 值写入的过程,fork往后是有两个进程的,都有各自的id变量,对应上的是虚拟地址上的地址,同一个id在被读取的时候,当父进程和子进程通过查各自的虚拟地址,再通过页表,映射到不同的地址处,即虽是一个变量名,但在物理内存中保存的值是不一样的
二:相关细节知识
1.什么叫做地址空间?
在32位的计算机中,有32位的地址和数据总线,而CPU要根据对应的地址总线进行访问对应的物理内存,CPU与内存之间连接的线为系统总线,内存与外设之间的为IO总线,通过这些线把数据从一个设备拷贝到另一个设备(实质为充放电的过程)
计算机只认识二进制,即对应总线上为0/1,用每根总线上的电频有/无来标识,在内存当中就可以读取对应总线上的0/1数据,获取到要访问的地址
地址总线上的0/1,本质就是高低电频,向内存当中寻址实际就是告诉内存要访问哪个地址,对应总线上的0/1数据,本质就是CPU向内存充电,之后就能识别高低电频,即就能识别为0/1,然后把这些0/1组合就形成一个物理地址
磁盘把数据拷贝到内存里,实际上是把磁盘当中高低电频通过总线把数据以电脉冲的形式向物理内存特定的位置进行充电,没电的为0
32位的计算机有32根总线,每根总线上是0/1,这样就一共有2^32种可能结果,也就是4GB,即一个32位的计算机最多能够装载4GB的内存空间
所以地址空间就是总线排列组合形成的范围空间[0,2^32]
2.理解地址空间上的区域划分
本质就是区域划分
struct area
{
int start;
int end;
}
用每个start和end对空间区域进行划分,对于空间区域的调整就是调整对应的start和end
在范围内,连续的空间中,还可以再次划分直至最小,每一个最小单位都有地址,这个地址是可以直接被使用的
3.地址空间深入的理解
所谓的进程地址空间,本质是一个描述进程可视范围的大小,地址空间内一定要存在各种区域划分,对线性地址进行start和end划分
地址空间本质就是一个内核数据结构对象,类似PCB一样,地址空间也是要被操作系统所管理的:先描述,再组织
4.进程理解以及进程地址空间的意义
在这里进程的定义变为内核数据结构PCB+mm_struct+页表+程序的代码和数据
意义:让进程以统一的视角看待内存
增加进程虚拟地址空间可以让我们访问内存的时候,增加一个转化的过程,在这个转化的过程中,可以让我们的寻址请求进行审查,所以一旦异常访问,直接拦截,该请求不会到物理内存,保护了物理内存
因为有地址空间和页表的存在,将进程管理模块和内存管理模块进行解耦合
5.页表的介绍
当创建一个进程时,要创建对应的内核数据结构PCB和mm_struct,为了进行虚拟地址和物理地址之间的转化,还需要创建对应的页表结构
在系统当中,当前正在执行进程的页表的起始地址是保存在CPU中的cr3寄存器当中的,当读取自己的代码时,操作系统和CPU首先会对代码位置进行寻址,去虚拟地址中寻找代码地址,操作系统会自动根据cr3寄存器中的地址找到对应的页表结构,将虚拟地址转化为物理地址进行使用
因为该进程在运行期间,cr3寄存器中保存的地址也叫做当前进程正在运行的临时数据(进程的上下文数据),当切换进程时会把寄存器中的内容带走,当回来时会把寄存器中的页表地址恢复,因此是不怕会找不到页表的
关于代码区,字符常量区的只读是如何实现的
在磁盘当中的大文件是不能一次性都加载到内存中的,此时操作系统就会采取分批加载的方式,而如果加载过多而没有使用的话,这样的行为操作系统也是不允许的,因此就会采用惰性加载的方式,一次少加载一点,而往物理内存中加载内容是没有可读可写这样的概念的,那么在读取代码区内容的只读权限是如何实现的,则是由页表来完成的
当进程被创建的时候,先是创建对应的数据结构PCB,再加载对应的可执行程序
页表添加了读写权限的一列,用来表示访问内容的可读可写
代码区和字符常量区所匹配的页表的虚拟地址和物理地址的映射关系的标志位都是r(只读权限)
如何确定程序的代码和数据是在内存当中的?
这里也是有页表的一列来保证的,当为1时表示是在内存当中的,0则表示不在内存当中
当我们访问一个虚拟地址时,查看页表,先看标志位,若为1表示已经被加载,直接读取物理地址,去对应物理内存中进行访问。若为0表示还没有被加载,此时操作系统就会触发缺页中断,即找到可执行程序,将代码在物理内存中申请一块空间,并加载到内存里,将空间的地址填到页表当中,重新构建映射关系,然后再进行访问