zoukankan      html  css  js  c++  java
  • Linux中的内存管理模型浅析[转]

     
    在weibo上看到梁大的这个贴子: 


    实际上这是一个内存方面的问题。要想研究这个问题,首先我们要将题目本身搞明白。由于我对Linux内核比较熟而对Windows的内存模型几乎毫不了解,因此在这篇文章中针对Linux环境对这个问题进行探讨。

    在Linux的世界中,从大的方面来讲,有两块内存,一块叫做内存空间,Kernel Space,另一块叫做用户空间,即User Space。它们是相互独立的,Kernel对它们的管理方式也完全不同。

    首先我们要知道,现代操作系统一个重要的任务之一就是管理内存。所谓内存,就是内存条上一个一个的真正的存储单元,实实在在的电子颗粒,这里面通过电信号保存着数据。

    Linux Kernel为了使用和管理这些内存,必须要给它们分成一个一个的小块,然后给这些小块标号。这一个一个的小块就叫做Page,标号就是内存地址,Address。

    Linux内核会负责管理这些内存,保证程序可以有效地使用这些内存。它必须要能够管理好内核本身要用的内存,同时也要管理好在Linux操作系统上面跑的各种程序使用的内存。因此,Linux将内存划分为Kernel Space和User Space,对它们分别进行管理。

    只有驱动模块和内核本身运行在Kernel Space当中,因此对于这道题目,我们主要进行考虑的是User Space这一块。

    在Linux的世界中,Kernel负责给用户层的程序提供虚地址而不是物理地址。举个例子:A手里有20张牌,将它们命名为1-20。这20张牌要分给两个人,每个人手里10张。这样,第一个人拿到10张牌,将牌编号为1-10,对应A手里面的1-10;第二个人拿到10张牌,也给编号为1-10,对应A的11-20。

    这里面,第二个人手里的牌,他自己用的时候编号是1-10,但A知道,第二个人手里的牌在他这里的编号是11-20。

    在这里面,A的角色就是Linux内核;他手里的编号,1-20,就是物理地址;两个人相当于两个进程,它们对牌的编号就是虚地址;A要负责给两个人发牌,这就是内存管理。

    了解了这些概念以后,我们来看看kernel当中具体的东西,首先是mm_struct这个结构体:

    C代码 复制代码 收藏代码
    1. struct mm_struct {   
    2.         struct vm_area_struct * mmap;           /* list of VMAs */  
    3.         struct rb_root mm_rb;   
    4.         struct vm_area_struct * mmap_cache;     /* last find_vma result */  
    5.         ...   
    6.         unsigned long start_code, end_code, start_data, end_data;   
    7.         unsigned long start_brk, brk, start_stack;   
    8.         ...   
    9. };  
    mm_struct负责描述进程的内存。相当于发牌人记录给谁发了哪些牌,发了多少张,等等。那么,内存是如何将内存进行划分的呢?也就是说,发牌人手里假设是一大张未裁剪的扑克纸,他是怎样将其剪成一张一张的扑克牌呢?上面的vm_area_struct就是基本的划分单位,即一张一张的扑克牌:

    C代码 复制代码 收藏代码
    1. struct vm_area_struct * mmap;  
    struct vm_area_struct * mmap;
    


    这个结构体的定义如下:

    C代码 复制代码 收藏代码
    1. struct vm_area_struct {   
    2.         struct mm_struct * vm_mm;       /* The address space we belong to. */  
    3.         unsigned long vm_start;         /* Our start address within vm_mm. */  
    4.         unsigned long vm_end;           /* The first byte after our end address  
    5.                                            within vm_mm. */  
    6.         ....   
    7.         /* linked list of VM areas per task, sorted by address */  
    8.         struct vm_area_struct *vm_next;   
    9.         ....   
    10. }  
      这样,内核就可以记录分配给用户空间的内存了。

    Okay,了解了内核管理进程内存的两个最重要的结构体,我们来看看用户空间的内存模型。

    Linux操作系统在加载程序时,将程序所使用的内存分为5段:text(程序段)、data(数据段)、bss(bss数据段)、heap(堆)、stack(栈)。


    text segment(程序段)

    text segment用于存放程序指令本身,Linux在执行程序时,要把这个程序的代码加载进内存,放入text segment。程序段内存位于整个程序所占内存的最上方,并且长度固定(因为代码需要多少内存给放进去,操作系统是清楚的)。

    data segment(数据段)

    data segment用于存放已经在代码中赋值的全局变量和静态变量。因为这类变量的数据类型(需要的内存大小)和其数值都已在代码中确定,因此,data segment紧挨着text segment,并且长度固定(这块需要多少内存也已经事先知道了)。


    bss segment(bss数据段)

    bss segment用于存放未赋值的全局变量和静态变量。这块挨着data segment,长度固定。

    heap(堆)

    这块内存用于存放程序所需的动态内存空间,比如使用malloc函数请求内存空间,就是从heap里面取。这块内存挨着bss,长度不确定。

    stack(栈)

    stack用于存放局部变量,当程序调用某个函数(包括main函数)时,这个函数内部的一些变量的数值入栈,函数调用完成返回后,局部变量的数值就没有用了,因此出栈,把内存让出来给另一个函数的变量使用(程序在执行时,总是会在某一个函数调用里面)。

    我们看一个图例说明:



    为了更好的理解内存分段,可以撰写一段代码:

    C代码 复制代码 收藏代码
    1. #include <stdio.h>   
    2.   
    3. // 未赋值的全局变量放在dss段   
    4. int global_var;   
    5.   
    6. // 已赋值的全局变量放在data段   
    7. int global_initialized_var = 5;   
    8.   
    9. void function() {     
    10.    int stack_var; // 函数中的变量放在stack中   
    11.   
    12.    // 放在stack中的变量   
    13.    // 显示其所在内存地值   
    14.    printf("the function's stack_var is at address 0x%08x\n", &stack_var);   
    15. }   
    16.   
    17.   
    18. int main() {   
    19.    int stack_var; // 函数中的变量放在stack中   
    20.       
    21.    // 已赋值的静态变量放在data段   
    22.    static int static_initialized_var = 5;   
    23.       
    24.    // 未赋值的静态变量放在dss段   
    25.    static int static_var;   
    26.       
    27.    int *heap_var_ptr;   
    28.   
    29.    // 由malloc在heap中分配所需内存,   
    30.    // heap_var_ptr这个指针指向这块   
    31.    // 分配的内存   
    32.    heap_var_ptr = (int *) malloc(4);   
    33.   
    34.    // 放在data段的变量   
    35.    // 显示其所在内存地值   
    36.    printf("====IN DATA SEGMENT====\n");   
    37.    printf("global_initialized_var is at address 0x%08x\n", &global_initialized_var);   
    38.    printf("static_initialized_var is at address 0x%08x\n\n", &static_initialized_var);   
    39.   
    40.    // 放在bss段的变量   
    41.    // 显示其所在内存地值   
    42.    printf("====IN BSS SEGMENT====\n");   
    43.    printf("static_var is at address 0x%08x\n", &static_var);   
    44.    printf("global_var is at address 0x%08x\n\n", &global_var);   
    45.   
    46.    // 放在heap中的变量   
    47.    // 显示其所在内存地值   
    48.    printf("====IN HEAP====\n");   
    49.    printf("heap_var is at address 0x%08x\n\n", heap_var_ptr);   
    50.   
    51.    // 放在stack中的变量   
    52.    // 显示其所在内存地值   
    53.    printf("====IN STACK====\n");   
    54.    printf("the main's stack_var is at address 0x%08x\n", &stack_var);   
    55.    function();    
    56. }  
    编译这个代码,看看执行结果: 




    理解了进程的内存空间使用,我们现在可以想想,这几块内存当中,最灵活的是哪一块?没错,是Heap。其它几块都由C编译器编译代码时预处理,相对固定,而heap内存可以由malloc和free进行动态的分配和销毁。

    有关malloc和free的使用方法,在本文中我就不再多说,这些属于基本知识。我们在这篇文章中要关心的是,malloc是如何工作的?实际上,它会去调用mmap(),而mmap()则会调用内核,获取VMA,即前文中看到的vm_area。这一块工作由c库向kernel发起请求,而由kernel完成这个请求,在kernel当中,有vm_operations_struct进行实际的内存操作:

    C代码 复制代码 收藏代码
    1. struct vm_operations_struct {   
    2.         void (*open)(struct vm_area_struct * area);   
    3.         void (*close)(struct vm_area_struct * area);   
    4.         ...   
    5. };  


    可以看到,kernel可以对VMA进行open和close,即收发牌的工作。理解了malloc的工作原理,free也不难了,它向下调用munmap()。

    下面是mmap和munmap的函数定义:

    C代码 复制代码 收藏代码
    1. void *   
    2. mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);  


    这里面,addr是希望能够分配到的虚地址,比如:我希望得到一张牌,做为我手里编号为2的那张。需要注意的是,mmap最后分配出来的内存地址不一定是你想要的,可能你请求一张编号为2的扑克,但发牌人控制这个编号过程,他会给你一张在你手里编号为3的扑克。

    prot代表对进程对这块内存的权限:

    C代码 复制代码 收藏代码
    1. PROT_READ 是否可读   
    2. PROT_WRITE 是否可写   
    3. PROT_EXEC IP指针是否可以指向这里进行代码的执行   
    4. PROT_NONE 不能访问  


    flags代表用于控制很多的内存属性,我们一会儿会用到,这里不展开。

    fd是文件描述符。我们这里必须明白一个基本原理,任何硬盘上面的数据,都要读取到内存当中,才能被程序使用,因此,mmap的目的就是将文件数据映射进内存。因此,要在这里填写文件描述符。如果你在这里写-1,则不映射任何文件数据,只是在内存里面要上这一块空间,这就是malloc对mmap的使用方法。

    offset是文件的偏移量,比如:从第二行开始映射。文件映射,不是这篇文章关心的内容,不展开。

    okay,了解了mmap的用法,下面看看munmap:

    C代码 复制代码 收藏代码
    1. int  
    2. munmap(void *addr, size_t len);  


    munmap很简单,告诉它要还回去的内存地址(即哪张牌),然后告诉它还回去的数量(多少张),其实更准确的说:尺寸。


    现在让我们回到题目上来,如何部分地回收一个数组中的内存?我们知道,使用malloc和free是无法完成的:

    C代码 复制代码 收藏代码
    1. #include <stdlib.h>   
    2. int main() {   
    3.         int *p = malloc(12);   
    4.         free(p);   
    5.         return 0;   
    6. }  


    因为无论是malloc还是free,都需要我们整体提交待分配和销毁的全部内存。于是自然而然想到,是否可以malloc分配内存后,然后使用munmap来部分地释放呢?下面是一个尝试:

    C代码 复制代码 收藏代码
    1. #include <sys/mman.h>   
    2. #include <stdio.h>   
    3. #include <stdlib.h>   
    4.   
    5. int main() {   
    6.     int *arr;   
    7.     int *p;   
    8.     p = arr = (int*) malloc(3 * sizeof(int));   
    9.     int i = 0;   
    10.        
    11.     for (i=0;i<3;i++) {   
    12.         *p = i;   
    13.         printf("address of arr[%d]: %p\n", i, p);   
    14.         p++;   
    15.     }   
    16.        
    17.     printf("munmap: %d\n", munmap(arr, 3 * sizeof(int)));   
    18. }  
    运行这段代码输出如下: 



    注意到munmap调用返回-1,说明内存释放未成功,这是由于munmap只能用于由mmap分配的内存。因此,我们可以用mmap来代替malloc,来分配一块内存空间:

    C代码 复制代码 收藏代码
    1. mmap(NULL, 3 * sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0)  
    注意上面mmap的使用方法。其中,我们不指定虚地址,让内核决定内存地址,也就是说,我们要是要一张牌,但不关心给牌编什么号。然后PROT_READ|PROT_WRITE表示这块内存可读写,接下来注意flags里面有MAP_ANONYMOUS,表示这块内存不用于映射文件。下面是完整代码:

    C代码 复制代码 收藏代码
    1. #include <sys/mman.h>   
    2. #include <stdio.h>   
    3. #include <stdlib.h>   
    4.   
    5. int main() {   
    6.     int *arr;   
    7.     int *p;   
    8.     p = arr = (int*) mmap(NULL, 3 * sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);   
    9.     int i = 0;   
    10.        
    11.     for (i=0;i<3;i++) {   
    12.         *p = i;   
    13.         printf("address of arr[%d]: %p\n", i, p);   
    14.         p++;   
    15.     }   
    16.        
    17.     printf("munmap: %d\n", munmap(arr, 3 * sizeof(int)));   
    18. }  
    注意上面的代码唯一的改动之处是使用mmap替代了malloc。运行结果如下: 
     


    注意munmap返回值为0,说明内存释放成功了。

    okay,了解了mmap和munmap的使用方法,我们接下来看看能不能用mmap来分配内存,用munmap来部分地释放内存。下面是实验代码:


    (未完待续)

    参考资料:

    http://linuxgazette.net/112/krishnakumar.html

    http://stackoverflow.com/questions/2440385/how-to-find-the-physical-address-of-a-variable-from-user-space-in-linux

    http://stackoverflow.com/questions/6252063/simplest-way-to-get-physical-address-from-the-logical-one-in-linux-kernel-module

    http://www.mjmwired.net/kernel/Documentation/vm/pagemap.txt

    http://man7.org/tlpi/code/online/dist/mmap/anon_mmap.c.html

    http://en.wikipedia.org/wiki/Mmap 

    http://publib.boulder.ibm.com/infocenter/iseries/v5r3/index.jsp?topic=%2Fapis%2Fmmap.htm
  • 相关阅读:
    CPU
    CentOS7 Tomcat 环境部署
    CentOS7 NTP 安装配置
    CISCO VLAN ACL
    ESXI6.0启用 snmp
    编译安装 varnish-4.1.2和yum 安装 varnish-4.0.3
    CentOS7 安装中文帮助手册
    CentOS7 学习笔记
    CentOS7 编译安装 nginx-1.10.0
    CentOS7 cacti 安装
  • 原文地址:https://www.cnblogs.com/viviancc/p/2713617.html
Copyright © 2011-2022 走看看