虚拟内存和物理内存

一、从程序到进程:
我们都知道Linux下一个C程序的生成分为4个阶段:
预编译(.i) --> 编译(.s) --> 汇编成目标文件(.o) --> 链接(可执行文件)


1.在预编译阶段,它会修改原始的C程序,将源程序翻译成一个ASCII码的以.i结尾的中间文件。它会读取系统头文件stdio.h的内容,并把它直接插入到程序文本中。
2.在编译阶段,编译器将以.i为扩展名的文本文件翻译成以.s作为扩展名的文本文件,它包含一个汇编语言程序。
3.在汇编阶段,汇编器将以.s为扩展名的文本文件翻译成机器语言指令,将结果存在以.o为扩展名的二进制目标文件中。目标文件包括着机器代码(可直接被计算机中央处理器履行)和代码在运行时使用的数据,如重定位信息,如用于链接或调试的程序符号(变量和函数的名字),另外还包括其他调试信息。
4.在链接阶段,会将文件中调用的库函数合并到上一步生成的二进制目标文件中。比如若调用了printf函数,在链接阶段,它会存在于一个名为printf.o的单独预编译好的目标文件中,这个文件会与我们上一步的.o文件合并成一个文件,它是一个可执行目标文件,shell调用操作系统中一个叫做加载器的函数,它拷贝可执行文件中的代码和数据到内存,由系统执行。(PS:任何UNIX程序都可以通过调用execve函数来调用加载器,加载器将可执行文件的代码和数据从磁盘拷贝到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序)。

注意:其实目标文件和可执行文件的格式几乎是一样的,广义上将两者都看作是ELF文件。在这里,我们主要讨论Linux下ELF格式的目标文件。实际上,在Linux中,不光目标文件和可执行文件是按照ELF文件格式存储的,还有动态链接库和静态链接库中的文件都是以ELF文件格式存储的,他们统称为ELF文件。

那么生成的这个可执行文件里都有什么内容呢?我们大概可以知道里面至少包含了编译后的机器指令代码,还有程序中的数据,除了 这些以外,文件中还包括链接时需要的一些信息,包括符号表,调试信息等。一般可执行文件将这些信息按不同的属性,以“段”的形式 存储。在UNIX中,段表示一个二进制文件的相关的内容块。当加载器运行后,它创建如下图所示的一个C程序的典型的存储空间结构:

可以看到,从低地址到高地址,分别是正文段、初始化的数据段、未初始化的数据段、堆、栈、命令行参数和环境变量。
正文段 又叫代码段,程序源代码编译后的 机器指令 就被放在代码段。代码段是可 共享的 ,所以即便是频繁执行的程序,在存储器中也只有一个副本。 当系统中运行着多个该程序的副本时,它们的指令都是一样的,所以 内存中只需要保存一份该程序的指令部分,因此, 代码段通常是 只读 的,以防止程序由于意外而修改其指令。

初始化数据段 即.data段,包含了程序中已明确赋值的全局变量和静态变量以及它们的值。
还包含.rodata段,存放只读数据,如const修饰的变量和字符串常量。

未初始化数据段, 通常将此称段为bss段(block started by symbol),与已初始化数据段相对应,它保存的是未初始化的全局变量和静态变量。有些全局未初始化的变量不会放到目标文件的bss段,会等最终链接时在放到bss段,这于强符号和弱符号有关。当全局变量和局部静态变量被初始化为0时,会被放到bss段中,因为未初始化都是0。
注意:未初始化段中存的是未初始化的全局变量和静态变量在实际内存中所需要的空间大小,它只是为它们预留了位置,并没有实质的内容,所以也不占空间。也就是说, 未初始化段的内容并不存放在磁盘程序文件中,因为内核在程序开始执行前将它们全部初始化为0或NULL,这样也许可以把它们放入初始化数据段,但因为它们都是0,所以为它们在初始化数据段分配空间并存放0是非常没有必要的。当这个内存区进入程序的地址空间后,包括初始化数据段的和bss段的整个区段此时统称为数据区。

目标文件格式中区分初始化和未初始化变量是为了空间效率:因为在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。

堆 位于未初始化数据段与栈之间,这部分内存是由低地址向高地址分配。我们通常在堆中进行 动态存储分配 ,即malloc申请的内存,这段空间会一直存在,直至我们使用free释放,如果没有主动释放,在进程运行结束时也会被系统释放掉。而内存泄露就是指我们我们没有释放堆空间,从而导致堆不断增长,使内存空间不断减少。

栈是从高地址向低地址分配的,栈中存放的是局部变量,以及每次函数调用时,返回的地址以及调用者的环境信息。栈也经常被叫做栈帧,因为栈以帧为单位,当程序调用函数的时侯,栈会向下增长一帧,帧中存储被调用函数的参数和局部变量,以及该函数的返回地址。此时,位于栈最下方的帧和全局变量一起,构成了当前的环境。一般来说,你只允许使用位于栈最下方的帧,当该函数又调用另一个函数的时候,栈又会向下增长一帧,此时控制权会转移到新的函数中,当函数返回时,会从栈中弹出,并根据帧中记录的地址信息,返回到调用它的函数中,然后删除帧。直到最后main()函数返回,其返回地址为空,进程结束。所以,递归太多层的话可能会让栈溢出。

环境变量 类似Linux下的PATH、HOME等环境变量。

二、虚拟内存和物理内存

     在早期的计算机中,是没有虚拟内存的概念的。我们要运行一个程序,会把程序全部装入内存,然后运行。
当运行多个程序时,经常会出现以下问题:
1.进程地址空间不隔离,没有权限保护。
由于程序都是直接访问物理内存,所以一个进程可以修改其他进程的内存数据,
甚至修改内核地址空间中的数据。
2.内存使用效率低
当内存空间不足时,要将其他程序暂时拷贝到硬盘,然后将新的程序装入内存运行。
由于大量的数据装入装出,内存使用效率会十分低下。
3.程序运行的地址不确定
因为内存地址是随机分配的,所以程序运行的地址也是不确定的。
     我们都知道,每个程序被运行起来后,它将拥有自己独立的线性空间,这个线性空间由cpu的位数决定,比如32位硬件平台决定了线性地址空间的寻 址空间为02^32-1,即0x000000000xFFFFFFFF,也就是4GB大小。如果每个进程都有4GB,那我们的内存根本就不够用。其实,这4GB并不是真正的内存空间,它是虚拟内存空间,顾名思义,之所以称为虚拟内存,是和系统中的物理内存相对而言的,它是操作系统内核为了对进程地址空间进行管理而设计的一个逻辑意义上的内存空间概念。我们程序中的指针其实都是这个虚拟内存空间中的地址。虚拟内存是将系统硬盘空间和系统实际内存联合在一起供进程使用,给进程提供了一个比内存大的多的虚拟空间。在程序运行时,只需要把虚拟内存空间的一小部分通过页映射模式,经过MMU部件转换映射到物理内存即可。
     那么这4GB的空间是不是就由该进程随意使用呢?遗憾的是,不可以。因为操作系统需要监控进程,进程只能使用那些操作系统分配给它们的地址,如果访问未经允许的空间,那么操作系统会捕获到这些访问,将进程的这种访问当作非法操作,强制结束进程。Linux操作系统将进程的4G虚拟空间分配为1:3的结构,1G是操作系统本身,剩下的3G是留给进程使用的。所以在一个进程的眼里,只有操作系统和它自己,就像整个计算机都被它“承包”了。

     我们可以看到,从0x8048000到0xc0000000的空间才是我们上文说到的进程的线性地址空间。可以发现这张图跟上一张图相比较,在堆和栈中间多了一部分叫作共享库和mmap内存映射区的内容。我们的程序运行后如果是动态链接的C语言运行时库的话,共享库会存在图示的动态库映射区。共享库是一个模块,在运行时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。无论使用C语言运行时库的程序无论有多少,运行时库的代码在内存里只会有一份。对于不同的程序,进行地址映射即可。比如很多程序都会用到的printf,函数共享库 printf.o 固定在某个物理内存位置上,让许多进程映射共享。这样就减少了每个可执行文件的长度。mmap是个系统函数,可以将一个文件映射到某个物理内存位置,让许多进程共享。共享库使得可执行文件中不再需要包含公用的库函数,在程序第一次执行或者第一次调用某个库函数时,用动态链接的方法将程序与共享库函数相链接。
-static参数是阻止gcc使用共享库,下图可明显看出不使用共享库的可执行文件比使用了共享库的大很多。

总是提到映射,可计算机到底是怎样将虚拟地址空间映射到实实在在的物理内存上的呢?这就要提到内存分段和分页模式了。这个转换过程有操作系统和CPU共同完成. 操作系统为CPU设置好页表。CPU通过MMU单元进行地址转换。简单的说,就是人为的把线性地址空间划分为一个一个4k的(几乎所有的PC上的操作都使用4KB大小的页)逻辑页,把内存页以同样的方法等分为固定大小的物理页。当程序执行时,cpu(中的MMU组件)会自动查找逻辑页到到物理页映射关系,从而将数据加载进内存。
虚拟内存长什么样子呢?如下:

其中,Linux内核把虚拟地址空间分为两部分:用户进程空间,内核进程空间。
对于32位系统,寻址指针为4字节,对应的虚拟地址空间为0-2^32,即0-4G。
对于64位系统,寻址指针为8字节,对应的虚拟地址空间为0-2^64,即0-16G

要注意的是,这个地址空间是虚拟的,并非实际存在的。
大家每次创建一个进程的时候,操作系统都会给进程分配这样的一个虚拟内存。

虚拟内存还有一个什么好处呢?
那就是能跑比物理内存大的应用程序。
在实际的应用中,如果需要运行的应用程序比较小,所需内存容量小于计算机实际所配置的内存空间,自然不会出什么问题。
但是,目前很多的应用程序都比较大,计算机实际所配置的内存空间无法满足。
实践和研究都证明:一个应用程序总是逐段被运行的,而且在一段时间内会稳定运行在某一段程序里。
这也就出现了一个方法:如下图所示,把要运行的那一段程序从辅存复制到内存中来运行,而其他暂时不运行的程序段就让它仍然留在辅存。

当需要执行另一端尚未在内存的程序段(如程序段2),如下图所示,就可以把内存中程序段1的副本复制回辅存,在内存腾出必要的空间后,再把辅存中的程序段2复制到内存空间来执行即可:

在计算机技术中,把内存中的程序段复制回辅存的做法叫做“换出”,而把辅存中程序段映射到内存的做法叫做“换入”。
经过不断有目的的换入和换出,处理器就可以运行一个大于实际物理内存的应用程序了。
或者说,处理器似乎是拥有了一个大于实际物理内存的内存空间。

虚拟内存如何和物理内存联系起来的呢?

既然虚拟内存是一个虚拟的概念。
也就表名了虚拟地址不能真正的存放数据。
数据在真正读写的时候,是对真实的地址空间进行读写的。
处理器运算器和应用程序设计人员看到的只是虚拟内存空间和虚拟地址,而处理器片外的地址总线看到的只是物理地址空间和物理地址。
将虚拟地址转换成物理地址的任务叫 【地址翻译】。
虚拟内存的地址如何转成真实地址。中间需要一个转换(映射)的机制。
这个东西我们叫做 MMU (存储器管理单元)。
MMU 把虚拟地址转化为实际物理地址的依据。
记录表与存储管理单元MMU的作用如下图所示:

到这里, 相信大家应该理解虚拟内存和物理内存是通过映射记录来建立联系的。
那大家可以再思考一个问题,映射记录里面存放的是什么值呢?
我们知道计算机的存储单元是字节,如果按照存储单元来管理内存,那也就意味着这个映射表会非常的大。
以存储单元为单位来管理显然不现实。
因此Linux把虚存空间分成若干个大小相等的存储分区,Linux把这样的分区叫做页。
在内存管理时,页是地址空间的最小单位。
为了换入、换出的方便,物理内存也就按也得大小分成若干个块。
由于物理内存中的块空间是用来容纳虚存页的容器,所以物理内存中的块叫做页框。
页与页框是Linux实现虚拟内存技术的基础。
虚拟内存的页、物理内存的页框及页表
在Linux中,页与页框的大小一般为4KB。
当然,根据系统和应用的不同,页与页框的大小也可有所变化。
物理内存和虚拟内存被分成了页框与页之后,其存储单元原来的地址都被自然地分成了两段,并且这两段各自代表着不同的意义:

高位段分别叫做页框码和页码,它们是识别页框和页的编码;
低位段分别叫做页框偏移量和页内偏移量,它们是存储单元在页框和页内的地址编码。
下图就是两段虚拟内存和物理内存分页之后的情况:

为了使系统可以正确的访问虚存页在对应页框中的映像,在把一个页映射到某个页框上的同时,就必须把页码和存放该页映像的页框码填入一个叫做页表的表项中。
这个页表就是之前提到的映射记录表。
一个页表的示意图如下所示:

页模式下,虚拟地址、物理地址转换关系的示意图如下所示:

也就是说:处理器遇到的地址都是虚拟地址。
虚拟地址和物理地址都分成页码(页框码)和偏移值两部分。
在由虚拟地址转化成物理地址的过程中,偏移值不变。
而页码和页框码之间的映射就在一个映射记录表——页表中。

虚拟内存带来的好处:
虚拟内存管理可以控制物理内存的访问权限
访问的虚拟页若没有读写权限,则触发一个保护异常,终止进程。

虚拟内存让每个进程有独立的地址空间
对于私有区域来说,当不同进程对该区域做修改时,会触发写时拷贝,为新进程维护私有的虚拟地址空间。

虚拟地址到物理地址的映射会给分配和释放内存带来方便。
物理内存不连续的地址,可映射到连续的虚拟内存地址。

内存效率高
使用了页面调度,不会造成大量的数据装入装出。

三、虚拟地址和物理地址应用

两个进程A和B是怎么进行交互的呢?比如media进程和servicemanager进程。
对于进程来说,看到的都是虚拟地址,进程media和servicemanager用户空间不是映射在一起的,所以即使访问的是同一个地址,但是通过MMU映射的是两个地址,故用户空间没法直接交互。但是进程的内核空间是映射在一起的。用户空间的内存是可以直接开辟和操作,内核空间不能直接操作和读取,要经过系统调用,通过系统中断进内核。
当media创建了一个对象,需要将对象地址发送给servicemanager时,流程是怎样的呢?首先media要将该地址通过系统拷贝到内核空间,然后再将内核空间的地址拷贝到servicemanager的用户空间。所以进程通讯一定要进内核。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值