【Linux】进程地址空间

在这里插入图片描述

欢迎来到Cefler的博客
博客主页:那个传说中的man的主页
个人专栏:题目解析
推荐文章:题目大解析(3)


目录

  • 虚拟地址空间和页表
  • 虚拟地址空间——画大饼?
  • 进程虚拟地址空间——内核数据结构体
  • 进程地址空间存在的意义
  • CPU和页表

虚拟地址空间和页表

在引入虚拟地址空间和页表,我们先来测试一段代码:

#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;
 printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
 }else{ //parent
 sleep(3);
 printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
 }
 sleep(1);
 return 0;
}

在linux测试环境中,我们可以看到如下情况:
【Linux】进程地址空间_第1张图片
这里我们发现在父子进程中,g_val的值分别是0和100,这个其实我们心知肚明,因为我们在代码中有做这方面的更改。

但是这里有个可能会让我们懵逼的东西,就是父子进程的g_val所指向的地址竟然都是同一地址,这简直震惊了,同一物理内存地址竟然会有两种不同的变量?

【Linux】进程地址空间_第2张图片

所以这里我们就要隆重引入我们一个新的概念——虚拟地址空间

概念引入

虚拟地址空间是操作系统为一个进程所提供的抽象地址空间,它将物理内存磁盘空间等多种资源组合成一个连续的、线性的地址空间,为进程提供了一种独立于物理硬件的编程环境。
在虚拟地址空间中,每个进程都有自己的地址空间,其大小通常为32位或64位。这个地址空间通常被分为多个段,其中包括代码段、数据段、堆区、栈区等,每个段都有自己的起始地址和大小,用于存储程序代码、全局变量、动态分配的内存等。

下面一张图可表示虚拟地址空间的分布状况

【Linux】进程地址空间_第3张图片
于是就有人问了,行,我知道虚拟地址空间这玩意的存在了,所以它到底是怎么让我们看到刚刚同一物理内存地址竟然会有两种不同的变量。别急,我们先进行了解下概念

这里我要纠正并引入一个新的全新概念:在C/C++中,我们使用看到的地址,其实是虚拟地址,而不是实际物理地址! !

我们平时不管是去访问变量干啥的,都是通过虚拟地址进行的。

但是我们在学习冯诺依曼体系时,我们知道,所有信息数据想要被CPU访问or计算,CPU只会从内存中拿数据。所以虽然我们在这里扯什么虚拟地址,虚拟地址空间这种虚无飘渺的东西,但是我们最后一定要去访问内存中的真实物理地址才能拿数据。

那么虚拟地址和物理地址之间的是怎么进行联系的呢?它们之间的纽带又是什么呢?

这里我们又将引入——页表

虚拟地址空间的页表是操作系统中用于管理内存映射关系的数据结构。它用于将进程的虚拟地址转换为物理地址,实现内存的分页和分配。
在现代操作系统中,虚拟地址空间被划分为固定大小的页面(通常为4KB),而物理内存也被划分为相同大小的页面。虚拟地址空间的页表记录了每个虚拟页面与物理页面之间的映射关系。
具体来说,页表由一系列页表项组成,每个页表项对应着一个虚拟页面。每个页表项记录着虚拟页面的状态和与之相关联的物理页面的信息。
常见的页表结构包括:

  1. 单级页表:将整个虚拟地址空间映射到一个大的页表中。这种结构适合较小的地址空间,但随着地址空间的增大,页表也会变得庞大且不易管理。

  2. 多级页表:将地址空间划分为多级的页表结构。通常使用两级或三级页表,其中高级的页表用于索引低级的页表。这种结构使得页表可以根据地址的范围进行动态调整,减少了页表的大小和搜索时间。
    在访问虚拟地址时,操作系统会使用页表转换机制将虚拟地址转换为物理地址。具体步骤如下:

  3. 从进程的页面目录寄存器(Page Directory Register)中获取页目录的地址。

  4. 根据虚拟地址的高位,查找页目录,在页目录中找到对应的页表。

  5. 从虚拟地址中提取中间部分的页表索引,并在对应的页表中找到对应的页表项。

  6. 在页表项中获取物理地址的基址,与虚拟地址的低位进行偏移计算,得到最终的物理地址。

虚拟地址空间的页表提供了一种有效管理内存的机制。它可以实现对虚拟地址空间的灵活映射和分配,同时通过页面的换入和换出来支持对物理内存的动态管理,提供了更高的内存利用率和更好的系统性能。
【Linux】进程地址空间_第4张图片

☂️☂️☂️☂️

页表中还存有访问权限字段,这是对于内存物理地址中的数据是否可读可写的权限
所以这解释了为什么常量不能被修改,因为字符常量区的数据的访问权限字段为只读不可写,所以不能被修改

页表中还存有内存中是否分配&&是否有内容,这样就可以判断一些进程的状态情况

所以我们可以用如下这张图表示:
【Linux】进程地址空间_第5张图片
我们现在就可以对为什么同一“物理”内存地址竟然会有两种不同的变量,这里我们上述已经纠正过了,这里的地址并不是我们平常认为的物理地址,这里是虚拟空间的虚拟地址
为什么同一虚拟内存地址竟然会有两种不同的变量,这是因为当父进程创建子进程时,子进程会创建新的虚拟地址空间,

每一个进程运行之后,都会有一个进程地址空间的存在

并且该虚拟地址空间完全复制父进程的虚拟地址空间。
关键的来了!虽然我子进程复制你父进程的虚拟地址空间,但是当我子进程在进行数据写入的时候,此时操作系统会在内存中开辟新的地址存放我子进程写入的数据,此时,我子进程的虚拟地址虽然不发生变化,但所对应的物理地址却发生了改变。
所以,虚拟地址相同只是假象,物理地址发生改变才是真
我们可以通过下图来更深来进一步理解:
【Linux】进程地址空间_第6张图片

【Linux】进程地址空间_第7张图片
☃️ 所以总结就一句话同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址

虚拟地址空间——画大饼?

上文中,我们大概了解了虚拟地址空间是,以及通过虚拟地址空间知道了同一个变量为什么看起来地址相同,但值不一样的原因。

这里我们再通过一个小故事来再从另一个视角认识一个虚拟地址空间:

有一个富翁有着10亿美金,有着2个私生子,2个私生女。
4个私生子女彼此不知道互相的存在。富翁有一天依次单独找来4个私生子女,单独分别对他们承诺说:等我G了,这10亿美金就是你的了。这给4个私生子女乐坏了,每个人都以为自己是这10亿美金的唯一继承人,但我们知道,富翁只有10亿美金,而只能有一个人才能继承这10亿美金,所以对四个人同时进行承诺,这无疑是在画一个大饼。
【Linux】进程地址空间_第8张图片

而在富翁G之前,这4个私生子女在平常生活中遇到了一些困难,也有向富翁老爹寻求一些经济资助,有的几百美金,有的几千或几万,但这些对于富翁来说都是毛毛雨,都满足了4个私生子女的需求。
所以这里就这里我们就可以将这个故事中的人物对应到地址空间知识中:

  • 富翁:操作系统
  • 4个私生子女:各自独立的进程
  • 10亿美金:虚拟地址空间——大饼
  • 向富翁老爹寻求一些经济资助:进程向操作系统请求CPU资源

进程虚拟地址空间——内核数据结构体

我们上面通过一个小故事,知道虚拟地址空间实际上就是操作系统给进程画的一个大饼,但是在计算机运行时,会存在很多进程,那么操作系统就要画很多大饼,大饼一旦多起来了,我们就需要对其管理,也就是说,操作系统是如何去管理这些进程虚拟地址空间呢?

这里提到了管理,我们就不得不引出我们经典的金言:先描述,再组织
我们需要对进程地址空间的数据先进行描述,才能将它们组织起来,所以因此,我们就像管理可执行程序一样,需要一个内核数据结构体来描述属性。
进程虚拟地址空间的属性如下图:
【Linux】进程地址空间_第9张图片

所以我们有一个专门的内核数据结构体来对其描述——struct mm_struct

struct mm_struct
{
 long code_start;
long code_end;
long data_start;
long data_end;
long heap_start;
long heap_end;
long stack_start;
long stack_end;
.//等等属性
.
.
}

进程地址空间存在的意义

地址空间的存在是为了有效管理和利用计算机内存资源,以支持多个进程同时运行并提供安全隔离。

以下是地址空间存在的几个重要意义和作用:

  1. 内存管理:地址空间允许操作系统对内存进行适当的划分和分配。每个进程都有自己独立的地址空间,这使得每个进程可以使用连续的、私有的虚拟地址范围。操作系统可以根据需求,动态地将虚拟地址映射到可用的物理内存页面,从而实现内存的高效利用和灵活分配。

  2. 进程隔离:通过地址空间的隔离,不同进程之间的数据和代码可以被有效地隔离开来。每个进程运行在自己的地址空间中,无法直接访问其他进程的地址空间,这提供了进程间的安全性隔离性如果一个进程出现错误或崩溃,不会对其他进程产生影响

  3. 虚拟化:地址空间的存在为虚拟化技术提供了基础。通过对物理地址和虚拟地址之间的映射关系进行管理,虚拟化软件可以在物理主机上同时运行多个虚拟机,每个虚拟机拥有自己的独立地址空间。这使得资源可以更充分地共享和利用,提高了硬件资源的利用率。

  4. 缓存管理:地址空间的存在也为缓存管理提供了基础。缓存是存储器层次结构中的一部分,用于加速访问频繁的数据。通过将最近使用的内存页面存储在更接近处理器的缓存中,地址空间允许相关的数据更快地被访问,提高了系统的整体性能。

比如我们下载一个3A大作游戏,我们不可能一次性将几十G的大小直接搞进内存中,内存也没那么大的空间存储,而是操作系统通过虚拟地址的页表中的是否在内存分配空间和内容,对已经在内存中分配的空间进行局部利用加载,一部分一部分的在这块内存空间进行加载。

  1. 地址有序化查找:数据可以在内存空间中随意存放不用担心,因为页表存储着相关的物理地址
  2. 安全检查:对于一些非法的地址,就不会被存入页表中,在页表中就会被截断
  3. 进程管理内存管理进行解耦:其实上述讲的那些对虚拟地址空间的管理实际上就是对进程的管理,而对内存进行读数据和写数据就是内存管理,地址空间的存在的意义就是将这两个模块进行解耦,让其相互独立但又可以互相连结。

总之,地址空间的存在使得操作系统能够灵活管理内存资源、实现进程隔离、支持虚拟化技术,并提供缓存管理等重要功能。它是计算机系统中关键的组成部分,对于高效运行和保障系统安全至关重要。

进程空间和页表每个进程私有一份!
进程 = 内核数据结构+代码数据

CPU和页表

我们知道,进程进行各种转换,各种访问,一定是这个进程正在被CPU调度运行!

那么前面学习了地址空间的概念,就知道,CPU调度一个进程,是如何访问到进程中的资源呢?
这个过程的目的地肯定是内存,因为数据存在内存中。
但这个中间需要借助一个驿站——页表
但页表中有虚拟地址和物理地址,CPU到底是根据虚拟地址还是物理地址去访问呢?
答案是:物理地址

因为虚拟地址是给进程画的一个大饼,它实际上根本不存在,而我CPU要访问的实实在在的数据,肯定得用真实的物理地址寻到内存上的数据才行。


如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注❤️ ,学海无涯苦作舟,愿与君一起共勉成长

【Linux】进程地址空间_第10张图片
在这里插入图片描述

你可能感兴趣的:(Linux,linux,进程,进程地址空间)