• 【进程概念④】:进程地址空间(虚拟内存与物理内存)


    一.进程地址空间

    你觉得我们代码中写的数据都在哪存储着呢?
    在内存里存着!确实是在内存里存储着,那你知道数据存在内存的哪里呢?
    你以前肯定听过栈,堆,静态区等等的。都是在"内存"上划分的。
    在这里插入图片描述
    比如局部变量是在栈上开辟的,动态申请的空间是在堆区申请的,全局变量是在全局区(这里又分成初始化数据和未初始化数据)代码都是存在代码段的。
    对于栈呢,它是向下增长,也就是向地址减少的地方增长的;对于堆呢,它是向上增长,也就是向地址增长的方向增长的。
    所以在我们没有学习进程地址空间时,我们对于内存的印象就是如上图所示。而学习进程地址空间后,你就会发现其实这个并不是真正的"内存"。

    理解进程地址空间,我们先理解一下下面这个问题:
    【问题】我们知道fork()创建进程时会返回两个值,为什么同一个变量可以接受两不同的值呢?

    #include 
    #include 
    #include 
    int g_val = 0;
    int main()
    {
     pid_t id = fork();
     if(id < 0)
     {
     perror("fork");
     return 0;
     }
     else if(id == 0)
     { //child子进程
     g_val=100;
     //将全局变量改成100;
     printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
     }
     else
     { //parent父进程
     printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
     }
     sleep(1);
     return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    在这里插入图片描述
    我们在全局定义一个变量g_val.并初始化为0.
    然后利用fork()函数创建两个进程,一个为子进程,一个为父进程,子进程先执行,在子进程中将全局变量修改成g_val修改成100.并打印它的值和地址。在父进程中修改全局变量的值,打印它的值和地址。最后结果显示,子进程和父进程中的全局变量值并不一样,而地址确实一样的,这是为什么呢?

    我们可以猜想:
    1.变量内容不相同,父子进程中输入的变量肯定不是同一个变量。
    2.但是父子进程中,变量的地址却又相同,这说明这个地址肯定不是物理地址,因为物理地址只能一对一,不存在一对多的情况。

    其实这里的地址是虚拟地址,在Linux中,我们用c/C++语言所看到的地址都是虚拟地址,都不是物理地址,物理地址用户是看不到的。

    二.分页与虚拟地址

    其实呢进程并不是直接与物理内存进行交互的,中间还存在着一个叫做进程地址空间的对象。进程地址空间其实就是虚拟内存,而物理内存才是真正的内存,除了进程地址空间外,中间还存在一个叫页表的对象,它是用来将虚拟地址转换映射成物理地址的。页表中其实有很多标识位,比如权限位和判断位等等。最主要还是它可以将虚拟地址映射到物理地址上去,页表的左边存的都是虚拟地址,页表的右边则是存着物理地址。这就形成了映射。当进程访问内存时,先访问的是虚拟内存,然后才是根据页表映射转换到物理内存。
    在这里插入图片描述
    当使用fork()创建子进程时,子进程也需要进程地址空间,而子进程的进程地址空间是继承父进程的,也就是直接从父进程那拷贝过来的。
    子进程的页表其实也是从父进程那拷贝过来的,所以父子进程默认情况下,数据和代码指向都是一样的,也就印证了父子代码共享,数据一般也是共享的,除非发生写实拷贝。
    在这里插入图片描述
    好的,我们现在再转回到问题上去:为什么fork()返回的两个值可以存在同一个变量里?为什么父子进程的变量值不相同,但地址却相同呢?

    首先我们要确定的是,他们这里面发生了写入数据导致发生写实拷贝了。fork()返回返回值时是不是写入?改变全局变量的值是不是写入?
    那么写时拷贝发生了什么呢?
    写时拷贝,父子进程先写入的就会将要访问的数据拷贝一份,重新开辟内存创建变量。所以关键在于重新开辟了一块空间了。
    这时子进程的物理地址就不再指向父进程原来的变量位置,而是指向一块新开辟的空间了。但是这个写时拷贝的过程,并不影响进程虚拟地址。
    所以我们就可以知道为什么父子进程中同一个变量值不同了,是因为变量的物理内存不同,它们的地址却相同,是因为写时拷贝不影响虚拟内存。
    在这里插入图片描述

    ①.what

    什么是进程地址空间呢?
    在这里插入图片描述
    什么是32位机器呢?就是它有32个总线。每一根地址总线只有0,1表示。那么可以表示的范围就是0–2^32种了。而这就是该机器下所能表示的内存范围,至于地址空间是什么,就是地址总线排列组合形成的地址范围,能访问的最大范围,也就是一段数据(地址)的范围。

    ②.how

    如何理解地址空间上的区域划分呢?区域又是如何进行划分的呢?
    在这里插入图片描述
    我们可以假设有一块连续空间,被小胖和小芳两个人共同使用,但是小胖总是喜欢跑到小芳的位置上吃零食,睡觉,小芳这能忍吗?这当然不能忍了。所以小芳就提出划分区域,定出一个三八线出来,不给小胖逾越。那么你想一想应该如何帮助小芳进行区域的划分呢?
    我们可以直接定义一个桌面区域结构体,它里面包含着四个变量,小胖的起始位置和小胖的终点位置,小芳的起始位置和小芳的终点位置。这里通过这个结构体对象,我们就可以对该区域进行划分,小芳想要公平公正一些就各自一人一般的区间,谁也不能逾越。

    所以结构体对象就可以这样定义:
    struct desk_area  justic{ 1,50,51,100 };
    
    • 1
    • 2

    所以对于每一个进程来说,除了要有PCB外,还需要进程地址空间。
    在进程创建时,操作系统处理创建一个PCB对象给进程,还要创建一个进程地址空间对象给进程。并且操作系统还要对这个地址空间进行管理,至于如何管理呢?先描述,再组织!
    在这里插入图片描述

    操作系统会像上面一样首先会对进程地址空间进行描述,将空间区域的划分边界变量确定下来。操作系统不是直接在PCB里创建进程地址空间对象,而是在PCB里存储一个指向进程地址空间的指针,然后再利用数据结构进行管理。

    1.其实所谓的进程地址空间,本质上就是一个描述进程可视范围的大小,就是进程可以看到内存的范围。
    2.地址空间里会存在区域的划分,划分的手段,就是将线性地址发个成不同区的start和end。
    3.进程空间本质也是内核的一个数据结构对象,类似于PCB。地址空间也是需要被操作系统管理的。

    在这里插入图片描述

    在进程PCB里存着一个指针,叫做struct mm_struct的对象指针。这个对象指针就是指向进程地址空间的。

    在这里插入图片描述

    ③.why

    为什么要有进程地址空间呢?

    在这里我们对进程的理解更深入了,进程的概念仍然是:内核数据结构+自己写的代码和数据。只不过这里的内核数据结构
    除了PCB对象外,还有进程地址空间对象。

    在没有进程地址空间以前,进程都是直接访问物理地址的,这很危险,进程管理和内存管理两个直接出现了强耦合。当内存管理出现了问题,进程就挂了,比如如果是越界访问,进程就直接挂掉,但就怕有人将物理地址空间修改了,内容改变了,这可能会影响其他进程。
    在这里插入图片描述
    而现在呢,有了进程空间地址,我们不再直接跟物理内存进行访问,而是通过虚拟内存进行访问,如果出现了问题,虚拟内存的机制会进行拦截除了的,并不会到达物理内存,这样就大大的保护了物理内存了。

    1.所以进程地址空间可以让我访问内存时,增加一个转换的过程,在这个转换的过程中,可以对我们的寻址请求进行审查,一旦出现异常访问,直接拦截,该请求不会到达物理内存,保护物理内存。
    2.可以让进程一统一的视角看待内部。因为物理内存是没有区域划分的,是可以随意任何地方开辟空间的,而虚拟内存则是划分区域的;最后可以让无序变有序。
    3.减少了强耦合,一旦内存管理出现问题,会影响其他进程。

    三.页表细节

    ①.标志位

    页表是在哪呢?在CPU的一个叫cr3的寄存器里,里面存储着页表的起始地址。
    我们要理解当进行需要使用页表时,肯定表面该进程正在运行使用中,那么肯定在CPU上运行着,所以进程直接通过CPU上的寄存器就可以找到该进程的页表。
    在深入理解页表之前,我们先理解一个问题
    【问题】为什么只读区的数据只读不给写的?静态成员为什么不能修改呢?代码是只读的?为什么呢?
    对于物理内存来说,没有只读概念,都是可访问的,所以实现这个操作的关键在页表。

    在页表中除了虚拟内存映射物理内存外,还有一个标识位。
    表示该内存是否可以读写,r表示只读,rw表示可读可写。
    所以对于代码区域的或者静态区的虚拟内存确实映射到物理内存上了,但页表还给它进行了标识r表示只读。这样该数据就只能是只读状态不可写。
    在这里插入图片描述

    ②.缺页中断

    我们知道进程挂起时,会将进程的代码和数据先放入磁盘中省出空间,给CPU使用,那么我们如何确定进程的代码和数据被放入磁盘里了呢?

    在这里插入图片描述
    对于虚拟内存,地址都可以先将加载到页表里,而物理内存可以先不加载进去,然后页表里存在一个标志位,用来表示该物理内存是否存在,0表示不存在,1表示存在。

    所以当CPU需要访问某个数据时,首先会根据页表从虚拟地址转换到物理地址,如果发现页表里没有加载该数据的物理地址,页表里的标识位也为0.操作系统会给这个数据申请内存,然后将申请的内存地址加载到页表里,并将标识位改成1,然后CPU再重新访问页表映射到物理地址。这时就可以正常访问数据了,这个过程叫做页表中断。
    在这里插入图片描述
    所以对于进程来说,一开始没有数据加载到内存里,也是可以创建PCB内核数据结构的,等当真正访问数据时再将数据加载进来。
    并且就算程序运行起来了,数据加载进来了,也不代表所有的数据都加载进来。

    写时拷贝的本质也就是发生了缺页中断。

    这里是引用

    四.总结意义

    当发生缺页中断时,内存会重新申请,页表的物理地址会被填充内存的释放等过程跟进程是没有关系的,进程不需要关系这些问题。
    进程只需向虚拟内存申请空间,释放空间。如果页表中的物理地址不存在,就会发生缺页中断自动调用内存管理的功能,去向内存中申请空间。
    所有进程地址空间和页表的存在,使得进程管理根本不需要关心内存管理。
    在这里插入图片描述

    1.总结进程地址空间意义:

    这里是引用
    2.什么是统一的视角看待内存呢?
    在这里插入图片描述

    3.并且我们可以从更深层去阐明进程之间为什么存在着独立性:虚拟地址可以完全一样,但物理地址不同!
    在这里插入图片描述
    4.进程地址空间的存在使得物理内存不再被看见!
    在这里插入图片描述

  • 相关阅读:
    使用 NodeJS(JavaScript 和 TypeScript)使用 MS Access (MDB) 文件的 3 种方法
    银行排号叫号管理系统(MyEclipse+Java+GUI+MySQL)
    巧用VBA实现:基于多个关键词模糊匹配Excel多行数据
    设计模式之代理模式与外观模式
    【曹工杂谈】Mysql-Connector-Java时区问题的一点理解--写入数据库的时间总是晚13小时问题
    angular、 react、vue框架对比
    男子遗失30万天价VERTU唐卡手机,警察2小时“光速”寻回
    推荐10个Vue 3.0开发的开源前端项目
    SSM框架-MyBatis核心配置文件详解与项目补充
    创邻科技Galaxybase分享Part1: 图数据库和主数据管理有什么关系?
  • 原文地址:https://blog.csdn.net/Extreme_wei/article/details/133992689