brk和sbrk主要的工作是实现虚拟内存到内存的映射.在GNUC中,内存分配是这样的:
      每个进程可访问的虚拟内存空间为3G,但在程序编译时,不可能也没必要为程序分配这么大的空间,只分配并不大的数据段空间,程序中动态分配的空间就是从这 一块分配的。如果这块空间不够,malloc函数族(realloc,calloc等)就调用sbrk函数将数据段的下界移动,sbrk函数在内核的管理 下将虚拟地址空间映射到内存,供malloc函数使用。(参见linux内核情景分析)

#include <unistd.h>

int brk(void *end_data_segment);

void *sbrk(ptrdiff_t increment);

DESCRIPTION
brk sets the end of the data segment to the value specified by end_data_segment, when that value is reasonable, the system does have enough memory and the process does not exceed its max data size (see setrlimit(2)).

sbrk increments the program's data space by increment bytes. sbrk isn't a system call, it is just a C library wrapper. 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, and sbrk returns a pointer to the start of the new area. On error, -1 is returned, and errno is set to ENOMEM.


sbrk不是系统调用,是C库函数。系统调用通常提供一种最小功能,而库函数通常提供比较复杂的功能。



在Linux系统上,程序被载入内存时,内核为用户进程地址空间建立了代码段、数据段和堆栈段,在数据段与堆栈段之间的空闲区域用于动态内存分配。



内核数据结构mm_struct中的成员变量start_code和end_code是进程代码段的起始和终止地址,start_data和end_data是进程数据段的起始和终止地址,start_stack是进程堆栈段起始地址,start_brk是进程动态内存分配起始地址(堆的起始 地址),还有一个 brk(堆的当前最后地址),就是动态内存分配当前的终止地址。



C语言的动态内存分配基本函数是malloc(),在Linux上的基本实现是通过内核的brk系统调用。brk()是一个非常简单的系统调用,只是简单地改变mm_struct结构的成员变量brk的值。



mmap系统调用实现了更有用的动态内存分配功能,可以将一个磁盘文件的全部或部分内容映射到用户空间中,进程读写文件的操作变成了读写内存的操作。在linux/mm/mmap.c文件的do_mmap_pgoff()函数,是mmap系统调用实现的核心。do_mmap_pgoff()的代码,只是 新建了一个vm_area_struct结构,并把file结构的参数赋值给其成员变量m_file,并没有把文件内容实际装入内存。


Linux内存管理的基本思想之一,是只有在真正访问一个地址的时候才建立这个地址的物理映射。



==================================================================================


C语言跟内存分配方式


(1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。


(2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运



算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。


(3)从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多



C语言跟内存申请相关的函数主要有alloc,calloc,malloc,free,realloc,sbrk等.其中alloc是向栈申请内存,因此无需释放.malloc分配的内存是位于堆中的,并且没有初始化内存的内容,因此基本上malloc之后,调用函数memset来初始化这部分的内存空 间.calloc则将初始化这部分的内存,设置为0.而realloc则对malloc申请的内存进行大小的调整.申请的内存最终需要通过函数free来释放. 而sbrk则是增加数据段的大小;



malloc/calloc/free基本上都是C函数库实现的,跟OS无关.C函数库内部通过一定的结构来保存当前有多少可用内存.如果程序malloc的大小超出了库里所留存的空间,那么将首先调用brk系统调用来增加可用空间,然后再分配空间.free时,释放的内存并不立即返回给os,而是保留在内部结构中. 可以打个比方:brk类似于批发,一次性的向OS申请大的内存,而malloc等函数则类似于零售,满足程序运行时的要求.这套机制类似于缓冲.



使用这套机制的原因: 系统调用不能支持任意大小的内存分配(有的系统调用只支持固定大小以及其倍数的内存申请,这样的话,对于小内存的分配会造成浪费; 系统调用申请内存代价昂贵,涉及到用户态和核心态的转换.


函数malloc()和calloc()都可以用来分配动态内存空间,但两者稍有区别。      


         malloc()函数有一个参数,即要分配的内存空间的大小:       


         void *malloc(size_t size); 


         calloc()函数有两个参数,分别为元素的数目和每个元素的大小,这两个参数的乘积就是要分配的内存空间的大小:      


         void *calloc(size_t numElements,size_t sizeOfElement);


         如果调用成功,函数malloc()和calloc()都将返回所分配的内存空间的首地址。


     malloc()函数和calloc()函数的主要区别是前者不能初始化所分配的内存空间,而后者能。如果由malloc()函数分配的内存空间原来没有被使用过,则其中 的每一位可能都是0;反之,如果这部分内存空间曾经被分配、释放和重新分配,则其中可能遗留各种各样的数据。也就是说,使用malloc()函数的程序开 始时(内存空间还没有被重新分配)能正常运行,但经过一段时间后(内存空间已被重新分配)可能会出现问题。


     calloc()函数会将所分配的内存空间中的每一位都初始化为零,也就是说,如果你是为字符类型或整数类型的元素分配内存,那么这些元素将保证会被初始化为零;如果你是 为指针类型的元素分配内存,那么这些元素通常(但无法保证)会被初始化为空指针;如果你是为实数类型的元素分配内存,那么这些元素可能(只在某些计算机 中)会被初始化为浮点型的零。


     malloc()函数和calloc()函数的另一点区别是calloc()函数会返回一个由某种对象组成的数组,但malloc()函数只返回一个对象。为了明确是为一 个数组分配内存空间,有些程序员会选用calloc()函数。但是,除了是否初始化所分配的内存空间这一点之外,绝大多数程序员认为以下两种函数调用方式 没有区别:


         calloc(numElements ,sizeOfElement);


         malloc(numElements *sizeOfElement) ;


    需要解释的一点是,理论上(按照ANSIC标准)指针的算术运算只能在一个指定的数组中进行,但是在实践中,即使C编译程序或翻译器遵循这种规定,许多C程序还是冲破了这种限制。因此,尽管malloc()函数并不能返回一个数组,它所分配的内存空间仍然能供一个数组使用(对realloc()函数来说同 样如此,尽管它也不能返回一个数组)。


         总之,当你在calloc()函数和malloc()函数之间作选择时,你只需考虑是否要初始化所分配的内存空间,而不用考虑函数是否能返回一个数组。


    当程序运行过程中malloc了,但是没有free的话,会造成内存泄漏.一部分的内存没有被使用,但是由于没有free,因此系统认为这部分内存还在使 用,造成不断的向系统申请内存,是的系统可用内存不断减少.但是,内存泄漏仅仅指程序在运行时,程序退出时,OS将回收所有的资源.因此,适当的重起一下 程序,有时候还是有点作用.



sbrk(int incr) 本函数用来增加分配给调用程序的数据段的空间数量,增加incr个字节的空间brk函数的原形是:int        brk(void        *endds) 


       它的功能是:更改数据段空间的分配 


       char        *p; 


       p=malloc(1); 


       这时p指向的内存空间大小是1        byte 


       brk(p+100) 


       这时p指向的内存空间大小是101        bytes 



        程式分配虚拟内存也不是你要一个字节就给你一个字节,而是你要一个字节给你一个页面,因为映射物理内存时只能以页为单位。你要另一个字节时,它在这个页面的剩余空间给你。


注意大部份UNIX虚拟内存的使用是只增不减的。



CODE:


malloc(32 * 1024) --->;sbrk += 32 * 1024
free()                    --->;sbrk 不减少。
但如如果再来一次
malloc(32 * 1024) ---->;sbrk 也不增,使用原有空间.


但对于LINUX来说它是要以内存的最大数收缩的;


CODE:



<code>
a = malloc(32 * 1024) -->;sbrk += 32 * 1024
b = malloc(32 * 1024) -->;sbrk += 32 * 1024
if(****){
free(b); --->;sbrk -= 32 * 1024;
}
else{
free(a); --->;sbrk 不减少。只是多了个空洞.
}
< /code>



CODE:

<code>
/* linux kernel code */
brk()
/*
* sys_brk() for the most part doesn't need the global kernel
* lock, except when an application is doing something nasty
* like trying to un-brk an area that has already been mapped
* to a regular file. in this case, the unmapping will need
* to invoke file system routines that need the global lock.
*/
asmlinkage unsigned long sys_brk(unsigned long brk)
{
unsigned long rlim, retval;
unsigned long newbrk, oldbrk;
struct mm_struct *mm = current->;mm;
down_write(&mm->;mmap_sem);
if (brk < mm->;end_code)
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 rlimit.. */
rlim = current->;rlim[RLIMIT_DATA].rlim_cur;
if (rlim < RLIM_INFINITY && brk - mm->;start_data >; rlim)
goto out;
/* Check against existing mmap mappings. */
if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))
goto out;
/* Check if we have enough memory.. */
if (!vm_enough_memory((newbrk-oldbrk) >;>; PAGE_SHIFT))
goto out;
/* Ok, looks good - let it rip. */
if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)
goto out;
set_brk:
mm->;brk = brk;
out:
retval = mm->;brk; /****这就是返回值*****/
up_write(&mm->;mmap_sem);
return retval;
}
< /code>


在LINUX中sbrk(0)能返回比较精确的虚拟内存使用情况,
在SOLARIS/HP中sbrk(0)返回以页为单位的虚拟内存使用情况。使用sbrk(0)来返回程式当前使用了多少内存。

main(){
int start,end;
start = sbrk(0);
....
malloc(***);
....
end = sbrk(0);
printf("hello I used %d vmemory",end - start);
}

注意:malloc分配的起始地址(bSS结束地址)是通过sys_brk(0)从内核获取。


​​asmlinkage​​​ unsigned long ​​sys_brk​​​(unsigned long brk)

​​​207​​​ {

​​​208​​​ unsigned long rlim, retval;

​​​209​​​ unsigned long newbrk, oldbrk;

​​​210​​​ struct ​​mm_struct​​​ *mm = ​​current​​​->mm;

​​​211​​​

​​​212​​​ ​​down_write​​​(&mm->mmap_sem);

​​​213​​​

​​​214​​​ if (brk < mm->end_code) <------------- 这里

​​​215​​​ goto ​​out​​​; <------------- 这里

​​​216​​​ newbrk = ​​PAGE_ALIGN​​​(brk);

​​​217​​​ oldbrk = ​​PAGE_ALIGN​​​(mm->brk);

​​​218​​​ if (oldbrk == newbrk)

​​​219​​​ goto ​​set_brk​​​;

​​​220​​​

​​​221​​ /* Always allow shrinking brk. */

​​222​​​ if (brk <= mm->brk) {

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

​​​224​​​ goto ​​set_brk​​​;

​​​225​​​ goto ​​out​​​;

​​​226​​​ }

​​​227​​​

​​​228​​ /* Check against rlimit.. */

​​229​​​ rlim = ​​current​​​->signal->rlim[​​RLIMIT_DATA​​​].rlim_cur;

​​​230​​​ if (rlim < ​​RLIM_INFINITY​​​ && brk - mm->start_data > rlim)

​​​231​​​ goto ​​out​​​;

​​​232​​​

​​​233​​ /* Check against existing mmap mappings. */

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

​​​235​​​ goto ​​out​​​;

​​​236​​​

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

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

​​​239​​​ goto ​​out​​​;

​​​240​​​ ​​set_brk​​​:

​​​241​​​ mm->brk = brk;

​​​242​​​ ​​out​​​: <------------- 这里

​​​243​​​ retval = mm->brk; <------------- 这里

​​​244​​​ ​​up_write​​​(&mm->mmap_sem);

​​​245​​​ return retval;

​​​246​​ }
因此,要对堆内存分配的起始地址进行随机化,
在LINUX只需在内核修改brk的初始值



Linux内核运行在X86机器的物理内存管理使用简单平坦内存模型,每个用户进程内存

(虚拟内存) 地址范围为从0到TASK_SIZE字节,超过此内存的限制不能被用户访问。用户进程被分为几个逻辑段,成为虚拟内存区域,内核跟踪和管理用户进程的虚拟内存区域提供适当的内存管理和内存保护处理。

    do_brk()是一个内核函数,用于间接调用管理进程的内存堆的增加和缩减 (brk),它是一个mmap(2)系统调用的简化版本,只处理匿名映射(如未初始化数据)。

    do_brk()改变进程的地址空间。地址是代表数据段结束的一个指针(事实上是进程的堆区域)。 do_brk()的参数是一个绝对逻辑地址,这个地址代表地址空间新的结尾。更实际地说,我们在编写用户程序的时候从来就不应该使用这个函数。使用这个函数的用户程序就不能再使用malloc(),这是一个大问题,因为标注库的许多部分依赖于malloc()。 如果在用户程序中使用do_brk()可能会导致难以发现的程序崩溃。

    do_brk(addr, len)函数给从addr到addr+len建立虚拟内存区vm_area_struct(该区的起始地址为addr;结束地址为addr+len),该 虚拟内存区作为进程的堆来使用。 malloc将从此区域获取内存空间(虚拟内存),free()将会把malloc()获取的虚拟空间释放掉(归还到该进程的堆的空闲空间中去)


      

|------------|
| |
| |Stack
| |
|------------|
| |
| |
| |
| |
| |
| |
| |
| |
|------------|<--- addr+len
| |
| |
| |Heap <---- do_brk()
| |
| |
| |
|------------|<--- addr
| |
| |Codes And Data
| |
|------------|






do_brk()函数实际上是仅处理匿名线性区的do_mmap()的简化版。参数addr是需要建立映射的新区间(memory region)的起点,len则是区间的长度

-----------------------------------------------------------------------------

unsigned long do_brk(unsigned long addr, unsigned long len)
{
struct mm_struct * mm = current->mm;
struct vm_area_struct * vma;
unsigned long flags, retval;

len = PAGE_ALIGN(len);
if (!len)
return addr;
if (mm->def_flags & VM_LOCKED) {
unsigned long locked = mm->locked_vm << PAGE_SHIFT;
locked += len;
if (locked > current->rlim[RLIMIT_MEMLOCK].rlim_cur)
return -EAGAIN;
}


通过do_munmap()把原有的映射解除


|-----------------------------------------|

| retval = do_munmap(mm, addr, len); |

| if (retval != 0) |

| return retval; |

|-----------------------------------------|


检查资源是否超限

total_vm 进程地址空间的大小(页数)

map_count 线性区的个数

|--------------------------------------------------|

| if ((mm->total_vm << PAGE_SHIFT) + len |

| > current->rlim[RLIMIT_AS].rlim_cur) |

| return -ENOMEM; |

| if (mm->map_count > MAX_MAP_COUNT) |

| return -ENOMEM; |

| if (!vm_enough_memory(len >> PAGE_SHIFT)) |

| return -ENOMEM; |

|--------------------------------------------------|



加工flags


vm_area_struct->vm_flags字段:一些标志给内核提供有关这个线性区(memory region)全部页的信息(包含内容,访问权限);另外的标志还描述线性区自身(如何增长)

|-----------------------------------------------------|

| flags = vm_flags(PROT_READ|PROT_WRITE|PROT_EXEC, |

| MAP_FIXED|MAP_PRIVATE) | mm->def_flags; |

| flags |= VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC; |

|-----------------------------------------------------|



看看是否可以和原有的区间(memory region)合并,即通过扩展原有区间来合并新增区间


|-------------------------------------------------------------|

| if (addr) { |

| struct vm_area_struct * vma = find_vma(mm, addr-1); |

| if (vma && vma->vm_end == addr && !vma->vm_file && |

| vma->vm_flags == flags) { |

| vma->vm_end = addr + len; |

| goto out; |

| } |

| } |

|-------------------------------------------------------------|



如果不行就要另行建立一个区间


|---------------------------------------------------------|

| vma = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL); |

| if (!vma) |

| return -ENOMEM; |

| vma->vm_mm = mm; |

| vma->vm_start = addr; |

| vma->vm_end = addr + len; |

| vma->vm_flags = flags; |

| vma->vm_page_prot = protection_map[flags & 0x0f]; |

| vma->vm_ops = NULL; |

| vma->vm_pgoff = 0; |

| vma->vm_file = NULL; |

| vma->vm_private_data = NULL; |

| insert_vm_struct(mm, vma); |

|---------------------------------------------------------|


out:

mm->total_vm += len >> PAGE_SHIFT;



如果flags中的VM_LOCKED位设置为1,则通过make_pages_present(),为新增的区间建立起对物理内存页面的映射。可是在哪里会设置VM_LOCKED位?

|-------------------------------------------------|

| if (flags & VM_LOCKED) { |

| mm->locked_vm += len >> PAGE_SHIFT; |

| make_pages_present(addr, addr + len); |

| } |

|-------------------------------------------------|


return addr;

}



注:


(a) vm_area_struct->vm_flags字段中的VM_LOCKED是从mm_struct->def_flags中获得的


(b) mm_struct->def_flags表示线性区缺省的访问标志


int make_pages_present(unsigned long addr, unsigned long end)

{

int write;

struct mm_struct *mm = current->mm;

struct vm_area_struct * vma;

vma = find_vma(mm, addr);

write = (vma->vm_flags & VM_WRITE) != 0;

if (addr >= end)

BUG();

do {

if (handle_mm_fault(mm, vma, addr, write) < 0)

return -1;

addr += PAGE_SIZE;

} while (addr < end);

return 0;

}