5. 虚拟内存管理

我们知道操作系统利用体系结构提供的VA到PA的转换机制实现虚拟内存管理。有了共享库的基础知识之后,现在我们可以进一步理解虚拟内存管理了。首先分析一个例子:

$ ps
PID TTY TIME CMD
29977 pts/0 00:00:00 bash
30032 pts/0 00:00:00 ps
$ cat /proc/29977/maps
08048000-080f4000 r-xp 00000000 08:15 688142 /bin/bash
080f4000-080f9000 rw-p 000ac000 08:15 688142 /bin/bash
080f9000-080fe000 rw-p 080f9000 00:00 0
09283000-09497000 rw-p 09283000 00:00 0 [heap]
b7ca8000-b7cb2000 r-xp 00000000 08:15 581665 /lib/tls/i686/cmov/libnss_files-2.8.90.so
b7cb2000-b7cb3000 r--p 00009000 08:15 581665 /lib/tls/i686/cmov/libnss_files-2.8.90.so
b7cb3000-b7cb4000 rw-p 0000a000 08:15 581665 /lib/tls/i686/cmov/libnss_files-2.8.90.so
...
b7e15000-b7f6d000 r-xp 00000000 08:15 581656 /lib/tls/i686/cmov/libc-2.8.90.so
b7f6d000-b7f6f000 r--p 00158000 08:15 581656 /lib/tls/i686/cmov/libc-2.8.90.so
b7f6f000-b7f70000 rw-p 0015a000 08:15 581656 /lib/tls/i686/cmov/libc-2.8.90.so
...
b7fbd000-b7fd7000 r-xp 00000000 08:15 565466 /lib/ld-2.8.90.so
b7fd7000-b7fd8000 r-xp b7fd7000 00:00 0 [vdso]
b7fd8000-b7fd9000 r--p 0001a000 08:15 565466 /lib/ld-2.8.90.so
b7fd9000-b7fda000 rw-p 0001b000 08:15 565466 /lib/ld-2.8.90.so
bfac5000-bfada000 rw-p bffeb000 00:00 0 [stack]

ps命令查看当前终端下的进程,得知bash进程的id是29977,然后用cat /proc/29977/maps命令查看它的虚拟地址空间。/proc目录中的文件并不是真正的磁盘文件,而是由内核虚拟出来的文件系统,当前系统中运行的每个进程在/proc下都有一个子目录,目录名就是进程的id,查看目录下的文件可以得到该进程的相关信息。此外,用pmap 29977命令也可以得到类似的输出结果。

图 20.4. 进程地址空间

第 4 节 “MMU”讲过,x86平台的虚拟地址空间是0x0000 0000~0xffff ffff,大致上前3GB(0x0000 0000~0xbfff ffff)是用户空间,后1GB(0xc000 0000~0xffff ffff)是内核空间,在这里得到了印证。0x0804 8000-0x080f 4000是从/bin/bash加载到内存的,访问权限为r-x,表示Text Segment,包含.text段、.rodata段、.plt段等。0x080f 4000-0x080f 9000也是从/bin/bash加载到内存的,访问权限为rw-,表示Data Segment,包含.data段、.bss段等。

0x0928 3000-0x0949 7000不是从磁盘文件加载到内存的,这段空间称为堆(Heap),以后会讲到用malloc函数动态分配内存是在这里分配的。从0xb7ca 8000开始是共享库的映射空间,每个共享库也分为几个Segment,每个Segment有不同的访问权限。可以看到,从堆空间的结束地址(0x0949 7000)到共享库映射空间的起始地址(0xb7ca 8000)之间有很大的地址空洞,在动态分配内存时堆空间是可以向高地址增长的。堆空间的地址上限(0x09497000)称为Break,堆空间要向高地址增长就要抬高Break,映射新的虚拟内存页面到物理内存,这是通过系统调用brk实现的,malloc函数也是调用brk向内核请求分配内存的。

/lib/ld-2.8.90.so就是动态链接器/lib/ld-linux.so.2,后者是前者的符号链接。标有[vdso]的地址范围是linux-gate.so.1的映射空间,我们讲过这个共享库是由内核虚拟出来的。0xbfac 5000-0xbfad a000是栈空间,其中高地址的部分保存着进程的环境变量和命令行参数,低地址的部分保存函数栈帧,栈空间是向低地址增长的,但显然没有堆空间那么大的可供增长的余地,因为实际的应用程序动态分配大量内存的并不少见,但是有几十层深的函数调用并且每层调用都有很多局部变量的非常少见。总之,栈空间是可能用尽的,并且比堆空间更容易用尽,在第 3 节 “递归”讲过,无穷递归会用尽栈空间最终导致段错误。

虚拟内存管理起到了什么作用呢?可以从以下几个方面来理解。

第一,虚拟内存管理可以控制物理内存的访问权限。物理内存本身是不限制访问的,任何地址都可以读写,而操作系统要求不同的页面具有不同的访问权限,这是利用CPU模式和MMU的内存保护机制实现的。例如,Text Segment被只读保护起来,防止被错误的指令意外改写,内核地址空间也被保护起来,防止在用户模式下执行错误的指令意外改写内核数据。这样,执行错误指令或恶意代码的破坏能力受到了限制,顶多使当前进程因段错误终止,而不会影响整个系统的稳定性。

第二,虚拟内存管理最主要的作用是让每个进程有独立的地址空间。所谓独立的地址空间是指,不同进程中的同一个VA被MMU映射到不同的PA,并且在某一个进程中访问任何地址都不可能访问到另外一个进程的数据,这样使得任何一个进程由于执行错误指令或恶意代码导致的非法内存访问都不会意外改写其它进程的数据,不会影响其它进程的运行,从而保证整个系统的稳定性。另一方面,每个进程都认为自己独占整个虚拟地址空间,这样链接器和加载器的实现会比较容易,不必考虑各进程的地址范围是否冲突。

继续前面的实验,再打开一个终端窗口,看一下这个新的bash进程的地址空间,可以发现和先前的bash进程地址空间的布局差不多:

$ ps
PID TTY TIME CMD
30697 pts/1 00:00:00 bash
30749 pts/1 00:00:00 ps
$ cat /proc/30697/maps
08048000-080f4000 r-xp 00000000 08:15 688142 /bin/bash
080f4000-080f9000 rw-p 000ac000 08:15 688142 /bin/bash
080f9000-080fe000 rw-p 080f9000 00:00 0
082d7000-084f9000 rw-p 082d7000 00:00 0 [heap]
b7cf1000-b7cfb000 r-xp 00000000 08:15 581665 /lib/tls/i686/cmov/libnss_files-2.8.90.so
b7cfb000-b7cfc000 r--p 00009000 08:15 581665 /lib/tls/i686/cmov/libnss_files-2.8.90.so
b7cfc000-b7cfd000 rw-p 0000a000 08:15 581665 /lib/tls/i686/cmov/libnss_files-2.8.90.so
...
b7e5e000-b7fb6000 r-xp 00000000 08:15 581656 /lib/tls/i686/cmov/libc-2.8.90.so
b7fb6000-b7fb8000 r--p 00158000 08:15 581656 /lib/tls/i686/cmov/libc-2.8.90.so
b7fb8000-b7fb9000 rw-p 0015a000 08:15 581656 /lib/tls/i686/cmov/libc-2.8.90.so
...
b8006000-b8020000 r-xp 00000000 08:15 565466 /lib/ld-2.8.90.so
b8020000-b8021000 r-xp b8020000 00:00 0 [vdso]
b8021000-b8022000 r--p 0001a000 08:15 565466 /lib/ld-2.8.90.so
b8022000-b8023000 rw-p 0001b000 08:15 565466 /lib/ld-2.8.90.so
bff0e000-bff23000 rw-p bffeb000 00:00 0 [stack]

该进程也占用了0x0000 0000-0xbfff ffff的地址空间,Text Segment也是0x0804 8000-0x080f 4000,Data Segment也是0x080f 4000-0x080f 9000,和先前的进程一模一样,因为这些地址是在编译链接时写进/bin/bash这个可执行文件的,两个进程都加载它。这两个进程在同一个系统中同时运行着,它们的Data Segment占用相同的VA,但是两个进程各自干各自的事情,显然Data Segment中的数据应该是不同的,相同的VA怎么会有不同的数据呢?因为它们被映射到不同的PA。如下图所示。

图 20.5. 进程地址空间是独立的

从图中还可以看到,两个进程都是bash进程,Text Segment是一样的,并且Text Segment是只读的,不会被改写,因此操作系统会安排两个进程的Text Segment共享相同的物理页面。由于每个进程都有自己的一套VA到PA的映射表,整个地址空间中的任何VA都在每个进程自己的映射表中查找相应的PA,因此不可能访问到其它进程的地址,也就没有可能意外改写其它进程的数据。

另外,注意到两个进程的共享库加载地址并不相同,共享库的加载地址是在运行时决定的,而不是写在/bin/bash这个可执行文件中。但即使如此,也不影响两个进程共享相同物理页面中的共享库,当然,只有只读的部分是共享的,可读可写的部分不共享。

使用共享库可以大大节省内存。比如libc,系统中几乎所有的进程都映射libc到自己的进程地址空间,而libc的只读部分在物理内存中只需要存在一份,就可以被所有进程共享,这就是“共享库”这个名称的由来了。

现在我们也可以理解为什么共享库必须是位置无关代码了。比如libc,不同的进程虽然共享libc所在的物理页面,但这些物理页面被映射到各进程的虚拟地址空间时却位于不同的地址,所以要求libc的代码不管加载到什么地址都能正确执行。

第三,VA到PA的映射会给分配和释放内存带来方便,物理地址不连续的几块内存可以映射成虚拟地址连续的一块内存。比如要用malloc分配一块很大的内存空间,虽然有足够多的空闲物理内存,却没有足够大的连续空闲内存,这时就可以分配多个不连续的物理页面而映射到连续的虚拟地址范围。如下图所示。

图 20.6. 不连续的PA可以映射为连续的VA

第四,一个系统如果同时运行着很多进程,为各进程分配的内存之和可能会大于实际可用的物理内存,虚拟内存管理使得这种情况下各进程仍然能够正常运行。因为各进程分配的只不过是虚拟内存的页面,这些页面的数据可以映射到物理页面,也可以临时保存到磁盘上而不占用物理页面,在磁盘上临时保存虚拟内存页面的可能是一个磁盘分区,也可能是一个磁盘文件,称为交换设备(Swap Device)。当物理内存不够用时,将一些不常用的物理页面中的数据临时保存到交换设备,然后这个物理页面就认为是空闲的了,可以重新分配给进程使用,这个过程称为换出(Page out)。如果进程要用到被换出的页面,就从交换设备再加载回物理内存,这称为换入(Page in)。换出和换入操作统称为换页(Paging),因此:

系统中可分配的内存总量 = 物理内存的大小 + 交换设备的大小

如下图所示。第一张图是换出,将物理页面中的数据保存到磁盘,并解除地址映射,释放物理页面。第二张图是换入,从空闲的物理页面中分配一个,将磁盘暂存的页面加载回内存,并建立地址映射。

图 20.7. 换页

linux c 链接详解5-虚拟内存管理的更多相关文章

  1. linux ln链接详解

    1.序 Linux具有为一个文件起多个名字的功能,称为链接.被链接的文件可以存放在相同的目录下,但是必须有不同的文件名,而不用在硬盘上为同样的数据重复备份.另外,被链接的文件也可以有相同的文件名,但是 ...

  2. [linux]vmstat命令详解-显示虚拟内存状态

    本文转载于http://man.linuxde.net/vmstat 前言:Linux系统的内存分为物理内存和虚拟内存两种.物理内存是真实的,也就是物理内存条上的内存.而虚拟内存则是采用硬盘空间补充物 ...

  3. linux c 链接详解3-静态库

    3静态库 摘自:Linux C编程一站式学习 透过本节可以学会编译静态链接库的shell脚本! 有时候需要把一组代码编译成一个库,这个库在很多项目中都要用到,例如libc就是这样一个库,我们在不同的程 ...

  4. linux c 链接详解4-共享库

    4. 共享库 4.1. 编译.链接.运行 组成共享库的目标文件和一般的目标文件有所不同,在编译时要加-fPIC选项,例如: $ gcc -c -fPIC stack/stack.c stack/pus ...

  5. linux c 链接详解1-多目标文件链接

    1. 多目标文件的链接 摘自:linux c编程一站式学习 http://learn.akae.cn/media/index.html 可以学会在linux下将多个c语言文件一起编译. 现在我们把例  ...

  6. linux c 链接详解2-定义和声明

    2定义和声明 摘自:linux c编程一站式学习 可以学会extern和static用法,头文件知识. 2.1. extern和static关键字 在上一节我们把两个程序文件放在一起编译链接,main ...

  7. Linux 软硬链接详解

    软链接 软链接: 类似于windows的快捷方式,—>文本文件,但是包含了真实文件的地址               源文件删除,则软连接也删除               软链接可以放在任何文 ...

  8. Linux 链接详解----静态链接实例分析

    由Linux链接详解(1)中我们简单的分析了静态库的引用解析和重定位的内容, 下面我们结合实例来看一下静态链接重定位过程. /* * a.c */ ; void add(int c); int mai ...

  9. linux lsof命令详解

    linux lsof命令详解 简介 lsof(list open files)是一个列出当前系统打开文件的工具.在linux环境下,任何事物都以文件的形式存在,通过文件不仅仅可以访问常规数据,还可以访 ...

随机推荐

  1. 牛客多校第10场J Wood Processing 分治优化/斜率优化 DP

    题意:你有n块木头,每块木头有一个高h和宽w,你可以把高度相同的木头合并成一块木头.你可以选择一些木头消去它们的一部分,浪费的部分是 消去部分的高度 * 木头的宽度,问把n块木头变成恰好m块木头至少要 ...

  2. gelera集群详解

    [MySQL]galera集群原理简介   特性 基于行复制的完全并行同步复制实时多主架构,任意节点可读写无延迟复制,事务零丢失,可靠健壮的读写体验.自动化节点关系控制:节点故障自动摘除,节点加入自动 ...

  3. Mysq修改引擎

    [数据库]Mysql更改默认引擎为Innodb的步骤方法   前言 InnoDB和MyISAM是许多人在使用MySQL时最常用的两个表类型,这两个表类型各有优劣,视具体应用而定. 基本的差别为:MyI ...

  4. python-字符串的处理

    s1 = '###12314##231###' print(s1.split('#')) #split,从左往右遇见# 就拆分一次['', '', '', '12314', '', '231', '' ...

  5. TCP协议的简单应用一

    TCP协议的应用,实现一个服务器处理多个客户端请求(向服务器发送消息,服务器接收到后以原消息返回给客户端.) 客户端类 import java.io.*; import java.net.Socket ...

  6. 多线程模拟生产者消费者示例之Lock

    public class Test { public static void main(String[] args) { List<String> list = new ArrayList ...

  7. 基于oracle 的PL/SQL编程 - 存储过程

    接上篇,游标使用的语句,相当于一段匿名的函数,窗口关闭了就不存在了.如果想要窗口关闭了,还能继续执行那段代码,就需要存储过程了: PLSQL是指一个个PLSQL的业务处理过程存储起来进行复用,这些被存 ...

  8. 数据挖掘:周期性分析SMCA算法

    数据挖掘:周期性分析SMCA算法 原文地址:http://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=1423978 算法介绍 以时间顺序挖掘周期性的模式 ...

  9. 【2019 Multi-University Training Contest 4】

    01:https://www.cnblogs.com/myx12345/p/11644076.html 02: 03:https://www.cnblogs.com/myx12345/p/116478 ...

  10. C#_winform登陆框验证码的实现

    验证码技术已愈来愈成熟,从最初的数字.字母.字符.汉字已经到目前的语言,其应用也甚广,之前大多数只有在网站上可以看到,现在在一些客户端软件也经常可见(比如证券相关软件).之前做的一个基于 C# 客户端 ...