Linux内存管理

一、地址相关概念

1. 物理地址(physical address)
        物理内存,真实存在的插在主板内存槽上的内存条的容量的大小。
        内存是由若干个存储单元组成的,每个存储单元有一个编号,这种编号可唯一标识一个存储单元,称为物理地址。

2. 虚拟地址(virtual address)
       虚拟内存地址就是每个进程可以直接寻址的地址空间,不受其他进程干扰。
       虚拟内存与物理内存的区别:虚拟内存就与物理内存相反,是指根据系统需要从硬盘虚拟地匀出来的内存空间,是一种计算机系统内存管理技术,属于计算机程序,而物理内存为硬件。因为有时候当你处理大的程序时候系统内存不够用,此时就会把硬盘当内存来使用,来交换数据做缓存区,不过物理内存的处理速度是虚拟内存的30倍以上。

       虚拟地址(线性地址)是逻辑地址到物理地址变换之间的中间层。跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。


3. 逻辑地址(logical address)
        源程序经过汇编或编译后,形成目标代码,每个目标代码都是以0为基址顺序进行编址的,原来用符号名访问的单元用具体的数据——单元号取代。这样生成的目标程序占据一定的地址空间,称为作业的逻辑地址空间,简称逻辑空间。
       在逻辑空间中每条指令的地址和指令中要访问的操作数地址统称为逻辑地址。即应用程序中使用的地址。要经过寻址方式的计算或变换才得到内存中的物理地址。
        例如,你在进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址,不和绝对物理地址相干。


【总结】

        CPU将一个虚拟内存空间中的地址转换为物理地址,需要进行两步:首先将给定一个逻辑地址,CPU要利用其段式内存管理单元,先将为个逻辑地址转换成一个线程地址,再利用其页式内存管理单元,转换为最终物理地址。如下图:

段式内存管理:能有效的提高内存利用率;

页式内存管理:能反映程序的逻辑结构并有利于共享;

Linux内存管理_第1张图片



二、物理内存、虚拟内存管理

1、物理内存管理

       物理内存,真实存在的插在主板内存槽上的内存条的容量的大小。 在内核中,一块均匀的物理内存介质,使用一个pg_data_t,如下图所示。为了方便管理,一个pg_data_t又一般分为三个区域:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。

Linux内存管理_第2张图片

ZONE_DMA:在IA-32计算机中一般是16M,适合DMA使用。

ZONE_NORMAL:表示可直接线性映射到内核段的内存域。

ZONE_HIGHMEM:这段物理内存是和以后讲的虚拟内存是动态映射的。

       讲到这里,也只是物理内存逻辑上的一种划分,实际中它真正怎么高效管理起来,比如你现在需要2M物理内存,linux内核马上就能分配给你,等会你又要100M内存,一会你用完了要释放,其他进程又要使用等等。现实是linux对这种管理高效到我们几乎感觉不到,这就得意于“伙伴系统”了。先看图:


       linux内核会将每个zone中的可用物理内存,按照2的0次方、1次方等等划分(每个块的物理地址是连续的),并按上图所示组织起来,当某用户需要一块内存时,找到刚好能满足它的块。例如,你需要一个1M内存,那就是4KB*2^8,linux内核就到上图中左侧为8对应的后面链表中去查找,如果找到了,就分配给你,没有就到9对应的里面去找,找到后会将2^9分为2^8和2^1两部分,2^8分配给你,2^1部分放到1对应的链表中。如果1对应的链表中可以合并为2^2,那么就会合并并将这个块调整到2对应的链表中。
       “伙伴系统”管理的最小单位是“页帧”(4KB),但实际上我们应用程序中使用的内存分配函数(比如c语言中的malloc)等是按字节计算的,4KB对我们来说太大,会造成“内部碎片”导致内存浪费,此时linux又有另外一种秘密武器——slab分配器,它就是专门针对这种小内存需求进行物理内存分配的。

        到此,关于物理内存管理,本节只给大家一个较为系统的介绍。

2、虚拟内存管理

        虚拟地址空间总体上分为两大部分:内核空间(1G)和用户空间(3G)。我们的app能操作的虚拟地址就是用户空间部分。

Linux内存管理_第3张图片

       内核空间:内核空间大体上又可分为两部分,3G~3G+896M,这部分到物理地址的映射是写死的,毕竟总得有内核自己的代码也是要加载到物理内存中才能执行啊!
       用户空间:用户空间一般有3G,又可细分为:stack、mmap、heap、bss、data、text等部分。

stack:比较常见,就是我们平时说的栈,内存的释放不需要我们管理;

mmap:这里一般是将我们要读的文件映射进来,实现文件的直接读写;

heap:就是我们平常说的堆, c/c++中的new、malloc就是在这个范围内分配,需要我们程序管理释放。

bss:通常是指用来存放程序中未初始化的全局变量的一块内存区域。

data:通常是指用来存放程序中已初始化的全局变量的一块内存区域。

text:通常是指用来存放程序执行代码的一块内存区域。

        用户空间的这些划分都是逻辑上的,内核代码中使用memory region对象实际管理整个虚拟地址空间,结构为vm_area_struct。一个进程中可能有多个memory region,每个memory region代表了一段地址空间范围。所有memory region以链表的形式连接在一起(同时也以红黑树的结构存储,为了快速定位一个具体的地址)。
        memory region和上面将的stack、heap等是模糊对应的,例如bss、heap等有可能在同一个memory region上。


3、虚拟地址到物理地址的映射

       经过编译后的程序中的地址都是虚拟地址,cpu再执行这个命令之前必须把虚拟地址转换为物理地址,这个转换使用的就是页表,每个app都有自己的页表。linux为了高度抽象化,规定页表有四级:页全局目录PGD、页上级目录PUD、页中间目录PMD和页表PT。如下图所示:

       


三、进程地址空间

       在讨论进程空间细节前,这里先要说明下面几个问题:
       第一、4G的进程地址空间被人为的分为两个部分——用户空间与内核空间。用户空间从0到3G(0xC0000000),内核空间占据3G到4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。只有用户进程进行系统调用(代表用户进程在内核态执行)等时刻可以访问到内核空间。
       第二、用户空间对应进程,所以每当进程切换,用户空间就会跟着变化;而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表(init_mm.pgd),用户进程各自有不同的页表。
       第三、每个进程的用户空间都是完全独立、互不相干的。不信的话,你可以把下面的程序同时运行10次(当然为了同时运行,让它们在返回前一同睡眠100秒吧),你会看到10个进程占用的线性地址一模一样。

#include<stdio.h>
#include<malloc.h>
#include<unistd.h>
int bss_var;
int data_var0=1;
int main(int argc,char **argv)
{
  printf("below are addresses of types of process's mem\n");
  printf("Text location:\n");
  printf("\tAddress of main(Code Segment):%p\n",main);
  printf("____________________________\n");
  int stack_var0=2;
  printf("Stack Location:\n");
  printf("\tInitial end of stack:%p\n",&stack_var0);
  int stack_var1=3;
  printf("\tnew end of stack:%p\n",&stack_var1);
  printf("____________________________\n");
  printf("Data Location:\n");
  printf("\tAddress of data_var(Data Segment):%p\n",&data_var0);
  static int data_var1=4;
  printf("\tNew end of data_var(Data Segment):%p\n",&data_var1);
  printf("____________________________\n");
  printf("BSS Location:\n");
  printf("\tAddress of bss_var:%p\n",&bss_var);
  printf("____________________________\n");
  char *b = sbrk((ptrdiff_t)0);
  printf("Heap Location:\n");
  printf("\tInitial end of heap:%p\n",b);
  brk(b+4);
  b=sbrk((ptrdiff_t)0);
  printf("\tNew end of heap:%p\n",b);
return 0;
 }

它的结果如下

below are addresses of types of process's mem
Text location:
Address of main(Code Segment):0x8048388
____________________________
Stack Location:
Initial end of stack:0xbffffab4
new end of stack:0xbffffab0
____________________________
Data Location:
Address of data_var(Data Segment):0x8049758
New end of data_var(Data Segment):0x804975c
____________________________
BSS Location:
Address of bss_var:0x8049864
____________________________
Heap Location:
Initial end of heap:0x8049868
New end of heap:0x804986c

利用size命令也可以看到程序的各段大小,比如执行size example会得到

text data bss dec hex filename
1654 280 8 1942 796 example
但这些数据是程序编译的静态统计,而上面显示的是进程运行时的动态值,但两者是对应的。


        内核使用内存描述符结构体表示进程的地址空间,该结构体包含了和进程地址空间有关的全部信息。内存描述符由mm_struct结构体表示,定义在文件<linux/sched.h>中。



       进程地址空间由每个进程的线性地址区(vm_area_struct)组成。通过内核,进程可以给自己的地址空间动态的添加或减少线性区域。如下图是内存描述符mm_struct和线性区域描述符vm_area_struct的关系:

Linux内存管理_第4张图片

        为了更好地管理这部分虚存空间.Linux主要定义了如下三个数据结构 :
struct vm_area_struct ,
struct vm_operations_struct
struct vmm_struct 

        vm_area_struct是描述进程地址空间的基本管理单元,对于一个进程来说往往需要多个内存区域来描述它的虚拟空间,如何关联这些不同的内存区域呢?大家可能都会想到使用链表,的确vm_area_struct结构确实是以链表形式链接,不过为了方便查找,内核又以红黑树(以前的内核使用平衡树)的形式组织内存区域,以便降低搜索耗时。并存的两种组织形式,并非冗余:链表用于需要遍历全部节点的时候用,而红黑树适用于在地址空间中定位特定内存区域的时候。内核为了内存区域上的各种不同操作都能获得高性能,所以同时使用了这两种数据结构。 每一个进程的所有vma由一个双向链表管理。 为了提高对vma的查询、 插入、 删除等操作的效率 .Linux把系统中所有进程的 vma组成了一棵 AVL树。 这是一棵平衡二叉树 . 当 vma数量特别 大时。 利用这棵 AVL树查找 vma的效率得到明显提 高。

       不同的 vma可能需要不同的操作处理方式 ,但同时考虑到统接口的统 一 性,Linux采用vm_operations_struct结 构和面向对象的思想来定义操作方式 ,一个vm_operations_struct结构体是一组 函数指针,对于不同的 vma,它可能指向不同的处理函数,例如当发生缺页错误时,共 享内存和代码 段 的 readpage所指向的页面读入函数可能就不同 。
       内存管理中另外一个非常重要的数据结构是vmm_struct 结构体。进程的task_struct中的mm成员指向它。当前运行进程的整个虚拟空间都由它来管理和描述。它不仅包含该进程的映像信息。而且它的mmap成员项指向该进程所有vma组成的链表。它的 mmap_avl 成员项指向整个系统的AVL树 。
      这三个数据结构之间相互关联,共同管理虚拟内存,它们之间的关系如图所示:


       

       这部分相关的系统调用主要有如下两个 :

do_mmap(   
struct   file *file,  
unsigned   long   addr  
unsigned   long   len ,   
unsigned   long   prot ,  
unsigned   long   flags ,  
unsigned   long   off   
);    
find_vma (   
struct   mm_struct mm ,  
unsigned   long   addr  
);   

        do _mmap函数实现了 内存映射。 find_vma函数的功能 是找到包含参数 addr指定的虚拟地址所属的 vma 。 当要运行一个可执行映像时。调用 do _mmap将其装入到该进程的虚拟地址空间,并且产生一组 vma结构,如前所述该进程的整个虚拟空间由 vmm_struct 结构管理。但是此时可执行文件仅仅被连接到进程的虚拟空间中,只有一小部分页面被装入到物理 内存,其余大部分并没有被真正装入到物理内存。在进程的运行过程 中,产生缺页错误 ,操作系统首先调用 find_vma ,找到该虚拟地址所在的 vma , 然后根据该 vma的成员变量 vm_ops指向的vm_operations_struct结构中的缺页操作 函数。 把页装入物理内存。 ·



你可能感兴趣的:(linux内存管理,进程地址空间)