1.Linux 进程在内存数据结构
可以看到一个可执行程序在存储(没有调入内存)时分为代码段,数据段,未初始化数据段三部分:
1) 代码段:存放CPU执行的机器指令。通常代码区是共享的,即其它执行程序可调用它。假如机器中有数个进程运行相同的一个程序,那么它们就可以使用同一个代码段。
2) 数据段:存放已初始化的全局变量,静态变量(包括全局和局部的),常量。static全局变量和static函数只能在当前文件中被调用。
3) 未初始化数据区(uninitializeddata segment,BSS):存放全局未初始化的变量。BSS的数据在程序开始执行之前被初始化为0或NULL。
代码区所在的地址空间最低,往上依次是数据区和BSS区,并且数据区和BSS区在内存中是紧挨着的。。
可执行程序在运行时又多出了两个区域:栈段(Stack)和堆段(Heap)。
4) 栈区:由编译器自动释放,存放函数的参数值,局部变量等。每当一个函数被调用时,该函数的返回类型和一些调用的信息被存储到栈中。然后这个被调用的函数再为它的自动变量和临时变量在栈上分配空间。每调用一个函数一个新的栈就会被使用。栈区是从高地址位向低地址位增长的,是一块连续的内在区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。
5) 堆段:用于存放进程运行中被动态分配的内存段,位于BSS和栈中间的地址位。由程序员申请分配(malloc)和释放(free)。堆是从低地址位向高地址位增长,采用链式存储结构。频繁地malloc/free造成内存空间的不连续,产生碎片。当申请堆空间时库函数按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。
这个5中内存区域中数据段、BSS和堆通常是被连续存储的——内存位置上是连续的,而代码段和栈往往会被独立存放。有趣的是堆和栈两个区域关系很“暧昧”,他们一个向下“长”(i386体系结构中栈向下、堆向上),一个向上“长”,相对而生。但你不必担心他们会碰头,因为他们之间间隔很大(到底大到多少,你可以从下面的例子程序计算一下),绝少有机会能碰到一起。
下图简要描述了进程内存区域的分布:
2. 地址相关概念
1. 物理地址(physical address)
物理内存,真实存在的插在主板内存槽上的内存条的容量的大小.
内存是由若干个存储单元组成的,每个存储单元有一个编号,这种编号可唯一标识一个存储单元,称为内存地址(或物理地址)。我们可以把内存看成一个从0字节一直到内存最大容量逐字节编号的存储单元数组,即每个存储单元与内存地址的编号相对应。
2. 虚拟内存(Virtual memory)(也叫虚拟存储器)
虚拟内存地址就是每个进程可以直接寻址的地址空间,不受其他进程干扰。每个指令或数据单元都在这个虚拟空间中拥有确定的地址。
虚拟内存就是进程中的目标代码,数据等虚拟地址组成的虚拟空间
虚拟内存不考虑物理内存的大小和信息存放的实际位置,只规定进程中相互关联信息的相对位置。每个进程都拥有自己的虚拟内存,且虚拟内存的大小由处理机的地址结构和寻址方式决定。
如直接寻址,如果cpu的有效地址长度为16位,则其寻址范围0 -64k。
再比如32位机器可以直接寻址4G空间,意思是每个应用程序都有4G内存空间可用。但是显然机器内存罕有如此之大,可以支持每个程序使用4G内存的。
虚拟内存与物理内存的区别:虚拟内存就与物理内存相反,是指根据系统需要从硬盘虚拟地匀出来的内存空间,是一种计算机系统内存管理技术,属于计算机程序,而物理内存为硬件。因为有时候当你处理大的程序时候系统内存不够用,此时就会把硬盘当内存来使用,来交换数据做缓存区,不过物理内存的处理速度是虚拟内存的30倍以上。
3. 逻辑地址(logical address)
源程序经过汇编或编译后,形成目标代码,每个目标代码都是以0为基址顺序进行编址的,原来用符号名访问的单元用具体的数据——单元号取代。这样生成的目标程序占据一定的地址空间,称为作业的逻辑地址空间,简称逻辑空间。
在逻辑空间中每条指令的地址和指令中要访问的操作数地址统称为逻辑地址。即应用程序中使用的地址。要经过寻址方式的计算或变换才得到内存中的物理地址。
很简单,逻辑地址就是你源程序里使用的地址,或者源代码经过编译以后编译器将一些标号,变量转换成的地址,或者相对于当前段的偏移地址。
逻辑地址是指由程序产生的与段相关的偏移地址部分。例如,你在进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址,不和绝对物理地址相干。只有在Intel实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,Cpu不进行自动地址转换);逻辑也就是在Intel保护模式下程序执行代码段限长内的偏移地址(假定代码段、数据段如果完全一样)。应用程序员仅需与逻辑地址打交道,而分段和分页机制对您来说是完全透明的,仅由系统编程人员涉及。应用程序员虽然自己可以直接操作内存,那也只能在操作系统给你分配的内存段操作。
不过有些资料是直接把逻辑地址当成虚拟地址,两者并没有明确的界限。
在linux内核,虚拟地址是3G-4G这段地址,它与物理地址通过页表来映射,逻辑地址是指3G-3G+main_memory_size这段虚拟地址,它与物理地址的映射是线性的,当然也可以通过页表映射。所以逻辑地址是虚拟地址的一部分。
逻辑地址的组成:是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]
图4.1 作业的名空间、逻辑地址空间和装入后的物理空间
4. 线性地址或Linux下也叫虚拟地址(virtual address)
这个地址很重要,也很不容易理解。分段机制下CPU寻址是二维的地址即,段地址:偏移地址,CPU不可能认识二维地址,因此需要转化成一维地址即,段地址*16+偏移地址,这样得到的地址便是线性地址(在未开启分页机制的情况下也是物理地址)。这样有什么意义呢?或者说这个一维地址的计算方法随便一个学计算机的人都知道,但是你真的理解它的意思吗?要想理解它的意思,必须要知道什么是地址空间,下文详述。
线性地址是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址可以再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel 80386的线性地址空间容量为4G(2的32次方即32根地址总线寻址)。
跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。
CPU将一个虚拟内存空间中的地址转换为物理地址,需要进行两步:首先将给定一个逻辑地址(其实是段内偏移量=),CPU要利用其段式内存管理单元,先将为个逻辑地址转换成一个线程地址,再利用其页式内存管理单元,转换为最终物理地址。
3.地址映射
2 .1 虚拟地址向线性地址的转换
用户进程要访问的虚拟地址包括一个1 6 位的段选择器和 一个32 位的段 内偏移 . 80386 的分段机制将段寄存器中所装的段选择器和32 位段 内偏移量相加. 得到32位的线性地址 , 如图1所示 , 16 位的段选择器最低两位表示请求者的特权级 , 那么
最多可以有16k个段 , 每段的最大尺寸为4GB。 但是它们都必 须被映射到4GB的线性地址空间。
图1
2 .2 线性地址向物理地址的转换
Linux的每个用 户进 程都可 以访 问4 GB的线 性地址空间, 而实际的物理 内存可能远 远少于4GB. 采用分页机制 。 Linux仅把可执行映像的一小部分 装入物理 内存. 当需要访问未装入的页面时 . 系统产生一个缺页中断 , 把需要的页读入 物理内存。
图2
Linux采用两级页表结构—— 页目录表和页表实现地址 映射. 当前正在运行进程的页 目录表的地址被保存在控制寄
存器 CR3 中。 由上面转换机制所得到的线性地址可以分为3 部分 , 高 10位是 DI R域—— 页 目录表的索引值 . 它与 CR3 中的地址一起 计算得到页表的物理地址 . 中间1O位保 存相对于页表的索引 值 . 通过它得到所需的物理页号。 物理页号与低1 2 位页内偏移 组合得到物理地址 。 其结构如图2 所示 。
4.虚拟地址管理
每个用户进程都可以有4 GB的虚存空 间. 为了更好地管 理这部分虚存空间.
Linux主要定义了如下三个数据结构 :
struct vm_area_struct ,
struct vm_operations_struct
struct vmm_struct
虚存段( vm_area_struct )
. 简称 vma是某个进程的一段 连续的虚存空间. 一个进程通常占用几个 vma段 . 例如代码段 、 数据段、 堆栈段等 。 vma不仅可以代表一段内存区间, 也可 以对应于一个文件、 共享内存或者对换设备。
每一个进程的所有 vma由一个双向链表管理。 为了提高 对 vma的查询、 插入、 删除等操作的效率 . Linux把系统中所 有进程的 vma组成了一棵 AVL树。 这是一棵平衡二叉树 . 当 vma数量特别 大时。 利用这棵 AVL树查找 v ma的效率得到 明显提 高 。
不同的 vma可能需要不同的操作处理方式 . 但同时考虑到统接口的统 一 性 . Linux采 用vm_operations_struct结 构和面向对象的思想来定义操作方式 . 一个vm_operations_struct结构体是一组 函数指针 , 对于不同的 vma . 它可能指向 不同的处理 函数.例如当发生缺页错误时 . 共 享内存和代码 段 的 readpage所 指 向的页面读入函数可能就不同 。
内存管理中另外 一个 非 常重要的数 据 结 构是vmm_struct 结构体 .进程 的 task_struct中的mm成员指向 它. 当前运行进程的整个虚拟空间都 由它来管理和描述 . 它不仅包含该进程的映像信 息. 而且它的 mma p成员项指向该进 程所有vma组成的链 表 。 它的 mmap_avl 成 员 项 指 向整个系统 的 AVL树 。
这三个数据结构之间相互关联. 共同管理虚拟内存 . 它们之 间的 关 系如图 3所 示 。
图 3
这部分相关的系统调用主要有如下两个 :
do_mmap( struct file *file, unsigned long addr unsigned long len , unsigned long prot , unsigned long flags , unsigned long off ); find_vma ( struct mm_struct mm , unsigned long addr );do _mmap函数实现了 内存映射 。 find_vma函数的功能 是找到包含参数 addr指定的虚拟地址所属的 vma 。 当要运行一个可执行映像时 . 调用 do _mmap将其装入 到该进程 的虚拟地址空间 . 并且产生一组 vma结构 . 如前所 述该进程的整个虚拟空间由 vmm_struct 结构管理 . 但是此时 可执行文件仅仅被连接到进程的虚拟空间中. 只有一小部分 页面被装入到物理 内存 . 其余大部分并没有被真正装入到物 理内存 . 在进程的运行过程 中. 产生缺页错误 . 操作 系统首先调用 find_vma. 找到该虚拟地 址所在的 vma . 然后根据该 vma的成员变量 vm_ops指向的vm_operations_struct结构 中的缺页操作 函数。 把页装入物理内存。 ·
5.对换空间
Linux的每个进程可以有4 GB的虚拟 内存空间 . 而且系统中还要同时存在多个进程 ,但是 ,事实上大多数计算机都没 有这么多物理内存空间 , 当系统中的物理内存紧缺时 . 就需要利用对换空间把一部分未来可能不用的页面从物理内存中移 到对换设备或对换文件中。
Linux采用两种方 式保存换 出的页面 。 一种是利 用整个块设备 , 如硬盘的一个分区 . 即对换设备, 另一种是利用文件系统中固定长度的文件 . 即对换文件。 它们统称为对换空间。 这两种方式的相同之处是它们的内部格式一致.
但是在执行效率方面 . 对换设备要好一些. 这是因为对换设备上同一页面 的数据块是连续存放的 . 故而可 以顺序存取 , 而在对换文件
中 。 同一页面的数据块实际的物理位置可能是不连续的 . 需要通过对换文件的 i n o d e检索. 这就降低了存取效率 。
每个对换文件或对换设备 由 s t r u c t s wa p — i n f o — s t r u c t 结构来描述。 有关对换设备的 函数主要是 g e t — s wa p — p a g e ( …) . 当 内 存中的页面需要被换 出时 . 调用 g e t — s wa p —p a g e函数 申请得到一个对换空间中的物理页面 。 如果成功, 就返 回一个非零代 码 . 否则 . 返 回0。
6.分页机制管理
L i n u x使用分页管理机制来更加有效地利用物理 内存 . 当创建一个进程时 . 仅仅把当前进程的一小部分真 正装入内
存. 其余部分需要访问时 . 处理 器产生一个页故障 . 由缺页中 断服务程序根据缺页虚拟地址和出错码调用写拷贝函数 d o —
wp —p a g e 、 此 地址 所 属 的 v ma的 v m—o p s指 向 的 n o p a g e 、 d o — s wa p — p a g e . s wa p — i n等函数将需要的页换入物理内存 。 随着可执行映像 的运行和页面的换入 . 系统 中的内存有 可能变得不足. 这时 L i n u x核心就必须调用 k s wa p d守护进 程释放部分物理 内存 。k s wa p d在 系统启动时 由 i n i t进程 建 立。 在系统的运行过程中 。 它被定期唤醒 。 检查系统中的空闲 物理 内存是否很少 。 如果是. 则释放一部分内存. 或者将一些 页 面换 出到对 换 空间 。 然 后继 续睡 眠 。
5 . 1 缺页中断和页面换入
页 面换 入 主 要 由缺 页 中断 服 务 入 口 函数 d o —p a g e —f a u l t 来实现 。 当系统中产生页面故障时 . 如果虚拟内存地址有效 . 则产生错误的原因有如下两种 :
·
虚拟内存地址对应的物理 页不在 内存 中。 那么它必然在 磁盘或 对换 空 间中. 如果 在磁盘 上. 那么我们 调用 d o — n O —
p a g e函 数 . 而 d o —n o —p a g e调 用 v ma 一 > v m—o p s 一 > n op a g e () 函数建立页面映射 . 从对换空间或磁盘中调入页面 . 或者通过 d o — s wa p — p a g e ( ) 函数调用 s wa p —i n ( ) 来换入页面。
·
该虚拟地址对应的物理页在内存 。 但是被写保护. 如果 这种情况发生在一个共享页面上 . 则需要“ 写拷 贝“ 函数 d o —
wp —p a g e来换 入 页 面.d o — wp — p a g e函数 首 先 调 用一 g e t — f r e e — p a g e获得一新 页面 . 然 后调用 c o p y — C O W— p a g e拷贝页 面的内容. 当然还要调用相应的刷新 函数刷新 TL B和缓存 等 。
5 . 2 页交换进程和页面换出
正如我们上面所描述的 . 系统使 用 k s wa p d守护进程来 定期 地 换 出页 面。 使 系 统 中 有足 够 的空 闲物 理 内存 页。
k s wa p d进程定期地检查系统中的空闲页面数 . 如果少于一定 值 . 则按照以下三中途径获得空闲页面 : ①减少缓冲区和页面
高速缓存的大小; ②把共享 内存 占用的页面置换到对换空 间;
③换出或丢弃物理内存页。