sbrk brk break linux-malloc底层实现原理

很明显是32位系统,寻址空间是4G,linux系统下0-3G是用户模式,3-4G是内核模式。而在用户模式下又分为代码段、数据段、.bss段、堆、栈。各个segment所含内容在图中有具体说明。



其中bss段:存放未初始化的全局变量和局部静态变量。数据段:存放已经初始化的全局变量和局部静态变量。至于局部变量存放在栈中。



可以看到heap段位于bss下方,而其中有个重要的标志:program break。Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。我们用malloc进行内存分配就是从break往上进行的。


进程所面对的虚拟内存地址空间,只有按页映射到物理内存地址,才能真正使用。受物理存储容量限制,整个堆虚拟内存空间不可能全部映射到实际的物理内存。Linux对堆的管理示意如下



获取了break地址,也就是内存申请的初始地址,下面是malloc的整体实现方案:



malloc 函数的实质是它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。 调用 malloc()函数时,它沿着连接表寻找一个大到足以满足用户请求所需要的内存块。 然后,将该内存块一分为二(一块的大小与用户申请的大小相等,另一块的大小就是剩下来的字节)。 接下来,将分配给用户的那块内存存储区域传给用户,并将剩下的那块(如果有的话)返回到连接表上。 调用 free 函数时,它将用户释放的内存块连接到空闲链表上。 到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段, 那么空闲链表上可能没有可以满足用户要求的片段了。于是,malloc()函数请求延时,并开始在空闲链表上检查各内存片段,对它们进行内存整理,将相邻的小空闲块合并成较大的内存块。



1、malloc分配内存前的初始化:



malloc_init 是初始化内存分配程序的函数。 它完成以下三个目的:将分配程序标识为已经初始化[3],找到操作系统中最后一个有效的内存地址,然后建立起指向需要管理的内存的指针。这里需要用到三个全局变量。
 malloc_init 分配程序的全局变量



int has_initialized = 0; /* 初始化标记 */



void managed_memory_start; / 管理内存起始地址 */



void last_valid_address; / 操作系统的最后一个有效地址*/



被映射的内存边界(操作系统最后一个有效地址)常被称为系统中断点或者当前中断点。为了指出当前系统中断点,必须使用 sbrk(0) 函数。 sbrk 函数根据参数中给出的字节数移动当前系统中断点,然后返回新的系统中断点。 使用参数 0 只是返回当前中断点。 这里给出 malloc()初始化代码,它将找到当前中断点并初始化所需的变量:



Linux通过brk和sbrk系统调用操作break指针。两个系统调用的原型如下:



int brk(void *addr);
void *sbrk(intptr_t increment);
brk将break指针直接设置为某个地址,而sbrk将break从当前位置移动increment所指定的增量。brk在执行成功时返回0,否则返回-1并设置errno为ENOMEM;sbrk成功时返回break移动之前所指向的地址,否则返回(void *)-1。如果将increment设置为0,则可以获得当前break的地址。



2、下为malloc_init()代码:可以看到使用sbrk(0)来获得break地址。



#include /*sbrk 函数所在的头文件 */
void malloc_init()
{
last_valid_address = sbrk(0); /* 用 sbrk 函数在操作系统中
取得最后一个有效地址 */
managed_memory_start = last_valid_address; /* 将 最 后 一 个
有效地址作为管理内存的起始地址 */
has_initialized = 1; /* 初始化成功标记 */
}
3、内存块的获取
所要申请的内存是由多个内存块构成的链表。



a、内存块的大致结构:每个块由meta区和数据区组成,meta区记录数据块的元信息(数据区大小、空闲标志位、指针等等),数据区是真实分配的内存区域,并且数据区的第一个字节地址即为malloc返回的地址。



typedef struct s_block t_block;
struct s_block {
size_t size; /
数据区大小 /
t_block next; /
指向下个块的指针 /
int free; /
是否是空闲块 /
int padding; /
填充4字节,保证meta块长度为8的倍数 /
char data[1] /
这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta */
};
现在,为了完全地管理内存,我们需要能够追踪要分配和回收哪些内存。在对内存块进行了 free 调用之后,我们需要做的是诸如将它们标记为未被使用的等事情,并且,在调用 malloc 时,我们要能够定位未被使用的内存块。因此, malloc 返回的每块内存的起始处首先要有这个结构:
struct mem_control_block
{
int is_available;//是否空闲
int size; //内存块大小
};



b、寻找合适的block



现在考虑如何在block链中查找合适的block。一般来说有两种查找算法:



First fit:从头开始,使用第一个数据区大小大于要求size的块所谓此次分配的块
Best fit:从头开始,遍历所有块,使用数据区大小大于size且差值最小的块作为此次分配的块
  两种方法各有千秋,best fit具有较高的内存使用率(payload较高),而first fit具有更好的运行效率。



find_block从frist_block开始,查找第一个符合要求的block并返回block起始地址,如果找不到这返回NULL。这里在遍历时会更新一个叫last的指针,这个指针始终指向当前遍历的block。这是为了如果找不到合适的block而开辟新block使用的。
c、如果现有block都不能满足size的要求,则需要在链表最后开辟一个新的block。下为利用sbrk()创建新的block示意代码:



#define BLOCK_SIZE 24 /* 由于存在虚拟的data字段,sizeof不能正确计算meta长度,这里手工设置 */



t_block extend_heap(t_block last, size_t s) {
t_block b;
b = sbrk(0);
if(sbrk(BLOCK_SIZE + s) == (void *)-1)
return NULL;
b->size = s;
b->next = NULL;
if(last)
last->next = b;
b->free = 0;
return b;
}



4、内存分配,下为内存分配代码
void *malloc(long numbytes)
{
void *current_location;
struct mem_control_block *current_location_mcb;
void *memory_location;
if(! has_initialized)
{
malloc_init();
}
numbytes = numbytes + sizeof(struct mem_control_block);
memory_location = 0;
current_location = managed_memory_start;
while(current_location ! = last_valid_address)
{
current_location_mcb =(struct mem_control_block *)current_location;
if(current_location_mcb->is_available)
{
if(current_location_mcb->size >= numbytes)
{
current_location_mcb->is_available = 0;
memory_location = current_location;
break;
}
}
current_location = current_location +current_location_mcb->size;
}
if(! memory_location)
{
sbrk(numbytes);
memory_location = last_valid_address;
last_valid_address = last_valid_address + numbytes;
current_location_mcb = memory_location;
current_location_mcb->is_available = 0;
current_location_mcb->size = numbytes;
}
memory_location = memory_location + sizeof (struct mem_control_block);
return memory_location;
}



内存数据段和堆内存数据段和堆



堆的末端有一个称为Break的指针来标识。当对管理器需要更多的内存时,他可以通过系统调用brk,sbrk来移动Break指针,一般情况下不必显示的调用brk,如果分配的内存容量很大,brk最终会自动调用。用于管理内存的调用是:



malloc和free——从堆中获得内存以及把内存返回给堆。



brk和sbrk——调整数据段的大小至一个绝对值(通过某个增量);



waring:你的程序可能无法同时调用malloc()和brk();如果你使用的malloc,malloc希望当你调用brk和sbrk时;它具有一定的控制权。由于sbrk向进程提供了唯一的方法将数据段内存返回给系统内核,所以使用malloc,就有效的防止了程序的数据段缩小的可能性。想获得以后能返回给系统的内核的内存,可以使用mmap系统调用来映射/dev/zero文件。需要返回给这中内存时,可以使



用mmap系统调用。



每次使用malloc分配内存时,注意在以后要调用相应的free来释放它。如果不知道如何调用free()与先前的malloc()相对应,那么很可能已经造成了内存的泄露。一种简单的方法就是在可能的时候用alloc()分配动态内存,以避免上述情况的发生。当离开调用alloc()的函数时,他所分配的内存会被自动释放,有些人不提倡使用alloc(),因为他并不是一种可移植的方法。如果处理器在硬件上是不支持堆栈,alloc()就很难高效的实现。



如何检测内存泄露的方法:



观察内存泄露是两步骤的过程:



1.使用swap命令观察好有多少可用的交换空间:/user/sbin/swap -s



total;17228k bytes allocated+5396k reserved = 22624k used, 29548 k avaliable



(共计:17228k 已分配 +5396k 用于保留 = 2262k 已用,19548 可用)



在一两分钟内键入该命令三到四次,看看可用的交换区是否在减少,还可以使用其他的/user/bin/*stat 工具如:netstat、vmstat等。如果发现不断有内存没被释放,可能的解释就是有进程出现了内存泄露。



2.确定可疑的进程,看看它是不是该为内存泄露负责。你可能知道哪个进程是罪魁祸首,不然可以使用“ps -lu username” 命令来显示所有进程的大小。如下所示:



F



S



UID



PID



PPID



C



PRI



Ni



ADDR



ZS



WCHAN



TTY



TIME



COMD



8



S



5303



226



224



80



1



20



Ff38f000



199



Ff38fld0



Pts/3



0:01



csh



8



O



5303



921



226



29



1



20



Ff38c000



143



Pts/3



0:00



ps



标题为zs的列就是以页面数表示的进程的大小同样的方法执行多次命令,会发现任何动态分配内存的进程的大小都在增长,如果某一个只增不减,那么是什么样的结果就不言而喻了;你懂的。对与malloc函数库中在速度上做了 一些优化,有些重视空间的存分利用,另外一些则是希望对调试有所帮助,键入命令: man -s 3c malloc 。



虚拟内存地址与物理内存地址
Linux 操作系统在处理内存地址时,普遍采用虚拟内存地址技术。
机器语言层面都是采用虚拟地址,当实际的机器码程序涉及到内存操作时,需要根据当前进程运行的实际上下文将虚拟地址转换为物理内存地址,才能实现对真实内存数据的操作。
虚拟内存和物理内存的转换是由页表这个数据结构来实现的。
有时MMU在工作时,会发现页表表明某个内存页不在物理内存中,此时会触发一个缺页异常(Page Fault),此时系统会到磁盘中相应的地方将磁盘页载入到内存中,然后重新执行由于缺页而失败的机器指令。



Heap
概念
理论上,64bit内存地址可用空间为0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF,这是个相当庞大的空间,Linux实际上只用了其中一小部分(256T)。
根据Linux内核相关文档描述,Linux64位操作系统仅使用低47位,高17位做扩展(只能是全0或全1)。所以,实际用到的地址为空间为0x0000000000000000 ~ 0x00007FFFFFFFFFFF和0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF,其中前面为用户空间(User Space),后者为内核空间(Kernel Space)。
User Space。将User Space放大后,可以看到里面主要分为如下几段:
Code代码段:这是整个用户空间的最低地址部分,存放的是指令(也就是程序所编译成的可执行机器码)
Data:这里存放的是初始化过的全局变量
BSS:这里存放的是未初始化的全局变量
Heap:堆,这是我们本文重点关注的地方,堆自低地址向高地址增长,后面要讲到的brk相关的系统调用就是从这里分配内存
Mapping Area:这里是与mmap系统调用相关的区域。大多数实际的malloc实现会考虑通过mmap分配较大块的内存区域,本文不讨论这种情况。这个区域自高地址向低地址增长
Stack:这是栈区域,就是函数内局部变量存放的区域。自高地址向低地址增长
mapped region 是由一个break指针来区分的。Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。
<————————–For Use ————–| —————-Unmapped Region—————>

break
brk 和sbrk函数
要增加一个进程实际的可用堆大小,就需要将break指针向高地址移动(堆是低位向高位增长)。Linux通过brk和sbrk系统调用操作break指针。两个系统调用的原型如下:



int brk(void *addr);
void *sbrk(intptr_t increment);
1
2
brk将break指针直接设置为某个地址,而sbrk将break从当前位置移动increment所指定的增量。brk在执行成功时返回0,否则返回-1并设置errno为ENOMEM;sbrk成功时返回break移动之前所指向的地址,否则返回(void *)-1,如果increment 是0,则会返回当前指针地址。
另外需要注意的是,由于Linux是按页进行内存映射的,所以如果break被设置为没有按页大小对齐,则系统实际上会在最后映射一个完整的页,从而实际已映射的内存空间比break指向的地方要大一些,。但是使用break之后的地址是很危险的(break指针和Unmapped region之前也许确实有一小块可用内存地址)。



sbrk 实现malloc大概实现是这样的:



void *my_malloc(size_t size)
{



void *result = sbrk(0);//result初始化为当前指针地址
if (sbrk(size) == (void*)-1) // 继续分配可用的size--mapping region
return NULL;
return result; } 经过测试,这个malloc可用简单的使用:


int Test4()
{
char *pDst = (char *)my_malloc(100);
char testSrc[] = “hello world”;
strcpy(pDst,testSrc);
printf(“the result is %s\n”, pDst);
printf(“\n”);
return 0;
}
但是他没有对已经分配的内存进行有效的记录,而且没有进行内存管理,所以还需要更多的处理。



Malloc实现



  1. 函数原型



(void *)malloc(int size)
1
malloc是一个标准库函数,并不是系统调用。函数的返回值是一个void类型的指针,参数为int类型数据,即申请分配的内存大小,单位是byte。内存分配成功之后,malloc函数返回这块内存的首地址。需要一个指针来接收这个地址。但是由于函数的返回值是void *类型的,所以必须强制转换成你所接收的类型。也就是说,这块内存将要用来存储什么类型的数据。比如:



char *p = (char *)malloc(100);
1
在堆上分配了100个字节内存,返回这块内存的首地址,把地址强制转换成char 类型后赋给char 类型的指针变量p。同时告诉我们这块内存将用来存储char类型的数据。也就是说你只能通过指针变量p来操作这块内存。这块内存本身并没有名字,对它的访问是匿名访问。
同样要注意:如果所申请的内存块大于目前堆上剩余内存块(整块),则内存分配会失败,函数返回NULL。注意这里说的“堆上剩余内存块”不是所有剩余内存块之和,因为malloc函数申请的是连续的一块内存。既然malloc函数申请内存有不成功的可能,那我们在使用指向这块内存的指针时,必须用if(NULL!=p)语句来验证内存确实分配成功了。



2.确定数据结构



将堆内存空间以块(Block)的形式组织起来,每个块由meta区和数据区组成,meta区记录数据块的元信息(数据区大小、空闲标志位、指针等等),数据区是真实分配的内存区域,并且数据区的第一个字节地址即为malloc返回的地址。



struct s_block
{
size_t size;//data region size,The sizeof unsigned long is 8
struct s_block *next;//next block 指针,The sizeof pointer is 8
int free; // free block or not,The sizeof int is 4
int padding;//填充4字节 来保证meta长度为8字节,The sizeof int is 4
char data[1];//malloc返回的地址。The sizeof char is 1
};



typedef struct s_block* t_block;
struct block 描述如下图

其中前四个域是meta 区,padding是为了字节对齐填充的,没有实际用途, data数据区的第一个字节,存malloc返回的地址。



3.寻找合适的block



3.1考虑如何在block链中查找合适的block。一般来说有两种查找算法:
1.> First fit:从头开始,使用第一个数据区大小大于要求size的块为此次分配的块



t_block find_block_from_first(t_block *last, size_t size) {
t_block res = (t_block)first_block;
while(res && !(res->free && res->size >= size))
{
*last = res;
res = res->next;
}
return res; } 2>Best fit:从头开始,遍历所有块,使用数据区大小大于size且差值最小的块作为此次分配的块


#define MIN_SIZE(size1, size2) (((size1) > (size2)) ? ((size1)-(size2)) : -1 )



t_block find_block_best(t_block *last, size_t size)
{
t_block res = (t_block)first_block;
size_t min_size = MIN_SIZE(res->size, size);



while(res && && !res->free && res->next)
{
if ((min_size > 0)
&& (min_size < MIN_SIZE(res->next->size, size))) {
min_size = MIN_SIZE(res->next->size, size);
}
*last = res;
res = res->next;
}

return (*last)->next; } 3.2 开辟新的block,如果现在所有block都不满足要求,则需要在链表最后开辟一个新的block,这里关键是如何只用sbrk创建一个struct,BLOCK_SIZE is the sizeof struct. 因为存在虚拟字段,所以手工设置


t_block extend_heap(t_block last, size_t size)
{
t_block res;
res = (t_block)sbrk(0);
if(sbrk(BLOCK_SIZE+size) == (void*)-1)
{
return NULL;
}
res->size = size;
res->free = 0;
res->next = NULL;
if(last)
last->next = res;



return res; } 3.3 分裂block. First fit有一个比较致命的缺点,就是可能会让很小的size占据很大的一块block,此时,为了提高payload,应该在剩余数据区足够大的情况下,将其分裂为一个新的block /**split the block_size into size and new size    block: the block need to split    size: the left block's new size    before:    ------Block1---------------------Block2--------------
|------------->next After: ------Block1-----Block2-----------Block3-----------
|--->next |------>next
---size---|---original size-size-BLOCK_SIZE */ void split_block(t_block block, size_t size) {
if (block->size < (BLOCK_SIZE + size))
{
//No need to split
return;
}

t_block new_block = (t_block)(block->data + size);
new_block->size = block->size - size - BLOCK_SIZE;
new_block->next = block->next;
new_block->free = 1;
block->size = size;
block->next = new_block; }


4.实现一个简单的molloc



我们可以利用上面的代码整合成一个简单但初步可用的malloc。注意首先我们要定义个block链表的头first_block,初始化为NULL;另外,我们需要剩余空间至少有BLOCK_SIZE + 8才执行分裂操作。
其次,由于我们希望malloc分配的数据区是按8字节对齐,所以在size不为8的倍数时,我们需要将size调整为大于size的最小的8的倍数。



#define align8(size) (((size&0x7) == 0) ? size : ((size»3)+1)«3)
void *my_malloc(size_t size)
{
t_block result, last;
size_t block_size;//final size
//alian
block_size = align8(size);



printf("The size is %d, %s, %d\n", block_size, __func__, __LINE__);
if(first_block) {
last = (t_block)first_block;
result = find_block_best(&last,size);
if(result) {
//if unused block > avaliable size + 8, split it
if((result->size-size) > (block_size+8)) {
split_block(result,size);
}
result->free = 0;//not free anymore
} else {
// No avaible block, extend new block
result = extend_heap(last,size);
if(!result)
return NULL;
}
} else {
if((result = extend_heap(NULL,size)) == NULL)
return NULL;
first_block = result;
}
return result->data; } 5. calloc的实现 1. calloc 相当于初始化一段size为0的内存,由于我们的数据区是按8字节对齐的,所以为了提高效率,我们可以每8字节一组置0,而不是一个一个字节设置。我们可以通过新建一个size_t指针,将内存区域强制看做size_t类型来实现。


/**
zero with 8 byte
*/
void *calloc(size_t num, size_t size)
{
size_t *new_block;
size_t size8, i;
/
continued memeory/
new_block = (size_t
)my_malloc(numsize);
if (new_block) {
size8 = align8(num
size) » 3;
}
for (i=0; i<size8; i++) {
new_block[i] = 0;
}
return new_block;



  1. free的实现



手册上说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的结束地址。
堆的管理
  上面的函数我们其实很少使用,大部分我们使用的是malloc和free函数来分配和释放内存。这样能够提高程序的性能,不是每次分配内存都调用brk或sbrk,而是重用前面空闲的内存空间。brk和sbrk分配的堆空间类似于缓冲池,每次malloc从缓冲池获得内存,如果缓冲池不够了,再调用brk或sbrk扩充缓冲池,直到达到缓冲池大小的上限,free则将应用程序使用的内存空间归还给缓冲池。
  
 任何一个用过或学过C的人对malloc都不会陌生。大家都知道malloc可以分配一段连续的内存空间,并且在不再使用时可以通过free释放掉。但是,许多程序员对malloc背后的事情并不熟悉,许多人甚至把malloc当做操作系统所提供的系统调用或C的关键字。实际上,malloc只是C的标准库中提供的一个普通函数,而且实现malloc的基本思想并不复杂,任何一个对C和操作系统有些许了解的程序员都可以很容易理解。



  这篇文章通过实现一个简单的malloc来描述malloc背后的机制。当然与现有C的标准库实现(例如glibc)相比,我们实现的malloc并不是特别高效,但是这个实现比目前真实的malloc实现要简单很多,因此易于理解。重要的是,这个实现和真实实现在基本原理上是一致的。



  这篇文章将首先介绍一些所需的基本知识,如操作系统对进程的内存管理以及相关的系统调用,然后逐步实现一个简单的malloc。为了简单起见,这篇文章将只考虑x86_64体系结构,操作系统为Linux。



  1 什么是malloc
  在实现malloc之前,先要相对正式地对malloc做一个定义。



  根据标准C库函数的定义,malloc具有如下原型:



void* malloc(size_t size);
  这个函数要实现的功能是在系统中分配一段连续的可用的内存,具体有如下要求:



malloc分配的内存大小至少为size参数所指定的字节数
malloc的返回值是一个指针,指向一段可用内存的起始地址
多次调用malloc所分配的地址不能有重叠部分,除非某次malloc所分配的地址被释放掉
malloc应该尽快完成内存分配并返回(不能使用NP-hard的内存分配算法)
实现malloc时应同时实现内存大小调整和内存释放函数(即realloc和free)
  对于malloc更多的说明可以在命令行中键入以下命令查看:



man malloc
  2 预备知识
  在实现malloc之前,需要先解释一些Linux系统内存相关的知识。



  2.1 Linux内存管理
  2.1.1 虚拟内存地址与物理内存地址
  为了简单,现代操作系统在处理内存地址时,普遍采用虚拟内存地址技术。即在汇编程序(或机器语言)层面,当涉及内存地址时,都是使用虚拟内存地址。采用这种技术时,每个进程仿佛自己独享一片2N字节的内存,其中N是机器位数。例如在64位CPU和64位操作系统下,每个进程的虚拟地址空间为264Byte。



  这种虚拟地址空间的作用主要是简化程序的编写及方便操作系统对进程间内存的隔离管理,真实中的进程不太可能(也用不到)如此大的内存空间,实际能用到的内存取决于物理内存大小。



  由于在机器语言层面都是采用虚拟地址,当实际的机器码程序涉及到内存操作时,需要根据当前进程运行的实际上下文将虚拟地址转换为物理内存地址,才能实现对真实内存数据的操作。这个转换一般由一个叫MMU(Memory Management Unit)的硬件完成。



  2.1.2 页与地址构成
  在现代操作系统中,不论是虚拟内存还是物理内存,都不是以字节为单位进行管理的,而是以页(Page)为单位。一个内存页是一段固定大小的连续内存地址的总称,具体到Linux中,典型的内存页大小为4096Byte(4K)。



  所以内存地址可以分为页号和页内偏移量。下面以64位机器,4G物理内存,4K页大小为例,虚拟内存地址和物理内存地址的组成如下:



内存地址构成



  上面是虚拟内存地址,下面是物理内存地址。由于页大小都是4K,所以页内便宜都是用低12位表示,而剩下的高地址表示页号。



  MMU映射单位并不是字节,而是页,这个映射通过查一个常驻内存的数据结构页表来实现。现在计算机具体的内存地址映射比较复杂,为了加快速度会引入一系列缓存和优化,例如TLB等机制。下面给出一个经过简化的内存地址翻译示意图,虽然经过了简化,但是基本原理与现代计算机真实的情况的一致的。



内存地址翻译



  2.1.3 内存页与磁盘页
  我们知道一般将内存看做磁盘的的缓存,有时MMU在工作时,会发现页表表明某个内存页不在物理内存中,此时会触发一个缺页异常(Page Fault),此时系统会到磁盘中相应的地方将磁盘页载入到内存中,然后重新执行由于缺页而失败的机器指令。关于这部分,因为可以看做对malloc实现是透明的,所以不再详细讲述,有兴趣的可以参考《深入理解计算机系统》相关章节。



  最后附上一张在维基百科找到的更加符合真实地址翻译的流程供大家参考,这张图加入了TLB和缺页异常的流程(图片来源页)。



较为完整的地址翻译流程



  2.2 Linux进程级内存管理
  2.2.1 内存排布
  明白了虚拟内存和物理内存的关系及相关的映射机制,下面看一下具体在一个进程内是如何排布内存的。



  以Linux 64位系统为例。理论上,64bit内存地址可用空间为0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF,这是个相当庞大的空间,Linux实际上只用了其中一小部分(256T)。



  根据Linux内核相关文档描述,Linux64位操作系统仅使用低47位,高17位做扩展(只能是全0或全1)。所以,实际用到的地址为空间为0x0000000000000000 ~ 0x00007FFFFFFFFFFF和0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF,其中前面为用户空间(User Space),后者为内核空间(Kernel Space)。图示如下:



Linux进程地址排布



  对用户来说,主要关注的空间是User Space。将User Space放大后,可以看到里面主要分为如下几段:



Code:这是整个用户空间的最低地址部分,存放的是指令(也就是程序所编译成的可执行机器码)
Data:这里存放的是初始化过的全局变量
BSS:这里存放的是未初始化的全局变量
Heap:堆,这是我们本文重点关注的地方,堆自低地址向高地址增长,后面要讲到的brk相关的系统调用就是从这里分配内存
Mapping Area:这里是与mmap系统调用相关的区域。大多数实际的malloc实现会考虑通过mmap分配较大块的内存区域,本文不讨论这种情况。这个区域自高地址向低地址增长
Stack:这是栈区域,自高地址向低地址增长
  下面我们主要关注Heap区域的操作。对整个Linux内存排布有兴趣的同学可以参考其它资料。



  2.2.2 Heap内存模型
  一般来说,malloc所申请的内存主要从Heap区域分配(本文不考虑通过mmap申请大块内存的情况)。



  由上文知道,进程所面对的虚拟内存地址空间,只有按页映射到物理内存地址,才能真正使用。受物理存储容量限制,整个堆虚拟内存空间不可能全部映射到实际的物理内存。Linux对堆的管理示意如下:



Linux进程堆管理



  Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。



  2.2.3 brk与sbrk
  由上文知道,要增加一个进程实际的可用堆大小,就需要将break指针向高地址移动。Linux通过brk和sbrk系统调用操作break指针。两个系统调用的原型如下:



int brk(void *addr);
void *sbrk(intptr_t increment);
  brk将break指针直接设置为某个地址,而sbrk将break从当前位置移动increment所指定的增量。brk在执行成功时返回0,否则返回-1并设置errno为ENOMEM;sbrk成功时返回break移动之前所指向的地址,否则返回(void *)-1。



  一个小技巧是,如果将increment设置为0,则可以获得当前break的地址。



  另外需要注意的是,由于Linux是按页进行内存映射的,所以如果break被设置为没有按页大小对齐,则系统实际上会在最后映射一个完整的页,从而实际已映射的内存空间比break指向的地方要大一些。但是使用break之后的地址是很危险的(尽管也许break之后确实有一小块可用内存地址)。



  2.2.4 资源限制与rlimit
  系统对每一个进程所分配的资源不是无限的,包括可映射的内存空间,因此每个进程有一个rlimit表示当前进程可用的资源上限。这个限制可以通过getrlimit系统调用得到,下面代码获取当前进程虚拟内存空间的rlimit:



int main() {
struct rlimit *limit = (struct rlimit *)malloc(sizeof(struct rlimit));
getrlimit(RLIMIT_AS, limit);
printf(“soft limit: %ld, hard limit: %ld\n”, limit->rlim_cur, limit->rlim_max);
}
  其中rlimit是一个结构体:



struct rlimit {
rlim_t rlim_cur; /* Soft limit /
rlim_t rlim_max; /
Hard limit (ceiling for rlim_cur) */
};
  每种资源有软限制和硬限制,并且可以通过setrlimit对rlimit进行有条件设置。其中硬限制作为软限制的上限,非特权进程只能设置软限制,且不能超过硬限制。



  3 实现malloc
  3.1 玩具实现
  在正式开始讨论malloc的实现前,我们可以利用上述知识实现一个简单但几乎没法用于真实的玩具malloc,权当对上面知识的复习:



/* 一个玩具malloc */
#include <sys/types.h>
#include
void *malloc(size_t size)
{
void *p;
p = sbrk(0);
if (sbrk(size) == (void *)-1)
return NULL;
return p;
}
  这个malloc每次都在当前break的基础上增加size所指定的字节数,并将之前break的地址返回。这个malloc由于对所分配的内存缺乏记录,不便于内存释放,所以无法用于真实场景。



  3.2 正式实现
  下面严肃点讨论malloc的实现方案。



  3.2.1 数据结构
  首先我们要确定所采用的数据结构。一个简单可行方案是将堆内存空间以块(Block)的形式组织起来,每个块由meta区和数据区组成,meta区记录数据块的元信息(数据区大小、空闲标志位、指针等等),数据区是真实分配的内存区域,并且数据区的第一个字节地址即为malloc返回的地址。



  可以用如下结构体定义一个block:



typedef struct s_block t_block;
struct s_block {
size_t size; /
数据区大小 /
t_block next; /
指向下个块的指针 /
int free; /
是否是空闲块 /
int padding; /
填充4字节,保证meta块长度为8的倍数 /
char data[1] /
这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta */
};
  由于我们只考虑64位机器,为了方便,我们在结构体最后填充一个int,使得结构体本身的长度为8的倍数,以便内存对齐。示意图如下:



Block结构



  3.2.2 寻找合适的block
  现在考虑如何在block链中查找合适的block。一般来说有两种查找算法:



First fit:从头开始,使用第一个数据区大小大于要求size的块所谓此次分配的块
Best fit:从头开始,遍历所有块,使用数据区大小大于size且差值最小的块作为此次分配的块
  两种方法各有千秋,best fit具有较高的内存使用率(payload较高),而first fit具有更好的运行效率。这里我们采用first fit算法。



/* First fit */
t_block find_block(t_block *last, size_t size) {
t_block b = first_block;
while(b && !(b->free && b->size >= size)) {
*last = b;
b = b->next;
}
return b;
}
  find_block从frist_block开始,查找第一个符合要求的block并返回block起始地址,如果找不到这返回NULL。这里在遍历时会更新一个叫last的指针,这个指针始终指向当前遍历的block。这是为了如果找不到合适的block而开辟新block使用的,具体会在接下来的一节用到。



  3.2.3 开辟新的block
  如果现有block都不能满足size的要求,则需要在链表最后开辟一个新的block。这里关键是如何只使用sbrk创建一个struct:



#define BLOCK_SIZE 24 /* 由于存在虚拟的data字段,sizeof不能正确计算meta长度,这里手工设置 */



t_block extend_heap(t_block last, size_t s) {
t_block b;
b = sbrk(0);
if(sbrk(BLOCK_SIZE + s) == (void *)-1)
return NULL;
b->size = s;
b->next = NULL;
if(last)
last->next = b;
b->free = 0;
return b;
}
  3.2.4 分裂block
  First fit有一个比较致命的缺点,就是可能会让很小的size占据很大的一块block,此时,为了提高payload,应该在剩余数据区足够大的情况下,将其分裂为一个新的block,示意如下:



分裂block



  实现代码:



void split_block(t_block b, size_t s) {
t_block new;
new = b->data + s;
new->size = b->size - s - BLOCK_SIZE ;
new->next = b->next;
new->free = 1;
b->size = s;
b->next = new;
}
  3.2.5 malloc的实现
  有了上面的代码,我们可以利用它们整合成一个简单但初步可用的malloc。注意首先我们要定义个block链表的头first_block,初始化为NULL;另外,我们需要剩余空间至少有BLOCK_SIZE + 8才执行分裂操作。



  由于我们希望malloc分配的数据区是按8字节对齐,所以在size不为8的倍数时,我们需要将size调整为大于size的最小的8的倍数:



size_t align8(size_t s) {
if(s & 0x7 == 0)
return s;
return ((s » 3) + 1) « 3;
}
#define BLOCK_SIZE 24
void *first_block=NULL;



/* other functions… */



void malloc(size_t size) {
t_block b, last;
size_t s;
/
对齐地址 /
s = align8(size);
if(first_block) {
/
查找合适的block /
last = first_block;
b = find_block(&last, s);
if(b) {
/
如果可以,则分裂 /
if ((b->size - s) >= ( BLOCK_SIZE + 8))
split_block(b, s);
b->free = 0;
} else {
/
没有合适的block,开辟一个新的 */
b = extend_heap(last, s);
if(!b)
return NULL;
}
} else {
b = extend_heap(NULL, s);
if(!b)
return NULL;
first_block = b;
}
return b->data;
}
  3.2.6 calloc的实现
  有了malloc,实现calloc只要两步:



malloc一段内存
将数据区内容置为0
  由于我们的数据区是按8字节对齐的,所以为了提高效率,我们可以每8字节一组置0,而不是一个一个字节设置。我们可以通过新建一个size_t指针,将内存区域强制看做size_t类型来实现。



void *calloc(size_t number, size_t size) {
size_t *new;
size_t s8, i;
new = malloc(number * size);
if(new) {
s8 = align8(number * size) » 3;
for(i = 0; i < s8; i++)
new[i] = 0;
}
return new;
}
  3.2.7 free的实现
  free的实现并不像看上去那么简单,这里我们要解决两个关键问题:



如何验证所传入的地址是有效地址,即确实是通过malloc方式分配的数据区首地址
如何解决碎片问题
  首先我们要保证传入free的地址是有效的,这个有效包括两方面:



地址应该在之前malloc所分配的区域内,即在first_block和当前break指针范围内
这个地址确实是之前通过我们自己的malloc分配的
  第一个问题比较好解决,只要进行地址比较就可以了,关键是第二个问题。这里有两种解决方案:一是在结构体内埋一个magic number字段,free之前通过相对偏移检查特定位置的值是否为我们设置的magic number,另一种方法是在结构体内增加一个magic pointer,这个指针指向数据区的第一个字节(也就是在合法时free时传入的地址),我们在free前检查magic pointer是否指向参数所指地址。这里我们采用第二种方案:



  首先我们在结构体中增加magic pointer(同时要修改BLOCK_SIZE):



typedef struct s_block t_block;
struct s_block {
size_t size; /
数据区大小 /
t_block next; /
指向下个块的指针 /
int free; /
是否是空闲块 /
int padding; /
填充4字节,保证meta块长度为8的倍数 /
void *ptr; /
Magic pointer,指向data /
char data[1] /
这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta */
};
  然后我们定义检查地址合法性的函数:



t_block get_block(void *p) {
char *tmp;
tmp = p;
return (p = tmp -= BLOCK_SIZE);
}



int valid_addr(void *p) {
if(first_block) {
if(p > first_block && p < sbrk(0)) {
return p == (get_block(p))->ptr;
}
}
return 0;
}
  当多次malloc和free后,整个内存池可能会产生很多碎片block,这些block很小,经常无法使用,甚至出现许多碎片连在一起,虽然总体能满足某此malloc要求,但是由于分割成了多个小block而无法fit,这就是碎片问题。



  一个简单的解决方式时当free某个block时,如果发现它相邻的block也是free的,则将block和相邻block合并。为了满足这个实现,需要将s_block改为双向链表。修改后的block结构如下:



typedef struct s_block t_block;
struct s_block {
size_t size; /
数据区大小 /
t_block prev; /
指向上个块的指针 /
t_block next; /
指向下个块的指针 /
int free; /
是否是空闲块 /
int padding; /
填充4字节,保证meta块长度为8的倍数 /
void *ptr; /
Magic pointer,指向data /
char data[1] /
这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta */
};
  合并方法如下:



t_block fusion(t_block b) {
if (b->next && b->next->free) {
b->size += BLOCK_SIZE + b->next->size;
b->next = b->next->next;
if(b->next)
b->next->prev = b;
}
return b;
}
  有了上述方法,free的实现思路就比较清晰了:首先检查参数地址的合法性,如果不合法则不做任何事;否则,将此block的free标为1,并且在可以的情况下与后面的block进行合并。如果当前是最后一个block,则回退break指针释放进程内存,如果当前block是最后一个block,则回退break指针并设置first_block为NULL。实现如下:



void free(void *p) {
t_block b;
if(valid_addr(p)) {
b = get_block(p);
b->free = 1;
if(b->prev && b->prev->free)
b = fusion(b->prev);
if(b->next)
fusion(b);
else {
if(b->prev)
b->prev->prev = NULL;
else
first_block = NULL;
brk(b);
}
}
}
  3.2.8 realloc的实现
  为了实现realloc,我们首先要实现一个内存复制方法。如同calloc一样,为了效率,我们以8字节为单位进行复制:



void copy_block(t_block src, t_block dst) {
size_t *sdata, *ddata;
size_t i;
sdata = src->ptr;
ddata = dst->ptr;
for(i = 0; (i * 8) < src->size && (i * 8) < dst->size; i++)
ddata[i] = sdata[i];
}
  然后我们开始实现realloc。一个简单(但是低效)的方法是malloc一段内存,然后将数据复制过去。但是我们可以做的更高效,具体可以考虑以下几个方面:



如果当前block的数据区大于等于realloc所要求的size,则不做任何操作
如果新的size变小了,考虑split
如果当前block的数据区不能满足size,但是其后继block是free的,并且合并后可以满足,则考虑做合并
  下面是realloc的实现:



void realloc(void *p, size_t size) {
size_t s;
t_block b, new;
void *newp;
if (!p)
/
根据标准库文档,当p传入NULL时,相当于调用malloc /
return malloc(size);
if(valid_addr(p)) {
s = align8(size);
b = get_block(p);
if(b->size >= s) {
if(b->size - s >= (BLOCK_SIZE + 8))
split_block(b,s);
} else {
/
看是否可进行合并 /
if(b->next && b->next->free
&& (b->size + BLOCK_SIZE + b->next->size) >= s) {
fusion(b);
if(b->size - s >= (BLOCK_SIZE + 8))
split_block(b, s);
} else {
/
新malloc */
newp = malloc (s);
if (!newp)
return NULL;
new = get_block(newp);
copy_block(b, new);
free(p);
return(newp);
}
}
return (p);
}
return NULL;
}
  3.3 遗留问题和优化
  以上是一个较为简陋,但是初步可用的malloc实现。还有很多遗留的可能优化点,例如:



同时兼容32位和64位系统
在分配较大快内存时,考虑使用mmap而非sbrk,这通常更高效
可以考虑维护多个链表而非单个,每个链表中的block大小均为一个范围内,例如8字节链表、16字节链表、24-32字节链表等等。此时可以根据size到对应链表中做分配,可以有效减少碎片,并提高查询block的速度
可以考虑链表中只存放free的block,而不存放已分配的block,可以减少查找block的次数,提高效率



参考
这篇文章大量参考了A malloc Tutorial,其中一些图片和代码直接引用了文中的内容,这里特别指出
Computer Systems: A Programmer’s Perspective, 2/E一书有许多值得参考的地方
关于Linux的虚拟内存模型,Anatomy of a Program in Memory是很好的参考资料,另外作者还有一篇How the Kernel Manages Your Memory对于Linux内核中虚拟内存管理的部分有很好的讲解
对于真实世界的malloc实现,可以参考glibc的实现



https://repo.or.cz/glibc.git/project_list?t=libc



首先我们已经知道linux下,malloc最后调用的是sbrk函数,而sbrk是对brk的简单封装。



用sbrk模仿malloc很简单,sbrk(0)得到当前breakpoint,再调用sbrk(size)即可。(PS:breakpoint表示堆结束地址)

一直以来让我困惑的是,怎么用brk去实现sbrk,换句话说,就是只有brk系统调用,如何能得知当前的breakpoint...难道就没有人想过这个问题嘛?搜索了各种关键字,来来回回都围绕着sbrk讲,算了,自己动手,丰衣足食,咱求人不如求己,还是自己分析分析好了,glibc中brk的wrapper如下:


?



1



2



#include



int brk(void *addr);
man手册中对此函数的描述:



   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 setrlimit(2)).


RETURN VALUE



   On success, brk() returns zero.  On error, -1 is returned, and errno is set to ENOMEM.  (But see Linux Notes below.)


可以看见,这个函数的功能是直接设置breakpoint指针为addr,成功返回0,失败-1,并没有返回当前breakpoint的功能,难道glibc是自己初始化了一个自以为正确的起始地址然后以此为基准分配内存的么?这也太不靠谱了吧,而且如果有其他不依赖于glibc的基础库如果直接调用brk分配内存了,再调用malloc岂不是天大的杯具?所以肯定不是这么搞的,又或者是还有一个咱不知道的系统调用,作用就是返回当前breakpoint位置?算了,先看看glibc是怎么实现的再说,遂去官网下了个最新的2.17的glibc,解压后通过grep找到sbrk定义位于glibc-2.17/misc/sbrk.c中
void *



__sbrk (intptr_t increment)



{



void *oldbrk;



/* If this is not part of the dynamic library or the library is used



 via dynamic loading in a statically linked program update

__curbrk from the kernel's brk value. That way two separate

instances of __brk and __sbrk can share the heap, returning

interleaved pieces of it. */










if (__curbrk == NULL   __libc_multiple_libcs)


if (__brk (0) < 0)      /* Initialize the break.  */

return (void *) -1;


if (increment == 0)



return __curbrk;


oldbrk = __curbrk;



if ((increment > 0



   ? ((uintptr_t) oldbrk + (uintptr_t) increment < (uintptr_t) oldbrk)

: ((uintptr_t) oldbrk < (uintptr_t) -increment))

|| __brk (oldbrk + increment) < 0)

return (void *) -1;


return oldbrk;



}



libc_hidden_def (__sbrk)



weak_alias (__sbrk, sbrk)
可以看见,当传入参数为0的时候,直接返回__curbrk,而__curbrk的初始化就是前一个if语句里面执行的,因此在在前面的__brk(0)调用过程中肯定设置了__curbrk的值。为了保证没有别的地方更新__curbrk什么的,再次祭出grep
kimo@ubuntu4710:~/gnu/glibc-2.17$ grep -n -r “__curbrk.*=” find ./ -name "*.c"



./ports/sysdeps/unix/sysv/linux/am33/brk.c:26:void *__curbrk = 0;



./ports/sysdeps/unix/sysv/linux/am33/brk.c:35: __curbrk = newbrk;



./ports/sysdeps/unix/sysv/linux/arm/brk.c:24:void *__curbrk = 0;



./ports/sysdeps/unix/sysv/linux/arm/brk.c:31: __curbrk = newbrk = (void *) INLINE_SYSCALL (brk, 1, addr);



./ports/sysdeps/unix/sysv/linux/m68k/brk.c:23:void *__curbrk = 0;



./ports/sysdeps/unix/sysv/linux/m68k/brk.c:37: __curbrk = newbrk;



./ports/sysdeps/unix/sysv/linux/hppa/brk.c:24:void *__curbrk = 0;



./ports/sysdeps/unix/sysv/linux/hppa/brk.c:31: __curbrk = newbrk = (void *) INLINE_SYSCALL (brk, 1, addr);



./ports/sysdeps/unix/sysv/linux/mips/brk.c:23:void *__curbrk = 0;



./ports/sysdeps/unix/sysv/linux/mips/brk.c:46: __curbrk = newbrk;



./ports/sysdeps/unix/sysv/linux/generic/brk.c:24:void *__curbrk = 0;



./ports/sysdeps/unix/sysv/linux/generic/brk.c:36: __curbrk = (void *) INTERNAL_SYSCALL (brk, err, 1, addr);











./misc/sbrk.c:42: if (__curbrk == NULL   __libc_multiple_libcs)


./sysdeps/unix/sysv/linux/s390/brk.c:24:void *__curbrk = 0;



./sysdeps/unix/sysv/linux/s390/brk.c:45: __curbrk = newbrk;



./sysdeps/unix/sysv/linux/sparc/sparc32/brk.c:25:void *__curbrk = 0;



./sysdeps/unix/sysv/linux/sparc/sparc32/brk.c:44: __curbrk = newbrk;



./sysdeps/unix/sysv/linux/i386/brk.c:26:void *__curbrk = 0;



./sysdeps/unix/sysv/linux/i386/brk.c:42: __curbrk = newbrk;



./sysdeps/unix/sysv/linux/x86_64/brk.c:24:void *__curbrk = 0;



./sysdeps/unix/sysv/linux/x86_64/brk.c:31: __curbrk = newbrk = (void *) INLINE_SYSCALL (brk, 1, addr);



./sysdeps/unix/sysv/linux/sh/brk.c:24:void *__curbrk = 0;



./sysdeps/unix/sysv/linux/sh/brk.c:37: __curbrk = newbrk;
果然,只有对应体系结构下的brk.c更新了这个值。我装的ubuntu64,默认应该就是./sysdeps/unix/sysv/linux/x86_64/brk.c



这个文件的代码不长,去掉前面那些gnu相关的注释也就这点内容
#include



#include



#include



/* This must be initialized data because commons can’t have aliases. */



void *__curbrk = 0;



int



__brk (void *addr)



{



void *newbrk;



__curbrk = newbrk = (void *) INLINE_SYSCALL (brk, 1, addr);



if (newbrk < addr)



{

__set_errno (ENOMEM);

return -1;

}


return 0;



}



weak_alias (__brk, brk)
这里的INLINE_SYSCALL(brk,1,addr)居然直接就能返回 当前的breakpoint,其实到这里我已经隐约感觉到了就是brk(0)系统调用返回了当前的breakpoint,但是…用linus 大神的话说,talk is cheap,show me the code….于是还是沿着这个宏一路向西追踪 陆续找到了如下定义, 在./sysdeps/unix/sysv/linux/x86_64/sysdep.h中
/* Define a macro which expands inline into the wrapper code for a system



call. */



undef INLINE_SYSCALL



define INLINE_SYSCALL(name, nr, args…) \



({ \



unsigned long int resultvar = INTERNAL_SYSCALL (name, , nr, args);        \

if (__builtin_expect (INTERNAL_SYSCALL_ERROR_P (resultvar, ), 0)) \

{ \

__set_errno (INTERNAL_SYSCALL_ERRNO (resultvar, )); \

resultvar = (unsigned long int) -1; \

} \

(long int) resultvar; }) # define INTERNAL_SYSCALL(name, err, nr, args...) \


INTERNAL_SYSCALL_NCS (_NR##name, err, nr, ##args)


define INTERNAL_SYSCALL_NCS(name, err, nr, args…) \



({ \



unsigned long int resultvar;                          \

LOAD_ARGS_##nr (args) \

LOAD_REGS_##nr \

asm volatile ( \

"syscall\n\t" \

: "=a" (resultvar) \

: "0" (name) ASM_ARGS_##nr : "memory", "cc", "r11", "cx"); \

(long int) resultvar; })
呵呵,又是asm内嵌的汇编,奇怪的是没有看见int 0x80或者 sysenter 指令,google了一下才知道64位系统中的syscall就是对应32位的sysenter,到此用户空间执行完毕,完全可以肯定是系统调用brk(0)返回了当前的breakpoint值。但是这个结果跟man手册描述的返回值不相同啊,brk不是返回0 or -1么。反正都折腾到这里了,不如在看看内核代码吧,在kernel.org上下载了个最新的稳定版linux-3.10.2。马上搜索系统调用的实现 kimo@ubuntu4710:~/gnu/linux-3.10.2$ grep -n -r "SYSCALL_DEFINE1.*brk" ./


./mm/mmap.c:261:SYSCALL_DEFINE1(brk, unsigned long, brk)



./mm/nommu.c:502:SYSCALL_DEFINE1(brk, unsigned long, brk)



./arch/alpha/kernel/osf_sys.c:54:SYSCALL_DEFINE1(osf_brk, unsigned long, brk)
看了下mm下的Makefile,只有在编译内核的时候没有选择mmu为y,才会使用nommu.c,所以果断vim mm/mmap.c +261,对应代码如下
SYSCALL_DEFINE1(brk, unsigned long, brk)



{



unsigned long rlim, retval;

unsigned long newbrk, oldbrk;

struct mm_struct *mm = current->mm;

unsigned long min_brk;

bool populate;



down_write(&mm->mmap_sem);


#ifdef CONFIG_COMPAT_BRK



/*

* CONFIG_COMPAT_BRK can still be overridden by setting

* randomize_va_space to 2, which will still cause mm->start_brk

* to be arbitrarily shifted

*/

if (current->brk_randomized)

min_brk = mm->start_brk;

else

min_brk = mm->end_data;


#else



min_brk = mm->start_brk;


#endif



if (brk < min_brk)

goto out;

/*

* Check against rlimit here. If this check is done later after the test

* of oldbrk with newbrk then it can escape the test and let the data

* segment grow beyond its set limit the in case where the limit is

* not page aligned -Ram Gupta

*/

rlim = rlimit(RLIMIT_DATA);

if (rlim < RLIM_INFINITY && (brk - mm->start_brk) +

(mm->end_data - mm->start_data) > rlim)

goto out;



newbrk = PAGE_ALIGN(brk);

oldbrk = PAGE_ALIGN(mm->brk);

if (oldbrk == newbrk)

goto set_brk;



/* Always allow shrinking brk. */

if (brk <= mm->brk) {

if (!do_munmap(mm, newbrk, oldbrk-newbrk))

goto set_brk;

goto out;

}



/* Check against existing mmap mappings. */

if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))

goto out;



/* Ok, looks good - let it rip. */

if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)

goto out;


set_brk:



mm->brk = brk;

populate = newbrk > oldbrk && (mm->def_flags & VM_LOCKED) != 0;

up_write(&mm->mmap_sem);

if (populate)

mm_populate(oldbrk, newbrk - oldbrk);

return brk;


out:



retval = mm->brk;

up_write(&mm->mmap_sem);

return retval;


}
原来只要传入的brk < min_brk,就会返回当前的breakpoint,而min_brk就是mm->start_brk,即堆首地址。看来是man手册搞错了,这种错误一定要狠狠地改掉!!!于是我开始细细品味man brk的内容,然后,我开始为这过去几个小时浪费的时间忏悔。。。因为那个关于返回值的说明,它….它居然后面还有 But see Linux Notes below. 我去年买了个表!!!



Linux Notes
The return value described above for brk() is the behavior provided by the glibc wrapper function for the Linux brk() system call. (On most other implementations, the return
value from brk() is the same; this return value was also specified in SUSv2.) However, the actual Linux system call returns the new program break on success. On failure, the
system call returns the current break. The glibc wrapper function does some work (i.e., checks whether the new break is less than addr) to provide the 0 and -1 return values
described above.



   On Linux, sbrk() is implemented as a library function that uses the brk() system call, and does some internal bookkeeping so that it can return the old break value.


心得:glibc封装的wrapper与linux的原生系统调用并不是一一对应的,这是因为glibc的wrapper要兼容很多*nix系统,而这些系统的系统调用之间是有一定差异的。所以,如果确定代码以后不会移植到非linux平台,最好还是使用syscall+参数的方式来使用系统调用。 


Category linux