目录
- x86-32 地址空间
- 用户地址空间
- 保留区
- 代码段 .text
- 初始化数据段 .data
- 未初始化数据段 .bss
- 堆 heap
- 内存映射段(mmap)
- 栈 Stack
- 内核地址空间
- 直接映射区 896M
- 高端内存(HIGH_MEMORY)
- VMALLOC_OFFSET
- VMALLOC
- 物理内存
x86-32 地址空间
Linux内核一般将处理器的虚拟地址空间划分为两个部分,底部比较大的部分用于用户进程,顶部则专用于内核,虽然(在两个用户进程之间的)上下文切换期间会改变下半部分,但虚拟地址空间的内核部分总是保持不变
也就是在进程切换的过程中,页表的用户部分会被更新,反映出新的内存布局,内核部分的页表项保持不变,因此无需刷新TLB(Translation Lookaside Buffer)中与内核相关的条目
Linux将虚拟地址空间划分为:0-3G为用户空间,3-4G为内核空间
用户地址空间
保留区
位于虚拟地址空间的最低部分,未赋予物理地址,任何对它的引用都是非法的,用于捕捉使用空指针和小整数型指针引用内存的异常情况
它并不是一个单一的内存区域,而是对地址空间中受到操作系统保护而禁止用户进程访问的地址区域的总称
大多数操作系统中,极小的地址通常都是不允许访问的,
如NULL,C语言将无效指针赋值为0,也是因为0地址上正常情况下不会存放有效的可访问数据
因为历史原因前面128.28125MB属于保留空间
代码段 .text
代码段也称正文段或文本段,通常用于存放程序执行代码(即CPU执行的机器指令)
一般C语言执行语句都编译成机器代码保存在代码段,通常代码段是可共享的,因此频繁执行的程序只需要在内存中拥有一份拷贝即可
代码段通常属于只读,以防止其他程序意外地修改其指令(对该段的写操作将导致段错误), 某些架构也允许代码段为可写即允许修改程序,
代码段指令根据程序设计流程依次执行,对于顺序指令,只会执行一次(每个进程);若有反复,则需使用跳转指令;若进行递归,则需要借助栈来实现
代码段指令中包括操作码*和操作对象**(或对象地址引用),若操作对象是立即数(具体数值),将直接包含在代码中;若是局部数据,将在栈区分配空间,然后引用该数据地址;
若位于BSS段和DATA数据段,同样引用该数据地址,代码段最容易受优化措施影响
初始化数据段 .data
数据段通常用于存放程序中已初始化且初值不为0的全局变量和静态局部变量,数据段属于静态内存分配(静态存储区),可读可写
数据段保存在目标文件中(在嵌入式系统里一般固化在镜像文件中),其内容由程序初始化DATA段与BSS段的区别如下:
BSS段不占用物理文件尺寸,但占用内存空间;数据段占用物理文件,也占用内存空间。对于大型数组如int ar0[10000] = {1, 2, 3, ...}和int ar1[10000],ar1放在BSS段,只记录共有10000*4个字节需要初始化为0,而不是像ar0那样记录每个数据1、2、3…
运行时数据段和BSS段的整个区段通常称为数据区,某些资料中数据段指代数据段 + BSS段 + 堆
未初始化数据段 .bss
BSS(Block Started by Symbol)段中通常存放程序中以下符号:
- 未初始化的全局变量和静态局部变量
- 初始值为
0的全局变量和静态局部变量(依赖于编译器实现) - 未定义且初值不为
0的符号(该初值即common block的大小)
C语言中,未显式初始化的静态分配变量被初始化为0(算术类型)或空指针(指针类型),
由于程序加载时,BSS会被操作系统清零,所以未赋初值或初值为0的全局变量都在BSS中,BSS段仅为未初始化的静态分配变量预留位置,在目标文件中并不占据空间,这样可减少目标文件体积
但程序运行时需为变量分配内存空间,故目标文件必须记录所有未初始化的静态分配变量大小总和(通过start_bss和end_bss地址写入机器代码)
当加载器(loader)加载程序时,将为BSS段分配的内存初始化为0
在嵌入式软件中,进入main()函数之前BSS段被C运行时系统映射到初始化为全零的内存(效率较高)
注意:bss段通过start_bss和end_bss地址和代码段相关联
注意,尽管均放置于BSS段,但初值为0的全局变量是强符号,而未初始化的全局变量是弱符号,若其他地方已定义同名的强符号(初值可能非0),
则弱符号与之链接时不会引起重定义错误,但运行时的初值可能并非期望值(会被强符号覆盖)
因此,定义全局变量时,若只有本文件使用,则尽量使用static关键字修饰;否则需要为全局变量定义赋初值(哪怕0值),保证该变量为强符号,
以便链接时发现变量名冲突,而不是被未知值覆盖
某些编译器将未初始化的全局变量保存在common段,链接时再将其放入BSS段,在编译阶段可通过-fno-common选项来禁止将未初始化的全局变量放入common段
此外,由于目标文件不含BSS段,故程序烧入存储器(Flash)后BSS段地址空间内容未知,
U-Boot启动过程中,将U-Boot的Stage2代码(通常位于lib_xxxx/board.c文件)搬迁(拷贝)到SDRAM空间后必须人为添加清零BSS段的代码,
而不可依赖于Stage2代码中变量定义时赋 0 值
堆 heap
堆用于存放进程运行时动态分配的内存段,可动态扩张或缩减,堆中内容是匿名的,不能按名字直接访问,只能通过指针间接访问
当进程调用malloc/new等函数分配内存时,新分配的内存动态添加到堆上(扩张)
当调用free/delete等函数释放内存时,被释放的内存从堆中剔除(缩减)
分配的堆内存是经过字节对齐的空间,以适合原子操作,堆管理器通过链表管理每个申请的内存
由于堆申请和释放是无序的,最终会产生内存碎片,堆内存一般由应用程序分配释放,回收的内存可供重新使用
若程序员不释放,程序结束时操作系统可能会自动回收
堆的末端由break指针标识,当堆管理器需要更多内存时,可通过系统调用br k和 sbrk来移动break指针以扩张堆,一般由系统自动调用
使用堆时经常出现两种问题:
- 释放或改写仍在使用的内存(内存破坏)
- 未释放不再使用的内存(内存泄漏),当释放次数少于申请次数时,可能已造成内存泄漏
内存映射段(mmap)
此处,内核可以将文件的内容直接映射到内存,任何应用程序都可通过Linux的mmap()系统调用或Windows的CreateFileMapping/MapViewOfFile请求这种映射
内存映射是一种方便高效的文件I/O方式, 因而被用于装载动态共享库,用户也可创建匿名内存映射,该映射没有对应的文件,可用于存放程序数据
在Linux中,若通过malloc请求一大块内存,C运行库将创建一个匿名内存映射,而不使用堆内存
大块意味着比阈值MMAP_THRESHOLD还大,缺省为128KB,可通过mallopt()调整
栈 Stack
栈又称堆栈,由编译器自动分配释放,行为类似数据结构中的栈(先进后出),堆栈主要有三个用途
为函数内部声明的非静态局部变量(C语言中称自动变量)提供存储空间
记录函数调用过程相关的维护性信息,称为栈帧(Stack Frame)或过程活动记录(
Procedure Activation Record), 它包括函数返回地址,不适合装入寄存器的函数参数及一些寄存器值的保存
除递归调用外,堆栈并非必需,因为编译时可获知局部变量,参数和返回地址所需空间,并将其分配于BSS段(也就是函数返回地址,不适合装入寄存器的函数参数,寄存器的保存值)
临时存储区,用于暂存长算术表达式部分计算结果或alloca()函数分配的栈内内存
- 持续地重用栈空间有助于使活跃的栈内存保持在
CPU缓存中,从而加速访问,进程中的每个线程都有属于自己的栈,向栈中不断压入数据时,若超出其容量就会耗尽栈对应的内存区域,从而触发一个页错误
此时若栈的大小低于堆栈最大值RLIMIT_STACK(通常是8M),则栈会动态增长,程序继续运行,映射的栈区扩展到所需大小后,不再收缩
Linux中ulimit -s命令可查看和设置堆栈最大值,当程序使用的堆栈超过该值时, 发生栈溢出(Stack Overflow),程序收到一个段错误(Segmentation Fault)
内核地址空间
直接映射区 896M
所谓的直接映射区,就是这一块空间是连续的,和物理内存是非常简单的映射关系,其实就是虚拟内存地址减去3G,就得到物理内存的位置__pa(vaddr)返回与虚拟地址vaddr相关的物理地址;__va(paddr)则计算出对应于物理地址paddr的虚拟地址
// PAGE_OFFSET => 3G 0x0c0000000#define__va(x)((void*)((unsignedlong)(x)+PAGE_OFFSET))#define__pa(x)__phys_addr((unsignedlong)(x))#define__phys_addr(x)__phys_addr_nodebug(x)#define__phys_addr_nodebug(x)((x)-PAGE_OFFSET)这896M还需要仔细分解,在系统启动的时候,物理内存的前1M已经被占用了,从1M开始加载内核代码段,然后就是内核的全局变量、BSS等,也是ELF里面涵盖的,
这样内核的代码段,全局变量,BSS也就会被映射到3G后的虚拟地址空间里面。具体的物理内存布局可以查看
cat/proc/iomem.....00100000-bffdbfff:System RAM01000000-01a02fff:Kernel code01a03000-021241bf:Kernel data02573000-02611fff:Kernel bss25000000-34ffffff:Crash kernel.....在内核运行的过程中,如果碰到系统调用创建进程,内核的进程管理代码会将实例创建在3G至3G+896M的虚拟空间中,当然也会被放在物理内存里面的前896M里面,相应的页表也会被创建
如果涉及内核栈的分配,内核的进程管理的代码会将内核栈创建在3G至3G+896M的虚拟空间中,
当然也就会被放在物理内存里面的前896M里面,相应的页表也会被创建
高端内存(HIGH_MEMORY)
x86-32下特有的(x64下没有这个东西),因为内核虚拟空间只有1G无法管理全部的内存空间
当内核想访问高于896MB物理地址内存时,从0xF8000000 ~ 0xFFFFFFFF地址空间范围内找一段相应大小空闲的逻辑地址空间,借用一会借用这段逻辑地址空间,建立映射到想访问的那段物理内存(即填充内核PTE页面表),临时用一会,用完后归还
这样别人也可以借用这段地址空间访问其他物理内存,实现了使用有限的地址空间,访问所有所有物理内存
VMALLOC_OFFSET
系统会在low memory和VMALLOC区域留8M,防止访问越界。因此假如理论上vmalloc size有300M,实际可用的也是只有292M
include/asm-x86/pgtable_32.h#defineVMALLOC_OFFSET(8*1024*1024)这个缺口可用作针对任何内核故障的保护措施,如果访问越界地址(即无意地访问物理上不存在的内存区),则访问失败并生成一个异常,报告该错误
如果vmalloc区域紧接着直接映射,那么访问将成功而不会注意到错误,在稳定运行的情况下,肯定不需要这个额外的保护措施,但它对开发尚未成熟的新内核特性是有用的
VMALLOC
虚拟内存中连续、但物理内存中不连续的内存区,可以在vmalloc区域分配,该机制通常用于用户过程,
内核自身会试图尽力避免非连续的物理地址,通常会成功,因为大部分大的内存块都在启动时分配给内核,那时内存的碎片尚不严重,
但在已经运行了很长时间的系统上,在内核需要物理内存时,就可能出现可用空间不连续的情况,此类情况,主要出现在动态加载模块时
include/asm-x86/pgtable_32.h#defineVMALLOC_START(((unsignedlong)high_memory+\2*VMALLOC_OFFSET-1)&~(VMALLOC_OFFSET-1))#ifdefCONFIG_HIGHMEM#defineVMALLOC_END(PKMAP_BASE-2*PAGE_SIZE)#else#defineVMALLOC_END(FIXADDR_START-2*PAGE_SIZE)#endifvmalloc区域的起始地址,取决于在直接映射物理内存时,使用了多少虚拟地址空间内存(因此也依赖于上文的high_memory变量)
内核还考虑到下述事实,即两个区域之间有至少为VMALLOC_OFFSET的一个缺口,而且vmalloc区域从可被VMALLOC_OFFSET整除的地址开始
VMALLOC_START到VMALLOC_END之间称为内核动态映射空间,也即内核想像用户态进程一样malloc申请内存,在内核里面可以使用vmalloc
假设物理内存里面,896M到1.5G之间已经被用户态进程占用了,并且映射关系放在了进程的页表中,内核vmalloc的时候,只能从分配物理内存1.5G开始,就需要使用这一段的虚拟地址进行映射,映射关系放在专门给内核自己用的页表里面
物理内存
物理内存的前4KiB是第一个页帧,一般会忽略,通常保留给BIOS使用,接下来的640KiB原则是可以用的,但是也不能用于内核加载,
原因是 该区域之后紧邻的区域由系统保留,用于映射各种ROM(通常是系统BIOS和显卡ROM)
因为不能向映射的ROM区域写入数据,但是内核一定会装载在一个连续的内存区中,如果要从4KiB处作为起始位置装载内核镜像,则要求内核必须要小于640KiB
在x86架构中内存有三种区域:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM, 不同类型的区域适合不同需要,
在32位系统中结构中,1G(内核空间)/3G(用户空间) 地址空间划分时,三种类型的区域如下:ZONE_DMA内存开始的16MBZONE_NORMAL16MB ~ 896MBZONE_HIGHMEM896MB~ 结束
ZONE_DMA
该区域的物理页面专门供I/O设备的DMA使用,之所以需要单独管理DMA的物理页面,是因为DMA使用物理地址访问内存,不经过MMU,
并且需要连续的缓冲区,所以为了能够提供物理上连续的缓冲区,必须从物理地址空间专门划分一段区域用于DMA
DMA技术就是我们在主板上放一块独立的芯片。在进行内存和I/O设备的数据传输的时候,我们不再通过CPU来控制数据传输,而直接通过DMA控制器(DMA Controller,简称DMAC)
ZONE_NORMALZONE_NORMAL的范围是16M~896M,该区域的物理页面是内核能够直接使用的,属于直接映射区ZONE_HIGHMEM(896M~结束)
是系统中剩下的可用内存,但因为内核的地址空间有限,这部分内存不直接映射到内核