现在的位置: 首页 > 综合 > 正文

Linux System Programming –Chapter Eight

2013年08月10日 ⁄ 综合 ⁄ 共 10418字 ⁄ 字号 评论关闭

内存管理

微笑一.分配动态内存的几个函数

用户空间内存分配: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(void* ptr)

free的实现原理:

操作系统在调用malloc函数时,会默认在malloc分配的物理内存前面分配一个数据结构,这个数据结构记录了这次分配内存的大小,在用户眼中这个操作是透明的。

那么当用户需要free时,free函数会把指针退回到这个结构体中,找到该内存的大小,这样就可以正确的释放内存了。


微笑二.对齐

说明:对于这部分,博主懂的也不是很多

数据的对齐(alignment)是指数据的地址和由硬件条件决定的内存块大小之间的关系。一个变量的地址是它大小的倍数的时候,这就叫做自然对齐(naturally aligned)。例如,对于一个32bit的变量,如果它的地址是4的倍数,-- 就是说,如果地址的低两位是0,那么这就是自然对齐了。所以,如果一个类型的大小是2n个字节,那么它的地址中,至少低n位是0。对齐的规则是由硬件引起的。一些体系的计算机在数据对齐这方面有着很严格的要求。在一些系统上,一个不对齐的数据的载入可能会引起进程的陷入。在另外一些系统,对不对齐的数据的访问是安全的,但却会引起性能的下降。在编写可移植的代码的时候,对齐的问题是必须避免的,所有的类型都该自然对齐。


微笑三.内存泄露

内存泄漏可能真正令人讨厌。下面的列表描述了一些导致内存泄漏的场景。

  • 重新赋值

    我将使用一个示例来说明重新赋值问题。

char *memoryArea = malloc(10);
char *newArea = malloc(10);

这向如下面的图 4 所示的内存位置赋值。



图 4. 内存位置
 

memoryArea 和 newArea 分别被分配了 10 个字节,它们各自的内容如图 4 所示。如果某人执行如下所示的语句(指针重新赋值)……

memoryArea = newArea; 

  • 则它肯定会在该模块开发的后续阶段给您带来麻烦。

    在上面的代码语句中,开发人员将 memoryArea 指针赋值给 newArea 指针。结果,memoryArea 以前所指向的内存位置变成了孤立的,如下面的图 5 所示。它无法释放,因为没有指向该位置的引用。这会导致 10 个字节的内存泄漏。

    图 5. 内存泄漏
     

    在对指针赋值前,请确保内存位置不会变为孤立的。

  • 首先释放父块

    假设有一个指针 memoryArea,它指向一个 10 字节的内存位置。该内存位置的第三个字节又指向某个动态分配的 10 字节的内存位置,如图 6 所示。

    图 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系统,那时堆和栈还在同一个段中。堆中动态存储器的分配由数据段的底部向上生长;栈从数据段的顶部向着堆向下生长。堆和栈的分界线叫做breakbreak point。在现代的系统里面,数据段存在于它自己的内存映射,我们继续用断点来标记映射的结束地址。

一个brk( )的调用设置断点(数据段的末端)的地址为end。在成功的时候,返回0。失败的时候,返回-1,并设置errnoENOMEM

一个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填充的大内存块,随时可供你使用。因为这种映射的存储不是基于堆的,所以并不会在数据段内产生碎片。

通过匿名映射来分配内存又下列好处:


•无需关心碎片。当程序不再需要这块内存的时候,只是撤销映射,这块内存就直接归还给系统了。

•匿名存储器映射能改变大小,有着改变大小的能力,还能像普通的映射一样接收命令(看第四章)。

•每个分配存在于独立的内存映射。没有必要再去管理一个全局的堆了。

下面是两个使用匿名存储器映射而不使用堆的劣处:

•每个存储器映射都是页面大小的整数倍。所以,如果大小不是页面整数倍的分配会浪费大量的空间。这些空间更值得忧虑,因为相对于被分配的空间,被浪费掉的空间往往更多。

•建立一个存储器映射比将堆里面的空间回收利用的负载更大,因为堆可能并不包含有任何的内核动作。越小的分配,这个劣处就明显。


跟变戏法似的,glibcmalloc( ) 能用用数据段来满足小的分配,用存储器映射来满足大的分配。临界点是可被设定的(看后面的高级内存分配),也有可能一个glibc版本是这样,另外一个就不是了。目前,临界点一般是128KB:比128KB小的分配由堆实现,相应地,更大的由匿名存储器映射来实现。

微笑六.基于堆栈的分配

到目前为止,我们学过的所有的动态内存分配机制都是使堆和存储器映射来实现的。我们可能觉得这么做是理所当然的,因为堆和存储器映射天生就是动态的。程序的自动变量(automatic variables)存在于地址空间中另外一个常见的结构,栈。

无论如何,实在是没有理由不让程序员使用栈来实现动态存储器的分配。只要一个分配不溢出栈外,这样的做法是很简单而完美的。如果要在一个栈中实现动态内存分配,使用系统调用alloca( ):

#include <alloca.h>
void * alloca (size_t size);

成功的时候,一个alloca( )调用会返回一个指向size字节大小的内存的指针。这块内存是在栈中的,当调用它的函数(例如main函数)返回时,这块内存将被自动释放。alloca( )的某些实现在失败时有时返回NULL,但大部分的实现没有失败的情况,或者不报告错误。其中常见的错误是栈溢出。


用法与malloc( )一样,但你不必(实际上,是不能)释放分配到的内存。这里有一个作为样例的函数,在系统配置 目录,可能是/etc, 里面打开一个给定的文件,在编译的时候就被确定了。这个函数必须申请一个新的缓冲区,复制系统配置路径到这个缓冲区里面,然后将提供的文件名拼接到缓冲区 的后面:

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);
}

在open_sysconf函数返回时,从alloca( )分配到的内存随着栈的收缩而被自动释放。这意味着当调用alloca( )的函数返回后,你不能再使用由alloca( )得到的那块内存!然而,你并不需要做任何释放工作,所以最终代码会简洁一些。这个是个用malloc( )实现的一样的函数:

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;
}

要注意的是你不能使用由alloca( )得到的内存来作为一个函数调用的参数,因为分配到的内存块会因保留参数而存在于栈的中间。例如,下面这样做是不行的:

/* DO NOT DO THIS! */
ret = foo (x, alloca (10));
alloca( )接口有着颠簸的历史。在许多系统,它表现得比较蹩脚,或者出现没被定义的行为。在栈大小较小而且是确定的系统中,使用alloca( )很容易导致栈溢出,使你的进程终止。在另外一些系统中,alloca( )甚至就不存在。随着年月增长,易产生bug和不协调的实现给了一个Over time坏名声。

所以,如果要让代码具有可移植性,你要避免使用alloca( )。然而,在Linux里,alloca( )是一个有用得出奇且没被充分使用的工具。它表现的异常出色― 在各种架构下,通过alloca( )的分配做的就和简单的增加栈指针一样少―直接得就比malloc( )好。对于Linux下较小的内存分配,alloca( )能收获让人激动的性能。

栈中的复制串

alloca( )经常被用来暂时性地复制一个字符串。例如:

/* we want to duplicate 'song' */
char *dup;
dup = alloca (strlen (song) + 1);
strcpy (dup, song);
/* manipulate 'dup'... */
return; /* 'dup' is automatically freed */

因为这种需要非常常见以及alloca( )提供的高速,Linux系统提供了一个strdup( )的变种来将一个给定的字符串复制到栈中:

#define _GNU_SOURCE
#include <string.h>
char * strdupa (const char *s);
char * strndupa (const char *s, size_t n);

一个strdupa( )的调用返回一个s的复制品。一个strndupa( )的调用返回s的前n个字节的复制品。如果s长度比n大,就复制s前n个字节,然后后面自动加上一个NULL。这些函数有着和一样的功能,当调用它的函数返回时复制品会被自动释放。POSIX并不定义alloca( ),strdupa( ),或者strndupa( )这些函数,因为他们在别的操作系统表现得劣迹斑斑。如果要考虑可移植性,这些函数是不鼓励使用的。但是,在Linux中,alloca( )和它的亲戚们表现得相当好,能得到激动人心的性能提高,仅仅用栈桢指针的调整就能代替复杂的动态存储器分配系统。


微笑七.变长数组

C99 引进了可变长数组(VLAs),可变数组的长度是在运行时决定的,而不是在编译的时候。GNUC有时候会支持可变长数组,但和C99定义的不一样,它的用处背后有着强烈的需求。VLAs用与alloca( )很相似的方法避免了动态存储的大负载。它的使用方法就跟你想象的一样:

for (i = 0; i < n; ++i) {
char foo[i + 1];
/* use 'foo'... */
}

在这个代码片段中,foo是一个由i + 1个char的数组。在每次循环的重复中,foo被动态的产生和在离开这个作用域时。如果我们使用alloca( )来代替VLA,这块内存不会被释放知道for返回。使用一个VLA确保了内存每次循环都被释放。所以,使用VLA最多使用n个字节而,而alloca( )会使用掉n*(n+1)/2个字节。使用一个变长数组,我们能够像这样重写我们的open_sysconf( )函数:

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);
}

alloca( )和变长数组的主要的不同点在于通过前者获得的内存在整个函数中都被保留着,而通过后者获得的内存除出了作用域便释放 了。这种做法好处见仁见智了。我们仅仅看for循环,在每次循环回收内存,不附带任何副作用地减少了内存的消耗(我们不再需要多余的内存放在周围了)。然 而,当由于某种原因我们想将内存保留得比单个循环长的时候,使用alloca( )会更合理。


在单个函数中混淆了alloca( )和变长数组会给程序引入怪异的行为。但还是好好享用它们带来的好处吧,使用其中的一个或者另外一个。



微笑八.操作内存

C语言提供了一系列的函数来操作内存的原始字节序列。这些函数的行为在很多方面和strcmp( )和strcpy( )等字符串操作接口相似,但它们以用户提供的缓冲区大小来分界而不是假定字符串结尾是NULL。要注意这些函数都不会返回错误信息。防范错误是程序员的责任-传递错误的内存区域作参数的话,毫无疑问,你将得到的是段错误。
字节的设置

在一系列的内存操作函数当中,最常见而简单的是memset( ):

#include <string.h>
void * memset (void *s, int c, size_t n);

调用memset( )会将地址s为开头的n个字节设为c然后返回s。常见的用法是将一块内存全设为0:

/* zero out [s,s+256) */
memset (s, '\0', 256);


bzero( )是更早的,被淘汰的接口,BSD引入它来实现一样的功能。新的代码应该使用,但Linux为了向下兼容和对其它系统的可移植性,提供了bzero( ):

#include <strings.h>
void bzero (void *s, size_t n);

下面的调用功能和先前memset( )的例子一样:

bzero (s, 256);

注意bzero( )-和其它b系列接口-需要头文件<strings.h>而不是<string.h>。

如果你能使用calloc( )不要使用memset( )!避免用malloc( )分配内存然后马上用memset( )将它清0.同样效果,单单使用一个calloc( )比使用两个函数好得多,而返回的都是清0后的内存块。不仅是少用了一个函数,还有可能calloc( )能直接从内核获得清0后的内存。在这种情况下,你不用手工地让每一个字节设为0,从而提高了效能。

字节的比较
和strcmp( )相似,memcmp( )比较两块内存是否一样:

#include <string.h>
int memcmp (const void *s1, const void *s2, size_t n);

一个调用比较s1和s2的前n字节,如果一样返回0,如果s1小于s2返回一个负值,如果s1大于s2返回一个正值。

BSD也提供了一个现在被反对的接口执行大致一样的任务:

#include <strings.h>
int bcmp (const void *s1, const void *s2, size_t n);

一个bcmp( )调用比较s1和s2的前n字节,如果两块内存就一样返回0,否则返回非0值。因为结构填充的存在(参照这章前些的“其它和对齐有关的”),通过memcmp( )或者bcmp( )来比较两个结构是否等价是不可靠的。同一个结构的两个实例也能有未初始化的垃圾内容在填充里面。因此,下面的代码是不安全的:

/* are two dinghies identical? (BROKEN) */
int compare_dinghies (struct dinghy *a, struct dinghy *b)
{
return memcmp (a, b, sizeof (struct dinghy));
}

作为替代,程序员想要比较两个结构时就应该比较结构的每一个字段,一个一个地来。这些例子应该有些优化,但它明显地比不安全的memcmp( )实现好。下面是一个等价的代码:

/* 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... */
}

移动字节

memmove( )复制src的前n字节到dst,返回dst:

#include <string.h>
void * memmove (void *dst, const void *src, size_t n);

同样,BSD提供了一个被批评的接口来实现同样的功能:

#include <strings.h>
void bcopy (const void *src, void *dst, size_t n);

注意虽然两个函数用的是同样的参数,但前两个的顺序是相反的。

bcopy( )和memmove( )在处理内存区域重叠时都是安全的(就是说,dst的一部分在src 里面)。例如,它们允许内存块在一个给定的区域内向上或下移动。由于这种情况比较少见,而且若果是这样的话,程序员应该知道。所以C标准定义了一个不支持 内存区域覆盖的memmove( )变种。这个变种可能会快一点:

#include <string.h>
void * memcpy (void *dst, const void *src, size_t n);

除了dst和 src间可能不能重叠,这个函数表现的和memmove( )一样。如真重叠了,结果是未被定义的。另外一个安全的复制函数是memccpy( ):

#include <string.h>
void * memccpy (void *dst, const void *src, int c, size_t n);

函数memccpy( )行为上和memcpy( )一样,除了函数在src的前n字节中发现了字节c。函数返回指向dst 中的c下一字节的指针,

抱歉!评论已关闭.