[Linux]进程地址空间

[Linux]进程地址空间

进程地址空间的概念

操作系统作为计算机软硬件资源管理者,当然也要管理各个进程的内存分配,因此要有描述各个进程的内存分配情况的数据结构,这个内核数据结构就是进程地址空间。在Linux操作系统中,该数据结构的变量名为mm_struct

进程地址空间的实现

为了更好的管理内存分配,mm_struct的实现采用了如下策略:

  • 作为描述内存分配情况的数据结构,会描述整个内存的空间。
  • 用连续的线性地址来描述内存中的地址。
  • 其中描述的地址都是虚拟地址。

mm_struct的伪代码如下:

struct mm_struct
{
    
    
    long code_begin; //代码区起始地址
    long code_end;	 //代码区结束地址
    //...
    long brk_begin;  //堆区起始地址
    long brk_end;	 //堆区结束地址
    long brk_begin;  //栈区起始地址
    long brk_end;	 //栈区结束地址
}

说明: 由于进程地址空间是通过连续的线性虚拟地址描述的内存地址,因此通过起始地址和结束地址就可以将内存中各个区域区分开来。如下图:

image-20230826134746803

虽然进程地址空间描述的是虚拟地址,但是进程要找到自己的数据和代码还是要落实到内存中的实际地址中查找,因此操作系统采用了映射的方式,将虚拟地址转换成实际的内存地址,在映射时使用了叫做页表的工具,页表中记录了虚拟地址和内存实际地址的映射,如下:

image-20230826135906135

理解写时拷贝

首先编写如下代码来观察写时拷贝现象:

#include <stdio.h>
#include <unistd.h>
#include <assert.h>

int main()
{
    
    
  int val = 100;
  pid_t id = fork();
  assert(id >= 0);
  if (id == 0)
  {
    
    
    //子进程
    while(1)
    {
    
    
      printf("我是子进程,pid:%d, ppid:%d, val:%d, &val:%p\n", getpid(), getppid(), val, &val);
      sleep(1);
      val = 200;
    }
  }
  else if (id > 0)
  {
    
    
    while(1)
    {
    
    
      printf("我是父进程,pid:%d, ppid:%d, val:%d, &val:%p\n", getpid(), getppid(), val, &val);
      sleep(1);
    }
  }
  return 0;
}

将如上代码编译运行查看现象:

image-20230826141508478

  • 在子进程修改val变量的值后,子进程的val值改变不影响父进程的val值。
  • 父子进程的val地址相同,但是值不同。

以上这种父子进程在代码共享同一个变量,当任意一方试图写入,双方便各自使用一份副本,称为写时拷贝。

首先,由于进程地址空间的地址都是虚拟地址,因此出现了父子进程在同一个地址上读取的数据不同。在子进程创建时,使用的进程地址空间和页表都是复制的父进程的,如下:

image-20230826142401503

在子进程修改val时,操作系统就会为了子进程重新开辟一段空间,来记录修改后的值,然后将页表中映射的实际地址修改来实现写时拷贝,如下:

image-20230826142642452

为什么要有进程地址空间

为了理解为什么要有进程地址空间,首先要了解一下两点知识:

malloc的本质

由于内存资源是有限的,操作系统作为了减少内存空间的浪费,在malloc申请内存时,只会在页表的虚拟地址中填入地址,而页表中实际内存地址的位置是空出来的,等待进程要使用的时候,再分配一块空间,将实际地址填入页表中。这样做避免了进程申请空间但是没有使用空间时,造成的空间浪费。

可执行程序的地址空间

源代码被编译的时候,就是按照虚拟地址空间的方式进行了早已编好的对应编址的方式编译代码和数据。

在Linux系统下,可以使用objdumop -S 可执行程序名查看程序的虚拟地址:

image-20230826155041035

进程在开始执行后,首先CPU执行的是虚拟的入口地址,该地址内容中也包含地址,CPU会通过入口地址中记录的内容开始进行页表的跳转,然后正常的执行进程的代码和数据。

为什么要有进程地址空间

首先,我们要知道如果没有进程地址空间,操作系统的工作方式。在没有进程地址空间时,也就不会有页表,进程加载到内存后,CPU按照进程内部的代码顺序执行。如下:

image-20230826150931606

  • 其一,在这种情况下,在执行进程的代码时,如果代码存在问题,出现了野指针的访存操作,CPU也就会进行错误的访问操作。而有了进程地址空间后,也就有了页表,在访问任何数据时,都要通过页表的映射,而页表中不仅仅存储了地址的映射关系,还存储了某一地址的读写权限等,如果使用的是野指针,页表就会进行拦截。
  • 其二,在这种情况下,进程要是想将数据按照全局区,静态区之类的分区就要在物理内存中找到实际的整块地址来用于划分,以至于进程的管理和内存的管理需要协同工作。但是有了进程地址空间和页表后,进程只需要关系进程地址空间中的地址是否能按照区域划分即可,而物理内存只需要找到空间即可,无论在内存中的具体哪个位置,因为最终的数据访问都是经过页表的映射的,同样的进程的管理和内存的管理得到了解耦。
  • 其三,在这种情况下,代码和数据要单独进行区分。有了进程地址空间和页表后,进程要访问代码和数据只需要通过进程地址空间,而在所有的进程地址空间中,代码和数据的分区地址的都是相同的,以至于所有的进程看自己的进程地址空间时的看法是一样的。

总结:

  1. 防止地址随意访问,保护物理内存和其他进程。
  2. 将进程管理和内存管理解耦。
  3. 让所有进程以统一的视角看待代码和数据。

猜你喜欢

转载自blog.csdn.net/csdn_myhome/article/details/132522396