内存映射
想理解这个知识点,我们首先要知道内存的概念和映射的概念。
内存的基本概念
我们先看一张计算的组成图:
内存一般分为只读存储器(ROM)和随机存储器(RAM),以及最强悍的高速缓冲存储器(CACHE),只读存储器应用广泛,它通常是一块在硬件上集成的可读芯片,作用是识别与控制硬件,它的特点是只可读取,不能写入。随机存储器的特点是可读可写,断电后一切数据都消失,这两者一起就构成了我们硬件上的内存条就是我们常说的4G 8G。
高速缓冲存储器就是数据交换的缓冲区(称作Cache),当某一硬件要读取数据时,会首先从缓存中查找需要的数据,如果找到了则直接执行,找不到的话则从内存中找。由于缓存的运行速度比内存快得多(接近CPU内部速度),故缓存的作用就是帮助硬件更快地运行,缓解CPU和主存之间的速度差异。
cpu不能直接访问硬盘的数据,只能通过把硬盘的数据先放到内存里, 然后再从内存里访问硬盘的数据。
主存就要物理内存,真实的插在板子上的内存是多大就是多大了。而在CPU中的概念,物理内存就是CPU的地址线可以直接进行寻址的内存空间大小。
地址线是用来传输地址信息用的。举个简单的例子:cpu在内存或硬盘里面寻找一个数据时通过地址总线找到该内存单元,然后通过控制总线确定操作方法,在通过数据总线将其数据送回来 如果有地址线32根.就可以访问2的32次方的空间,也就是4GB。
如何找到内存?
光有内存还不够,还需要找到它,才能使用起来。这里有个专业的叫法:寻址,分为物理寻址,和虚拟寻址。
寻址空间一般指的是CPU对于内存寻址的能力。通俗地说,就是能最多用到多少内存的一个问题。数据在存储器(RAM)中存放是有规律的 ,CPU在运算的时候需要把数据提取出来就需要知道数据在那里 ,这时候就需要挨家挨户的找,这就叫做寻址,但如果地址太多超出了CPU的能力范围,CPU就无法找到数据了。 CPU最大能查找多大范围的地址叫做寻址能力 ,CPU的寻址能力以字节为单位。 通常人们认为,内存容量越大,处理数据的能力也就越强,但内存容量不可能无限的大,它要受到系统结构、硬件设计、制造成本等多方面因素的制约,一个最直接的因素取决于系统的地址总线的地址寄存器的宽度(位数)。 计算机的寻找范围由总线宽度(处理器的地址总线的位数)决定的,也可以理解为cpu寄存器位数,这二者一般是匹配的
物理寻址:内存通常被组织为一个由M个连续的字节大小的单元组成的数组,每个字节都有一个唯一的物理地址(Physical Address PA),作为到数组的索引。CPU访问内存最简单直接的方法就是使用物理地址,这种寻址方式被称为物理寻址。
虚拟寻址:现代处理器使用的是一种称为虚拟寻址(Virtual Addressing)的寻址方式。使用虚拟寻址,CPU需要将虚拟地址翻译成物理地址,这样才能访问到真实的物理内存。怎么翻译等下说。
虚拟内存
上面所说的还是物理内存,我们当然可以直接使用物理内存运行程序,但是这样会有几个问题:
1.现在有多个程序需要运行,但是内存空间不足了,就需要将其他程序暂时拷贝到硬盘当中,然后将新的程序装入内存运行.由于大量的数据装入装出,内存的使用效率会十分低。
2.由于程序都是直接访问物理内存的,所以一个进程可以修改其他进程的内存数据,甚至修改内核地址空间中的数据。
3.因为内存地址是随机分配的,所以程序运行的地址也是不正确的。
为了解决以上问题,虚拟内存就出来了。程序不再直接使用物理内存。而是使用 操作linux给每个进程分配的虚拟内存。此内存结构:
内核区:用户代码不可见的区域,页表就存放在这个区域中。 用户区:
a、只读段:只可读,不可写,程序代码段。
b、数据段:保存全局变量,静态变量的区域。
c、堆区:就是动态内存,通过malloc,new申请内存,有一个堆指针,可以通过brk系统调用调整堆指针。
d、文件映射区域:通过mmap系统调用,如动态库,共享内存等映射物理空间的内存区域。可以单独释放,不会产生内存碎片。
e、栈区:用于维护函数调用的上下文空间,用ulimit -s 查看。一般默认为8M
Stack空间(进栈和出栈)由操作系统控制,其中主要存储函数地址、函数参数、局部变量等等,所以Stack空间不需要很大,一般为几MB大小。
Heap空间由程序控制,程序员可以使用malloc、new、free、delete等函数调用来操作这片地址空间。Heap为程序完成各种复杂任务提供内存空间,所以空间比较大,一般为几百MB到几GB。正是因为Heap空间由程序员管理,所以容易出现使用不当导致严重问题。
进程内存分配
每个进程运行的时候,都会拿到4G的虚拟内存,在32位Linux下,其中3G是交给用户的,1G是交给内核的,而task_struct就是存储在这1G的内核系统空间中。
什么是进程和PCB
对于一个进程,它在被执行前其实是一个可执行程序。这个程序是被放在磁盘上的,当它要被执行的时候,它先被加载到内存当中,然后再放入到寄存器中,最后再让cpu执行该程序,这个时候一个静态的程序就变成了进程。
那么操作系统是怎么来管理这些进程的呢?操作系统通过一个双向链表把进程连起来。但是,对于进程其实它是一个抽象的概念,系统肯定要通过一个东西来描述进程,然后才能管理进程。于是PCB就出来了,操作系统通过PCB来描述进程,于是这个双向链表连接的其实是PCB(progress control block)进程控制块,它就是一个C语言中的结构体,用来描述进程,在Linux下,就是task_struct结构体。 主要有以下内容:
标识相关:pid,ppid等等
文件相关:进程需要记录打开的文件信息,于是需要文件描述符表
内存相关:内存指针,指向进程的虚拟地址空间(用户空间)信息
优先级相关:进程相对于其他进程的调度优先级
上下文信息相关:CPU的所有寄存器中的值、进程的状态以及
堆栈上的内容,当内核需要切换到另一个进程时,需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。
状态相关:进程当前的状态,说明该进程处于什么状态
信号相关:进程的信号处理函数,以及记录当前进程是否还有待处理的信号
9.I/O相关:记录进程与各种I/O设备之间的交互
映射
两个非空集合A与B间存在着对应关系f,而且对于A中的每一个元素x,B中总有有唯一的一个元素y与它对应,就这种对应为从A到B的映射,记作f:A→B。其中,b称为元素a在映射f下的象,记作:b=f(a)。a称为b关于映射f的原象。集合A中所有元素的象的集合称为映射f的值域,记作f(A)
这个是数学上的定义,在计算中也是类似。hashmap就是映射,哈希函数就是 映射关系。这个函数你可以自己定义。简答来说就是 集合A和集合B是映射关系的话,你通过集合A和映射函数就能得到B。建立某种关系 是的2者 对其他人来说就是同一个。不正确的描述:2个池子A B。如果没有建立映射,那么我存取水,要分别 往A B池子倒水和抽水。但是如果我 把池子A和池子B用一根水管连接起来 建立了映射关系。那么当我想从B池子倒水取水时,我往A池子倒水取水就行了。
内存映射
总算到主题了,虚拟内存毕竟只是虚拟的真实是不存在的。只是逻辑上存在。如果有效果那就需要把虚拟内存和真实的物理内存 建立起联系。这样 进程访问 虚拟内存的地址 比如0x00008 经过处理后 可以访问到 物理内存的地址 比如0x00004。这个建立联系的过程就叫映射。处理就叫做地址翻译。
内存映射:指物理内存与虚拟内存建立映射关系。使得进程A访问虚拟内存时就像访问物理内存一样。
到这里大家肯定会有很多疑问如:
这个映射关系具体是怎么样的?
要是物理内存不够怎么办?
在回答这些问题前,我们需要了解 linux操作系统是怎么去管理内存的。
Linux管理内存方式。
常见的内存管理方式有块式管理、页式管理、段式和段页式管理。
(1)块式管理:把主存分为一大块一大块的,当所需的程序片段不在主存时就分配一块主存空间,把程序片段load入主存,就算所需的程序片段只有几个字节也只能把这一块分配给它。这样会造成很大的浪费,平均浪费了50%的内存空间,但是易于管理。
(2)页式管理:把主存分为一页一页的,每一页的空间要比一块一块的空间小很多,这种方法的空间利用率要比块式管理高很多
(3)段式管理:把主存分为一段一段的,每一段的空间又要比一页一页的空间小很多,这种方法在空间利用率上又比页式管理高得多,但是也有另外一个缺点。一个程序片段可能会被分为几十段,这样很多时间就会被浪费在计算每一段的物理地址上。
(4)段页式管理:结合了段式管理和页式管理的优点。把主存先分成若干段,每个段又分成若干页。段页式管理每取一护具,要访问3次内存。
计算机会对虚拟内存地址空间(32位为4G)分页产生页(page),对物理内存地址空间(假设256M)分页产生页帧(page frame),这个页和页帧的大小是一样大的都是4KB,所以呢,在这里,虚拟内存页的个数势必要大于物理内存页帧的个数。在计算机上有一个页表(page table),就是映射虚拟内存页到物理内存页的,更确切的说是页号到页帧号的映射,而且是一对一的映射。这里要好好理解。
但是问题来了,虚拟内存页的个数 > 物理内存页帧的个数,岂不是有些虚拟内存页的地址永远没有对应的物理内存地址空间?不是的,操作系统是这样处理的。操作系统有个页面失效(page fault)功能。操作系统找到一个最少使用的页帧,让他失效,并把它写入磁盘,随后把需要访问的页放到页帧中,并修改页表中的映射,这样就保证所有的页都有被调度的可能了。这就是处理虚拟内存地址到物理内存的步骤。这就回答了第一个问题。
虚拟内存地址由页号和偏移量组成。页号对应的映射到一个页帧。那么,说说偏移量。偏移量就是我上面说的页(或者页帧)的大小,即这个页(或者页帧)到底能存多少数据。举个例子,有一个虚拟地址它的页号是4,偏移量是20,那么他的寻址过程是这样的:首先到页表中找到页号4对应的页帧号(比如为8),如果页不在内存中,则用失效机制调入页,否则把页帧号和偏移量传给MMU(CPU的内存管理单元)组成一个物理上真正存在的地址,接着就是访问物理内存中的数据了。总结起来说,虚拟内存地址的大小是与地址总线位数相关,物理内存地址的大小跟物理内存条的容量相关。
进程是如何访问物理内存
进程开始要访问一个地址,它可能会经历下面的过程
每次我要访问地址空间上的某一个地址,都需要把地址翻译为实际物理内存地址
所有进程共享这整一块物理内存,每个进程只把自己目前需要的虚拟地址空间映射到物理内存上
进程需要知道哪些地址空间上的数据在物理内存上,哪些不在(可能这部分存储在磁盘上),还有在物理内存上的哪里,这就需要通过页表来记录
页表的每一个表项分两部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址(如果在的话)
当进程访问某个虚拟地址的时候,就会先去看页表,如果发现对应的数据不在物理内存上,就会发生缺页异常
缺页异常的处理过程,操作系统立即阻塞该进程,并将硬盘里对应的页换入内存,然后使该进程就绪,如果内存已经满了,没有空地方了,那就找一个页覆盖,至于具体覆盖的哪个页,就需要看操作系统的页面置换算法是怎么设计的了。
我们的cpu想访问虚拟地址所在的虚拟页(VP3),根据页表,找出页表中第三条的值.判断有效位。如果有效位为1,DRMA缓存命中,根据物理页号,找到物理页当中的内容,返回。
若有效位为0,参数缺页异常,调用内核缺页异常处理程序。内核通过页面置换算法选择一个页面作为被覆盖的页面,将该页的内容刷新到磁盘空间当中。然后把VP3映射的磁盘文件缓存到该物理页上面。然后页表中第三条,有效位变成1,第二部分存储上了可以对应物理内存页的地址的内容。
缺页异常处理完毕后,返回中断前的指令,重新执行,此时缓存命中,执行1。
将找到的内容映射到告诉缓存当中,CPU从告诉缓存中获取该值,结束。
简单说就是:当每个进程创建的时候,内核会为进程分配4G的虚拟内存,当进程还没有开始运行时,这只是一个内存布局。实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射)。这个时候数据和代码还是在磁盘上的。当运行到对应的程序时,进程去寻找页表,发现页表中地址没有存放在物理内存上,而是在磁盘上,于是发生缺页异常,于是将磁盘上的数据拷贝到物理内存中。
另外在进程运行过程中,要通过malloc来动态分配内存时,也只是分配了虚拟内存 new也是如此,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。
可以认为虚拟空间都被映射到了磁盘空间中(事实上也是按需要映射到磁盘空间上,通过mmap,mmap是用来建立虚拟空间和磁盘空间的映射关系的)
利用虚拟内存机制的优点
既然每个进程的内存空间都是一致而且固定的(32位平台下都是4G),所以链接器在链接可执行文件时,可以设定内存地址,而不用去管这些数据最终实际内存地址,这交给内核来完成映射关系
当不同的进程使用同一段代码时,比如库文件的代码,在物理内存中可以只存储一份这样的代码,不同进程只要将自己的虚拟内存映射过去就好了,这样可以节省物理内存
在程序需要分配连续空间的时候,只需要在虚拟内存分配连续空间,而不需要物理内存时连续的,实际上,往往物理内存都是断断续续的内存碎片。这样就可以有效地利用我们的物理内存
使用虚存也是有代价的,主要表现在以下几个方面: (1)虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存 (2)虚拟地址到物理地址的转换,增加了指令的执行时间 (3)页面的换入换出需要磁盘I/O,这是很耗时间的。 (4)如果一页中只有一部分数据,会很浪费内存。
总结
存储单元一般应具有存储数据和读写数据的功能,一般以8位二进制作为一个存储单元,也就是一个字节。每个单元有一个地址,是一个整数编码,可以表示为二进制整数。 程序中的变量和主存储器的存储单元相对应。变量的名字对应着存储单元的地址,变量内容对应着单元所存储的数据。
由程序产生的地址被称为虚拟地址,它们构成了一个虚拟地址空间。在使用虚拟存储器的情况下,虚拟地址不是被直接送到内存总线上,而且是被送到内存管理单元(Memory Management Unt,MMU),MMU把虚拟地址映射为物理内存地址。
虚拟内存空间被组织为一个存放在硬盘上的M个连续的字节大小的单元组成的数组,每个字节都有一个唯一的虚拟地址,作为到数组的索引(这点其实与物理内存是一样的)。虚拟寻址需要硬件与操作系统之间互相合作。CPU中含有一个被称为内存管理单元(Memory Management Unit, MMU)的硬件,它的功能是将虚拟地址转换为物理地址。MMU需要借助存放在内存中的页表来动态翻译虚拟地址,该页表由操作系统管理。
操作系统通过将虚拟内存分割为大小固定的块来作为硬盘和内存之间的传输单位,这个块被称为虚拟页(Virtual Page, VP),每个虚拟页的大小为P=2^p字节。物理内存也会按照这种方法分割为物理页(Physical Page, PP),大小也为P字节。
页表是一种数组结构,存放着各个虚拟页的状态,是否映射,是否缓存.进程要知道那些内存地址上的数据在物理内存上,那些不在,还有物理内存上的哪里需要用页表来记录.页表的每一个表项分为两部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址(如果在的话)当进程访问某个虚拟地址,去查看页表的时候如果发现对应的数据不在物理内存中,则发生缺页异常.但是我们马上就要使用这个数据啊.所以我们需要尽快解决掉缺页异常.缺页异常的处理过程:就是把进程需要的数据从磁盘上面拷贝到物理内存中,如果物理内存已经满了,没有空地方了,那就找一个页覆盖,当然如果被覆盖的页曾经被修改过,需先将此页写回磁盘.页表的状态>> 内存管理器会将物理内存页(通常大小为 4 KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动。
mmap() 文件映射
void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
mmap的作用是映射文件描述符fd指定文件的 [off,off + len]区域至调用进程的[addr, addr + len]的内存区域。
mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。 如:
参数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的
MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的
进程间通信)。
len是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。
prot 参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。
flags由以下几个常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必
选其一,而MAP_FIXED则不推荐使用。
offset参数一般设为0,表示从文件头开始映射。
参数addr指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。函
数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。