<Linux>进程地址空间
Posted beyond->myself
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了<Linux>进程地址空间相关的知识,希望对你有一定的参考价值。
进程地址空间
文章目录
一、程序地址空间的空间排布
相信我们在学习C的过程中,下面这幅图都见过:
1、验证程序地址空间的排布:
- 程序地址空间不是内存。我们在linux操作系统中通过代码对该布局进行如下的验证:
#include<stdio.h> #include<stdlib.h> int g_unval;//未初始化 int g_val = 100;//初始化 int main(int argc,char *argv[],char *envp[]) printf("code addr:%p\\n",main);//代码区起始地址 const char* p = "hello world";//p是指针变量(栈区),p指向字符常量h(字符常量区) printf("read only addr:%p\\n",p);//初始化数据 printf("global val:%p\\n",&g_val);//未初始化数据 printf("global uninit val:%p\\n",&g_unval); char *q = (char*)malloc(10); printf("heap addr:%p\\n",q);//堆区 printf("stack addr:%p\\n",&p);//栈区 p先定义,先入栈 printf("stack addr:%p\\n",&q);//栈区 int i = 0; for(int i = 0; i < argc; i++) printf("argc addr:%p\\n", argv[i]);//命令行参数 i = 0; while(envp[i]) printf("env addr:%p\\n", envp[i]);//环境变量 i++; return 0;
总结:
- 堆区向地址增大方向增长(箭头向上)
- 栈区向地址减少方向增长(箭头向下)
- 堆,栈相对而生
- 我们一般在C函数中定义的变量,通常在栈上保存,那么先定义的一定是地址比较高(先定义先入栈,后定义后入栈)的。
3、如何理解static变量:
- 先前我们知道如果一个变量被static修饰,它的作用域不变,依旧在该函数内有效,但是其声明周期会随着程序一直存在,可为什么呢?
先来看下正常定义的变量:
#include <stdio.h> #include <stdlib.h> int un_g_val; int g_val = 100; int main() printf("code addr:%p\\n", main); printf("init global addr:%d\\n", &g_val); printf("uninit global addr:%d\\n", &un_g_val); char* m1 = (char*)malloc(100); char* m2 = (char*)malloc(100); int s = 100; printf("heap addr:%d\\n", m1); printf("heap addr:%d\\n", m2); printf("stack addr:%d\\n", &m1); printf("stack addr:%d\\n", &m2); printf("s stack addr addr:%d\\n", &s); return 0;
此时正常定义的变量s就符合先前栈的地址分布规则:后定义的变量在地址较低处,下面来看下static定义的变量:
根据图示:变量s一旦被static修饰,尽管s是在代码函数里面被定义,可是此变量已经不在栈上面了,此时变成了全局变量,这也就
是为什么生命周期会一直存在。
**总结:**函数内定义的变量static修饰,本质是编译器会把该变量编译进全局数据区内。
二、进程地址空间是什么?
用下面代码为示例:
当父子进程没有人修改全局数据的时候,父子是共享该数据的!可以通过下面的运行结果看出:
如果尝试写入呢?
代码共享,所以看到前五次打印的g_val的地址都是一样的,这我们不意外,等到了第六次时,我们发现父进程g_val依然是100,子进程的g_val变成了300,因为我们将它改了,这也不意外,因为前面说了,父子进程之间代码共享,而数据是各自私有一份的(写时拷贝),但是令人奇怪的是地址竟然是一样的!
如果我们看到的地址,是物理地址,这种情况可不可能呢?
这是绝对不可能的,如果可能,那么父子进程在同一个地址处读取数据怎么会是不同的值呢?所以,我们曾经所学到的所有的地址,绝对不是物理地址。
这种地址我们称之为虚拟地址、线性地址、逻辑地址!!!
任何我们学过的语言,里面的地址都绝对不会是物理地址,虚拟地址这种地址是由操作系统给我们提供的,操作系统如何给我们提供呢?既然这种地址是虚拟地址,那么一定有某种途径将虚拟地址转化为物理地址,因为数据和代码一定在物理内存上,因为冯诺依曼规定任何数据在启动时必须加载到物理内存,所以肯定需要将虚拟地址转化成物理地址,这里的转化工作由操作系统完成,所有的程序都必须运行起来,运行起来之后,该程序立即变成了进程,那么刚刚打印的虚拟地址大概率和进程有某种关系
我们在上学期间,经常可能会和同桌画三八线,比如一张课桌是100cm,我们用一把尺子来划分区域,女孩的区域是0,50,男孩的区域是51,100,那么我们再计算机当中怎么描述这个事情呢?我们可以这样定义:
struct area unsigned long start; unsigned long end; ; struct area girl = 0,50; struct area boy = 50,100;
此时我们就划分好了区域,这时,不管是男孩还是女孩,大脑里都有了这样的一个区域:
当女孩觉得自己活动范围不够,想扩大自己的区域时,就可以调整自己认为的[start,end],划分三八线的过程,就是划分区域的过程,调整区域的过程,本质就是调整自己认为的[start,end]
其中我们将桌子认为是物理内存,男孩和女孩认为是每一个进程,而男孩和女孩本质上都认为自己有一把尺子(脑海里的尺子),这把尺子就是进程地址空间,男孩想放自己的书包、铅笔等物品时,男孩就在自己的进程地址空间再划分区域放自己的物品。
那么如何划分进程地址空间的区域呢?在Linux当中,进程地址空间本质上是一种数据结构,是多个区域的集合。
在Linux内核中,有这样一个结构体:struct mm_struct,在这个结构体去表示我们开始说的一个一个的区域呢?这样去表示:
struct mm_struct unsigned long code_start;//代码区 unsigned long code_end; unsigned long init_start;//初始化区 unsigned long init_end; unsigned long uninit_start;//未初始化区 unsigned long uninit_end; unsigned long heap_start;//堆区 unsigned long heap_end; unsigned long stack_start;//栈区 unsigned long stack_end; //...等等
在上面的例子中,男孩脑海里有一把尺子,想着自己拥有桌子的一半,女孩脑海里也有一把尺子,想着自己也拥有桌子的一半,而此时我们改变了:男孩和女孩关系比较好,不进行什么划分三八线,男孩脑海里有一把尺子,想着自己拥有0-100cm的桌子,女孩脑海里有一把尺子,想着自己也拥有0-100cm的桌子,他们在放东西时,只要记住了尺子的刻度就可以了。
为了更深一步的理解进程地址空间,我们再来举一个例子:
比如有一个富豪,他拥有10个亿的身家,这个富豪有3个私生子,这3个私生子互相并不知道自己的存在,富豪对自己的每一个私生子都说孩子你好好工作、好好学习,好好干,老爸现在有10个亿的家产,以后就全是你的,这大饼画的666,请问在这3个私生子的视觉来看,他们认为他们有多少的家产?当然是10亿,当每个私生子向这个富豪要钱时,只要能接受,富豪肯定都会给,不能接受,富豪可以直接拒绝,在这个例子中富豪给每个私生子脑海里建立了虚拟的10个亿,此时每个私生子都认为自己有10个亿,每个人要的钱都是不一样的。
在这个例子中:富豪称之为操作系统,私生子称之为进程,富豪给私生子画的10亿家产,当前私生子的地址空间,对比言之:操作系统默认会给每个进程构成一个地址空间的概念(32位下,地址空间是从000000…0000到FFFFFF…FFF)4GB的空间,每个进程都认为自己有4GB的空间,每个进程都可以向内存申请空间,只要能接受都会给你,不能接受操作系统会直接拒绝,但是对进程来说并没有什么影响,进程依旧认为自己有4GB的空间。
再回到男孩和女孩的例子,我们的进程地址空间就相当于是那把尺子,而尺子是有刻度的,进程地址空间也是从00000000的地址到FFFFFFFF的地址,可以在这上面进行区域划分:比如代码区:[code_start,code_end],比如代码区的地址区间是这个:[0x10000,0x20000],那么区间的每一个地址单位就称为虚拟地址。
我们之前将那张布局图称为程序地址空间实际上是不准确的,那张布局图实际上应该叫做进程地址空间,进程地址空间本质上是内存中的一种内核数据结构,在Linux当中进程地址空间具体由结构体mm_struct实现。之前说"程序的地址空间"其实是不准确的,准确的应该说成"进程地址空间",概念如下:
- 每一个进程在启动的时候,都会让操作系统给它创建一个地址空间,该地址空间就是进程地址空间。每一个进程都会由一个自己的进程地址空间。操作系统需要管理这些进程地址空间,依旧是先描述,再组织。所谓的进程地址空间,其实是内核的一个数据结构(struct mm_struct )
总结:
进程地址空间本质是进程看待内存的方式,抽象出来的一个概念,内核:struct mm_struct,这样的每个进程,都认为自己独占系统内存资源(每个私生子都认为自己独占10亿家产),地址空间区域划分本质:将线性地址空间划分成为一个一个的area,[start,end]。虚拟地址本质,在[start,end]之间的各个地址叫做虚拟地址。所谓的进程地址空间,其实就是OS通过软件的方式,给进程提供一个软件视角,让其认为自己会独占系统的所有资源(内存)。
三、地址空间和物理内存的关系
在Linux内核中,每个进程都有task_struct结构体,该结构体有个指针指向一个结构mm_struct(程序地址空间),我们假设磁盘的一个程序被加载到物理内存,我们需要将虚拟地址空间和物理内存之间建立映射关系,这种映射关系是通过页表(映射表)的结构完成的(操作系统会给每一个进程构建一个页表结构)。
我们的可执行程序里面有没有地址???(在没有加载到内存的时候?)
- 可执行程序还没加载到内存的时候,内部早就已经有地址了。编译器编译你得代码的时候,就是按照虚拟地址空间的方式进行对我们的代码和数据进行编址的。这个地址是我们的程序内部使用的地址,程序加载到内存中,天然就具有一个外部的物理地址。意思就是我们有两套地址:
1、标识物理存在中代码和数据的地址的物理地址,除了栈区和堆区不会申请地址,因为它们是动态的。
2、在程序内部互相跳转的时候使用的是虚拟地址
程序是如何变成进程的?
- 程序被编译出来,没有被加载的时候,程序内部是有地址和区域的。不过这里的地址采用的是相对地址的方式,而区域实际是在磁盘上已经划分好了。加载无非就是按照区域加载到内存。
为什么先前修改一个进程时,地址是一样的,但是父子进程访问的内容却是不一样的?
- 当父进程被创建时,有自己的task_struct和地址空间mm_struct,地址空间会通过页表映射到物理内存,当fork创建子进程的时候,也会有自己的task_struct和地址空间mm_struct,以及子进程对应的页表,如下:
- 而当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。所以先前打印的g_val的值和内容均是一样的。当子进程需要修改数据g_val时,结果就变了,先看图:
- 无论父进程还是子进程,因为进程具有独立性,如果子进程把变量g_val修改了,那么就会导致父进程识别此变量的时候出现问题,但是独立性的要求是互不影响,所以此时操作系统会给你子进程重新开辟一块空间,把先前g_val的值100拷贝下来,重新给此进程建立映射关系,所以子进程的页表就不再指向父进程的数据100了,而是指向新的100,此时把100修改为300,无论怎么修改,变动的永远都是右侧,左侧页表间的关系不变,所以最终读到的结果为子进程是300,父进程是100.
- 总结:当父子对数据修改的时候,操作系统会给修改的一方,重新开辟一段空间,并且把原始数据拷贝到新空间中,这种行为我们称之为写时拷贝。通过页表,将父子进程的数据就可以通过写时拷贝的方式,进行了分离。从而做到父子进程具有独立性的特点。
fork有两个返回值,pid_t id,同一个变量,怎么会有不同的值?
- 一般情况下,pid_t id是属于父进程的栈空间中定义的变量,fork内部,return会被执行两次,return的本质就是通过寄存器将返回值写入到接收返回值的变量中!当id = fork()的时候,谁先返回,谁就要发生写时拷贝,所以,同一个变量,会有不同的内容值,本质是因为大家的虚拟地址是一样的,但是大家对应的物理地址是不一样的。
为何fork返回之后,给父进程返回子进程pid,给子进程返回0?
- 举个例子,假如一个父亲有三个孩子,这个父亲想要抱抱其中的某个孩子,那他肯定会直呼某个孩子的名字,否则他说孩子,让爸爸抱抱,这时候三个孩子一拥而上。但是这三个孩子只有一个父亲,所以不会出错。
同一个id,怎么可能会保存两个不同的值,让 if,else if同时执行?
- 返回的本质就是写入。父子进程谁先返回不确定。谁先返回,谁就先写入id,写入就要发生写时拷贝,同一个id,地址是一样的,但是内容却不一样。
四、为什么会存在进程地址空间?
为什么进程不直接访问物理内存呢?这样不行吗?为什么要存在地址空间呢?
- 保护物理内存不受到任何进程内地址的直接访问,在虚拟地址到物理地址的转化过程中方便进行合法性校验
在早些时候是没有地址空间的:
如果进程直接访问物理内存,那么看到的地址就是物理地址,而语言中有指针,如果指针越界了,一个进程的指针指向了另一个进程的代码和数据,那么进程的独立性,便无法保证,因为物理内存暴露,其中就有可能有恶意程序直接通过物理地址,进行内存数据的篡改,如果里面的数据有账号密码就可以改密码,即使操作系统不让改,也可以读取。
后来就发展出来了虚拟地址空间,那么虚拟地址空间如何避免这样的问题呢?
由上面我们所了解的知识,一个进程有它的task_struct,有地址空间,有页表,页表当中有虚拟地址和物理内存的映射关系,有了页表的存在,虚拟地址到物理地址的一个转化,由操作系统来完成的,同时也可以帮系统进行合法性检测
我们在写代码的时候肯定了解过指针越界,我们知道地址空间有各个区域,那么指针越界一定会出现错误吗?
不一定,越界可能他还是在自己的合法区域。比如他本来指向的是栈区,越界后它依然指向栈区,编译器的检查机制认为这是合法的,当你指针本来指向数据区,结果指针后来指向了字符常量区,编译器就会根据mm_struct里面的start,end区间来判断你有没有越界,此时发现你越界了就会报错了,这是其中的一种检查,第二种检查为:页表因为将每个虚拟地址的区域映射到了物理内存,其实页表也有一种权限管理,当你对数据区进行映射时,数据区是可以读写的,相应的在页表中的映射关系中的权限就是可读可写,但是当你对代码区和字符常量区进行映射时,因为这两个区域是只读的,相应的在页表中的映射关系中的权限就是只读,如果你对这段区域进行了写,通过页表当中的权限管理,操作系统就直接就将这个进程干掉。
所以进程地址空间的存在也使得可以通过start和end以及页表的权限管理来判断指针是否合法访问
- 将内存管理和进程管理进行解耦
这里我们主要讲的是进程管理和内存管理:
如果没有进程地址空间,进程直接访问物理内存,当进程退出时,内存管理需要尽快将该进程回收,在这个过程当中必须得保证内存管理得知道某个进程退出了,并且内存管理也得知道某个进程开始了,这样才能给他们及时的分配资源和回收资源,这就意味着内存管理和进程管理模块是强耦合的,也就是说内存管理和进程管理关系比较大,通过我们上面的理解,如果有了进程地址空间,当一个进程需要资源的时候,通过页表映射去要就可以了,内存管理就只需要知道哪些内存区域(配置)是无效的,哪些是有效的(被页表映射的就是有效的,没有被页表映射的就是无效的),当一个进程退出时,它的映射关系也就没了,此时没有了映射关系,物理内存这里就将该进程的数据设置为无效,所以第二个好处就是将内存管理和进程管理进行解耦,内存管理是怎么知道有效还是无效的呢?比如说在一块物理内存区域设置一个计数器count,当页表中有映射到这块区域时,count就++,当一个映射去掉时,就将count–,内存管理只需要检测这个count是不是0,如果为0,说明它是没人用的。
有了进程地址空间后,每个进程都认为自己在独占内存,这样能更好的完成进程的独立性以及合理使用内存空间(当实际需要使用内存空间的时候再在内存进行开辟),并能将进程调度与内存管理进行解耦或分离。
没有进程地址空间时,内存也可以和进程进行解耦,但是代码会设计的特别复杂,所以最终会有进程地址空间
有了进程地址空间后,每个进程都认为看得到都是相同的空间范围,包括进程地址空间的构成和内部区域的划分顺序等都是相同的,这样一来我们在编写程序的时候就只需关注虚拟地址,而无需关注数据在物理内存当中实际的存储位置。
五、Linux2.6内核进程调度队列(选学)
一个CPU拥有一个runqueue
如果有多个CPU就要考虑进程个数的父子均衡问题。
优先级
queue下标说明:
- 普通优先级:100~139。
- 实时优先级:0~99。
我们进程的都是普通的优先级,前面说到nice值的取值范围是-2019,共40个级别,依次对应queue当中普通优先级的下标100139。
注意: 实时优先级对应实时进程,实时进程是指先将一个进程执行完毕再执行下一个进程,现在基本不存在这种机器了,所以对于queue当中下标为0~99的元素我们不关心。
活动队列
时间片还没有结束的所有进程都按照优先级放在该队列
nr_active: 总共有多少个运行状态的进程
queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!
从该结构中,选择一个最合适的进程,过程是怎么的呢?
- 从0下表开始遍历queue[140]
- 找到第一个非空队列,该队列必定为优先级最高的队列
- 拿到选中队列的第一个进程,开始运行,调度完成!
- 遍历queue[140]时间复杂度是常数!但还是太低效了!
bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!
总结: 在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不会随着进程增多而导致时间成本增加,我们称之为进程调度的O(1)算法。
过期队列
- 过期队列和活动队列结构一模一样
- 过期队列上放置的进程,都是时间片耗尽的进程
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
active指针和expired指针
- active指针永远指向活动队列。
- expired指针永远指向过期队列。
可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。
没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!
时间成本增加,我们称之为进程调度的O(1)算法。
参考文章:进程地址空间
Linux进程地址空间与虚拟内存
http://blog.csdn.net/xu3737284/article/details/12710217
32位机器上linux操作系统中的进程的地址空间大小是4G,其中0-3G是用户空间,3G-4G是内核空间。进程的地址空间存在于虚拟内存中。虚拟内存不能被禁用。
进程地址空间
进程地址空间分为内核空间和用户空间
因为每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。
A.正文段。这是由cpu执行的机器指令部分。通常,正文段是可共享的,所以即使是经常执行的程序(如文本编辑程序、C编译程序、shell等)在存储器中也只需要有一个副本,另外,正文段常常是只读的,以防止程序由于意外事故而修改器自身的指令。
B.初始化数据段。通常将此段称为数据段,它包含了程序中需赋初值的变量。例如,C程序中任何函数之外的说明:
int maxcount = 99;(全局变量)
C.非初始化数据段。通常将此段称为bss段,这一名称来源于早期汇编程序的一个操作,意思是"block started by symbol",在程序开始执行之前,内核将此段初始化为0。函数外的说明:
long sum[1000];
使此变量存放在非初始化数据段中。
D.栈。自动变量以及每次函数调用时所需保存的信息都存放在此段中。每次函数调用时,其返回地址、以及调用者的环境信息(例如某些机器寄存器)都存放在栈中。然后,新被调用的函数在栈上为其自动和临时变量分配存储空间。通过以这种方式使用栈,C函数可以递归调用。
E.堆。通常在堆中进行动态存储分配。由于历史上形成的惯例,堆位于非初始化数据段顶和栈底之间。
从上图我们看到栈空间是下增长的,堆空间是从下增长的,他们会会碰头呀?一般不会,因为他们之间间隔很大
内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间。
在linux操作系统中,每个进程都通过一个task_struct的结构体描叙,每个进程的地址空间都通过一个mm_struct描叙,c语言中的每个段空间都通过vm_area_struct表示,他们关系如下 :
当运行一个程序时,操作系统需要创建一个进程,这个进程和程序之间都干了些什么呢?
当一个程序被执行时,该程序的内容必须被放到进程的虚拟地址空间,对于可执行程序的共享库也是如此。可执行程序并非真正读到物理内存中,而只是链接到进程的虚拟内存中。
当一个可执行程序映射到进程虚拟地址空间时,一组vm_area_struct数据结构将被产生。每个vm_area_struct数据结构表示可执行印象的一部分;是可执行代码,或是初始化的数据,以及未初始化的数据等。
linux操作系统是通过sys_exec对可执行文件进行映射以及读取的,有如下几步:
1.创建一组vm_area_struct
2.圈定一个虚拟用户空间,将其起始结束地址(elf段中已设置好)保存到vm_start和vm_end中。
3.将磁盘file句柄保存在vm_file中
4.将对应段在磁盘file中的偏移值(elf段中已设置好)保存在vm_pgoff中;
5.将操作该磁盘file的磁盘操作函数保存在vm_ops中
注意:这里没有对应 的页目录表项创建页表,更不存在设置页表项了。
假设现在程序中有一条指令需要读取上面vm_start--vm_end之间的某内容
例如:mov [0x08000011],%eax,那么将会执行如下序列:
1.cpu依据CR3(current->pgd)找到0x08000011地址对应的pgd[i],由于该pgd[i]内容保持为初始化状态即为0,导致cpu异常.
2.do_page_fault被调用,在该函数中,为pgd[i]在内存中分配一个页表,并让该表项指向它,如下图所示:
注意:这里i为0x08000011高10位,j为其中间10位,此时pt表项全部为0(pte[j]也为0);
3.为pte[j]分配一个真正的物理内存页面,依据vm_area_struct中的vm_file、vm_pgoff和vm_ops,调用filemap_nopage将磁盘file中vm_pgoff偏移处的内容读入到该物理页面中,如下图所示:
①。分配物理内存页面;
②。从磁盘文件中将内容读取到物理内存页面中
从上面我们可以知道,在进程创建的过程中,程序内容被映射到进程的虚拟内存空间,为了让一个很大的程序在有限的物理内存空间运行,我们可以把这个程序的开始部分先加载到物理内存空间运行,因为操作系统处理的是进程的虚拟地址,如果在进行虚拟到物理地址的转换工程中,发现物理地址不存在时,这个时候就会发生缺页异常(nopage),接着操作系统就会把磁盘上还没有加载到内存中的数据加载到物理内存中,对应的进程页表进行更新。也许你会问,如果此时物理内存满了,操作系统将如何处理?
下面我们看看linux操作系统是如何处理的:
如果一个进程想将一个虚拟页装入物理内存,而又没有可使用的空闲物理页,操作系统就必须淘汰物理内存中的其他页来为此页腾出空间。
在linux操作系统中,物理页的描叙如下:
struct mem_map
{
1.本页使用计数,当该页被许多进程共享时计数将大于1.
2.age描叙本页的年龄,用来判断该页是否为淘汰或交换的好候选
3.map_nr描叙物理页的页帧号
}
如果从物理内存中被淘汰的页来自于一个映像或数据文件,并且还没有被写过,则该页不必保存,它可以丢掉。如果有进程在需要该页时就可以把它从映像或数据文件中取回内存。
然而,如果该页被修改过,操作系统必须保留该页的内容以便晚些时候在被访问。这种页称为"脏(dirty)页",当它被从内存中删除时,将被保存在一个称为交换文件的特殊文件中。
相对于处理器和物理内存的速度,访问交换文件要很长时间,操作系统必须在将页写到磁盘以及再次使用时取回内存的问题上花费心机。
如果用来决定哪一页被淘汰或交换的算法不够高效的话,就可能出现称为"抖动"的情况。在这种情况下,页面总是被写到磁盘又读回来,操作系统忙于此而不能进行真正的工作。
linux使用"最近最少使用(Least Recently Used ,LRU)"页面调度技巧来公平地选择哪个页可以从系统中删除。这种设计系统中每个页都有一个"年龄",年龄随页面被访问而改变。页面被访问越多它越年轻;被访问越少越老。年老的页是用于交换的最佳候选页。
以上是关于<Linux>进程地址空间的主要内容,如果未能解决你的问题,请参考以下文章
Linux进程概念——下验证进程地址空间的基本排布 | 理解进程地址空间 | 进程地址空间如何映射至物理内存(页表的引出) | 为什么要存在进程地址空间 | Linux2.6内核进程调度队列
Linux进程概念——下验证进程地址空间的基本排布 | 理解进程地址空间 | 进程地址空间如何映射至物理内存(页表的引出) | 为什么要存在进程地址空间 | Linux2.6内核进程调度队列
Linux进程概念——下验证进程地址空间的基本排布 | 理解进程地址空间 | 进程地址空间如何映射至物理内存(页表的引出) | 为什么要存在进程地址空间 | Linux2.6内核进程调度队列