Linux的虚拟存储及动态内存管理及共享内存

物理内存与虚拟内存

虽然应用程序操作的对象是映射到物理内存之上的虚拟内存,但是处理器直接操作的却是物理内存。所以当用程序访问一个虚拟地址时,首先必须将虚拟地址转化成物理地址,然后处理器才能解析地址访问请求。地址的转换工作需要通过查询页表才能完成,概括地将,地址转换需要将虚拟地址分段,使每段虚拟地址都作为一个索引指向页表,而页表则指向下一级别的页表或者指向最终的物理页面。

    linux中使用三级页表完成地址转换。利用多级页表能够节约地址转换需占用的存放空间。如果利用三级页表转换地址,即使64位机器,占用的空间也很有限。linux使用的机制:

    顶级页表示页全局目录(PGD),它包含一个pgd_t类型数组,多数体系结构中pgd_t类型等同于无符号长整型。PGD中的表项指向二级页目录中的表项:PMD

    二级页表是中间页目录(PMD),它是个pmd_t类型数据,其中的表项指向PTE中的表项。

    最后一级的页表简称页表,其中包含了pte_t类型的页表项,该页表项指向物理页面。多数体系结构中,搜索页表的工作由硬件完成。每个进程都有自己的页表,内存描述符的pgd域指向的就是进程的页全局目录。

 

虚拟存储器

虚拟存储器提供了三个重要的能力:

1)将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,高效的使用的主存。

2)为每个进程提供了一致的地址空间,从而简化了存储器管理

3)保护了每个进程的地址空间不被其他进程破坏

Linux操作系统同样也采用了虚拟存储技术,对一个技术而言,好像可以访问整个系统的所有物理内存,即使单独一个进程而言,它拥有的地址空间可以远远大于系统物理内存。

 

用户空间与内核空间

4G的地址空间被人为的分为两个部分,用户空间和内核空间,

用户空间从0-3G,内核空间占据3G-4G.用户进程通常情况下只能访问用户空间的虚拟地址,不能范围内和空间的虚拟地址

除非用户进程执行系统调用时,切换进内核态执行,可以访问到内核空间。

每个进程的用户空间时完全独立的,互不相关的。

Linux的虚拟存储及动态内存管理及共享内存_第1张图片

mm_struct

Linux为每个进程都维持了一个单独的虚拟地址空间,这个虚拟地址空间例包括我们所熟知的代码段,数据段,堆栈等。

Linux的虚拟存储及动态内存管理及共享内存_第2张图片

 

内核为系统中的每个进程维护一个单独的数据结构task_struct,里面除了之前所学到的,还包括一个mm_struct的指针,指向一个结构体mm_struct

Linux的虚拟存储及动态内存管理及共享内存_第3张图片

这个结构体描述了虚拟存储器的当前状态,该结构体里有两个字段我们是关心的:pgd和mmap

pgd指向第一级页表的基址,mmap指向一个vm_area_structs的链表,其中每个vm_area_structs都描述了当前虚拟地址空间的一个区域。

其中的字段:

vm_end表示该区域的结束处

vm_start:表示该区域的起始处

vm_next:指向链表中的下一个区域结构

缺页中断

发生缺页中断后,执行了那些操作?

1. 检查要访问的虚拟地址是否合法

2.查找/分配一个物理页

3.填充物理页内容

4.重新建立映射关系

通俗地来说,你给出的这个要访问的地址,操作系统会在页表中查询,发现此时你要访问的页不在内存中,触发缺页中断机制,操作系统会给你分配一个新的页,如果此时内存中的页已经满了,操作系统会将内存中的一页换出(置于磁盘),然后将你要访问的该页换入内存。

 

动态内存分配

动态内存分配器维护这一个进程的虚拟存储器区域,称为堆,并且维护了一个brk指针指向堆的顶部

分配器将堆视为一组不同大小的块的集合来维护,每个块就是一个连续的虚拟存储器片,要么是已分配的,要么是空闲的。

分配器分为两种:

显式分配器:像malloc,new这种

隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,自动释放未使用的已分配的块的过程叫做垃圾收集,像java这种高级语言就依赖此机制。

内存分配的原理

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。

1、brk是将数据段(.data)的最高地址指针_edata往高地址推;

2、mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存

     这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。


下面以一个例子来说明内存分配的原理:

情况一、malloc小于128k的内存,使用brk分配内存,将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系),如下图:

1、进程启动的时候,其(虚拟)内存空间的初始布局如图1所示。

      其中,mmap内存映射文件是在堆和栈的中间(例如libc-2.2.93.so,其它数据文件等),为了简单起见,省略了内存映射文件。    _edata指针(glibc里面定义)指向数据段的最高地址。 

2、进程调用A=malloc(30K)以后,内存空间如图2:

      malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配。

      你可能会问:只要把_edata+30K就完成内存分配了?

      事实是这样的,_edata+30K只是完成虚拟地址的分配,A这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的。 


3、进程调用B=malloc(40K)以后,内存空间如图3。

Linux的虚拟存储及动态内存管理及共享内存_第4张图片

 

情况二、malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0),如下图:

Linux的虚拟存储及动态内存管理及共享内存_第5张图片

 

 

4、进程调用C=malloc(200K)以后,内存空间如图4:

      默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存。

      这样子做主要是因为:

      brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,这就是内存碎片产生的原因,什么时候紧缩看下面),而mmap分配的内存可以单独释放。

      当然,还有其它的好处,也有坏处,再具体下去,有兴趣的同学可以去看glibc里面malloc的代码了。 
5、进程调用D=malloc(100K)以后,内存空间如图5;
6、进程调用free(C)以后,C对应的虚拟内存和物理内存一起释放。

 

Linux的虚拟存储及动态内存管理及共享内存_第6张图片

 

7、进程调用free(B)以后,如图7所示:

        B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,如果往回推,那么D这块内存怎么办呢

当然,B这块内存,是可以重用的,如果这个时候再来一个40K的请求,那么malloc很可能就把B这块内存返回回去了。 
8、进程调用free(D)以后,如图8所示:

        B和D连接起来,变成一块140K的空闲内存。

9、默认情况下:

       当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩,变成图9所示。

参考:http://m.blog.csdn.net/article/details?id=39496057

动态内存管理

提到动态内存管理,就很容易想到malloc/new,在此整理。

一.brk与mmap系统调用

上文刚刚详细的讲解了,mmap系统调用用处非常多,比如一个进程的所有动态库文件.so的加载,都需要通过mmap系统调用映射指定大小的虚拟地址区间,然后将.so代码动态映射到这些区域,以供进程其他部分代码访问;这也正是虚存的好处之一,节省了空间,多份代码都需要该库,只需要建立起映射关系即可。

无论是brk还是mmap返回的都是虚拟地址,在第一次访问这块地址的时候,会触发缺页异常,然后内核为这块虚拟地址申请并映射物理页框,建立页表映射关系,后续对该区间虚拟地址的访问,通过页表获取物理地址,然后就可以在物理内存上读写了。

 

二.malloc/free 库函数

malloc/free是 libc实现的库函数,主要实现了一套内存管理机制,当其管理的内存不够时,通过brk/mmap等系统调用向内核申请进程的虚拟地址区间,如果其维护的内存能满足malloc调用,则直接返回,free时会将地址块返回空闲链表。

malloc(size) 的时候,这个函数会多分配一块空间,用于保存size变量,free的时候,直接通过指针前移一定大小,就可以获取malloc时保存的size变量,从而free只需要一个指针作为参数就可以了calloc 库函数相当于 malloc + memset(0)

 

三.new/delete/new[]/delete[]

new/delete 是c++ 内置的运算符,相当于增强版的malloc/free. c++是兼容c的,一般来说,同样功能的库,c++会在安全性和功能性方面比c库做更多工作。动态内存管理这块也一样。

new的实现会调用malloc,对于基本类型变量,它只是增加了一个cookie结构, 比如需要new的对象大小是 object_size, 则事实上调用 malloc 的参数是 object_size + cookie, 这个cookie 结构存放的信息包括对象大小,对象前后会包含两个用于检测内存溢出的变量,所有new申请的cookie块会链接成双向链表。由于内置了内存溢出检测,所以比malloc更安全。

对于自定义类型,new会先申请上述的大小空间,然后调用自定义类型的构造函数,对object所在空间进行构造。c++比c强大的一个方面就是c++编译器可以自动做构造和析构,new运算符会自动计算需要的空间大小,然后根据类型自己调用构造函数,如果存在子类型对象,或者存在继承的基类型,new都会自动调用子类型的构造函数和基类型的构造函数完成构造。同样,delete 操作符根据cookie的size知道object的大小,如果是自定义类型,会调用析构函数对object所在空间进行析构,如果有子类型或继承,自动调用子类型和基类型的析构函数,然后将cookie块从双向链表摘除,最后调用 free_dbg 释放。

new[] 和delete[]是另外两个操作符,用于数组类型的动态内存获取和释放,实现过程类似new/delete 

共享内存

Linux中的两种共享内存。一种是我们的IPC通信System V版本的共享内存,另外的一种就是存储映射I/O(mmap函数),posix的共享内存就是建立在mmap之上的。

mmapI/O的描述符间接说明内存映射文件,另外,mmap另外可以在无亲缘的进程之间提供共享内存区,这样,类似的两个进程之间就是可以进行了通信。

  Linux提供了内存映射函数mmap, 它把文件内容映射到一段内存上(准确说是虚拟内存上), 通过对这段内存的读取和修改, 实现对文件的读取和修改,mmap()系统调用使得进程之间可以通过映射一个普通的文件实现共享内存。普通文件映射到进程地址空间后,进程可以向访问内存的方式对文件进行访问,不需要其他系统调用(read,write)去操作。

  内存映射,简而言之就是将用户空间的一段内存区域映射到内核空间,映射成功后,用户对这段内存区域的修改可以直接反映到内核空间,相反,内核空间对这段区域的修改也直接反映用户空间。那么对于内核空间<—->用户空间两者之间需要大量数据传输等操作的话效率是非常高的。

mmap系统调用并不完全是为了共享内存来设计的,它本身提供了不同于一般对普通文件的访问的方式,进程可以像读写内存一样对普通文件进行操作,IPC的共享内存是纯粹为了共享。

 

mmap用于共享内存的方式

1、我们可以使用普通文件进行提供内存映射,例如,open系统调用打开一个文件,然后进行mmap操作,得到共享内存,这种方式适用于任何进程之间。

2、可以使用特殊文件进行匿名内存映射,这个相对的是具有血缘关系的进程之间,当父进程调用mmap,然后进行fork,这样父进程创建的子进程会继承父进程匿名映射后的地址空间,这样,父子进程之间就可以进行通信了。相当于是mmap的返回地址此时是父子进程同时来维护。

3、另外POSIX版本的共享内存底层也是使用了mmap。

与systerm V版本的共享内存相比:

1、mmap是在磁盘上建立一个文件,每个进程地址空间中开辟出一块空间进行映射。而对于shm而言,shm每个进程最终会映射到同一块物理内存。shm保存在物理内存,这样读写的速度要比磁盘要快,但是存储量不是特别大。

2、相对于shm来说,mmap更加简单,调用更加方便,所以这也是大家都喜欢用的原因。

3、另外mmap有一个好处是当机器重启,因为mmap把文件保存在磁盘上,这个文件还保存了操作系统同步的映像,所以mmap不会丢失,但是shmget就会丢失。

 

 

你可能感兴趣的:(操作系统,Linux)