Linux内存管理

来源:互联网 发布:无尽的传说2 mac 编辑:程序博客网 时间:2024/06/17 16:47

1. 概念

参考《深入Linux内核结构》 1.1和1.2

内核:内核是硬件与软件之间的中间层。

内核的作用是将应用程序的请求传递给硬件,并充当底层驱动程序,对系统的各种设备和组件进行寻址。

进程:UNIX操作系统下运行的应用程序、服务器及其他程序都称为进程。

每个进程都在虚拟内存空间中分配地址空间。各个进程的地址空间完全独立,因此进程不会意识到彼此的存在。

线程:一个进程可能由若干个线程组成,这些线程共享同样的数据和资源,但是可能执行程序中不同的代码。

进程可以看作一个正在执行的程序,而线程则是与主程序并行运行的函数或例程。

2. 虚拟地址空间

参考《深入Linux内核结构》 1.3.3

在介绍进程的概念的时候提到了虚拟地址空间,这个部份进行详细介绍。

由于内存区域是通过指针寻址,因此CPU的字长决定了所能管理的地址空间的大小。对于32位系统,所能管理的虚拟地址空间范围为:0 - 2^32B = 4GB;对于现在的64位处理器,所能管理的虚拟地址空间为:0 - 2^64B

这里所说的虚拟地址空间与Windows下的虚拟内存无关,与设备中的实际物理内存也无关,因此被称为虚拟地址空间。

Linux将虚拟地址空间划分成两个部份,分布称为内核空间和用户空间,如下图中的左图所示:

这里写图片描述

PS: Anatomy of a Program in Memory

系统中每个用户进程都有自身的虚拟地址空间,从0到TASK_SIZE。用户之上的区域从(TASK_SIZE 到 2^32,针对32位系统)保留给内核专用,用户进程不能访问。TASK_SIZE是一个特定于计算机体系结构的常数,把地址空间按定比例分成两部分。例如:在32位系统中,地址空间在3GB处划分,因此每个进程的虚拟地址空间为3GB,所以内核空间为1GB可用。

这种划分与可用的内存数量无关。由于地址空间虚拟化的结果,每个用户进程都认为自身有3GB内存。各个系统进程的用户空间是完全彼此分离的,不论当前执行哪个进程,内核空间总是相同的。

PS: 64位计算机的情况可能不太一样,用于它们实际上能管理巨大的理论虚拟地址空间,远远大于现在实际的物理内存,因此64位计算机一般使用一个小于64的位数,比如42位或者47位,这仍能确保大于计算机的物理内存。这样为CPU节省一些工作量。

3. 虚拟和物理地址空间

大多数情况下,单个虚拟地址空间就比系统中可用的物理内存要大。因此,内核和CPU必须考虑如何将可用的物理内存映射到虚拟地址空间的区域。

可取的方法是用页表来为物理地址分配虚拟地址。虚拟地址关系到进程的用户空间和内核空间,而物理地址则用来寻址实际可用的内存。原理如下图所示:

这里写图片描述

图中左侧和右侧表示进程A和进程B的虚拟地址空间,它们都被内核划分成很多等长的部份,这些部份称为页。物理内存也被划分成同样大小的页,经常称为页帧。

图中的箭头标明来虚拟地址空间中的页如何分配到物理内存页。例如:

进程A :页1 -> 物理内存页帧4, 页5 -> 物理内存页帧5进程B :页1 -> 物理内存页帧5, 页5 -> 物理内存页帧2

由此可见: 1. 两个进程的物理内存页可能映射到同一物理内存页帧。 2. 不同进程的同一虚拟地址可能映射到不同的物理内存页帧。 3. 并非虚拟空间中的所有页都映射到物理内存页帧。

4. 页表

参考《深入Linux内核结构》 1.3.3

用来将虚拟地址空间映射到物理内存空间的数据结构称为页表。

如下图所示,CPU有一个专门的部份称为MMU(Memory Management Unit,内存管理单元),该单元完成虚拟地址空间映射到物理内存空间。也就是说,CPU在寻址的时候,按照虚拟地址空间寻址,然后通过MMU将虚拟地址转换为物理地址。

这里写图片描述

其中的TLB(Translation Lookaside Buffer,地址转换后备缓存区),用于缓存地址转换过程中出现最频繁的哪些地址。

5. 进程虚拟地址空间的布局

参考:《深入Linux内核结构》 4.2 Anatomy of a Program in Memory

内存中的程序剖析

地址虚拟空间由许多不同长度的段(segment)组成,用于不同的目的。

其组成结构如下所示:

  1. 内核空间(Kernel Space)
  2. 用户空间(User Mode Space)
    1. 栈(Stack)
    2. 内存映射区(Memory Mapping Segment)
    3. 堆(Heap)
    4. BSS段(BSS segment)
    5. 数据段(Data segment)
    6. 代码段(Text segment)

如下图所示:

这里写图片描述

其中,蓝色的区域表示的是已经映射到物理内存的虚拟地址,而白色的区域表示没有映射的。

当计算机一起正常时,所有的进程开始时的虚拟地址空间布局与上图很相似。这就导致容易被破解引入安全漏洞。(一个漏洞通常需要引用一个绝对的物理地址:一个栈上的地址,或者一个库函数的地址等)。因此,操作系统对地址空间引入了随机化的机制。Linux会对栈,内存映射段和堆的开始地址加上一个随机化的偏移。不行的是,32位的地址是相对紧张的,只给这些随机化留下了很少的空间,所以会影响该机制的效果。

  • 栈 遵循严格的后进先出(LIFO)原则。在大部分的编程语言中,栈用于保存局部变量和函数参数。调用一个新的函数或者方法时,会把函数参数压入进程栈中,函数退出时,函数参数也会相应的删除。进程中的每个线程都有各自的栈

    栈的大小一般是固定的,在Windows下是2M,在Linux是8M(???)。因此,如果变量过多或者过大,将超出栈的容量,出现栈溢出的错误。例如,定义局部变量数组时,如果数组过大,将超出栈的容量,此时需要考虑其他办法。

    int main(){    const int c = 6;    int l_i = 3;}

    此代码中的局部常量c和局部变量l_i都存在栈中。

  • 内存映射段 内存映射段位于栈的下面,堆的上面。内核用来把文件内容映射到内存。在Linux下,如果通过malloc()来申请一块大的内存,C库就会在内存映射段创建一个匿名的内存映射,而不是使用堆空间。这里的意味着大于MMAP_THRESHOLD字节,默认是128kb,可用通过mallopt()来进行调整。

  • 堆 堆位于内存映射段的下面。它提供来运行时的内存分配。用于存放进程运行中被动态分配的内存段,它的大小不固定,可动态扩张或伸缩。在c和c++中,当进程调用malloc或new来分配内存时,新分配的内存就被动态添加到堆上。使用完之后,要使用free或delete释放内存。 malloc和内核之间的经典接口是brk系统调用,负责扩展/收缩堆。堆是一个连续的内存区域,在扩展是是自下而上增长。堆在虚拟地址空间中的起始和当前结束地址为start_brk和brk。 《深入Linux内核结构》 4.9 堆的管理

    int main(){    int *p_i = new int;    int *pp_i = (int *) malloc(sizeof(int)); //}

    此代码中的局部指针p_ipp_i都指向堆区,但是局部指针p_ipp_i的地址都位于栈区。

  • BSS段 BSS段用于存储静态的(全局的)未初始化的变量,BSS段属于静态内存分配。

    int g_iu;static int s_iu;int main(){...}

    此代码中的未初始化的静态变量s_iu和全局变量g_iu都存在BSS段中。

  • 数据段 与BSS段类似,数据段页属于静态内存分配,只不过,数据段用于存储存储静态的(全局的)初始化的变量.

    int g_i = 3;static int s_i = 4;int main(){...}

    此代码中的初始化的静态变量s_i和全局变量g_i都存在数据段中。

  • 代码段 用于存放当前运行程序的二进制代码。这部份区域的大小在程序运行前就已经确定,并且在内存区域通常都属于只读。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。如果常数修改代码段的内容,就会导致错误。

    看下面的代码:

    static char *gonzo = "God's own prototype";

    内存分布如下图所示:

    这里写图片描述

    从图中可用看出,gonzo指针属于静态指针,位于数据段。而该指针指向的字符串却保存在代码段。

6. 测试代码

为了更好的验证上述内存布局,本文在 Ubuntu 13.04 64位上进行了测试,测试代码如下所示:

#include <stdio.h>#include <stdlib.h>int g_i = 3; // 全局变量int g_iu; // 全局变量static int s_i = 4; // 静态常量static int s_iu; // 静态常量const int c_i = 5; // 全局常量char *g_p = "abc"; // 全局char指针,指向常量字符串char *g_pu; // 全局char指针char *g_p2 = "abcd"; // 全局char指针,指向常量字符串void fun() // 函数{}static void s_fun() // 静态函数{}int main(){    const int c = 6; // 局部常量    char *str = "abc"; // 指向字符串常量的字符指针    int l_i = 3; // 局部变量    int *p_i = new int; // int指针指向new开辟的int空间    int *p_i2 = new int; // int指针指向new开辟的int空间    int *pp_i = (int *) malloc(sizeof(int) * 20000); // int指针指向malloc开辟的int空间    int *p_i3 = new int; // int指针指向new开辟的int空间    int arr[200000]; // 局部int型数组    printf("%08x : array end address\n", &arr[199999]);    printf("%08x : array start address\n", &arr[0]);    printf("%08x : local int pointer p_i3\n", &p_i3);    printf("%08x : local int pointer pp_i\n", &pp_i);    printf("%08x : local int pointer p_i2\n", &p_i2);    printf("%08x : local int pointer p_i\n", &p_i);    printf("%08x : local char pointer\n", &str);    printf("%08x : local int\n", &l_i);    printf("%08x : local const int\n", &c);    printf(".............................................................\n");    printf("%08x : local int pointer point address(new) p_i3\n", p_i3);    printf("%08x : local int pointer point address(malloc) pp_i\n", pp_i);    printf("%08x : local int pointer point address(new) p_i2\n", p_i2);    printf("%08x : local int pointer point address(new) p_i\n", p_i);    printf(".............................................................\n");    printf("%08x : static int i s_iu\n", &s_iu);    printf("%08x : global char pointer g_pu\n", &g_pu);    printf("%08x : global int i g_iu\n", &g_iu);    printf(".............................................................\n");    printf("%08x : global char pointer g_p2\n", &g_p2);    printf("%08x : global char pointer  g_p\n", &g_p);    printf("%08x : static int i s_i\n", &s_i);    printf("%08x : global int i g_i\n", &g_i);    printf(".............................................................\n");    printf("%08x : global const int\n", &c_i);    printf("%08x : global char pointer point content g_p2\n", g_p2);    printf("%08x : global char pointer point content g_p\n", g_p);    printf("%08x : local char pointer point content\n", str);    printf("%08x : static function addresss\n", s_fun);    printf("%08x : function address\n", fun);    printf(".............................................................\n");    printf("%08x : global char pointer point content g_pu\n", g_pu);    return 0;}

7. 测试结果

为了对比32位编译和64位编译的结果,本文分别使用-m32-m64参数进行编译。

g++ main.cpp -m32 -o maing++ main.cpp -m64 -o main

结果如下图所示:

这里写图片描述

8. 结果分析

从实验结果可用看出,内存的分配的确是按照《进程虚拟地址空间的布局》部份讲的那样:

  1. 局部变量,局部常量存在
  2. mallco和new分配的内存位于
  3. 未初始化的静态变量、全局变量位于Bss段
  4. 初始化的静态变量、全局变量位于数据段
  5. 全局字符常量和代码位于代码段

另外:

  1. 64位下,字符指针是8Byte;32位下是4Byte。
  2. 数组是按从前往后的顺序进栈的。
  3. 64位下,new和malloc分配的内存空间都是从xxxxxx0开始的;32位下都是从xxxxxx0或0000008开始的。

在《内存映射段》中讲到,当使用malloc开辟较大空间时,将不会在堆区开辟内存空间,而是在内存映射段开辟空间,所以此处进行的补充测试:

将测试代码中的:

int *pp_i = (int *) malloc(sizeof(int) * 20000);

修改为:

 int *pp_i = (int *) malloc(sizeof(int) * 40000);

此时的输入如下:

008d5050 : local int pointer point address(new) p_i3732c3010 : local int pointer point address(malloc) pp_i008d5030 : local int pointer point address(new) p_i2008d5010 : local int pointer point address(new) p_i

可用看出,malloc开辟的空间已经与new开辟的空间不在一个位置。

0 0
原创粉丝点击