Linux虚拟地址空间

Linux虚拟地址空间

注:本文来自多篇博客整理,具体博客链接在博客下方

在多任务操作系统中,每个进程都运行在属于自己的内存沙盘中。这个沙盘就是虚拟地址空间(Virtual Address Space),在32位模式下它是一个4GB的内存地址块,这篇博客均是X86架构的

1. 地址空间分布

在这里插入图片描述

2. 内核地址空间

其中高1G为内核空间,所有进程共享内核地址空间,内核空间分三部分:DMA映射区,一致映射区、高端内存区一致映射区的虚拟地址均一一对应了物理页框,因此此区间虚拟地址的访问可以直接通过偏移量得到物理内存而不需进行页表的转换,其中的高128M空间为高端内存,当物理内存大于4G时内核用128M的地址空间作为高端内存,扮演着临时映射的作用。我的能力现在有限主要解释用户地址空间,以后阅读内核代码会对内核空间进行解释。
Temporary Kernel Mapping 为固定映射空间
在这个空间中,有一部分用于高端内存的临时映射。当要进行一次临时映射的时候,需要指定映射的目的,根据映射目的,可以找到对应的小空间,然后把这个空间的地址作为映射地址。
Persistent Kernel Mapping 永久映射空间
从PKMAP_BASE 到 FIXADDR_START用于映射高端内存
Vmalloc Area loremap Area 动态映射空间

3. 用户地址空间

Envioment Variables 为环境变量
Command line arguments 为命令行参数
Random stack offset和Random mmap offset等随机值意在防止恶意程序。Linux通过对栈、内存映射段、堆的起始地址加上随机偏移量来打乱布局,以免恶意程序通过计算访问栈、库函数等

(1). 栈

栈可以被称为堆栈,由编译器进行管理(自动释放和分配)并且是先入后出,这里说的堆栈是进程栈(栈分为进程栈,内核栈,线程栈, 中断栈)
进程栈的初始化大小是由编译器和链接器计算出来的,但是栈的实时大小并不是固定的,Linux 内核会根据入栈情况对栈区进行动态增长(其实也就是添加新的页表)。但是并不是说栈区可以无限增长,它也有最大限制 RLIMIT_STACK (一般为 8M)。
栈的特点和用途:
1. 进程调用函数产生的栈帧建立在栈区(首先压入主调函数中下条指令(函数调用语句的下条可执行语句)的地址,然后是函数实参,然后是被调函数的局部变量。本次调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的指令地址,程序由该点继续运行下条可执行语句)
2. 临时存储区,用于暂存长算术表达式部分计算结果或alloca()函数分配的栈内内存,或者c++的临时对象。
3. 栈由计算机底层提供支持:分配专门的寄存器存放栈地址,压栈出栈由专门的指令执行, 其相比堆效率较高

进程栈的动态增长实现
进程在运行的过程中,通过不断向栈区压入数据,当超出栈区容量时,就会耗尽栈所对应的内存区域,这将触发一个 缺页异常 (page fault)。通过异常陷入内核态后,异常会被内核的 expand_stack() 函数处理,进而调用 acct_stack_growth() 来检查是否还有合适的地方用于栈的增长。
如果栈的大小低于 RLIMIT_STACK(通常为8MB),那么一般情况下栈会被加长,程序继续执行,感觉不到发生了什么事情,这是一种将栈扩展到所需大小的常规机制。然而,如果达到了最大栈空间的大小,就会发生 栈溢出(stack overflow),进程将会收到内核发出的 段错误(segmentation fault) 信号。
动态栈增长是唯一一种访问未映射内存区域而被允许的情形,其他任何对未映射内存区域的访问都会触发页错误,从而导致段错误。一些被映射的区域是只读的,因此企图写这些区域也会导致段错误。

(2). 内存映射段(mmap)

内存映射是一种方便高效的文件I/O方式(内核直接将硬盘文件映射到虚拟内存中), 因而被用于装载动态共享库, 同时可以用于映射可执行文件用到的动态链接库。
在内存映射段, 进程可以不使用read() ,write()等函数对文件操作,像访问普通内存对文件进行访问,可通过Linux的mmap()系统调用函数将一个普通文件映射到此段,然后进行操作,另外mmap并不分配空间, 只是将文件映射到调用进程的地址空间里(但是会占掉虚拟内存地址)可以通过munmap()来取消内存映射。
注意:在 Linux中,若通过malloc()请求一大块内存,C运行库将创建一个匿名内存映射,而不使用堆内存。”大块” 意味着比阈值 MMAP_THRESHOLD还大,缺省为128KB,可通过mallopt()调整
另外线程栈是也是在内存映射段
因为线程和进程共享同一地址空间,如果线程和进行共用同一栈会造成调用栈混乱,因此在线程创建的时候使用mmap 系统调用为线程在内存映射段映射一块固定大小空间作为线程栈,因此线程栈不能动态增长。

(3). 堆

堆用于存放进程运行时动态分配的空间,可动态扩张或者缩减,堆中内容是匿名的,不能按名字直接访问,只能通过指针间接访问。当进程调用malloc©/new(C++)等函数分配内存时,新分配的内存动态添加到堆上(扩张);当调用free©/delete(C++)等函数释放内存时,被释放的内存从堆中剔除(缩减)

堆的特点:
1> 堆管理器通过链表管理堆,由于申请和释放堆内存是无序的因此会产生内存碎片,堆的释放由程序员完成,回收的内存可以重新使用, 如果不释放只有在程序结束时才会统一释放。
2> 堆的末端由break指针标识,当堆管理器需要更多内存时,可通过系统调用brk()和sbrk()来移动break指针以扩张堆(向上生长并且32位Linux系统中堆内存理论上可达2.9G空间),一般由系统自动调用。
3> 操作系统为堆维护一个记录空闲内存地址的链表。当系统收到程序的内存分配申请时,会遍历该链表寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点空间分配给程序。若无足够大小的空间(可能由于内存碎片太多),有可能调用系统功能去增加程序数据段的内存空间,以便有机会分到足够大小的内存,然后进行返回。,大多数系统会在该内存空间首地址处记录本次分配的内存大小,供后续的释放函数(如free/delete)正确释放本内存空间。此外,由于找到的堆结点大小不一定正好等于申请的大小,系统会自动将多余的部分重新放入空闲链表中。

使用堆时经常出现两种问题
1> 释放或改写仍在使用的内存(“内存破坏”);
2> 未释放不再使用的内存(“内存泄漏”)。当释放次数少于申请次数时,可能已造成内存泄漏。泄漏的内存往往比忘记释放的数据结构更大,因为所分配的内存通常会圆整为下个大于申请数量的2的幂次(如申请212B,会圆整为256B)。

扫描二维码关注公众号,回复: 4128692 查看本文章

(4). BSS段

BSS段用于存放程序的以下符号:

  • 程序中的未初始化的全局变量和静态局部变量
  • 初始值为0的全局变量和静态局部变量(依赖于编译器实现)
  • 未定义且初值不为0的符号

由于程序加载时,BSS会被操作系统清零,所以未赋初值或初值为0的全局变量都在BSS中。BSS段仅为未初始化的静态分配变量预留位置,在目标文件中并不占据空间,这样可减少目标文件体积。但程序运行时需为变量分配内存空间,故目标文件必须记录所有未初始化的静态分配变量大小总和(通过start_bss和end_bss地址写入机器代码)。当加载器(loader)加载程序时,将为BSS段分配的内存初始化为0

尽管均放置于BSS段,但初值为0的全局变量是强符号,而未初始化的全局变量是弱符号。若其他地方已定义同名的强符号(初值可能非0),则弱符号与之链接时不会引起重定义错误,但运行时的初值可能并非期望值(会被强符号覆盖)。因此,定义全局变量时,若只有本文件使用,则尽量使用static关键字修饰;否则需要为全局变量定义赋初值(哪怕0值),保证该变量为强符号,以便链接时发现变量名冲突,而不是被未知值覆盖。
gcc将未初始化的全局变量保存在common段,链接时再将其放入BSS段。在编译阶段可通过-fno-common选项来禁止将未初始化的全局变量放入common段。

(5). 数据段

数据段通常用于存放程序中已初始化且初值不为0的全局变量和静态局部变量。数据段属于静态内存分配(静态存储区),可读可写。数据段保存在目标文件中,其内容由程序初始化。例如,对于全局变量int gVar = 10,必须在目标文件数据段中保存10这个数据,然后在程序加载时复制到相应的内存。

数据段与BSS段的区别如下:
1> BSS段不占用物理文件尺寸,但占用内存空间;数据段占用物理文件,也占用内存空间。
对于大型数组如int ar0[1000] = {1, 2, 3, …}和int ar1[1000],ar1放在BSS段,只记录共有1000*4个字节需要初始化为0,而不是像ar0那样记录每个数据1、2、3…,此时BSS为目标文件所节省的磁盘空间相当可观。
2) 当程序读取数据段的数据时,系统会出发缺页故障,从而分配相应的物理内存;当程序读取BSS段的数据时,内核会将其转到一个全零页面,不会发生缺页故障,也不会为其分配相应的物理内存。

(6). 代码段

代码段也称正文段或文本段,通常用于存放程序执行代码(即CPU执行的机器指令)。代码段通常属于只读,以防止其他程序意外地修改其指令(对该段的写操作将导致段错误)。
代码段还存放一些只读数据如字符串常量。

代码段指令中包括操作码和操作对象(或对象地址引用)。若操作对象是立即数(具体数值),将直接包含在代码中;若是局部数据,将在栈区分配空间,然后引用该数据地址;若位于BSS段和数据段,同样引用该数据地址

(7) 保留区

位于虚拟地址空间的最低部分,未赋予物理地址。任何对它的引用都是非法的,用于捕捉使用空指针和小整型值指针引用内存的异常情况。
它并不是一个单一的内存区域,而是对地址空间中受到操作系统保护而禁止用户进程访问的地址区域的总称。大多数操作系统中,极小的地址通常都是不允许访问的,如NULL。C语言将无效指针赋值为0也是出于这种考虑,因为0地址上正常情况下不会存放有效的可访问数据。

在32位X86架构的Linux系统中,用户进程可执行程序一般从虚拟地址空间0x08048000开始加载。该加载地址由ELF文件头决定,可通过自定义链接器脚本覆盖链接器默认配置,进而修改加载地址。0x08048000以下的地址空间通常由C动态链接库、动态加载器ld.so和内核VDSO(内核提供的虚拟共享库)等占用。通过使用mmap系统调用,可访问0x08048000以下的地址空间。

4. 内存描述符对虚拟地址空间的定义

struct mm_struct {
    struct vm_area_struct *mmap;           /* 内存区域链表 */
    struct rb_root mm_rb;                  /* VMA 形成的红黑树 */
    ...
    struct list_head mmlist;               /* 所有 mm_struct 形成的链表 */
    ...
    unsigned long total_vm;                /* 全部页面数目 */
    unsigned long locked_vm;               /* 上锁的页面数据 */
    unsigned long pinned_vm;               /* Refcount permanently increased */
    unsigned long shared_vm;               /* 共享页面数目 Shared pages (files) */
    unsigned long exec_vm;                 /* 可执行页面数目 VM_EXEC & ~VM_WRITE */
    unsigned long stack_vm;                /* 栈区页面数目 VM_GROWSUP/DOWN */
    unsigned long def_flags;
    unsigned long start_code, end_code, start_data, end_data;    /* 代码段、数据段 起始地址和结束地址 */
    unsigned long start_brk, brk, start_stack;                   /* 栈区 的起始地址,堆区 起始地址和结束地址 */
    unsigned long arg_start, arg_end, env_start, env_end;        /* 命令行参数 和 环境变量的 起始地址和结束地址 */
    ...
    /* Architecture-specific MM context */
    mm_context_t context;                  /* 体系结构特殊数据 */
 
    /* Must use atomic bitops to access the bits */
    unsigned long flags;                   /* 状态标志位 */
    ...
    /* Coredumping and NUMA and HugePage 相关结构体 */
};

在这里插入图片描述

来源博客
Linux虚拟地址空间布局
Linux用户空间与内核地址空间
linux内存映射mmap原理分析

猜你喜欢

转载自blog.csdn.net/Jocker_D/article/details/83659465