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
  • 相关阅读:
    我爱java系列之---【微服务间的认证—Feign拦截器】
    我爱java系列之---【设置权限的三种解决方案】
    581. Shortest Unsorted Continuous Subarray
    129. Sum Root to Leaf Numbers
    513. Find Bottom Left Tree Value
    515. Find Largest Value in Each Tree Row
    155. Min Stack max stack Maxpop O(1) 操作
    painting house
    Minimum Adjustment Cost
    k Sum
  • 原文地址:https://www.cnblogs.com/viviancc/p/2713617.html
Copyright © 2011-2022 走看看