概述
前言
这篇文章是我对虚拟内存、可执行文件的编译以及内存分布的总结,这篇文章要搞清楚以下几个问题。
1、为什么要虚拟内存;
2、虚拟地址的内存分布;
3、虚拟地址与物理地址的映射;
4、可执行文件的编译过程;
5、可执行文件内存结构;
6、可执行文件映射到虚拟内存过程。
虚拟内存
在学习操作系统之前,有过嵌入式裸机编程经验的人知道,嵌入式裸机就是直接把可执行文件烧写到内存上,用这种直接物理映射的方式。但是,当可执行文件太大,内存不够用怎么办,在嵌入式裸机程序时候,我遇到这种情况都是在删减一些工程里面的无用代码。显然操作系统需要一种方法来解决这个问题,如果是要我们自己来想象一个办法,我们理所当然会想到,内存不够,先把程序放在硬盘里面,需要用的时候在调到内存。虚拟内存就是这么操作的,当然也需要基于更多复杂的考虑。
在我的理解中,虚拟内存不光解决的内存不够用的问题,还完美的配合了进程的调度。
内存为什么会不够用,把内存弄大一点不行吗,对于32为的芯片,地址寻址最大就是4G空间,在大也没用,当然还有64位的芯片,地址寻址可以达到128T,这下够了吧。在操作系统设计的时候还没有64位芯片,而且内存也是很贵的,现在买电脑8G的内存条得几百块钱,整个1T内存条,疯了吧。而对于除了电脑的嵌入式应用来说,普遍的仍然使用的32为芯片。ARM公司64位芯片也主要是应用在手机上面。
1、LINUX虚拟地址空间布局
一、3G的用户空间
1、保留区的大小为128M,是不可访问区。例如定义一个指针p,int*p = NULL;之后不能对p进行使用,因为p的地址为NULL,即0x0000 0000,该地址属于保留区即不可访问区。
2、.text 段存放指令,区域的大小在程序执行前已经确认,可能包含一些只读的常数变量,如字符串常量。
3、.data段存放已初始化数据段的全局变量和静态变量(static修饰的变量)。
4、.bss段存放未初始化以及初始化为0的全局变量和静态变量,在程序加载的时候由内核清0,
5、.heap即堆区,是由用户自己管理,先进先出,动态分配内存malloc、ralloc、calloc是在堆区,通过free()函数释放空间,增长方向是由低地址向高地址增长。
6、共享库:加载共享库和使用mmap共享内存。
5.stack即栈区,是由系统管理,先进后出,保存局部变量、函数形参、自动变量。增长方向是由高地址向低地址增长。
7、命令行参数:C语言中的命令行参数涉及到程序的主函数main(int argc,char *argv[]),argc表示命令行参数的个数,无需用户传递,自动确定,argv[]指向命令行传递进来的参数,其中argv[0]指向的是可执行文件的文件名。
8、环境变量:int execve(char *pathname,char *argv[],char *envp),char *envp即环境变量,它包含一个或许多应用程序所使用到的配置信息
看到这,可以思考一个问题,还是用裸机程序的思维,这个虚拟内存就相当于裸机程序的内存,每个程序就相当于裸机的可执行程序,其实还有一个虚拟处理器的概念,这个虚拟处理器说白了就是保存芯片的一些寄存器数据,称之为上下文环境。程序被系统调用变成进程,每个进程都有自己的虚拟处理器和虚拟内存,当这个进程处于被调度运行时候,虚拟处理器和虚拟内存就有一个’转正’过程,称之为上下文切换(Context Switch)。**粗糙一点可以把操作系统想象成很多个裸机程序堆叠一起,然后在通过某种方法对它们进行切换与通信。**平常用交叉编译器得到的程序就是一个裸机程序。
如果用裸机程序来考虑Linux的可执行程序,这些就体现了虚拟内存的一个优点,就是方便程序的编译器设计,每个可执行程序都是考虑在4G内存上,这样的话它们由c语言最终生成的可执行文件格式就可以都设计成一样的。
2、虚拟地址到物理地址的映射
通过前面的内容,可以了解虚拟内存是什么,但马上就会有一种不可思议的感觉(我是这么感觉的),LINUX每个进程配一个4G虚拟内存,1000个进程就配4000G,这不可能啊,虽然有4T的硬盘,但是这样肯定不科学。这就涉及到内存分页的一些知识。
直接物理寻址
通过虚拟地址到物理地址
VM系统通过将虚拟内存分割为固定大小的虚拟页(VP),物理内存被分为相同固定大小的物理页(PP)。
虚拟页任何时候由三种页组成:
- 未分配的:VM系统未分配的页(或者未创建),也就不占磁盘空间
- 未缓存的:没有缓存的物理内存的页
- 已缓存的:已经缓存到物理内存的页
(虚拟内存4G的意义只是对应于32为的地址总线,并不会真的占磁盘4G空间,)
通过这个图,程序在虚拟内存上地址是连续的,但是虚拟内存映射到物理内存时,地址可以是分散的。这样的好处就是,有限的物理内存可以得到充分的利用。
2.1页表
VM系统必须能判断虚拟页每个页是否已经缓存在内存上,如果是,对于的物理地址是什么。如果不是,虚拟页存放在磁盘的那个位置,同时在内存上找到一个页(牺牲页)用来缓存这个页。这是由软硬件联合提供的,由操作系统、MMU以及一个叫页表的数据结构。
上图展示了一个页表,页表就是页表条目(PTE)的数组,假设PTE是由一个有效位与地址组成(实际上还有其他的位)。
有效位为1:表示已缓存,地址为物理页的其实位置
有效位为0:表示未缓存,空地址表示未分配,否则地址为磁盘地址
当处理器给出虚拟地址寻址,通过查询页表来确认页命中(缓存)还是不命中(未缓存)
命中的话,直接通过MMU翻译出物理地址。
不命中就会导致缺页异常,缺页会调用内核的缺页异常程序,在内存上选择一个牺牲页来缓存未命中的页,缓存完成以后,PTE更新,再进行寻址,就会命中。
2.2虚拟内存对内存的管理与保护
对于多个进程而言
每个进程都有虚拟内存,自然都有一个页表,这种按需页面调度和独立的虚拟地址空间结合的方法,对内存管理有着深远的影响,不仅解决的内存本身的问题,还有其他积极的作用,特别的,简化了链接与加载、代码与数据共享、应用程序内存管理。
- 简化链接:这个很好理解,独立的地址空间是代码映像使用相同的格式,不用管实际存放的位置;
- 简化加载:LINUX加载器在加载程序时候,只需要把PTE标记为未缓存的,不需要其他操作,剩下的就等实际寻址的时候调用缺页异常来进行页面加载。
- 简化共享:一般而言,每个进程都有自己的私有代码、数据、堆栈区。但是对于系统内核以及标准C库,如printf,是需要共享的。而不是每个进程都包涵内核与C库的副本。
- 简化内存分配:用malloc分配连续的虚拟地址时候,在实际内存上可以不连续。
通过对PTE添加一些位可以起到对物理内存的保护。SUP=1是只有运行在特权模式下才能访问,用户模式只能运行SUP=0的页,还有读写的保护位
当操作异常时候,就是导致一般性故障,触发内核异常处理程序。
2.3地址翻译
单级页表
需要知道页表的位置,进程创建时候,页表就也被创建。虚拟地址分为两部分,前面的虚拟页号用来查询页表的物理页地址,再加上后面的偏移,即可组成实际的物理地址。
多级页表
一个4G虚拟空间,每页4KB,那么需要常留页表空间是4M,程序都可能是几KB,页表就4M,这肯定不合适啊,要是进程太多,内存就只够放页表了。为此有了多级页表。
上图的二级页表就相当于单级页表的1级页表,想个办法不让二级页表一直在内存中,一级页表中地址为空的话,相对于的二级页表部分就不存在,那就是只有一级页表常留在内存,二级页表根据实际情况进行创建。这下就是需要KB级的内存资源了。
LINUX系统使用的是四级页表
更详细的内容可参考 <深入理解计算机操作系统>
最后
以上就是欣慰星月为你收集整理的LINUX 关于虚拟内存、程序内存(一)前言虚拟内存2、虚拟地址到物理地址的映射的全部内容,希望文章能够帮你解决LINUX 关于虚拟内存、程序内存(一)前言虚拟内存2、虚拟地址到物理地址的映射所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复