吃透进程地址空间,理清OS内存管理机制

一、前言

Hello,大家好。本文要给大家带来的是有关Linux中的进程地址空间的讲解

  • 首先我们来看着一张图,相信有学习过 C/C++内存管理 的同学一定可以清楚下面的这张图。知道内存中划分了很多的区域,包括 栈区、堆区、静态区、只读常量区、代码段、共享区等等
  • 但是呢却不知道为什么要存在这样一个分布?以及为什么要这样来分布?

💬 在本文中我将会带大家去理解一下这个进程地址空间

二、细说进程地址空间

1、一段测试的代码

c 复制代码
  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <assert.h>
  4 
  5 int g_val = 100;    // 全局变量
  6 
  7 int main(void)
  8 {
  9     pid_t id = fork();
 10     assert(id >= 0);
 11     while(1)
 12     {
 13         if(id == 0)
 14         {
 15             // child
 16             printf("我是子进程,我的id是:%d, 我的父进程是:%d, g_val = %d, &g_val = %p\n",     getpid(), getppid(), g_val, &g_val);
 17             sleep(1);
 18         }                                                                                   
 19         else
 20         {   
 21             // father
 22             printf("我是父进程,我的id是:%d, 我的父进程是:%d, g_val = %d, &g_val = %p\n",     getpid(), getppid(), g_val, &g_val);                                                         23             sleep(2);               
 24         }            
 25     }    
 26     return 0;
 27 }  
  • 然后我们来看执行的结果,可以发现因为fork()的原因,在返回结果的时候进行了分流,继而父子进程所在的分支就都被执行了,因为父子进程所访问的都是同一个全局变量,所以我们所看到打印出来的结果的值都是一样的,而且地址也都是一样的

那现在我对上面的代码做一个小小的修改~

  • 在子进程内部我们去修改一下这个g_val的值
c 复制代码
if(id == 0)
{
	// child
     printf("我是子进程,我的id是:%d, 我的父进程是:%d, g_val = %d, &g_val = %p\n", getpid(), getppid(), g_val, &g_val);
     sleep(1);
     g_val++;                                                                       
}
  • 然后再去执行一下可以发现 子进程 每次打印出来的g_val一直在递增,但是呢 父进程 所打印出来的g_val却始终没有发生变化,而是保持【100】不变
  • 但是呢很奇怪的一个现象是,父子进程所访问这个全局变量的地址却是同一个,这是为什么?相信有很多同学对此产生了很大的疑惑

所以由上面的这个现象可以联想到我们在讲解 进程的基本概念 的所提到的相关概念

  • 也就是对于进程而言是具备独立性的,所以子进程对于全局数据的修改,不会影响父进程。这也联想到了一个知识点叫做【写时拷贝】,子进程若是需要修改数据的时候,会将父进程的数据拷贝一份进行修改,而不是直接去进行修改,导致数据出现了问题

那我们便可以发出这样的疑问了:同样去读取一个地址,竟然读到了不同的值,它是一个普通的地址吗?

  • 很明显这不是一个物理地址,还记得我们在讲解 C语言指针 的时候所提到的【地址】吗?那是我么说到指针其实就是地址,现在我们又要深入地去谈一谈了,对于我们之前一直所聊到的地址,其实并不指的是 物理地址 ,却是一个 虚拟地址
  • 也就是说我所打印出来看到的,其实并不是一个内存中真实的地址,只是我们看到的是这个地址罢了,在内存中所对应的可能又是另一个地址

好,有了虚拟地址的概念,我们来小结一下:

💬 父子进程在访问同一变量的时候,这个变量的地址绝对不是【物理地址】,因为它们读到了不同的值,我们在语言层面所用的地址,叫 虚拟地址 / 线性地址

2、引入地址空间

① 富豪与他的私生子👨

接下去我会先通过一个故事来引入一下虚拟地址空间

:book:在十九世纪美国纽约呢,有一个大富翁,坐拥千万美金,他呢有4个私生子,留下了一笔遗产给到他们💴

  1. 其中【A】是 做生意 的,自己本身就是一个商人,也不是很缺钱。
  2. 其中【B】是 卖化妆品 的,也靠着这个赚了很多钱
  3. 其中【C】正在美国的一所重点大学 -- 哈佛大学 读书,靠着努力学习获得了很多的奖学金
  4. 其中【D】是最小的,在高二的那一年就 辍学 了,现在在混社会

A B C D 呢彼此并不知道彼此的存在 那此时大富翁分别对这几个孩子说:

  • 小A啊,你做生意呢就好好做,到时候等我临走的时候就把这10亿美金一并给你
  • 小B啊,你这个化妆品行业最近挺火的,好好干争取上市,到时候再给你一笔钱就当是投资了
  • 小C啊,书要好好读,一定要出人头地,到时候拿着我给你的这一笔钱自己开家公司当老板
  • 小D啊,你要混的话就好好混,争取有一年可以混成想黑帮教父那样,再给你一笔钱就更好过了

于是富翁就给这四个孩子分别都画了张大饼⚪,虽然不知是否会兑现承诺,但是先给每个人都说到这个

那对于我们上面所讲的故事我们可以对应计算机去做一个抽象💻

  • 这个大富翁呢则是对应我们所熟知的【操作系统】,拥有最大的掌管权;
  • 这些孩子们呢就是操作系统中的各个【进程】,由OS来进行管理;
  • 对于富豪给各个孩子所画的饼我们称之为【进程地址空间】,每个饼都对应一个具体的区域;
  • 对于这10亿美金来说呢我们称之为【内存】,用于分配给各个进程来使用;

那富豪既然给孩子们画了这些饼的话,按需不需要将这些饼给统一收好然后一一分发给各个孩子呢 ?即操作系统是否需要将各个进程地址空间给组织管理起来

答案是:当然要。因为管理的本质就是 ------ ==先描述,再组织==

  • 所以画的这一个个的饼即【进程地址空间】,其实就是一个个的结构体对象,我们将其称作为是mm_struct

② 38线竟是这么来的!

首先我们来看到这个进程地址空间,刚才我们讲到画的这一个个大饼就对应着每个进程的【进程地址空间】

  • 我们看到在这个进程地址空间中有着很多的区域:栈、堆、共享区等等 ,这些我们在上面就有讲说到过,在这里面的【正文代码段】中呢,可能就有我们在前面所讲到过的虚拟地址,它呢可能并不是内存中的一个实际地址,而是需要通过一定的手段才能找到内存中的那一个物理地址,继而找到正确的内容
  • 那不管是画的饼还是这一个个的分块的区域结构,都是抽象的,我们若是想看到一些实在的东西,就还需要将其转换为 物理层面的内容。就像富豪要给私生子们拿这笔钱就需要去银行里实际地取出来才行(那时没有网银)

不过对于mm_struct这个地址空间中的【代码区】、【数据区】、【堆区】、【栈区】到底该如何理解?

📕这里我还是通过一个故事来进行引入

  • 小花和小胖两个人呢在一所学校读中学,有一天小胖惹小花生气了,不小心打翻了她的水杯,于是呢小花就在桌子的中间画了一条 "38线":表示小胖不可以越过这条线,一旦越过的话就算是违规了

那我现在想问:小花画这条38线的本质是什么?

  • 相信反应快的同学一下子就能说个大概:没错,就是了【区域划分】。那如果使用计算机的术语去表述的话该如何去表述呢?

那就是使用我们在C语言中所学习到的struct结构体,内部呢有【start】和【end】这两个成员 ⇒ 那么对线性区域进行 指定start和end的划分即可完成区域的划分!

c 复制代码
struct area
{
	int start;
    int end;
}
  • 对这两块区域去做一个初始化的话就可以像下面这样
c 复制代码
struct area xiaohua_area = {1, 50};	
struct area xiaopang_area = {50 100};

但是呢在某一天呢,小胖又惹小花生气了,于是呢小花把这条线从55对半划到了给小胖只剩 3 的区域,那也就变成了真正的 "38线"

  • 那对应到代码层面我们就可以去做这样一个修整。将小花的末节区域end修改为【80】,并且将小胖的初始区域start设置为【80】
c 复制代码
xiaohua_area.end = 80
xiaopang_area.start = 80

③ 地址空间的深层理解

清楚了什么叫做区域划分之后,我们再通过进一步的理解加深对地址空间认识

  • 但是在上面讲了这么半天【线性划分】和【线性区域】,这我们本节所要讲的 地址空间 有什么关系呢?

那在这里我可以先给出结论:

地址空间本质就是一个线性区域

首先呢我们可以先来理解一下什么叫做【线性空间】👈

  • 我在 C语言指针章节 里也有说到过在32位系统中有32根总线,可以有2^32^个排列组合,即有2^32^个地址,从最低的地址0x00000000到最高的地址0xFFFFFFFF,这里的每一个地址都是连续的,换算成十进制就是 0 ~ 42亿多。所以我们把这个地址空间称之为【线性空间】
  • 因为这些数字是线性的,所以地址空间整体是线性的。每一个数字表示一个地址,一个地址表示一个字节

知道了什么是线性空间后,我们还要去进一步理解类型的相关概念

  • C语言数据类型章节 以及上面的指针章节我有将其过对于一个数据类型于我们而言可以去区分各种不同的数据;于计算机而言呢例如对于 指针来说决定了它走一步可以跨过多大的范围

💬 所以我们要明白类型存在的意义

  • 在计算机里可以帮我们确认当前变量申请的起始地址,然后会再根据类型来看它到底能取几个字节

在一开始我们就有提到过有关进程地址空间中的各种区域,那现在在熟知了【线性空间】这个概念之后呢,知道了一个区域有,那此时我们如何使用代码去维护这一段段的空间呢?

  • 下面是我在Linux的源代码中节选出来的:
    • 例如【代码段】,它的区域就是使用code_startcode_end来进行维护的
    • 例如【栈区】,它的区域就是使用stack_startstack_end来进行维护的
c 复制代码
struct mm_struct
{
    long code_start;
    long code_end;
    long init_start;
    long init_end;
    ...
    long brk_start;
    long brk_end;
    long stack_start;
    long stack_end;       
}

那既然可以使用结构体来表示这些区域的话,那我想请问:那么区域之间的数据叫做什么呢?

  • 例如[1000, 2000]之间的这段数据,12001500 or 1700这些,上面说到过,对于一个区域内的数据都是连续的,而且每一个数字代表一个地址,因此我们可以称它们为【虚拟地址 / 线性地址】

:book: 最后我们来总结一下:

  • 地址空间就是一段线性范围,从全0到全F,换算成十进制就是 0 ~ 42亿多。每一个数组不叫做整数,而叫做地址。因为数字是线性的,每个数字表示一个地址,每个地址对应1字节,因为 CPU寻址的最小单位就是字节,如果需要多个地址的话就连续申请多个字节,但一般是把 首地址 返回,在应用层再根据这个首地址去确定其在内存当中的位置,然后再加上类型,确定所申请的内存空间有多大(类型的本质就叫做偏移量)
  • 那上面说了这么多,其实本质我们还是想要讲一点:==地址空间本身就是线性结构==

三、分页 & 虚拟地址空间

经过上面的学习,我们明白了【区域划分】的意义所在,但是光划分出来区域是不够的,还需要有数据在里面存在,那我们必须明白一点:数据和代码真正只能在内存中!

1、页表的概念

  • 上面我们有谈到【虚拟地址】和【物理地址】,但是对它们之间的联系还是不太清楚,现在我们再通过引入 ==页表== 这个概念来进一步理解一下:
  • 可以看到对于task_struct来说它是指向一个内存中的地址空间,我们的有一块在这个地址空间中的 虚拟地址,但是呢我们的目的不是找到这个地址,而是要拿到这块地址中的内容,那么在Linux中呢,就会使用到 ==页表== 这个东西来做一个【映射】的操作,经过页表的转化变为物理内存之后,继而得到这个内存中的地址,确定清楚地址后内容也就读出来了,放到CPU里就可以被操作执行了

💬 那对于上面这个呢,就是 ==页表== 的基本模型了,它所呈现出来的是一个KV的结构,类似于我们在C++中所学习过的《map》,一个 key 值对应一个 value

2、疑难解答:为何父子进程没有发生同步修改?

那有了页面这个概念之后呢,我们就可以通过其来解释一下我们在前面所提出的问题了:父子进程访问的是同一个地址,那为何子进程在修改了g_val后父进程并没有发生同步改变呢?

  • 这里的话我们就需要通过父子两个进程来进一同观察了。通过下图我们可以看出,子进程按照着父进程拷贝了一份代码和数据,并且连虚拟地址也是一样的,那么通过页表的映射之后,它们便指向了物理内存中的同一块空间

💬 那为何子进程在修改完数据之后父进程也跟着一同修改了呢?

  • 还记得我们在 进程基本概念 的时候所说到过的【写时拷贝】这个 概念吗,因为进程在运行的时候是具有独立性的,所以为了不引起 ==并发修改异常==,当子进程需要对父进程中的数据做修改时,就会在系统的某一个位置开辟一段空间,将父进程中的数据拷贝到此处,在此处去做一个修改,然后再去改变一下页表的映射值,此时就不会对原先父进程中的数据产生影响
  • 在物理内容中申请完空间之后,并且修改的是当前进程所对应的 value值【物理地址】 ,不会影响到 key值【虚拟地址】,所以即使两个进程所访问的地址是同一块,所看到的也是不同的两个值

💬 解答:为何在fork之后父子进程得到的值不一样?

  • fork在返回的时候,父子都有了,return两次。id是不是pid_t类型的变量呢?返回的本质就是写入!谁先返回,谁就让OS发生写时拷贝
  • 所以父进程读到的就是子进程的pid,而给子进程返回的就是0

3、无进程地址空间的危害

上面我们谈及的是【页表】,但其实【进程地址空间】也同样得重要,我们再来看看这块

💬 如果没有地址空间,我们的OS是如何工作的

  • 如果没有虚拟地址空间的话,我们平常写代码用到的地址全都是 物理地址,那么若有些进程在访问地址的时候发生了越界,便可通过指针的方式修改这个地址中的数据,那就影响了其他进程的正常运行
  • 例如这里有个用于【客户端登录】的进程,就需要让用户输账号和密码,所以有的人在通过某种手段故意越界访问别的地址,就可以获取到别的用户的账号和密码,这就导致了安全问题

⭐ 但是当我们有了【页表】和【虚拟地址空间】之后,任何一个被CPU读进来的数据进行访问的时候,不是直接去访问这个物理内存了,而是需要通过 虚拟地址结合页表 进行映射才能访问到物理内存 ⭐ 只要我们需要映射的话,就可以决定你是否能成功进行映射,比方说当前的这个进程通过页表映射去访问了一个错误的位置,那么就会被操作系统给检测出来

4、页表的意义所在💡

明白了进程地址空间的重要性之后呢,页表也同样得重要,接下去就让我们来学习一下 页表的意义所在

① 谨防滥操,保护物理内存

  • 还是一样,我会通过一个案例来进行引出:到了春节我们都要走亲访友,去给长辈拜年的时候难免会收到一些压岁钱,现在可不像从前只是一、两百得给,而是几千几千得给了,那么此时我们的手上就会有一大笔钱了💴,于是就可能会拿着这笔钱去买一些自己喜欢的东西。
  • 那这时呢店家看你是小孩子就会狠狠地敲诈你一笔,说:小朋友,你看你现在已经XX年级了,比你高一年级的同学都要买这个书籍的,看看你是不是也有必要买一本呢😁
  • 那你的这种行为被你妈妈知道之后呢,就开始对你进行『制裁』,开始帮你去 "保管" 手上的这笔前,那妈妈这么做也是有道理的:核心工作是为了拦住你,不让你乱花钱

💬 那这对应到我们所说的【页表映射机制】其实也是同样的道理

  • 进程地址空间想要访问物理内存需要先经过 ==页表映射==,但页表映射的时候不合理时就会拦截你的映射,操作系统识别到就会不让你访问物理内存。

光是感知层面的事,我们再来看看在代码层面该如何去进行理解:

  • 还记得我们在C语言中所学习过的常量字符串吗,那学习了 C/C++内存分布 之后我们明白了这些字符串都是存放在进程地址空间中【常量区】,且都是不可修改的,因此*str = 'H'就属于 非法操作 了
  • 于是此刻 ==页表== 的功能就显现出来了,当我们通过代码去做这样非法的操作时,就会在页表映射的时候发生被OS检测到,从而告知用户此操作是非法的,
c 复制代码
char* str = "hello world";
*str = 'H';	
  • 所以在CPU操控进程访问内存的时候,中间相当于加了一层转化的过程,这转化的过程就是由OS帮我们去转的,它相当于在进程和内存之间呢加了一层 软件层 ,你想做转换的话如果合法的话我就load把你加载进去,但如果你并不合法的话我就就会拦截你,这就叫做 保护物理内存与其他进程

所以在学习了【进程地址空间】之后,我们之前在C语言中所学习的一些知识就可以进一步作加深和理解了,这些边界性的知识在我们学习了《操作系统》这门课后就可以更加地融汇贯通了

⇒ 因此我们可以得出页表的第一个意义所在:防止地址随意访问,保护物理内存与其他进程


当然除此之外页表还存在着其他的意义,不知大家对C语言中malloc是否还有印象?

💬 那首先我想问一个问题:当我们使用malloc去申请内存的时候,操作系统立马给你,还是 需要的时候 给你呢?

  • 可能我们平常不会去关注这个点,因此在调用了malloc()之后执行了程序后,感觉操作系统立马就将内存空间分配给我们了,但其实呢并不是这样的
  • 我们在上面通过这幅图已经理清了内部的访问机制,那我们在使用malloc申请内存的过程其实也类似:当一个申请内存的进程访问时,就在物理内存中为其申请一块空间,然后通过也页表映射,建立起【物理地址】和【虚拟地址】之间的关系,然后从虚拟地址映射出来堆空间的起始地址返回给到这个进程,此时我们就通过代码申请到了这个空间的地址

理清了内部的这个流程之后呢,我们更要清楚的一点是:这块内存虽然会给到你,但并不是立即给你的,因为在操作系统内部一般有着这么几点共识:

  1. OS一般不允许任何的浪费或者不高效
  2. 申请内存不一定立马使用
  3. 在你申请成功之后,和你使用之前,就会存在一段小小的时间窗口,这个空间就没有被正常使用,但是别人用不了,因此我们将其称作为【闲置状态】
  • 于是呢操作系统内部就产生了这么一个机制,当有进程想要在物理内存中申请一块空间的时候,页表会先为虚拟地址先建立映射,相当于是 做个标记 🔰,代表你需要申请内存空间,但是呢先不设置value值即相对应的不在内存中申请空间
  • 说得再通俗一点:就是当OS在识别到有下面这段代码的时候,知道了你将会有申请内存空间的这个需求,但是呢先不给你申请,知道你将所有的代码写完之后,将程序给运行起来了,此时当进程执行到这句代码的时候,就会通过一开始在页表中建立的映射关系,对应地在物理内存中也申请一块空间并返回起始地址,将两个地址通过页表再关联起来✌
c 复制代码
int* a = (int*)malloc(sizeof(int) * 10);

💬 那有同学此时就要问了:这样子确实蛮好的,在执行的再去申请具体的空间,但感觉意义也不是特别得大╮(╯▽╰)╭

  • 同学,这你就不懂了吧,其实它内部还蕴含着很重大的意义呢,再往深层次一面去想:因为有着 ==页表映射== 的存在,所以进程在执行的时候完全不需要关系操作系统为其在物理内存中所分配的是哪一块空间,只需要去执行当前进程中的那个代码块即可
  • 那由这个页表为中心,我们可以很好地将【地址空间】和【物理内存】分为两块来进行看待,左侧为进程管理,右侧呢即为内存管理,如果有学习过《操作系统》这门课的同学就可以清楚这两种管理
  • 因为有了 ==页表映射机制== 所以作为一个进程来讲,它永远知道自己虚拟地址的这个范围,但具体在哪个物理内存当中它并不关心,对于进程的代码和数据可以放在任意的位置,只要最终能找到就可以。因此这就使得进程管理内存管理进行了一个 解耦合 操作,二者既有关联、又互不影响

举个很简单易懂的例子:

🎯 比方说你这个月向你爸要生活费,然后你爸给你大了1000块,但是呢你并不需要关心这笔钱是从哪里来的,是你爸工作赚来的、或者是打零工赚来的,这是你爸应该关心的事,要怎么给你去弄到这一千块钱的生活费,而你要关心的则是如何拿到这笔钱并且怎么合理地使用它

⇒ 因此我们可以得出页表的第二个意义所在:将进程管理和内存管理进行解耦合,使二者既有关联、又互不影响

② 解耦『进程管理』和『内存管理』

接下去我们再来继续挖挖【页表】这个东西,它的存在还有这什么更加深层次的意义

💬 首先还是先以问题引入:我们的程序在编译完成之后,没有加载到内存,那么此时程序的内部还有没有地址呢?

  • 答案是:有的! 那有些同学就会很疑惑?程序都还没有装入到内存,何来地址呢?
  • 其实对于我们的程序而言,它在编译完之后并不是混乱地放在磁盘内部的,而是也会像进程地址空间那样去做一一的区域划分,类似于:已初始化全局数据段、未初始化全局数据段等等,内部的代码和数据在加载到内存的时候是以分批的形式进行加载的

③ 统一视角,进程循环

所以读者在看了虚拟地址空间之后不要认为这样的策略只会影响OS,==对于我们的编译器而言,其实也遵守着这样的规则==

  • 当源代码被编译的时候,就是按照虚拟地址空间的方式对代码和数据早就编号了对应的编址,不过具体是怎么去编译的读者如果有兴趣的话可以去学习一下 《编译原理》 这门课,里面会有相关的涉及
  • 可以带读者真实地来到Linux下看一看,对于可执行程序为何会存在编址
shell 复制代码
objdump -S 可执行程序
  • 我们透过一个具体的案例再来理解一下,现在我们的磁盘中有一个可执行程序,它是一个函数调用,然后当这个call 0x1122ff80加载到内存中的时候,就会产生一个物理地址。此时当外界的进程开始通过进程地址空间进行访问的时候,就可以由 ==页表映射== 找到这块物理地址,从而找到里面的这个函数调用
  • 接下去呢,便可以通过将读取到的这个数据返回,因为它是个地址调用,所以CPU会将其有不一样的看法,在这里就会将其看作为是虚拟地址
  • 那么既然它是一个虚拟地址的话,就可以通过【进程地址空间】去转换到 页表 进行映射,然后再去取到下一个物理地址中的内容,发现地址中还是一个虚拟地址,读取到CPU内再度调用,这也就开启了一个[进程读取循环]
  • 以进程地址空间【正文代码】中的main()函数作为起始地址开始执行,一句句执行下去,若是碰到有函数调用的话就进行跳转执行。这就使得每一个进程都遵循一个统一的规则去进行运转。

⇒ 因此我们可以得出页表的第三个意义所在:可以让进程以统一的视角,看待自己的代码和数据!

🎁小彩蛋:手机为何会发烫呢?

看那么久文章脑瓜子一定嗡嗡的吧😵 马上进入我们的彩蛋时刻

💬 那么再问一个小问题:进程的代码和数据必须一直在内存中吗?

  • 答案是:不一定。因为有【进程地址空间】的存在,你用多少我给你加载多少,其中执行完的代码就直接扔掉了,再重新去加载。这样就可以边加载边执行,需要的时候就把数据提前加载到内存里,不需要就把它扔掉,这样就可以使操作系统中的内存在很低的使用量的同时还可以把大软件给跑起来

  • 现在市面上有很多的游戏,像:LOL、王者荣耀、刀塔传奇、原神、炉石传说等等,这些都是大型的网游,下载下来需要很大内容空间10/20G。不知大家日常在打游戏的时候会不会越玩越卡,然后手机就渐渐发烫然后掉帧呢(处理器好的同学请忽略)
  • 这是因为我们的手机在运行起来的时候就会加载一些系统自带的内容,大部分情况下我们的时候正在进行网络IO,还有一种情况呢我们的手机可能把当前进程正在加载的进程中的数据换出到固态硬盘中,也就是我们之前所学习过的【进程的换入换出】操作 ,不断地在做数据交换,说白了就是在做 二进制写入,所以我们手机的CPU在工作久了之后压力就比较大,就导致了发热的情况

四、总结与提炼

那么经过上面的这么一番分析之后呢,大家应该对页表在操作系统管理的时候所呈现的重要性有了一定的认识,最后来总结以下本文所学习的内容:book:

本文我们主要是在讲Linux下的『进程地址空间』,我们可以通过三个方面来回顾一下:

💬 进程地址空间是什么?

  • 在操作系统内部为进程创建出来的一种具体的数据结构对象,用来让进程以统一的视角去看待物理内存。因为有进程地址空间的存在,便可以让进程管理和内存管理独立开来
  • 并且有进程地址空间的存在,我们可以在磁盘当中编译程序时就把编译好的程序以地址空间的形式把它排布好,这样加载到内存CPU在进行读取识别时,它读到的就都是虚拟地址。根据这个虚拟地址经过页表映射之后再进行相应的读取,内部的就开始转起来了

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

  1. 防止地址随意访问,保护物理内存与其他进程

  2. 将进程管理和内存管理进行解耦合

  3. 可以让进程以统一的视角,看待自己的代码和数据!

💬 怎么去用这个进程地址空间?

  • 在内核当中定义mm_struct这个数据结构,这个数据结构里充满了大量的区域,所谓的区域就是startend这样的指针,用来限定各种区域的起始和结束。然后通过页表再经过映射到物理内存

以上就是本文要介绍的所有内容,感谢您的阅读:rose::rose::rose:

相关推荐
Bert.Cai2 分钟前
Linux chown命令详解
linux·运维·服务器
XMAIPC_Robot5 分钟前
基于RK3588 ARM+FPGA电火花数控机床控制系统设计,兼顾ethercat软硬件实时
linux·arm开发·人工智能·嵌入式硬件·fpga开发
青梅橘子皮5 分钟前
Linux---进程切换与调度
linux·运维·服务器
底层开发智库12 分钟前
C1-Ultra FVP调试并运行Linux kernel全程记录,硬核演示如何解决启动问题
linux·arm开发·内核·嵌入式·arm
承渊政道22 分钟前
Linux系统学习【进程控制:进程创建、终止与等待、进程程序替换、自主shell命令行解释器详解】
linux·服务器·c++·学习·ubuntu·bash·远程工作
Kurisu57527 分钟前
深度拆解:从 Linux 内核 Namespace 与 Cgroups 洞察容器技术的底层本质
java·linux·运维
liulilittle32 分钟前
Linux SS快速诊断命令
linux·运维·智能路由器
晚风吹红霞1 小时前
Linux下的趣味编程 —— 进度条、Git版本控制与GDB调试实战
linux·运维·git
nan madol1 小时前
Rocky Linux 9.5 部署 Percona XtraDB Cluster (PXC) 集群
linux·运维·服务器
zincsweet1 小时前
Linux 命名管道(FIFO)详解:原理分析、源码封装与通信流程图解
linux·服务器·c++·流程图