内存管理
一.分配动态内存的几个函数
用户空间内存分配:malloc、calloc、realloc
1、malloc原型如下:
extern void *malloc(unsigned int num_bytes);
功能:
分配长度为num_bytes字节块。
工作机制:
malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。
2、calloc原型如下:
void *calloc(unsigned n,unsigned size);
功能:
在内存的动态存储区中分配n个长度为size的连续空间。
3、realloc原型如下:
extern void *realloc(void *mem_address, unsigned int newsize);
功能:
先按照newsize指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来mem_address所指内存区域,同时返回新分配的内存区域的首地址。即重新分配存储器块的地址。
注意:malloc和calloc的区别:
calloc在动态分配完内存后,自动初始化该内存空间为零,而malloc不初始化,里边数据是随机的垃圾数据。
realloc注意事项:
a、realloc失败的时候,返回NULL。
b、realloc失败的时候,原来的内存不改变,不会释放也不会移动。
c、假如原来的内存后面还有足够多剩余内存的话,realloc的内存等于原来的内存加上剩余内存,realloc还是返回原来内存的地址; 假如原来的内存后面没有足够多剩余内存的话,realloc将申请新的内存,然后把原来的内存数据拷贝到新内存里,原来的内存将被free掉,realloc返回新内存的地址。
d、如果size为0,效果等同于free()。
e、传递给realloc的指针必须是先前通过malloc(), calloc(), 或realloc()分配的。
f、传递给realloc的指针可以为空,等同于malloc。
以上三者的事例代码如下:
#include <stdio.h> #include <malloc.h> #include <string.h> int main() { //最好每次内存申请都检查申请是否成功 //下面这段仅仅作为演示的代码没有检查 char *pt1; char *pt2; char *pt3; pt1 = (char *)malloc(sizeof(char)*10); printf("pt1 = %p\n", pt1); //以下可能会输出乱码,说明malloc分配的空间没有被初始化为0 printf("%s\n", pt1); scanf("%s", pt1); pt2 = (char *)calloc(10,sizeof(char)); printf("pt2 = %p\n", pt2); //以下输出为空,说明calloc分配的空间被初始化为0 printf("%s\n", pt2); pt3 = (char *)realloc(pt1, sizeof(char)*20); printf("pt3 = %p\n", pt3); //以下输出pt1中原先的内容。 printf("%s\n", pt3); //以下是释放申请的内存空间 free(pt2); free(pt3); return 0; }
1、calloc在动态分配完内存后,自动初始化该内存空间为零,而malloc不初始化,里边数据是随机的垃圾数据
2、realloc是给一个已经分配了地址的指针重新分配空间,参数ptr为原有的空间地址,newsize是重新申请的地址长度
3、alloca不调用free.
4、free可用于释放ptr所指向的内存。参数ptr的值必须是先前所调用的malloc(),calloc()或realloc()的返回值。也就是说,你无法通过将指针指向所分配的块的某个部门,让free释放部分内存块。
free的实现原理:
操作系统在调用malloc函数时,会默认在malloc分配的物理内存前面分配一个数据结构,这个数据结构记录了这次分配内存的大小,在用户眼中这个操作是透明的。
那么当用户需要free时,free函数会把指针退回到这个结构体中,找到该内存的大小,这样就可以正确的释放内存了。
二.对齐
说明:对于这部分,博主懂的也不是很多
数据的对齐(alignment)是指数据的地址和由硬件条件决定的内存块大小之间的关系。一个变量的地址是它大小的倍数的时候,这就叫做自然对齐(naturally aligned)。例如,对于一个32bit的变量,如果它的地址是4的倍数,-- 就是说,如果地址的低两位是0,那么这就是自然对齐了。所以,如果一个类型的大小是2n个字节,那么它的地址中,至少低n位是0。对齐的规则是由硬件引起的。一些体系的计算机在数据对齐这方面有着很严格的要求。在一些系统上,一个不对齐的数据的载入可能会引起进程的陷入。在另外一些系统,对不对齐的数据的访问是安全的,但却会引起性能的下降。在编写可移植的代码的时候,对齐的问题是必须避免的,所有的类型都该自然对齐。
三.内存泄露
内存泄漏可能真正令人讨厌。下面的列表描述了一些导致内存泄漏的场景。
我将使用一个示例来说明重新赋值问题。
char *memoryArea = malloc(10); char *newArea = malloc(10);
这向如下面的图 4 所示的内存位置赋值。
memoryArea
和 newArea
分别被分配了 10 个字节,它们各自的内容如图 4 所示。如果某人执行如下所示的语句(指针重新赋值)……
memoryArea = newArea;
则它肯定会在该模块开发的后续阶段给您带来麻烦。
在上面的代码语句中,开发人员将 memoryArea
指针赋值给 newArea
指针。结果,memoryArea
以前所指向的内存位置变成了孤立的,如下面的图 5 所示。它无法释放,因为没有指向该位置的引用。这会导致 10 个字节的内存泄漏。
在对指针赋值前,请确保内存位置不会变为孤立的。
假设有一个指针 memoryArea
,它指向一个 10 字节的内存位置。该内存位置的第三个字节又指向某个动态分配的 10 字节的内存位置,如图 6 所示。
free(memoryArea)
如果通过调用 free 来释放了 memoryArea
,则 newArea
指针也会因此而变得无效。newArea
以前所指向的内存位置无法释放,因为已经没有指向该位置的指针。换句话说,newArea
所指向的内存位置变为了孤立的,从而导致了内存泄漏。
每当释放结构化的元素,而该元素又包含指向动态分配的内存位置的指针时,应首先遍历子内存位置(在此例中为newArea
),并从那里开始释放,然后再遍历回父节点。
这里的正确实现应该为:
free( memoryArea->newArea); free(memoryArea);
有时,某些函数会返回对动态分配的内存的引用。跟踪该内存位置并正确地处理它就成为了 calling
函数的职责。
char *func ( ) { return malloc(20); // make sure to memset this location to ‘\0’… } void callingFunc ( ) { func ( ); // Problem lies here }
callingFunc()
函数中对 func()
函数的调用未处理该内存位置的返回地址。结果,func()
函数所分配的 20 个字节的块就丢失了,并导致了内存泄漏。
四.数据段的管理
Unix系统在历史上提供过直接管理数据段的接口。然而,程序都没有直接地使用这些接口,因为malloc( )和其它的申请方法更容易使用和更加强大。我会在这里说一下这些接口来满足一下大家的好奇心,同时也给那些想实现他自己的基于堆栈的动态内存申请机制的人一个参考:
#include <unistd.h>int brk (void *end);void * sbrk (intptr_t increment);这些功能的名字源于老学校的Unix系统,那时堆和栈还在同一个段中。堆中动态存储器的分配由数据段的底部向上生长;栈从数据段的顶部向着堆向下生长。堆和栈的分界线叫做break或break point。在现代的系统里面,数据段存在于它自己的内存映射,我们继续用断点来标记映射的结束地址。
一个brk( )的调用设置断点(数据段的末端)的地址为end。在成功的时候,返回0。失败的时候,返回-1,并设置errno为ENOMEM。
一个sbrk( )的调用将数据段末端生长increment字节,increment可能是正数,也可能是负数。sbrk( )返回修改后的断点。所以,increment为0时得到的是现在断点的地址:
printf ("The current break point is %p\n", sbrk (0));特意地,POSIX和C都没有定义这些函数。但几乎所有的Unix系统,都提供其中一个或全部。可移植的程序应该坚持使用基于标准的接口。
五.匿名内存映射
glibc的动态存储器使用了数据段和内存映射。实现malloc( )的经典方法是将数据段分为一系列的大小为2的幂的分区,返回最小的符合要求的那个块来满足请求。释放内存就像免费的似的和标记内存一样简单了。如果临近的分区是空闲的,他们会被合成一个更大的分区。如果断点的下面是空的,系统可以用brk( )来降低断点,使堆收缩,将内存返回给系统。
这个算法叫做伙伴内存分配算法(buddy memory allocation scheme)。它的优势是高速和简单,但不好的地方是引入了两种碎片。内部碎片(Internal fragmentation)发生在用更大的块来满足一个分配。这样导致了内存的低使用率。当有着足够的空闲内存来满足要求但这“块”内存分布在两个不相邻空间的时候,外部碎片(External fragmentation)就产生了。这会导致内存的低使用率(因为一块更大的不够适合的块可能被使用了),或者内存分配失败(在没有可供选择的块时)。
更有甚者,这个算法允许一个内存的分配“钉”住另外一个,使得glibc不能向内核归还内存。想象内存中的已被分配的两个块,块A和块B。块A刚好在断点的下面,块B刚好在A的下面,就算释放了B,glibc也不能相应的调整断点直到A被释放。在这种情况,一个长期存在的内存分配就把另外的空闲空间“钉”住了。
但这不需太过担忧。因为glibc无论如何也不会总例行公事一成不变地将内存返回给系统。*通常来说,在每次释放后堆并不收缩。相反,glibc为后续的分配保留着些自由的空间。只有在堆与已分配的空间相比明显太大的时候,glibc才会把堆缩小。然而,一个更大的分配,就能防止这个收缩了。
*glibc也使用比这伙伴系统更加先进的存储分配算法,叫做arena algorithm.因此,对于较大的分配,glibc并不使用堆。glibc使用一个匿名存储器映射(anonymous memory mapping)来满足请求。匿名存储器映射和在第四章讨论的基于文件的映射是相似的,只是它并不基于文件-所以为之“匿名”。实际上,匿名存储器映射是一个简单的全0填充的大内存块,随时可供你使用。因为这种映射的存储不是基于堆的,所以并不会在数据段内产生碎片。
通过匿名映射来分配内存又下列好处:
•无需关心碎片。当程序不再需要这块内存的时候,只是撤销映射,这块内存就直接归还给系统了。
•匿名存储器映射能改变大小,有着改变大小的能力,还能像普通的映射一样接收命令(看第四章)。
•每个分配存在于独立的内存映射。没有必要再去管理一个全局的堆了。
下面是两个使用匿名存储器映射而不使用堆的劣处:
•每个存储器映射都是页面大小的整数倍。所以,如果大小不是页面整数倍的分配会浪费大量的空间。这些空间更值得忧虑,因为相对于被分配的空间,被浪费掉的空间往往更多。
•建立一个存储器映射比将堆里面的空间回收利用的负载更大,因为堆可能并不包含有任何的内核动作。越小的分配,这个劣处就明显。
跟变戏法似的,glibc的malloc( ) 能用用数据段来满足小的分配,用存储器映射来满足大的分配。临界点是可被设定的(看后面的高级内存分配),也有可能一个glibc版本是这样,另外一个就不是了。目前,临界点一般是128KB:比128KB小的分配由堆实现,相应地,更大的由匿名存储器映射来实现。
六.基于堆栈的分配
到目前为止,我们学过的所有的动态内存分配机制都是使堆和存储器映射来实现的。我们可能觉得这么做是理所当然的,因为堆和存储器映射天生就是动态的。程序的自动变量(automatic variables)存在于地址空间中另外一个常见的结构,栈。
无论如何,实在是没有理由不让程序员使用栈来实现动态存储器的分配。只要一个分配不溢出栈外,这样的做法是很简单而完美的。如果要在一个栈中实现动态内存分配,使用系统调用alloca( ):
#include <alloca.h> void * alloca (size_t size);
int open_sysconf (const char *file, int flags, int mode) { const char *etc = SYSCONF_DIR; /* "/etc/" */ char *name; name = alloca (strlen (etc) + strlen (file) + 1); strcpy (name, etc); strcat (name, file); return open (name, flags, mode); }
int open_sysconf (const char *file, int flags, int mode) { const char *etc = SYSCONF_DIR; /* "/etc/" */ char *name; int fd; name = malloc (strlen (etc) + strlen (file) + 1); if (!name) { perror ("malloc"); return -1; } strcpy (name, etc); strcat (name, file); fd = open (name, flags, mode); free (name); return fd; }
/* we want to duplicate 'song' */ char *dup; dup = alloca (strlen (song) + 1); strcpy (dup, song); /* manipulate 'dup'... */ return; /* 'dup' is automatically freed */
#define _GNU_SOURCE #include <string.h> char * strdupa (const char *s); char * strndupa (const char *s, size_t n);
七.变长数组
C99 引进了可变长数组(VLAs),可变数组的长度是在运行时决定的,而不是在编译的时候。GNUC有时候会支持可变长数组,但和C99定义的不一样,它的用处背后有着强烈的需求。VLAs用与alloca( )很相似的方法避免了动态存储的大负载。它的使用方法就跟你想象的一样:
for (i = 0; i < n; ++i) { char foo[i + 1]; /* use 'foo'... */ }
int open_sysconf (const char *file, int flags, int mode) { const char *etc; = SYSCONF_DIR; /* "/etc/" */ char name[strlen (etc) + strlen (file) + 1]; strcpy (name, etc); strcat (name, file); return open (name, flags, mode); }
C语言提供了一系列的函数来操作内存的原始字节序列。这些函数的行为在很多方面和strcmp( )和strcpy( )等字符串操作接口相似,但它们以用户提供的缓冲区大小来分界而不是假定字符串结尾是NULL。要注意这些函数都不会返回错误信息。防范错误是程序员的责任-传递错误的内存区域作参数的话,毫无疑问,你将得到的是段错误。
字节的设置
在一系列的内存操作函数当中,最常见而简单的是memset( ):
#include <string.h> void * memset (void *s, int c, size_t n);
/* zero out [s,s+256) */ memset (s, '\0', 256);
#include <strings.h> void bzero (void *s, size_t n);
bzero (s, 256);
#include <string.h> int memcmp (const void *s1, const void *s2, size_t n);
#include <strings.h> int bcmp (const void *s1, const void *s2, size_t n);
/* are two dinghies identical? (BROKEN) */ int compare_dinghies (struct dinghy *a, struct dinghy *b) { return memcmp (a, b, sizeof (struct dinghy)); }
/* are two dinghies identical? */ int compare_dinghies (struct dinghy *a, struct dinghy *b) { int ret; if (a->nr_oars < b->nr_oars) return -1; if (a->nr_oars > b->nr_oars) return 1; ret = strcmp (a->boat_name, b->boat_name); if (ret) return ret; /* and so on, for each member... */ }
#include <string.h> void * memmove (void *dst, const void *src, size_t n);
#include <strings.h> void bcopy (const void *src, void *dst, size_t n);
#include <string.h> void * memcpy (void *dst, const void *src, size_t n);
#include <string.h> void * memccpy (void *dst, const void *src, int c, size_t n);
#define _GNU_SOURCE #include <string.h> void * mempcpy (void *dst, const void *src, size_t n);
搜索字节
函数memchr( )和memrchr( )在一个内存块里面搜索一个给定的字节:
#include <string.h> void * memchr (const void *s, int c, size_t n);
#define _GNU_SOURCE #include <string.h> void * memrchr (const void *s, int c, size_t n);
#define _GNU_SOURCE #include <string.h> void * memmem (const void *haystack, size_t haystacklen, const void *needle, size_t needlelen);
#define _GNU_SOURCE #include <string.h> void * memfrob (void *s, size_t n);
#include <sys/mman.h> int mlock (const void *addr, size_t len);
合法的errno包括:
EINVAL
参数 len 是负数。
ENOMEM
函数要锁定的页面数比RLIMIT_MEMLOCK限制的要多(看下面一节“锁定的限制”).
EPERM
RLIMIT_MEMLOCK是0,但进程并没有CAP_IPC_LOCK权限。(再次,看“锁定的限制”)。
一个由fork( )产生的子进程并不继承锁定的内存。然而,由于Linux对地址空间的写时复制,一个子进程的页面被有效地锁定在内存中直到子进程对它们执行写操作。
作为例子,假设程序内存中包含着未加密地串。包含下面代码的进程能锁住页面:
int ret; /* lock 'secret' in memory */ ret = mlock (secret, strlen (secret)); if (ret) perror ("mlock");
#include <sys/mman.h> int mlockall (int flags);
#include <sys/mman.h> int munlock (const void *addr, size_t len); int munlockall (void);系统调用munlock( )解除addr开始长为len的内存所在的页面地锁定。它是mlock( )的逆函数。系统调用munlockall( )是mlockall( )的逆函数。两个函数在成功时都返回0,失败时返回-1,像如下设置errno:
#include <unistd.h> #include <sys/mman.h> int mincore (void *start, size_t length, unsigned char *vec);