内存管理

Linux内存管理

前一段时间看了《深入理解Linux内核》对其中的内存管理部分花了不少时间,但是还是有很多问题不是很清楚,最近又花了一些时间复习了一下,在这里记录下自己的理解和对Linux中内存管理的一些看法和认识。

我比较喜欢搞清楚一个技术本身的发展历程,简而言之就是这个技术是怎么发展而来的,在这个技术之前存在哪些技术,这些技术有哪些特点,为什么会被目前的技术所取代,而目前的技术又解决了之前的技术所存在的哪些问题。弄清楚了这些,我们才能比较清晰的把握某一项技术。有些资料在介绍某个概念的时候直接就介绍这个概念的意义,原理,而对其发展过程和背后的原理丝毫不提,仿佛这个技术从天上掉下来的一样。介于此,还是以内存管理的发展历程来讲述今天的主题。

首先,我必须要阐述一下这篇文章的主题是Linux内存管理中的分段和分页技术。

让我们来回顾一下历史,在早期的计算机中,程序是直接运行在物理内存上的。换句话说,就是程序在运行的过程中访问的都是物理地址。如果这个系统只运行一个程序,那么只要这个程序所需的内存不要超过该机器的物理内存就不会出现问题,我们也就不需要考虑内存管理这个麻烦事了,反正就你一个程序,就这么点内存,吃不吃得饱那是你的事情了。然而现在的系统都是支持多任务,多进程的,这样CPU以及其他硬件的利用率会更高,这个时候我们就要考虑到将系统内有限的物理内存如何及时有效的分配给多个程序了,这个事情本身我们就称之为内存管理。

下面举一个早期的计算机系统中,内存分配管理的例子,以便于大家理解。

加入我们有三个程序,程序1,2,3.程序1运行的过程中需要10M内存,程序2运行的过程中需要100M内存,而程序3运行的过程中需要20M内存。如果系统同时需要运行程序A和B,那么早期的内存管理过程大概是这样的,将物理内存的前10M分配给A, 接下来的10M-110M分配给B。这种内存管理的方法比较直接,好了,假设我们这个时候想让程序C也运行,同时假设我们系统的内存只有128M,显然按照这种方法程序C由于内存不够是不能够运行的。大家知道可以使用虚拟内存的技术,内存空间不够的时候可以将程序不需要用到的数据交换到磁盘空间上去,已达到扩展内存空间的目的。下面我们来看看这种内存管理方式存在的几个比较明显的问题。就像文章一开始提到的,要很深层次的把握某个技术最好搞清楚其发展历程。

 

1. 进程地址空间不能隔离

 

由于程序直接访问的是物理内存,这个时候程序所使用的内存空间不是隔离的。举个例子,就像上面说的A的地址空间是0-10M这个范围内,但是如果A中有一段代码是操作10M-128M这段地址空间内的数据,那么程序B和程序C就很可能会崩溃(每个程序都可以系统的整个地址空间)。这样很多恶意程序或者是木马程序可以轻而易举的破快其他的程序,系统的安全性也就得不到保障了,这对用户来说也是不能容忍的。

 

2.   内存使用的效率低

 

如上面提到的,如果我们要像让程序A、B、C同时运行,那么唯一的方法就是使用虚拟内存技术将一些程序暂时不用的数据写到磁盘上,在需要的时候再从磁盘读回内存。这里程序C要运行,将A交换到磁盘上去显然是不行的,因为程序是需要连续的地址空间的,程序C需要20M的内存,而A只有10M的空间,所以需要将程序B交换到磁盘上去,而B足足有100M,可以看到为了运行程序C我们需要将100M的数据从内存写到磁盘,然后在程序B需要运行的时候再从磁盘读到内存,我们知道IO操作比较耗时,所以这个过程效率将会十分低下。

 

3. 程序运行的地址不能确定

 

程序每次需要运行时,都需要在内存中非配一块足够大的空闲区域,而问题是这个空闲的位置是不能确定的,这会带来一些重定位的问题,重定位的问题确定就是程序中引用的变量和函数的地址,如果有不明白童鞋可以去查查编译原理方面的资料。

内存管理无非就是想办法解决上面三个问题,如何使进程的地址空间隔离,如何提高内存的使用效率,如何解决程序运行时的重定位问题?

这里引用计算机界一句无从考证的名言:“计算机系统里的任何问题都可以靠引入一个中间层来解决。”

现在的内存管理方法就是在程序和物理内存之间引入了虚拟内存这个概念。虚拟内存位于程序和物理内存之间,程序只能看见虚拟内存,再也不能直接访问物理内存。每个程序都有自己独立的进程地址空间,这样就做到了进程隔离。这里的进程地址空间是指虚拟地址。顾名思义既然是虚拟地址,也就是虚的,不是现实存在的地址空间。

既然我们在程序和物理地址空间之间增加了虚拟地址,那么就要解决怎么从虚拟地址映射到物理地址,因为程序最终肯定是运行在物理内存中的,主要有分段和分页两种技术。

分段(Segmentation):这种方法是人们最开始使用的一种方法,基本思路是将程序所需要的内存地址空间大小的虚拟空间映射到某个
物理地址空间。

               段映射机制



每个程序都有其独立的虚拟的独立的进程地址空间,可以看到程序A和B的虚拟地址空间都是从0x00000000开始的。我们将两块大小相同的虚拟地址空间和实际物理地址空间一一映射,即虚拟地址空间中的每个字节对应于实际地址空间中的每个字节,这个映射过程由软件来设置映射的机制,实际的转换由硬件来完成。

这种分段的机制解决了文章一开始提到的3个问题中的进程地址空间隔离和程序地址重定位的问题。程序A和程序B有自己独立的虚拟地址空间,而且该虚拟地址空间被映射到了互相不重叠的物理地址空间,如果程序A访问虚拟地址空间的地址不在0x00000000-0x00A00000这个范围内,那么内核就会拒绝这个请求,所以它解决了隔离地址空间的问题。我们应用程序A只需要关心其虚拟地址空间0x00000000-0x00A00000,而其被映射到哪个物理地址我们无需关心,所以程序永远按照这个虚拟地址空间来放置变量,代码,不需要重新定位。

无论如何分段机制解决了上面两个问题,是一个很大的进步,但是对于内存效率问题仍然无能为力。因为这种内存映射机制仍然是以程序为单位,当内存不足时仍然需要将整个程序交换到磁盘,这样内存使用的效率仍然很低。那么,怎么才算高效率的内存使用呢。事实上,根据程序的局部性运行原理,一个程序在运行的过程当中,在某个时间段内,只有一小部分数据会被经常用到。所以我们需要更加小粒度的内存分割和映射方法,此时是否会想到Linux中的Buddy算法和slab内存分配机制呢,哈哈。另一种将虚拟地址转换为物理地址的方法分页机制应运而生了。

分页机制:

分页机制就是把内存地址空间分为若干个很小的固定大小的页,每一页的大小由内存决定,就像Linux中ext文件系统将磁盘分成若干个Block一样,这样做是分别是为了提高内存和磁盘的利用率。试想以下,如果将磁盘空间分成N等份,每一份的大小(一个Block)是1M,如果我想存储在磁盘上的文件是1K字节,那么其余的999K字节是不是浪费了。所以需要更加细粒度的磁盘分割方式,我们可以将Block设置得小一点,这当然是根据所存放文件的大小来综合考虑的,好像有点跑题了,我只是想说,内存中的分页机制跟ext文件系统中的磁盘分割机制非常相似。

Linux中一般页的大小是4KB,我们把进程的地址空间按页分割,把常用的数据和代码页装载到内存中,不常用的代码和数据保存在磁盘中,我们还是以一个例子来说明,如下图:


进程虚拟地址空间、物理地址空间和磁盘之间的页映射关系

我们可以看到进程1和进程2的虚拟地址空间都被映射到了不连续的物理地址空间内(这个意义很大,如果有一天我们的连续物理地址空间不够,但是不连续的地址空间很多,如果没有这种技术,我们的程序就没有办法运行),甚至他们共用了一部分物理地址空间,这就是共享内存。

进程1的虚拟页VP2和VP3被交换到了磁盘中,在程序需要这两页的时候,Linux内核会产生一个缺页异常,然后异常管理程序会将其读到内存中。

这就是分页机制的原理,当然Linux中的分页机制的实现还是比较复杂的,通过了也全局目录,也上级目录,页中级目录,页表等几级的分页机制来实现的,但是基本的工作原理是不会变的。

分页机制的实现需要硬件的实现,这个硬件名字叫做MMU(Memory Management Unit),他就是专门负责从虚拟地址到物理地址转换的,也就是从虚拟页找到物理页。

 

 

参考文献:

《深入理解Linux内核》

《程序员的自我修养》

 

 

C语言内存管理

对于一个C语言程序而言,内存空间主要由五个部分组成代码段(.text)、数据段(.data)BSS(.bss),堆和栈组成,其中代码段,数据段和BSS段是编译的时候由编译器分配的,而堆和 栈是程序运行的时候由系统分配的。布局如下

 

 

在上图中,由编译器分配的地址空间都是在连接的时候分配的,而运行时分配的空间是在程序运行时由系统分配的

 

BSS段:BSS段(bsssegment)通常是指用来存放程序中未初始化的全局变量和静态变量(这里注意一个问题:一般的书上都会说全局变量和静态变量是会自动初始化的,那么哪来的未初始化的变量呢?变量的初始化可以分为显示初始化和隐式初始化,全局变量和静态变量如果程序员自己不初始化的话的确也会被初始化,那就是不管什么类型都初始化为0,这种没有显示初始化的就是我们这里所说的未初始化。既然都是0那么就没必要把每个0都存储起来,从而节省磁盘空间,这是BSS的主要作用)的一块内存区域。BSS是英文BlockStarted by Symbol的简称。BSS段属于静态内存分配。 BSS节不包含任何数据,只是简单的维护开始和结束的地址,即总大小,以便内存区能在运行时分配并被有效地清零。BSS节在应用程序的二进制映象文件中并不存在,即不占用磁盘空间 而只在运行的时候占用内存空间 ,所以如果全局变量和静态变量未初始化那么其可执行文件要小很多。
  

数据段:数据段(datasegment)通常是指用来存放程序中已初始化的全局变量和静态变量的一块内存区域。数据段属于静态内存分配,可以分为只读数据段和读写数据段。 字符串常量等,但一般都是放在只读数据段中 
  

代码段:代码段(codesegment/text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等,但一般都是放在只读数据段中  
  

堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减) 
  

(stack):栈又称堆栈,是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。注意:栈空间是向下增长的,每个线程有一个自己的栈,在linux上默认的大小是8M,可以用ulimit查看和修改。

栈系统提供的功能,特点是快速高效,缺点是有限制,数据不灵活;而堆是函数库提供的功能,特点是灵活方便,数据适应面广泛,但是效率有一定降低。

 

 

真正认识 realloc 的工作方式。

Posted on 2008-11-20 13:12 啊夏 阅读(142) 评论(0)  编辑 收藏 网摘 所属分类c/c++ 

realloc 用过很多次了。无非就是将已经存在的一块内存扩大。

char* p =malloc(1024);
char* q = realloc(p,2048);

现在的问题是我们应该如何处理指针 p刚开始按照我最直观的理解,如果就是直接将 p = NULL;到最后只需要释放 q的空间就可以了。

因为最近在做个封装。结果在做单元测试的时候发现。有时候我在 free(q); 的时候会出错。这样我就郁闷了。

后来仔细一跟踪,发现 realloc 完以后 q p 的指针地址是一样。不过有时候又不一样。

仔细查了下资料。得到如下信息:

      1.如果当前连续内存块足够 realloc 的话,只是将p所指向的空间扩大,并返回p的指针地址。这个时候 q p 指向的地址是一样的。

       2.如果当前连续内存块不够长度,再找一个足够长的地方,分配一块新的内存,q,并将 p指向的内容 copy q,返回 q。并将p所指向的内存空间删除。

这样也就是说 realloc 有时候会产生一个新的内存地址有的时候不会。所以在分配完成后。我们需要判断下 p 是否等于 q。并做相应的处理。

这里有点要注意的是要避免 p = realloc(p,2048); 这种写法。有可能会造成 realloc 分配失败后,p原先所指向的内存地址丢失。

 

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

关于realloc函数说明的补充:
函数定义:
void *realloc(void *ptr, size_t size);
上面的分析基本没有问题,但有两点要注意:
1.
返回值可能与ptr的值不同,如果是不同的话,那么realloc函数完成后,ptr指向的旧内存已被free掉了。
2
。如果返回NULL值,则分配不成功,而原来的ptr指向的内存还没有被free掉,要求程序显式free.

p = (int *) realloc (p, sizeof(int) *15);语句有这么一个问题,
调用前p指向一个已分配成功的内存,而调用realloc时却失败(即返回NULL),此时,p原来指向的内存还没有free掉,而现在又找不到地址,这样就出现memory leak了。

关于这一点的确要注意,最好如下:
int *q
q = (int *) realloc (p, sizeof(int) *15);

if(!q) p =q;

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

首先看一下下面的C程序片断:

 

#include <malloc.h>

char  *p;

p = (char * ) malloc (10);

p = (char * ) realloc(p,20);

…………………………

 

    这段程序的意思很简单,只有稍有点C基础的人都可以看懂。函数首先定义了一个字符型的指针p,然后为指针p分配了一个10个字节大小的内存空间,接着将这个内存块的大小增加到20个字节。

 

    这里有什么问题吗?上机运行一下,好像没有问题!

 

    是的,这样上机运行是没有问题的,但是这里存在着也许我们不太注意的隐患!隐患在那里?这就是我在本文中要详细说明的realloc()函数了。

 

    再看一下下面一段来自MSDN的话:

realloc returns a void pointerto the reallocated (and possibly moved) memory block. The return value isNULL ifthe size is zero and the buffer argument is not NULL, or if thereis not enough available memory to expand the block to the given size. In thefirst case, the original block is freed. In the second, the original block isunchanged. The return value points to a storage space that is guaranteed to besuitably aligned for storage of any type of object. To get a pointer to a typeother than void, use a type cast on the return value.

这段E文还不算是晦涩难懂,所以我就不翻译了,大致的意思是说关于realloc返回值的。但是这里对他的返回值分了几种情况:

1  返回void * 指针,调用成功。

2  返回NULL,当需要扩展的大小(第二个参数)为0并且第一个参数不为NULL,此时原内存变成了“freed(游离)”的了。

3  返回NULL,当没有足够的空间可供扩展的时候,此时,原内存空间的大小维持不变。

 

第一种情况告诉了我们在得到需要的内存空间后需要做类型转换的工作;

第二种情况可能只有傻瓜才会去使用吧!

第三种情况,内存空间不够的时候就会维持未来的大小不变。

 

        MSDN上面说内存空间不够的时候就不会扩展原来的内存空间的大小,这话固然没有错,但是有点含糊,似乎遗漏了一种情况!我们知道,realloc是从堆上分配内存的,当扩大一块内存空间时, realloc()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,自然天下太平;可如果数据后面的字节不够的话,问题就出来了,那么就使用堆上第一个有足够大小的自由块,现存的数据然后就被拷贝至新的位置,而老块则放回到堆上。这句话传递的一个重要的信息就是数据可能被移动!看到这里,也许我们已经发现一开始我给出的程序的问题了。为了更清楚地说明问题,可以将上面的程序改成下面的形式:

 

#include <malloc.h>

char  *p*q;

p = (char * ) malloc (10);

q=p;

p = (char * ) realloc(p,20);

…………………………

 

    这段程序也许在编译器中没有办法通过,因为编译器可能会为我们消除一些隐患!在这里我们只是增加了一个记录原来内存地址的指针q,然后记录了原来的内存地址p,如果不幸的话,数据发生了移动,那么所记录的原来的内存地址q所指向的内存空间实际上已经放回到堆上了!这样一来,我们应该终于意识到问题的所在和可怕了吧!

 

    这个问题似乎有点牛角尖的味道,因为我们也许从来不曾遇上过,但是我们应该明白这样的事情的始终存在,只有这样,在万一我们碰上的时候才会去有意识的去避免这种隐患,否则,一旦这样的隐患一旦发作,程序崩溃不说,恐怕查错也不是一件容易的事!

 

C语言中常见的内存错误

一、指针没有指向一块合法的内存

定义了指针变量,但是没有为指针分配内存,即指针没有指向一块合法的内存。浅显的例子就不举了,这里举几个比较隐蔽的例子。

1、结构体成员指针未初始化
struct student
{
   char *name;
   int score;
}stu,*pstu;
intmain()
{
   strcpy(stu.name,"Jimy");
   stu.score = 99;
   return 0;
}
很多初学者犯了这个错误还不知道是怎么回事。这里定义了结构体变量stu,但是他没想到这个结构体内部char *name 这成员在定义结构体变量stu 时,只是给name 这个指针变量本身分配了4 个字节。name 指针并没有指向一个合法的地址,这时候其内部存的只是一些乱码。所以在调用strcpy函数时,会将字符串"Jimy"往乱码所指的内存上拷贝,而这块内存name 指针根本就无权访问,导致出错。解决的办法是为name 指针malloc 一块空间。

同样,也有人犯如下错误:
intmain()
{
   pstu = (struct student*)malloc(sizeof(struct student));
   strcpy(pstu->name,"Jimy");
   pstu->score = 99;
   free(pstu);
   return 0;
}
为指针变量pstu 分配了内存,但是同样没有给name 指针分配内存。错误与上面第一种情况一样,解决的办法也一样。这里用了一个malloc 给人一种错觉,以为也给name 指针分配了内存。

2、没有为结构体指针分配足够的内存
intmain()
{
   pstu = (struct student*)malloc(sizeof(struct student*));
   strcpy(pstu->name,"Jimy");
   pstu->score = 99;
   free(pstu);
   return 0;
}
为pstu 分配内存的时候,分配的内存大小不合适。这里把sizeof(structstudent)误写为sizeof(struct student*)。当然name 指针同样没有被分配内存。解决办法同上。

3、函数的入口校验
不管什么时候,我们使用指针之前一定要确保指针是有效的。

一般在函数入口处使用assert(NULL != p)对参数进行校验。在非参数的地方使用if(NULL != p)来校验。但这都有一个要求,即p 在定义的同时被初始化为NULL 了。比如上面的例子,即使用if(NULL != p)校验也起不了作用,因为name 指针并没有被初始化为NULL,其内部是一个非NULL 的乱码。

assert 是一个宏,而不是函数,包含在assert.h 头文件中。如果其后面括号里的值为假,则程序终止运行,并提示出错;如果后面括号里的值为真,则继续运行后面的代码。这个宏只在Debug 版本上起作用,而在Release 版本被编译器完全优化掉,这样就不会影响代码的性能。

有人也许会问,既然在Release 版本被编译器完全优化掉,那Release版本是不是就完全没有这个参数入口校验了呢?这样的话那不就跟不使用它效果一样吗?

是的,使用assert 宏的地方在Release 版本里面确实没有了这些校验。但是我们要知道,assert 宏只是帮助我们调试代码用的,它的一切作用就是让我们尽可能的在调试函数的时候把错误排除掉,而不是等到Release 之后。它本身并没有除错功能。再有一点就是,参数出现错误并非本函数有问题,而是调用者传过来的实参有问题。assert 宏可以帮助我们定位错误,而不是排除错误。

二、为指针分配的内存太小

为指针分配了内存,但是内存大小不够,导致出现越界错误。
   char *p1 = “abcdefg”;
   char *p2 = (char *)malloc(sizeof(char)*strlen(p1));
   strcpy(p2,p1);
p1 是字符串常量,其长度为7 个字符,但其所占内存大小为8 个byte。初学者往往忘了字符串常量的结束标志“\0”。这样的话将导致p1 字符串中最后一个空字符“\0”没有被拷贝到p2 中。解决的办法是加上这个字符串结束标志符:
   char *p2 = (char *)malloc(sizeof(char)*strlen(p1)+1*sizeof(char));
这里需要注意的是,只有字符串常量才有结束标志符。比如下面这种写法就没有结束标志符了:
   char a[7] = {‘a’,’b’,’c’,’d’,’e’,’f’,’g’};
另外,不要因为char 类型大小为1 个byte 就省略sizof(char)这种写法。这样只会使你的代码可移植性下降。

三、内存分配成功,但并未初始化

犯这个错误往往是由于没有初始化的概念或者是以为内存分配好之后其值自然为0。未初始化指针变量也许看起来不那么严重,但是它确确实实是个非常严重的问题,而且往往出现这种错误很难找到原因。

曾经有一个学生在写一个windows 程序时,想调用字库的某个字体。而调用这个字库需要填充一个结构体。他很自然的定义了一个结构体变量,然后把他想要的字库代码赋值给了相关的变量。但是,问题就来了,不管怎么调试,他所需要的这种字体效果总是不出来。我在检查了他的代码之后,没有发现什么问题,于是单步调试。在观察这个结构体变量的内存时,发现有几个成员的值为乱码。就是其中某一个乱码惹得祸!因为系统会按照这个结构体中的某些特定成员的值去字库中寻找匹配的字体,当这些值与字库中某种字体的某些项匹配时,就调用这种字体。但是很不幸,正是因为这几个乱码,导致没有找到相匹配的字体!因为系统并无法区分什么数据是乱码,什么数据是有效的数据。只要有数据,系统就理所当然的认为它是有效的。

也许这种严重的问题并不多见,但是也绝不能掉以轻心。所以在定义一个变量时,第一件事就是初始化。你可以把它初始化为一个有效的值,比如:
   int i = 10;
   char *p = (char *)malloc(sizeof(char));
但是往往这个时候我们还不确定这个变量的初值,这样的话可以初始化为0 或NULL。
   int i = 0;
   char *p = NULL;
如果定义的是数组的话,可以这样初始化:
   int a[10] = {0};
或者用memset 函数来初始化为0:
   memset(a,0,sizeof(a));
memset 函数有三个参数,第一个是要被设置的内存起始地址;第二个参数是要被设置的值;第三个参数是要被设置的内存大小,单位为byte。这里并不想过多的讨论memset 函数的用法,如果想了解更多,请参考相关资料。

至于指针变量如果未被初始化,会导致if 语句或assert 宏校验失败。这一点,上面已有分析。

四、内存越界

内存分配成功,且已经初始化,但是操作越过了内存的边界。这种错误经常是由于操作数组或指针时出现“多1”或“少1”。比如:
int a[10] = {0};
for (i=0; i<=10; i++)
{
   a[i] = i;
}
所以,for 循环的循环变量一定要使用半开半闭的区间,而且如果不是特殊情况,循环变量尽量从0 开始。

五、内存泄漏

内存泄漏几乎是很难避免的,不管是老手还是新手,都存在这个问题。甚至包括windows,Linux 这类软件,都或多或少有内存泄漏。也许对于一般的应用软件来说,这个问题似乎不是那么突出,重启一下也不会造成太大损失。但是如果你开发的是嵌入式系统软件呢?比如汽车制动系统,心脏起搏器等对安全要求非常高的系统。你总不能让心脏起搏器重启吧,人家阎王老爷是非常好客的。

会产生泄漏的内存就是堆上的内存(这里不讨论资源或句柄等泄漏情况),也就是说由malloc 系列函数或new 操作符分配的内存。如果用完之后没有及时free 或delete,这块内存就无法释放,直到整个程序终止。

1、告老还乡求良田
怎么去理解这个内存分配和释放过程呢?先看下面这段对话:
万岁爷:爱卿,你为朕立下了汗马功劳,想要何赏赐啊?
某功臣:万岁,黄金白银,臣视之如粪土。臣年岁已老,欲告老还乡。臣乞良田千亩以荫后世,别无他求。
万岁爷:爱卿,你劳苦功高,却仅要如此小赏,朕今天就如你所愿。户部刘侍郎,查看湖广一带是否还有千亩上等良田未曾封赏。
刘侍郎:长沙尚有五万余亩上等良田未曾封赏。
万岁爷:在长沙拨良田千亩封赏爱卿。爱卿,良田千亩,你欲何用啊?
某功臣:谢万岁。长沙一带,适合种水稻,臣想用来种水稻。种水稻需要把田分为一亩一块,方便耕种。
。。。。

2、如何使用malloc 函数
不要莫名其妙,其实上面这段小小的对话,就是malloc 的使用过程。malloc 是一个函数,专门用来从堆上分配内存。使用malloc 函数需要几个要求:
内存分配给谁?这里是把良田分配给某功臣。
分配多大内存?这里是分配一千亩。
是否还有足够内存分配?这里是还有足够良田分配。
内存的将用来存储什么格式的数据,即内存用来做什么?
这里是用来种水稻,需要把田分成一亩一块。分配好的内存在哪里?这里是在长沙。

如果这五点都确定,那内存就能分配。下面先看malloc 函数的原型:
   (void *)malloc(int size)
malloc 函数的返回值是一个void 类型的指针,参数为int类型数据,即申请分配的内存大小,单位是byte。内存分配成功之后,malloc 函数返回这块内存的首地址。你需要一个指针来接收这个地址。但是由于函数的返回值是void *类型的,所以必须强制转换成你所接收的类型。也就是说,这块内存将要用来存储什么类型的数据。比如:
   char *p = (char *)malloc(100);
在堆上分配了100 个字节内存,返回这块内存的首地址,把地址强制转换成char *类型后赋给char *类型的指针变量p。同时告诉我们这块内存将用来存储char 类型的数据。也就是说你只能通过指针变量p 来操作这块内存。这块内存本身并没有名字,对它的访问是匿名访问。

上面就是使用malloc 函数成功分配一块内存的过程。但是,每次你都能分配成功吗?

不一定。上面的对话,皇帝让户部侍郎查询是否还有足够的良田未被分配出去。使用malloc函数同样要注意这点:如果所申请的内存块大于目前堆上剩余内存块(整块),则内存分配会失败,函数返回NULL。注意这里说的“堆上剩余内存块”不是所有剩余内存块之和,因为malloc 函数申请的是连续的一块内存。

既然malloc 函数申请内存有不成功的可能,那我们在使用指向这块内存的指针时,必须用if(NULL != p)语句来验证内存确实分配成功了。

3、用malloc 函数申请0 字节内存
另外还有一个问题:用malloc 函数申请0 字节内存会返回NULL 指针吗?

可以测试一下,也可以去查找关于malloc 函数的说明文档。申请0 字节内存,函数并不返回NULL,而是返回一个正常的内存地址。但是你却无法使用这块大小为0 的内存。这好尺子上的某个刻度,刻度本身并没有长度,只有某两个刻度一起才能量出长度。对于这一点一定要小心,因为这时候if(NULL != p)语句校验将不起作用。

4、内存释放
既然有分配,那就必须有释放。不然的话,有限的内存总会用光,而没有释放的内存却在空闲。与malloc 对应的就是free 函数了。free 函数只有一个参数,就是所要释放的内存块的首地址。比如上例:
   free(p);
free 函数看上去挺狠的,但它到底作了什么呢?其实它就做了一件事:斩断指针变量与这块内存的关系。比如上面的例子,我们可以说malloc 函数分配的内存块是属于p 的,因为我们对这块内存的访问都需要通过p 来进行。free 函数就是把这块内存和p 之间的所有关系斩断。从此p 和那块内存之间再无瓜葛。至于指针变量p 本身保存的地址并没有改变,但是它对这个地址处的那块内存却已经没有所有权了。那块被释放的内存里面保存的值也没有改变,只是再也没有办法使用了。

这就是free 函数的功能。按照上面的分析,如果对p 连续两次以上使用free 函数,肯定会发生错误。因为第一使用free 函数时,p 所属的内存已经被释放,第二次使用时已经无内存可释放了。关于这点,我上课时让学生记住的是:一定要一夫一妻制,不然肯定出错。

malloc 两次只free 一次会内存泄漏;malloc 一次free 两次肯定会出错。也就是说,在程序中malloc 的使用次数一定要和free 相等,否则必有错误。这种错误主要发生在循环使用malloc 函数时,往往把malloc 和free 次数弄错了。这里留个练习:
写两个函数,一个生成链表,一个释放链表。两个函数的参数都只使用一个表头指针。

5、内存释放之后
既然使用free 函数之后指针变量p 本身保存的地址并没有改变,那我们就需要重新把p的值变为NULL:
   p = NULL;
这个NULL 就是我们前面所说的“栓野狗的链子”。如果你不栓起来迟早会出问题的。比如:
在free(p)之后,你用if(NULL != p)这样的校验语句还能起作用吗?例如:
   char *p = (char *)malloc(100);
   strcpy(p, “hello”);
   free(p); /* p 所指的内存被释放,但是p 所指的地址仍然不变*/
   …
   if (NULL != p)
   {
      /* 没有起到防错作用*/
      strcpy(p, “world”); /* 出错*/
   }
释放完块内存之后,没有把指针置NULL,这个指针就成为了“野指针”,也有书叫“悬垂指针”。这是很危险的,而且也是经常出错的地方。所以一定要记住一条:free 完之后,一定要给指针置NULL。

同时留一个问题:对NULL 指针连续free 多次会出错吗?为什么?如果让你来设计free函数,你会怎么处理这个问题?

六、内存已经被释放了,但是继续通过指针来使用

这里一般有三种情况:
第一种:就是上面所说的,free(p)之后,继续通过p 指针来访问内存。解决的办法就是给p 置NULL。
第二种:函数返回栈内存。这是初学者最容易犯的错误。比如在函数内部定义了一个数组,却用return 语句返回指向该数组的指针。解决的办法就是弄明白栈上变量的生命周期。
第三种:内存使用太复杂,弄不清到底哪块内存被释放,哪块没有被释放。解决的办法是重新设计程序,改善对象之间的调用关系。

上面详细讨论了常见的六种错误及解决对策,希望读者仔细研读,尽量使自己对每种错误发生的原因及预防手段烂熟于胸。一定要多练,多调试代码,同时多总结经验。

 

linux下检查内存泄露的工具--mtrace

最令linux程序员头疼的莫过于内存泄露了,即使你是在优秀的程序员,你也不能保证所以的malloc操作都有对应的free,那必要的工具就是必不可少的了。在一般的linux发行版中,有一个自带的工具可以很方便的替你完成这些事,这个工具就是mtrace

 

下面是它的用法

[cpp] view plaincopy

1.  #include <stdio.h>  

2.  #include <stdlib.h>  

3.  #include <string.h>  

4.     

5.  #include <mcheck.h>  

6.     

7.     

8.  int main(){  

9.      setenv("MALLOC_TRACE""output", 1);  

10.     mtrace();  

11.    

12.     char * text = ( char * ) malloc (sizeof(char) * 100);  

13.     memset(text,'/0',100);  

14.     memcpy(text,"hello,world!",12);  

15.    

16.     printf("%s/n",text);  

17.     return 0;  

18. }  

 

可以看出,只需要在你的程序中插入三行代码,就行。

第一句,#include <mcheck.h>,包含头文件

第二句,setenv("MALLOC_TRACE", "output",1);output表示输出的中间文件

第三句,mtrace(),调用mtrace.

 

将这个文件编译,注意,编译的时候一地要加上gcc-g选项。

gccmtrace_test.c -g -o mtrace_test

接着执行可执行文件,然后你会发现当前目录下多了一个output的文件。

这个文件自然不是文本文件,所以需要工具来查看。

 

$ mtracemtrace_test output

 

这时会打印出一串信息

-0x000000000129a010 Free 3 was never alloc'd 0x7f62844d89ba
- 0x000000000129a100 Free 4 was never alloc'd 0x7f6284583a1d
- 0x000000000129a120 Free 5 was never alloc'd 0x7f62845d20ec
 
Memory not freed:
-----------------
          Address     Size     Caller
0x000000000129a5a0     0x64  at/home/dianping/peter/mtrace/mtrace_test.c:12

 

其它别的直接忽略,注意那句要命的 Memory not freedcaller表示那段代码对应的malloc操作没有释放。顺便说一句,如果你编译的时候没有使用-g的选项,那么Caller这个地方就不会出现代码的信息,而是一串二进制的地址信息。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值