微信公众号:二进制人生
专注于嵌入式linux开发。问题或建议,请发邮件至hjhvictory@163.com。
更新日期:2020/1/25,内容整理自网络,转载请注明出处。
内存管理系列文章:
内容整理自网络和自己的认知,旨在学习交流,请勿用于商业用途。
linux内存管理(一)开篇介绍
linux内存管理(二)两种内存架构和三种内存模型
linux内存管理(三)内存管理三级结构
linux内存管理(四)分页机制概述
linux内存管理(五)内存源头
linux内存管理(六)分页机制的演进
linux内存管理(七)arm页表机制
linux内存管理(七七)arm硬件页表
linux内存管理(八)页表最初的初始化--从内核启动的第一段代码谈起
linux内存管理(九)内存管理临时机制-memblock
linux内存管理(十)内核空间虚拟内存布局
linux内存管理(十一)arm低端内存映射
linux内存管理(十二)arm高端内存映射之kmap映射(永久映射)
linux内存管理(十三)arm高端内存映射之fix映射(临时映射)
linux内存管理(十四)arm高端内存之vmalloc(非连续内存区映射)
linux内存管理(十五)伙伴系统概述
linux内存管理(十六)伙伴系统实现
linux内存管理(十七)slab分配器实现
linux内存管理(十八)进程内存分配底层机制--vma引入
linux内存管理(十九)brk和sbrk介绍(番外篇)
linux内存管理(二十)malloc背后的机制--简单阐述(番外篇)
linux内存管理(二十一)malloc背后的机制--深入探索
目录
前言brk和sbrk的定义随机的堆起始地址malloc和brk的关系
前言
这两个系统调用很多人可能都没见过,因为几乎用不到,glibc里的malloc函数会调用。为方便后面理解malloc,需要了解下这两个系统调用。
brk和sbrk的定义
#include <unistd.h>
int brk(void *addr);
void *sbrk(intptr_t increment);
下面是截自man手册的描述:
DESCRIPTION
brk() and sbrk() change the location of the program break, which defines the end of the process's
data segment (i.e., the program break is the first location after the end of the uninitialized data
segment). Increasing the program break has the effect of allocating memory to the process; decreas‐
ing the break deallocates memory.
brk() sets the end of the data segment to the value specified by addr, when that value is reasonable,
the system has enough memory, and the process does not exceed its maximum data size (see setr‐
limit(2)).
sbrk() increments the program's data space by increment bytes. Calling sbrk() with an increment of 0
can be used to find the current location of the program break.
RETURN VALUE
On success, brk() returns zero. On error, -1 is returned, and errno is set to ENOMEM.
翻译一下:
手册上说brk和sbrk会改变program break的位置,program break被定义为程序data segment的结束位置。感觉这句话不是很好理解,从下面程序地址空间的分布来看,data segment后面还有bss segment,显然和手册说的不太一样。一种可能的解释就是手册中的data segment和下图中的data segment不是一个意思,手册中的data segment应该包含了下图中的data segment、bss segment和heap,所以program break指的就是下图中heap的结束地址。增加 program break 意味着给进程分配内存;减少 program break 意味着释放内存。
brk()设置heap的结束地址为addr,当addr的值合理,且系统有足够内存,进程没有超出它的最大数据段大小时(参看setrlimit(2))。成功则返回0,否则返回-1。
sbrk()增加程序的heap increment字节,返回增加前的heap的program break,调用sbrk(0)可以用于获取进程program break的当前位置。
通俗讲就是可以改变下图program break的位置,而brk()是以绝对地址的形式,sbrk()是以相对当前地址偏移的形式。
上面这个图是进程的虚拟地址空间划分,我们非常熟悉。
linux提供了以下接口用于获取或设定资源使用限制。每种资源都有相关的软硬限制,软限制是内核强加给相应资源的限制值,硬限制是软限制的最大值。非授权调用进程只可以将其软限制指定为0~硬限制范围中的某个值,同时能不可逆转地降低其硬限制。授权进程可以任意改变其软硬限制。RLIM_INFINITY的值表示不对资源限制。
include <sys/time.h>
#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);
其中有一项是限制进程的data空间大小,这个data包含了data段、bss段和heap段,所以在调整brk时,会检测是否超出设定的资源限制。
随机的堆起始地址
从上面的图可以看出heap的起始地址并不是bss segment的结束地址,而是随机分配的,下面我们用一个程序来验证下:
#include <stdio.h>
#include <unistd.h>
int bss_end;
int main(void)
{
void *pret;
printf("bss end: %p\n", (char *)(&bss_end) + 4);
pret = sbrk(0);
if (pret != (void *)-1)
printf ("heap start: %p\n", pret);
return 0;
}
运行结果:
root@AI-Machine:/home/hongjh/2019_project# ./a.out
bss end: 0x804a028
heap start: 0x9f7f000
root@AI-Machine:/home/hongjh/2019_project# ./a.out
bss end: 0x804a028
heap start: 0x90ca000
root@AI-Machine:/home/hongjh/2019_project# ./a.out
bss end: 0x804a028
heap start: 0x92c1000
从上面运行结果可以知道bss和heap是不相邻的,并且同一个程序bss的结束地址是固定的,而heap的起始地址在每次运行的时候都会改变。你可能会说sbkr(0)返回的是heap的结束地址,怎么上面确把它当做起始地址呢?由于程序开始运行时heap的大小是为0,所以起始地址和结束地址是一样的,不信我们可以用下面的程序验证下。
malloc和brk的关系
上面的函数我们其实很少使用,大部分我们使用的是malloc和free函数来分配和释放内存。这样能够提高程序的性能,不是每次分配内存都调用brk或sbrk,而是重用前面空闲的内存空间。brk和sbrk分配的堆空间类似于缓冲池,每次malloc从缓冲池获得内存,如果缓冲池不够了,再调用brk或sbrk扩充缓冲池,直到达到缓冲池大小的上限,free则将应用程序使用的内存空间归还给缓冲池
。
前面是网上的说法,我发现即使malloc一段很小的空间,brk也会一次性调整到brk和mmap调用的分界值,也就是根本不存在缓冲池扩充的机会。
下面的程序的关键点打印会在后面标出序号ABCDEFGH。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void)
{
void *tret;
char *pmem1,*pmem2;
tret = sbrk(0);
if (tret != (void *)-1)
printf ("heap start: %p\n", tret);//------A
pmem1 = (char *)malloc(128); //分配内存1
if (pmem1 == NULL) {
perror("malloc");
exit (EXIT_FAILURE);
}
printf ("pmem1:%p\n", pmem1);//------B
pmem2 = (char *)malloc(0x20bc0); //分配内存2
if (pmem2 == NULL) {
perror("malloc");
exit (EXIT_FAILURE);
}
printf ("pmem2:%p\n", pmem2);//------C
tret = sbrk(0);
if (tret != (void *)-1)
printf ("heap size on each load: %p\n", (char *)tret - pmem1);//---D
printf ("heap end before free1: %p\n", tret);//---F
free(pmem1);
tret = sbrk(0);
printf ("heap end after free1: %p\n", tret);//---G
free(pmem2);
tret = sbrk(0);
printf ("heap end after free2: %p\n", tret);//---H
return 0;
}
运行结果如下:
root@AI-Machine:/home/hongjh/2019_project# ./a.out
heap start: 0x804b000
pmem1:0x804b410
pmem2:0xb7d5d008
heap size on each load: 0x20bf0
heap end before free1: 0x806c000
heap end after free1: 0x806c000
heap end after free2: 0x806c000
root@AI-Machine:/home/hongjh/2019_project#
从结果可以看出调用malloc(128)后缓冲池大小从0变成了0x20bf0,将上面的malloc(64)改成malloc(1)结果也是一样,只要malloc分配的内存数量小于0x20bf0,缓冲池都是默认扩充0x20bf0大小。值得注意的是如果malloc一次分配的内存超过了0x20bf0(含等于),malloc不再从堆中分配空间,而是使用mmap()这个系统调用从映射区寻找可用的内存空间。
第二次的内存分配了0x20bc0,加上第一分配128,显然超过了缓存池大小,所以会在mmap区分配,可以看到pmem1的地址和pmem2的地址相差很大。
另外我们可以发现free前后堆的结束地址并没有发生变化,印证了前面缓存的说法。
为啥是0x20bf0这个奇怪的值,暂时不清楚。我们前面的文章说是128K,看来不准确了。我查了下资料,网上说如今的glibc使用了动态的阈值,初始值为1281024,下限为0,上限由DEFAULT_MMAP_THRESHOLD_MAX决定,32位系统为5121024,64位系统为410241024*sizeof(long)。依据什么机制动态调整,暂时不清楚。
而且pmem1地址并不等于分配之前堆的起始地址,中间存在了 0x410 的空缺。
种种的疑问我们现在还无法解释,源码面前没有秘密,我们下回分解。
每天进步一点点…