linux进程内存布局

进程内存布局

1
在 x86_64 架构的 32 位操作系统中,linux 的进程内存布局如图所示。这个是进程的虚拟地址空间,这些虚拟地址通过页表映射到物理内存。页表由操作系统维护,由处理器引用。每一个进程都有一个自己的页表。内核也是一个特殊的进程,因为虚拟地址被使能会应用于所有软件,所以内核需要在每个进程的地址空间中都保留一部分虚拟地址专门给内核使用。

内核空间

从 0xC0000000 到 0xFFFFFFFF 这 1G 的空间是内核空间,而 0x00000000 到 0xBFFFFFFF 是用户空间。用户空间无法直接访问内核的虚拟内存空间,仅能通过系统调用来进入内核态,从而来访问内核空间的内存地址。只要用户态的程序试图访问这些页,就会导致一个页错误(page fault)。在 linux 中,内核空间持续存在,并且所有进程中都映射到同样的物理内存。内核的代码和数据总是可以被寻址的,因为随时为系统调用和中断做准备。另外,用户进程也是无法访问 0x00000000 ~ 0x08048000 这一段虚拟内存地址的,在这段地址上有诸多例如 C 库,动态加载器如 ld.so 等的映射地址。 如果用户进程访问到该区间会返回段错误。

用户空间

在用户空间的最顶部的部分被叫做栈空间,它一般用于存放函数参数或局部变量。例如:调用一个函数会将函数参数压入到栈空间中,在函数返回时,参数会被栈弹出清理。进程中的每一个线程都有属于自己的栈

mmap

在栈的低一段便是 mmap,mmap 是一种高效便捷的文件 I/O 方式。内核将文件内容映射在此段内存中,例如加载动态链接库。另外,在 linux 中,如果你通过 malloc 申请一块大于 MMAP_THRESHOLD(默认大小是 128KB)大小的堆空间时,glibc 会返回一块匿名的 mmap 内存块而非一块堆内存。

堆同栈一样,都是为进程运行提供动态的内存分配,但其和栈的的一个很大区别在于堆上内存的生命周期和执行分配的函数的生命周期不同,堆上分配的内存只有在对应进程通过系统调用主动释放进程结束后才会释放。堆的内存分配效率比栈要低得多。因为栈是由操作系统提供管理的,会在底层堆栈提供支持,分配专门的寄存器($esp)存放栈的地址,包括压栈出栈也都有专门的指令执行,所以执行效率很高。而堆则是由 C 函数库提供支持,它的机制相对复杂,例如分配一块内存,库函数会按照一定的算法在堆内存空间中搜索可用的足够大的内存空间,如果没有足够大的连续空间,则需要操作系统来重新整理堆内存,这样才有机会分到足够大小的空间,然后才返回。对于堆来说,频繁的 malloc/free(new/delete)势必会造成内存空间的不连续,从而造成大量的内存碎片,程序的运行效率降低。而对于栈来说,分配的一定是连续的内存空间。

BSS段

堆段再往下便是 BSS 段这个静态内存区域,它是用来存储静态局部静态全局变量的,其在编译期间便决定了虚拟内存的消耗,BSS 段存放的是未初始化的变量。另外根据 C 语言标准规定,未初始化的静态成员变量的初始值必须为 0,所以内核在加载二进制文件后执行程序前会将 BSS 段清 0。

DATA段

DATA 段也是个静态内存区域,也是用来存储静态局部或静态全局变量。但是放的是已经初始化的变量。

代码段

DATA 段再往下便是代码段,这段中存有程序的指令代码。TEXT 段是通过只读的方式加载到内存中的,它可以在多个进程中被安全共享。