在linux中,4g内存可分为两部分——内核空间1g(3~4g)与用户空间3g(0~3g),我们通常写的c代码都是在对用户空间即0~3g的内存进行操作。而且,用户空间的代码不能直接访问内核空间,因此内核空间提供了一系列的函数,实现用户空间进入内核空间的接口,这一系列的函数称为系统调用(system call)。比如我们经常使用的open、close、read、write等函数都是系统级别的函数(man 2 function_name),而像fopen、fclose、fread、fwrite等都是用户级别的函数(man 3 function_name)。不同级别的函数能够操作的内存区域自然也就不同。
我们用一幅图来描述函数的调用过程:
对于c 中new与delete的底层则是用malloc和free实现。而我们所用的malloc()、free()与内核之间的接口(桥梁)就是sbrk()等系统函数;当然我们也可以直接调用系统调用(系统函数),达到同样的作用。我们可以用下面这幅图来描述基本内存相关操作之间的关系:
虽然使用系统调用会带来一定的好处,但是物极必反,系统调用并非能频繁使用。由于程序由用户进入内核层时,会将用户层的状态先封存起来,然后到内核层运行代码,运行结束以后,从内核层出来到用户层时,再把数据加载回来。因此,频繁的系统调用效率很低。今天我们就系统调用层面来对内存操作做进一步的了解。
1、brk()与sbrk():
(1)、函数原型与实现:
//函数原型:
#include
int brk(void * addr);
void * sbrk(intptr_t increment);
由于sbrk()与brk()这两个系统函数有点所谓怪异,我们先来看看man手册对于sbrk()与brk()的描述:
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; decreasing 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
setrlimit(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.
on success, sbrk() returns the previous program break. (if the break
was increased, then this value is a pointer to the start of the newly
allocated memory). on error, (void *) -1 is returned, and errno is
set to enomem.
描述:
brk()和sbrk()改变程序间断点的位置。程序间断点就是程序数据段的结尾。(程序间断点是为初始化数据段的起始位置).通过增加程序间断点进程可以更有效的申请内存 。当addr参数合理、系统有足够的内存并且不超过最大值时brk()函数将数据段结尾设置为addr,即间断点设置为addr。sbrk()将程序数据空间增加increment字节。当increment为0时则返回程序间断点的当前位置。
返回值:
brk()成功返回0,失败返回-1并且设置errno值为enomem(注:在mmap中会提到)。
sbrk()成功返回之前的程序间断点地址。如果间断点值增加,那么这个指针(指的是返回的之前的间断点地址)是指向分配的新的内存的首地址。如果出错失败,就返回一个指针并设置errno全局变量的值为enomem。
总结:
这两个函数都用来改变 “program break” (程序间断点)的位置,改变数据段长度(change data segment size),实现虚拟内存到物理内存的映射。
brk()函数直接修改有效访问范围的末尾地址实现分配与回收。sbrk()参数函数中:当increment为正值时,间断点位置向后移动increment字节。同时返回移动之前的位置,相当于分配内存。当increment为负值时,位置向前移动increment字节,相当与于释放内存,其返回值没有实际意义。当increment为0时,不移动位置只返回当前位置。参数increment的符号决定了是分配还是回收内存。而关于program break的位置如图所示:
(2)、简单测试:
对于分配好的内存,我们只要有其首地址old与长度max*max即可不越界的准确使用(如下图所示),其效果与malloc相同,只不过sbrk()与brk()是c标准函数的底层实现而已,其机制较为复杂(测试中,死循环是为了查看maps文件,不至于进程消亡文件随之消失)。
虽然,sbrk()与brk()均可分配回收兼职,但是我们一般用sbrk()分配内存,而用brk()回收内存,上例中回收内存可以这样写:
int err = brk(old);
/**或者brk(p);效果与sbrk(-max*max);是一样的,但brk()更方便与清晰明了。**/
if(-1 == err){
perror("brk");
exit(exit_failure);
}
2、mmap()与munmap():
mmap函数(地址映射):mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零(linux堆空间未使用内存均清零)。这里我们只研究mmap的内存映射,而暂时不讨论文件方面的问题。关于mmap的文件映射的更详细的内容可参考认真分析mmap:是什么 为什么 怎么用
//函数原型:
#incldue
void * mmap(void * addr, size_t length,int prot,int flags,int fd,off_t offset);
参数:
(1)、addr:
起始地址,置零让系统自行选择并返回即可.
(2)、length:
长度,不够一页会自动凑够一页的整数倍,我们可以宏定义#define min_length_mmap 4096为一页大小
(3)、prot:
读写操作权限,prot_read可读、prot_write可写、prot_exec可执行、prot_none映射区域不能读取。(注意prot_xxxxx与文件本身的权限不冲突,如果在程序中不设定任何权限,即使本身存在读写权限,该进程也不能对其操作)
(4)、flags常用标志:
①map_shared【share this mapping】、map_private【create a private copy-on-write mapping】
map_shared只能设置文件共享,不能地址共享,即使设置了共享,对于两个进程来说,也不会生效。而map_private则对于文件与内存都可以设置为私有。
②map_anon【deprecated】、map_anonymous:匿名映射,如果映射地址需要加该参数,如果不加默认映射文件。map_anon已经过时,只需使用map_anonymous即可
(5)、文件描述符:fd
(6)、文件描述符偏移量:offset
(fd和offset对于一般性内存分配来说设置为0即可)返回值:
失败返回map_failed,即(void * (-1))并设置errno全局变量。
成功返回指向mmap area的指针pointer。常见errno错误:
①enomem:内存不足;
②eagain:文件被锁住或有太多内存被锁住;
③ebadf:参数fd不是有效的文件描述符;
④eacces:存在权限错误,。如果是map_private情况下文件必须可读;使用map_shared则文件必须能写入,且设置prot权限必须为prot_write。
⑤einval:参数addr、length或者offset中有不合法参数存在。
munmap函数:解除映射关系
int munmap(void * addr, size_t length);//addr为mmap函数返回接收的地址,length为请求分配的长度。
这张图描述了mmap内存地址映射的位置关系(栈区以上为内核空间)。关于这一点我们可以作以简单的测试(我采用min_length_mmap宏,当然你也可以用多少申请多少,系统总是以最小1页来映射的,关于内存分页与虚拟地址映射可参考linux系统内存管理与内存分页机制 ):
mmap映射的地址处于堆区与栈区中间,malloc映射的堆区内存为33页(最小映射大小),而mmap映射的内存为3页,也是4096b的整数倍。