理解brk和sbrk
brk和sbrk的定义
在man手册中定义了这两个函数:
1 #include <unistd.h>
2 int brk(void *addr);
3 void *sbrk(intptr_t increment);
手册上说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的概念后,我们来看下brk和sbrk的作用。brk通过传递的addr来重新设置program break,成功则返回0,否则返回-1。而sbrk用来增加heap,增加的大小通过参数increment决定,返回增加大小前的heap的program break,如果increment为0则返回program break。
从上面的图可以看出heap的起始地址并不是bss segment的结束地址,而是随机分配的,下面我们用一个程序来验证下:
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int bss_end;
5
6 int main(void)
7 {
8 void *tret;
9
10 printf("bss end: %p\n", (char *)(&bss_end) + 4);
11 tret = sbrk(0);
12 if (tret != (void *)-1)
13 printf ("heap start: %p\n", tret);
14 return 0;
15 }
运行的结果为:
从上面运行结果可以知道bss和heap是不相邻的,并且同一个程序bss的结束地址是固定的,而heap的起始地址在每次运行的时候都会改变。你可能会说sbkr(0)返回的是heap的结束地址,怎么上面确把它当做起始地址呢?由于程序开始运行时heap的大小是为0,所以起始地址和结束地址是一样的,不信我们可以用下面的程序验证下。
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4
5 int bss_end;
6
7 int main(void)
8 {
9 void *tret;
10 char *pmem;
11
12 printf("bss end: %p\n", (char *)(&bss_end) + 4);
13 tret = sbrk(0);
14 if (tret != (void *)-1)
15 printf ("heap1 start: %p\n", tret);
16
17 if (brk((char *)tret - 1) == -1)
18 printf("brk error\n");
19
20 tret = sbrk(0);
21 if (tret != (void *)-1)
22 printf ("heap2 start: %p\n", tret);
23
24 pmem = (char *)malloc(32);
25 if (pmem == NULL) {
26 perror("malloc");
27 exit (EXIT_FAILURE);
28 }
29 printf ("pmem:%p\n", pmem);
30
31 tret = sbrk(0);
32 if (tret != (void *)-1)
33 printf ("heap1 end: %p\n", tret);
34
35 if (brk((char *)tret - 10) == -1)
36 printf("brk error\n");
37
38 tret = sbrk(0);
39 if (tret != (void *)-1)
40 printf ("heap2 end: %p\n", tret);
41 return 0;
42 }
运行结果为:
程序开始的时候打印出来heap的结束地址,并用这个地址减1来重新设置heap的结束地址,结果两次的结束地址居然是一样的,那说明这个结束地址就是heap的起始地址,再减小这个起始地址是不允许的,不过brk也不会报错。然后调用malloc获取内存,并打印出该内存的起始地址pmem,可以发现pmem与heap的起始地址相差8个字节,为什么会有8个字节没有?这8个字节应该是用来管理heap空间的(不深究)。最后再次获得heap的结束地址,并用这个地址减10来重新设置heap的结束地址,这下地址设置成功了。
堆的管理
上面的函数我们其实很少使用,大部分我们使用的是malloc和free函数来分配和释放内存。这样能够提高程序的性能,不是每次分配内存都调用brk或sbrk,而是重用前面空闲的内存空间。brk和sbrk分配的堆空间类似于缓冲池,每次malloc从缓冲池获得内存,如果缓冲池不够了,再调用brk或sbrk扩充缓冲池,直到达到缓冲池大小的上限,free则将应用程序使用的内存空间归还给缓冲池。
如果缓冲池需要扩充时,一次扩充多少呢?先运行下面的程序看看:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4
5 int main(void)
6 {
7 void *tret;
8 char *pmem;
9
10 tret = sbrk(0);
11 if (tret != (void *)-1)
12 printf ("heap start: %p\n", tret);
13
14 pmem = (char *)malloc(64); //分配内存
15 if (pmem == NULL) {
16 perror("malloc");
17 exit (EXIT_FAILURE);
18 }
19 printf ("pmem:%p\n", pmem);
20 tret = sbrk(0);
21 if (tret != (void *)-1)
22 printf ("heap size on each load: %p\n", (char *)tret - pmem);
23 free(pmem)
24 return 0;
25 }
运行结果如下:
从结果可以看出调用malloc(64)后缓冲池大小从0变成了0x20ff8,将上面的malloc(64)改成malloc(1)结果也是一样,只要malloc分配的内存数量不超过0x20ff8,缓冲池都是默认扩充0x20ff8大小。值得注意的是如果malloc一次分配的内存超过了0x20ff8,malloc不再从堆中分配空间,而是使用mmap()这个系统调用从映射区寻找可用的内存空间。
参考
http://blog.youkuaiyun.com/sgbfblog/article/details/7772153
================================================================================================================================================================================================
1,ftruncate会将参数fd指定的文件大小改为参数length指定的大小。(改变文件大小)
相关函数open,truncate
2,
C/C++ 使用mmap/munmap函数分配内存
在C/C++ 中常用的内存分配和管理的方式有很多,如智能指针, STL容器, new/delete, malloc/free, brk, sbrk等等,最近研究了一下Unix比较底层的一种内存管理方式mmap/munmap,需要完全自己来维护分配的虚拟内存,没有任何其他辅助的数据结构来帮助维护内存空间,但其优点就是效率比其他的内存分配方式要高。
一、在终端里输入 man mmap 可以查看此函数的API文档,此函数的具体描述如下:
void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);
具体参数含义
start : 指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。
length: 代表将文件中多大的部分映射到内存。
prot : 映射区域的保护方式。可以为以下几种方式的组合:
PROT_EXEC 映射区域可被执行
PROT_READ 映射区域可被读取
PROT_WRITE 映射区域可被写入
PROT_NONE 映射区域不能存取
flags : 影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标。
MAP_SHARED 对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容。
MAP_ANONYMOUS建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。
MAP_DENYWRITE只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
MAP_LOCKED 将映射区域锁定住,这表示该区域不会被置换(swap)。
fd : 要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支持匿名内存映射,则可以使用fopen打开/dev/zero文件,
然后对该文件进行映射,可以同样达到匿名内存映射的效果。
offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是PAGE_SIZE的整数倍。
返回值:
若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno 中。
错误代码:
EBADF 参数fd 不是有效的文件描述词
EACCES 存取权限有误。如果是MAP_PRIVATE 情况下文件必须可读,使用MAP_SHARED则要有PROT_WRITE以及该文件要能写入。
EINVAL 参数start、length 或offset有一个不合法。
EAGAIN 文件被锁住,或是有太多内存被锁住。
ENOMEM 内存不足。
用户层的调用很简单,其具体功能就是直接将物理内存直接映射到用户虚拟内存,使用户空间可以直接对物理空间操作。但是对于内核层而言,其具体实现比较复杂。
二、例子程序如下
-
//
-
// main.cpp
-
//
-
//
-
// Created by ChengChao on 14-9-27.
-
// Copyright (c) 2014年 cc. All rights reserved.
-
//
-
#include <iostream>
-
#include <sys/mman.h>
-
#include <unistd.h>
-
#include <stdlib.h>
-
#include <stdio.h>
-
int main(int argc, const char * argv[]) {
-
//申请内存
-
int* arr = static_cast<int*>(mmap(
-
NULL, //分配的首地址
-
getpagesize(), //分配内存大小(必须是页的整数倍, 32位1页=4k)
-
PROT_READ | PROT_WRITE, //映射区域保护权限:读|写
-
MAP_ANON | MAP_SHARED, //匿名映射(不涉及文件io), 后面两个参数忽略
-
0, //要映射到内存中的文件描述符
-
0 //文件映射的偏移量,通常设置为0,必须是页的整数倍
-
));
-
printf("申请内存大小=%dk\n", sizeof(arr));
-
*arr = 10;
-
*(arr + 1) = 20;
-
*(arr + 2) = 30;
-
printf("arr[2]=%d\n", arr[2]);
-
//释放指针arr指向的内存区域,并制定释放的内存大小
-
munmap(arr, getpagesize());
-
return 0;
-
}
我是在64位的Mac OS X系统下编译运行的,64位系统内存页的大小为8k, 32位为4k
================================================================================================================================================================================================================================================================================================
7.1.1 调整program break:brk()和sbrk()
改变堆的大小(即分配或释放内存),其实就像命令内核改变进程的program break位置一样简单。最初,program break正好位于未初始化数据段末尾之后(如图6-1所示,与&end位置相同)。
在program break的位置抬升后,程序可以访问新分配区域内的任何内存地址,而此时物理内存页尚未分配。内核会在进程首次试图访问这些虚拟内存地址时自动分配新的物理内存页。
传统的UNIX系统提供了两个操纵program break的系统调用:brk()和sbrk(),在Linux中依然可用。虽然代码中很少直接使用这些系统调用,但了解它们有助于弄清内存分配的工作过程。
系统调用brk()会将program break设置为参数end_data_segment所指定的位置。由于虚拟内存以页为单位进行分配,end_data_segment实际会四舍五入到下一个内存页的边界处。
当试图将program break设置为一个低于其初始值(即低于&end)的位置时,有可能会导致无法预知的行为,例如,当程序试图访问的数据位于初始化或未初始化数据段中当前尚不存在的部分时,就会引发分段内存访问错误(segmentation fault)(SIGSEGV信号,在20.2节描述)。program break可以设定的精确上限取决于一系列因素,这包括进程中对数据段大小的资源限制(36.3节中描述的RLIMIT_DATA),以及内存映射、共享内存段、共享库的位置。
调用sbrk()将program break在原有地址上增加从参数increment传入的大小。(在Linux中,sbrk()是在brk()基础上实现的一个库函数。)用于声明increment的intptr_t类型属于整数数据类型。若调用成功,sbrk()返回前一个program break的地址。换言之,如果program break增加,那么返回值是指向这块新分配内存起始位置的指针。
调用sbrk(0)将返回program break的当前位置,对其不做改变。在意图跟踪堆的大小,或是监视内存分配函数包的行为时,可能会用到这一用法。
SUSv2定义了brk()和sbrk(),标记为Legacy(传统)。但SUSv3删除了这些定义