linux内核—进程地址空间

本文借鉴:https://www.cnblogs.com/fengliu-/p/9243004.html
https://blog.csdn.net/wsq119/article/details/82110757
https://www.cnblogs.com/wuchanming/p/4339770.html

进程地址空间分布图:
在这里插入图片描述
程序段(Text):程序代码在内存中的映射,存放函数体的二进制代码。
初始化过的数据(Data):在程序运行初已经对变量进行初始化的数据。
未初始化过的数据(BSS):在程序运行初未对变量进行初始化的数据。
栈 (Stack):存储局部、临时变量,函数调用时,存储函数的返回指针,用于控制函数的调用和返回。在程序块开始时自动分配内存,结束时自动释放内存,其操作方式类似于数据结构中的栈。
堆 (Heap):存储动态内存分配,需要程序员手工分配,手工释放.注意它与数据结构中的堆是两回事,分配方式类似于链表。

早期的内存分配机制

在早期的计算机中,要运行一个程序,会把这些程序全都装入内存,程序都是直接运行在内存上的,也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运行多个程序时,必须保证这些程序用到的内存总量要小于计算机实际物理内存的大小。
那当程序同时运行多个程序时,操作系统是如何为这些程序分配内存 的呢?下面通过实例来说明当时的内存分配方法:
某台计算机总的内存大小是 128M ,现在同时运行两个程序 A 和 B , A 需占用内存 10M , B 需占用内存 110 。计算机在给程序分配内存时会采取这样的方法:先将内存中的前 10M 分配给程序 A ,接着再从内存中剩余的 118M 中划分出 110M 分配给程序 B 。这种分配方法可以保证程序 A 和程序 B 都能运行,但是这种简单的内存分配策略问题很多。

分段

为 了解决上述问题,人们想到了一种变通的方法,就是增加一个中间层,利用一种间接的地址访问方法访问物理内存。按照这种方法,程序中访问的内存地址不再是实际的物理内存地址,而是一个虚拟地址,然后由操作系统将这个虚拟地址映射到适当的物理内存地址上。这样,只要操作系统处理好虚拟地址到物理内存地址的映射,就可以保证不同的程序最终访问的内存地址位于不同的区域,彼此没有重叠,就可以达到内存地址空间隔离的效果。
当创建一个进程时,操作系统会为该进程分配一个 4GB 大小的虚拟进程地址空间。之所以是 4GB ,是因为在 32 位的操作系统中,一个指针长度是 4 字节,而 4 字节指针的寻址能力是从 0x00000000~0xFFFFFFFF,最大值 0xFFFFFFFF 表示的即为 4GB 大小的容量。与虚拟地址空间相对的,还有一个物理地址空间,这个地址空间对应的是真实的物理内存。如果你的计算机上安装了 512M 大小的内存,那么这个物理地址空间表示的范围是 0x00000000~0x1FFFFFFF 。当操作系统做虚拟地址到物理地址映射时,只能映射到这一范围,操作系统也只会映射到这一范围。当进程创建时,每个进程都会有一个自己的 4GB 虚拟地址空间。要注意的是这个 4GB 的地址空间是“虚拟”的,并不是真实存在的,而且每个进程只能访问自己虚拟地址空间中的数据,无法访问别的进程中的数据,通过这种方法实现了进程间的地址隔离。
人们之所以要创建一个虚拟地址空间,目的是为了解决进程地址空间隔离的问题。但程序要想执行,必须运行在真实的内存上,所以,必须在虚拟地址与物理地址间建立一种映射关系。这样,通过映射机制,当程序访问虚拟地址空间上的某个地址值时,就相当于访问了物理地址空间中的另一个值。人们想到了一种分段(Sagmentation) 的方法,它的思想是在虚拟地址空间和物理地址空间之间做一一映射。比如说虚拟地址空间中某个 10M 大小的空间映射到物理地址空间中某个 10M 大小的空间。这种思想理解起来并不难,操作系统保证不同进程的地址空间被映射到物理地址空间中不同的区域上,这样每个进程最终访问到的。
物理地址空间都是彼此分开的。通过这种方式,就实现了进程间的地址隔离。还是以实例说明,假设有两个进程 A 和 B ,进程 A 所需内存大小为 10M ,其虚拟地址空间分布在 0x00000000 到 0x00A00000 ,进程 B 所需内存为 100M ,其虚拟地址空间分布为 0x00000000 到 0x06400000 。那么按照分段的映射方法,进程 A 在物理内存上映射区域为 0x00100000 到 0x00B00000 ,,进程 B 在物理内存上映射区域为0x00C00000 到 0x07000000 。于是进程 A 和进程 B 分别被映射到了不同的内存区间,彼此互不重叠,实现了地址隔离。从应用程序的角度看来,进程 A 的地址空间就是分布在 0x00000000 到 0x00A00000 ,在做开发时,开发人员只需访问这段区间上的地址即可。应用程序并不关心进程 A 究竟被映射到物理内存的那块区域上了,所以程序的运行地址也就是相当于说是确定的了。
这种分段的映射方法虽然解决了上述中的问题一和问题三,但并没能解决问题二,即内存的使用效率问题。在分段的映射方法中,每次换入换出内存的都是整个程序, 这样会造成大量的磁盘访问操作,导致效率低下。所以这种映射方法还是稍显粗糙,粒度比较大。实际上,程序的运行有局部性特点,在某个时间段内,程序只是访问程序的一小部分数据,也就是说,程序的大部分数据在一个时间段内都不会被用到。基于这种情况,人们想到了粒度更小的内存分割和映射方法,这种方法就是分页 (Paging) 。

分页

分页的基本方法是,将地址空间分成许多的页。每页的大小由 CPU 决定,然后由操作系统选择页的大小。目前 Inter 系列的 CPU 支持 4KB 或 4MB 的页大小,而 PC上目前都选择使用 4KB 。按这种选择, 4GB 虚拟地址空间共可以分成 1048576 页, 512M 的物理内存可以分为 131072 个页。显然虚拟空间的页数要比物理空间的页数多得多。
在分段的方法中,每次程序运行时总是把程序全部装入内存,而分页的方法则有所不同。分页的思想是程序运行时用到哪页就为哪页分配内存,没用到的页暂时保留在硬盘上。当用到这些页时再在物理地址空间中为这些页分配内存,然后建立虚拟地址空间中的页和刚分配的物理内存页间的映射。
下面通过介绍一个可执行文件的装载过程来说明分页机制的实现方法。一个可执行文件 (PE 文件 ) 其实就是一些编译链接好的数据和指令的集合,它也会被分成很多页,在 PE 文件执行的过程中,它往内存中装载的单位就是页。当一个 PE 文件被执行时,操作系统会先为该程序创建一个 4GB 的进程虚拟地址空间。前面介绍过,虚拟地址空间只是一个中间层而已,它的功能是利用一种映射机制将虚拟地址空间映射到物理地址空间,所以,创建 4GB 虚拟地址空间其实并不是要真的创建空间,只是要创建那种映射机制所需要的数据结构而已,这种数据结构就是页目和页表。
当创建完虚拟地址空间所需要的数据结构后,进程开始读取 PE 文件的第一页。在PE 文件的第一页包含了 PE 文件头和段表等信息,进程根据文件头和段表等信息,将 PE 文件中所有的段一一映射到虚拟地址空间中相应的页 (PE 文件中的段的长度都是页长的整数倍 ) 。这时 PE 文件的真正指令和数据还没有被装入内存中,操作系统只是据 PE 文件的头部等信息建立了 PE 文件和进程虚拟地址空间中页的映射关系而已。当 CPU 要访问程序中用到的某个虚拟地址时,当 CPU 发现该地址并没有相相关联的物理地址时, CPU 认为该虚拟地址所在的页面是个空页面, CPU 会认为这是个页错误 (Page Fault) , CPU 也就知道了操作系统还未给该 PE 页面分配内存,CPU 会将控制权交还给操作系统。操作系统于是为该 PE 页面在物理空间中分配一个页面,然后再将这个物理页面与虚拟空间中的虚拟页面映射起来,然后将控制权再还给进程,进程从刚才发生页错误的位置重新开始执行。由于此时已为 PE 文件的那个页面分配了内存,所以就不会发生页错误了。随着程序的执行,页错误会不断地产生,操作系统也会为进程分配相应的物理页面来满足进程执行的需求。
分页方法的核心思想就是当可执行文件执行到第 x 页时,就为第 x 页分配一个内存页 y ,然后再将这个内存页添加到进程虚拟地址空间的映射表中 , 这个映射表就相当于一个 y=f(x) 函数。应用程序通过这个映射表就可以访问到 x 页关联的 y 页了。
从上面我们可以知道,在进程创建的过程中,程序所有内容被映射到进程的虚拟内存空间,为了让一个很大的程序在有限的物理内存空间运行,我们可以把这个程序的开始部分先加载到物理内存空间运行,因为操作系统处理的是进程的虚拟地址,如果在进行虚拟到物理地址的转换工程中,发现物理地址不存在时,这个时候就会发生缺页异常(nopage),接着操作系统就会把磁盘上还没有加载到内存中的数据加载到物理内存中,对应的进程页表进行更新。也许你会问,如果此时物理内存满了,操作系统将如何处理?
下面我们看看linux操作系统是如何处理的:
如果一个进程想将一个虚拟页装入物理内存,而又没有可使用的空闲物理页,操作系统就必须淘汰物理内存中的其他页来为此页腾出空间。
在linux操作系统中,物理页的描叙如下:

struct mem_map
{
    
    
1、本页使用计数,当该页被许多进程共享时计数将大于1
2、age描叙本页的年龄,用来判断该页是否为淘汰或交换的好候选
3、map_nr描叙物理页的页帧号
}

如果从物理内存中被淘汰的页来自于一个映像或数据文件,并且还没有被写过,则该页不必保存,它可以丢掉。如果有进程在需要该页时就可以把它从映像或数据文件中取回内存。
然而,如果该页被修改过,操作系统必须保留该页的内容以便晚些时候在被访问。这种页称为"脏(dirty)页",当它被从内存中删除时,将被保存在一个称为交换文件的特殊文件中。
相对于处理器和物理内存的速度,访问交换文件要很长时间,操作系统必须在将页写到磁盘以及再次使用时取回内存的问题上花费心机。
如果用来决定哪一页被淘汰或交换的算法不够高效的话,就可能出现称为"抖动"的情况。在这种情况下,页面总是被写到磁盘又读回来,操作系统忙于此而不能进行真正的工作。
linux使用"最近最少使用(Least Recently Used ,LRU)“页面调度技巧来公平地选择哪个页可以从系统中删除。这种设计系统中每个页都有一个"年龄”,年龄随页面被访问而改变。页面被访问越多它越年轻;被访问越少越老。年老的页是用于交换的最佳候选页。

linux虚拟存储组织

每个进程(线程)都对应一个进程描述符(task_struct),每个进程描述符内都有一个内存描述符(mm_struct)即唯一的进程地址空间,而内存描述符内包含一个虚拟内存区域链表(vm_area_struct)用于分别标识地址空间中用到的区域
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/chengcheng1024/article/details/114382951