Linux内存布局详解

本文详细介绍了32位Linux系统中进程的内存空间布局,特别是堆段、栈段和内存映射段的使用,涉及brk/sbrk、malloc/free、mmap等系统调用,以及栈段的自动管理和内存管理中的问题,如内存破坏、内存泄漏和内存碎片。
摘要由CSDN通过智能技术生成

在32位的Linux中,进程的虚拟地址空间大小为4GB,其中低地址空间的3GB属于用户空间,高地址空间的1GB属于内核空间。

在用户空间的3GB中,又划分出6个区域:代码段、数据段、BSS段、堆段、内存映射区、栈段。虚拟地址空间布局如下所示:
在这里插入图片描述
代码段:具有只读权限,用于存放可执行程序、字符串、只读变量等。如定义的const变量、printf函数的格式化字符串等。

数据段:具有读写权限,用于存放初始值非0的全局变量、静态变量。

BSS段:具有读写权限,用于存放初始值为0或未初始化的全局变量、静态变量,这块内存会由操作系统初始化为0。

堆段:运行时可动态分配的内存段,向上生长,由用户进行申请和释放等管理操作。

内存映射段:也称为文件映射区和匿名映射区,加载的动态库、打开的文件等均映射到该区域。

栈段:用于存放非静态的局部变量、函数调用过程的栈帧信息等,地址空间向下生长,由编译器自动分配和释放,栈大小在运行时由内核动态调整,栈动态增长后就不会再收缩

主要介绍下堆段、栈段和内存映射段。

内存映射段:
内存映射就是在进程的虚拟地址空间中创建一个映射,分为文件映射和匿名映射。
文件映射:文件支持的内存映射,把文件映射到进程的虚拟地址空间中,进程打开的那些文件都会映射到这块区域。
匿名映射:没有文件支持的内存映射,把物理内存映射到进程的虚拟地址空间,如进程间通信

堆段:
堆段范围由两个指针进行限制,start_brk指针表示堆段的起始地址,brk指针表示堆段的结束地址。堆段的扩张和收缩实际上都是通过移动brk指针来实现的,扩张堆段就将brk指针向高地址移动(申请内存),收缩堆段就将brk指针向低地址移动(释放内存)。系统调用brk()和sbrk()专门用于移动brk指针。

#include <unistd.h>
int brk(void *addr);
void *sbrk(intptr_t increment);

brk()函数用于让brk指针指向给定地址addr,sbrk()函数用于让堆内存增长指定大小的字节数并返回上一次的brk所指的位置,即本次申请内存的起始地址。

对于开发者来说,申请/释放堆内存通常是使用libc库提供的malloc/free函数族,libc库实现了对堆内存的管理。

#include <stdlib.h>
void *malloc(size_t size);
void free(void *ptr);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
void *reallocarray(void *ptr, size_t nmemb, size_t size);

malloc():从堆段分配出size字节的内存,不会初始化这块内存,返回这块内存的起始地址。不执行整数相乘后的溢出检查。
free():释放ptr指向的内存块,ptr的值必须是malloc()calloc()realloc()返回值。ptr为NULL,不会执行任何操作;不能对同一个地址释放多次,会产生未定义行为。
calloc():从堆段分配出nmemb*size字节的内存,即分配一块可容纳nmemb个元素、每个元素大小为size字节的数组的内存,将这块内存的初始化为0,并返回这块内存的起始地址。若nmemb或size为0,则返回NULL。会执行整数相乘后的溢出检查。
realloc():重新调整ptr所指向的已分配的内存大小为size字节,不会改变原来的内存的内容,也不会初始化新增的内存。若ptr为NULL,就等价于malloc(size)。若size为0,ptr不为NULL,等价于free(ptr)。ptr指针的值必须是NULL或malloc()calloc()realloc()的返回值。不执行整数相乘后的溢出检查。
reallocarray():调整ptr所指内存块的大小,使其足以容纳具有nmemb个元素、每个元素size字节的数组,等价于realloc(ptr, nmemb * size)。会进行整数相乘后的溢出检查,若相乘的结果发生溢出,则返回NULL,设置errno为ENOMEM,且不会改变原有内存。

malloc/free函数族里面使用了brksbrkmmap等系统调用,从而可以在堆段和内存映射区分配内存。
当用户申请的内存小于128KB时,malloc会通过brk()从堆上申请内存,申请方式就是移动堆段的brk指针,并返回这块内存的起始地址。当用户释放从堆上申请的内存时,malloc不会直接把内存返回给Linux内核,而是缓存在它自己的内存池中,待下次使用。
malloc什么时候才会真正地归还堆段内存呢?默认情况下,当堆段最高地址有连续的超过128KB的空闲内存时,会执行内存紧缩操作(trim)。这个128KB可通过malloc_trim()进行调节,比如要求堆段最高地址有连续的超过20KB的空闲内存时进行紧缩,调用malloc_trim(20)
那不是位于堆段最高地址的那些已释放内存什么时候归还呢?得等它上面的内存全部为空闲时才行。这也是内存碎片产生的原因。

当用户申请的内存大于等于128KB时,malloc会通过mmap从内存映射区申请内存,且释放时也可直接归还内存映射区的这块内存给内核,但是内核需要建立一系列复杂的数据结构来完成内存映射区的内存申请。
在这里插入图片描述

mmap();
malloc_get_state();
malloc_info();
malloc_trim();
malloc_usable_size();
mallocopt();
mcheck();
mtrace();

在使用堆段时,经常会出现两种问题:内存破坏和内存泄漏。内存破坏指释放或篡改正在使用的内存,如踩内存。内存泄漏指未释放不再使用的内存。还有内存碎片问题,大量空闲的小块内存无法归还给内核,申请新内存时这些小块内存又太小。

栈段:
栈段由编译器自动分配和释放,主要有三个用途:

  1. 存储函数内部的非静态局部变量
  2. 记录函数调用过程相关的维护性信息(栈帧),包括函数返回地址、不适合装入寄存器的函数参数和一些寄存器值。
  3. 作为临时存储区,暂存算术表达式部分的计算结果和alloca函数分配的栈内内存

栈的最大容量RLIMIT_STACK可使用命令ulimit -s [LIMIT]查看,LIMIT为0就返回栈的最大容量,否则就将栈的最大容量设为新值。当程序使用的栈超过该最大容量时,就会发生栈溢出,导致段错误。

栈支持静态分配和动态分配。静态分配由编译器完成,动态分配由alloca函数在栈上申请空间,用完后自动释放,但不会释放物理内存。

#include <alloca.h>
void *alloca(size_t size);

alloca():从调用者的栈帧上分配一块size字节的空间,当调用函数返回时会自动释放这块临时空间。不能用free()来释放alloca()申请的内存空间。

栈帧:记录函数调用过程相关的维护性信息,这块信息存储在栈段上。实际上栈段就是由一个个栈帧组成的。
ESP寄存器:始终指向栈顶(也是最后一个栈帧的顶部)
EBP寄存器:始终指向最后一个栈帧的底部
在这里插入图片描述

以一个实例来对栈进行说明:

void func(int m, int n)
{
	int a, b;
	a = m;
	b = n;
}

int main()
{
	...
	func(m, n);
	/* 下一条语句L */
	...
	return 0;
}

在main函数中调用func函数前,栈中只有一个main函数的栈帧,范围是esp到ebp这块区域:
在这里插入图片描述

当main函数调用func函数时,需要将返回地址(当前PC值的下一个值)、传给func函数的参数填入main函数的栈帧中,并跳转到func函数中执行。在调用func函数但未跳转前,main函数的栈帧结构变为:
在这里插入图片描述
跳转到func函数前,还需要将调用者函数main栈帧的ebp值存入main栈帧中,然后形成被调用者函数func的栈帧,并调整ebp和esp寄存器所指的位置。
在这里插入图片描述

当从被调用函数func中返回时,上述EBP和ESP变化:

  1. 将ESP移动到当前栈帧底部,即func函数的栈帧底部,释放整个当前栈帧
  2. 将调用函数main的栈帧底部的值弹出到EBP中,再将返回地址弹出到CS:IP。至此,EBP和ESP都恢复到main函数调用func函数前的状态,即恢复了main函数的栈帧。

Linux内核对栈的分类:

  • 进程栈
  • 线程栈
  • 内核栈
  • 中断栈
  • 信号栈

进程栈:实际上就是上面的虚拟地址空间中的栈段。
线程栈:
Linux内核实际上没有线程的概念,它把所有线程都当作进程来实现,都使用task_struct来表示,仅仅将线程视为一个与其他进程共享某些资源的进程,是否共享地址空间几乎是进程和线程的唯一区别。
线程栈位于进程虚拟地址空间的内存映射段,线程栈大小在创建线程时决定(由线程属性pthread_attr_t或pthread_attr_setstacksize来设置)。
线程栈默认大小和进程栈大小的关系:

  1. 进程栈大小为无限制,则线程栈默认大小为2MB
  2. 进程栈大小小于16KB,则线程栈默认大小为16KB
  3. 进程栈大小大于16KB,则线程栈默认大小由ulimit决定

参考资料:
1 Linux系统–栈帧详解
2 linux系统进程的内存布局
3 Linux虚拟地址空间布局以及进程栈和线程栈总结
4 Linux中的进程栈和线程栈
5 内存泄漏之malloc_trim
6 GLIBC内存管理机制
7 malloc是如何分配内存的?
8 共享内存(内存映射的使用、注意事项、进程间通信、systemV共享内存)

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值