目录
一、引入问题:
在C++中,我们了解到了如上的空间布局图,这到底是个什么呢,我们首先在Linux下对其进行验证,看看是不是也遵循上述的空间分布
如上可以看到这个地址和之前C++中的空间布局基本是一样的,从低地址的代码区到高地址的栈区,值得一提的是
1、栈区是往低地址增长的,堆区是往高地址增长的,并且二者之间有很大的空间
2、在C语言/C++中的static修饰的局部变量,在编译之后就会变到全局变量中,这样就能保证其的静态属性
接下来看看如下代码:
如上代码,这个代码我们在介绍fork的时候就已经运行过了,但是当时有一个问题没有解决:为什么id既能够等于0又能够不等于0?
上面代码主要逻辑是在全局变量定义一个变量val为100,在子进程中过了5秒之后就将val改为200,
如上,这就是运行结果,可以发现:这个全局变量val在被子进程修改后地址仍然是不变的,但是子进程中的val被修改了,父进程中的val却没有改变,但是它们的地址却是一模一样的,所以我们得到结论:这个地址不是一般的物理地址而是线性地址(虚拟地址)
我们在用C/C++语言所看到的地址,全部都是虚拟地址
而物理地址,用户是看不到的,由OS统一管理OS必须负责将虚拟地址 转化成物理地址, 这是通过页表完成的
二、进程地址空间:
上述那个表在进程中的位置,
这个进程地址空间,代码在上面访问的地址就是虚拟地址,然后在通过KV结构的页表,K是虚拟地址,V是物理地址,这样的话就可以通过虚拟地址找到对应的物理地址。
mm_struct(进程地址空间)里的成员变量:
进程地址空间也是通过结构体进行描述的,这个结构体叫mm_struct,这个结构体是在PCB(Linux中的task_struct)中的,里面的成员变量,其实,所谓的区域的调整变大或者变小,本质上就是对这些struct结构体管理的end还有start变大或者变小。
cpp
mm_struct
{
//代码区
long long code_start;
long long code_end;
//堆区
long long heap_start;
long long heap_end;
//栈区
long long stack_start;
long long stack_end;
......
}
虚拟地址:
在早期的计算机,程序是直接与物理内存进行交互的,但是这样的话,在多个进程都在与物理进程进行交互的时候就会发生越界行为,并且当进程过多的时候资源分配也会很紧张,这样运行速率也会下降,
为了解决上述的问题,就提出了虚拟地址的概念:让程序不直接与物理地址进行交互,而是通过程序给出的虚拟地址,使用一种间接的地址访问方法,通过某些映射的方法,将虚拟地址转化为实际的物理地址。这样只需要控制好这个虚拟地址到物理地址的映射过程,就可以更好的维护各个进程
这样当发生越界行为时,在映射中就会检测出是否发生越界行为,如果发生了,能在其对物理地址造成影响前进行拦截
再通过页表的寻址机制就可以进行映射寻址,那为什么在32位计算机中有4GB的虚拟地址空间呢?
地址和数据总线:
在32位的计算机中,有32根地址和数据总线,每一根地址总线是只有0和1的概念的,实际上可以理解为有电就是1,没电就是0,
地址总线意味着它可以表示2^32个不同的地址,每个地址对应一个内存位置或I/O设备的特定位置,然后这些总线进行排列组合就是有2^32种情况,所以32位地址总线能够寻址的最大内存空间为2^32*1byte = 4GB,这就是4GB的虚拟地址空间
数据总线用于在CPU和内存或其他设备之间传输数据。32位数据总线意味着每次可以传输32位的数据,即4个字节。这意味着CPU可以一次处理或传输32位的数据,这对于提高数据处理效率非常重要
三、页表:
下面来粗略地聊聊页表:
每一个进程都有属于它自己的页表,页表是在物理地址中存储的,
页表的主要作用是实现虚拟地址到物理地址的转换,确保进程间的内存隔离和保护,操作系统通过页表将虚拟地址转换为物理地址,从而实现内存的访问
进程退出时,页表就会被删除掉
CPU中有一个cr3寄存器保存当前页表的起始地址,这样在进行进程切换的时候就会在cr3中保存上下文,这样在下次运行的时候就会从上次存在的数据中继续进行运行
权限:
我们是怎么知道一个变量(字符常量区,代码区是只读的)是可读还是可写的呢?在页表中的kv值后有可读可写的概念,
如上第二个就是可读可写,第三个就是只读,比如常量字符串就是只读的,一般局部变量就是可读可写的,那么页表是如何进行管理的呢?
如上,当对常量字符串进行修改的时候就会出现错误,这是因为当对一个只读的数据进行修改之前必须先找到对应的物理地址,在找到物理地址之前必须要找到对应的页表通过虚拟地址来转化,但是在转化前页表发现这个是只读的,竟然想进行可写,那么页表就会对这种越权行为进行拦截,然后这个进程就会被操作系统干掉
缺页中断:
共识:现代操作系统,几乎不做任何浪费空间和浪费时间的事情, 你怎么知道你的进程代码数据在不在内存,
操作系统对于大文件,可以实现分批加载:比如说我们加载了100M的空间,当代码在加载的时候,那些未加载的部分是不会被释放的。这就造成了空间上的浪费。所以实际上,操作系统对于内存的加载是采用惰性加载的方式
这里再把页表"升级一下":
在页表最后有一个标志位,如果标志位中的P状态为1,表示可以在物理内存中找到对应数据;如果P位为0,则表示在物理内存中找不到对应数据,访问时会触发页面错误,发生缺页中断
此时就说明我们需要访问对应的物理地址中的数据但是操作系统却没有把数据加载到物理地址,这个时候操作系统就会重新加载物理内存,再将这个物理内存的地址映射到页表的物理地址存放处,然后这个进程就可以访问物理内存中的数据了
四、回到问题:
为什么同一个id,连地址都是相同的,却是不同的值呢?
当子进程被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。
修改后:
当子进程想要修改父进程的数据的时候,就会发生写时拷贝,会在物理地址中开辟另一块空间,然后在这块新开的空间中存入数据,之后映射就进行修改即可,
这个时候,就会通过这个虚拟地址找到对应新的物理地址,就会发现数据是不一样的
所以看上去父子进程的地址是一样的,但这个一样是虚拟地址,二者会通过这个虚拟地址找到对应的物理地址,这样就会达到看上去是同一个变量,甚至地址(虚拟地址)都一样,但是它们的值却不同
五、总结:
因为虚拟地址的存在,让进程在管理它自己的PCB的时候不需要其去取物理地址,这部分内存管理就交给操作系统来完成,如果页表发生缺页中断,就会自动取物理内存中缓存数据并且形成映射
为什么要有地址空间:
1、让进程以统一的视角看待内存:主要是因为地址空间为每个进程提供了一个独立的虚拟地址空间,使得每个进程都认为自己独占整个系统内存资源。这种机制称为虚拟内存或进程地址空间,它并不是物理内存,而是虚拟内存的一部分
2、增加虚拟地址空间可以让我们在访问的时候增加一个转换的过程,在这个转化的过程中,可以对我们的寻址进行审查,所以一旦异常访问,直接拦截,该请求不会到达物理内存以保护物理内存
3.因为有地址空间和页表的存在,将进程管理模块和内存管理模块进行解耦合,让它们各自好好地完成自己的事
重新理解进程:
进程在被创建的时候是先创建内核数据结构,在加载对应的可执行程序的,当一个程序被执行时,系统会首先创建一个进程控制块(PCB),这个数据结构包含了进程的各种属性,如进程编号、状态、优先级等。随后,系统会将程序加载到内存中并执行
在以后的学习中,是在一个个的基础上进行"升级的"就像以前我们只知道一个进程的PCB和PCB中所指向的代码和数据,到现在我们知道
进程 = 内核数据结果(task_struct && mm_struct && 页表) + 程序的代码和数据