Linux内存管理的相关知识
2010-07-02 14:44:39| 分类: Linux C | 标签:linux内存管理的相关知识 |字号 订阅
内存管理
1、C程序结构
C程序在没有调入内存之前(也就是在存储时),分为代码区(text)、数据区(data)和未初始化数据区(bss)3个部分。
¨ 代码区 存放CPU执行的机器指令,即函数体的二进制代码。由于对于频繁被执行的程序,只需要在内存中有一份代码即可,所以代码区是可共享的(可以被别的程序调用)。为了防止程序意外地修改代码区的机器指令,通常代码区是只读的。
¨ 全局初始化数据区和静态数据区 包含已经被初始化的全局变量、静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。
¨ 未初始化数据区 存储的是全局未初始化变量。
以上谈的是存储时C语言的程序结构,下面看看运行时C语言的程序结构。
¨ 代码区 该区的机器指令根据程序设计流程依次执行。代码区的指令包括操作码和要操作的对象(或对象地址引用,即寄存器间接寻址等)。如果操作对象是立即数,则将直接包含在代码中;如果操作对象是局部数据,则将在栈区分配空间,然后引用该数据地址;如果操作对象存在于BSS区和数据区,则在代码区中也将引用该数据地址。
¨ 全局初始化数据区和静态数据区 只做一次初始化
¨ 未初始化数据区 在运行时改变其值
¨ 栈区 由编译器自动分配释放,存放函数的参数值、局部变量的值等等,其操作方式类似于数据结构中的栈。 这里简要谈一下C程序函数调用过程中栈框架的建立过程:
1) 第一个进栈的是主函数中函数调用后的下一条指令(函数调用语句的下一条可执行语句)的地址(目的是为了恢复现场)
2) 然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的
3) 然后是函数中的局部变量(注意静态变量是不入栈的)
4) 当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
¨ 堆区 用于动态分配内存,位于未初始化数据区和栈区之间,一般由程序员分配和释放,若程序员不释放,程序结束时可能由OS回收。
问题:为什么要专门开辟代码区
一个进程在运行的过程中,代码是根据流程一次执行的,只需要执行一次(当然跳转和递归也可使代码执行多次),然而程序可能会对数据进行多次访问(但没有必要为了对数据进行多次访问而多次访问代码),此时我们就有必要把代码区和数据区区分开来管理。
一例程序
//main.cpp
int a = 0; //a在全局初始化数据区
char *p1; //p1在全局未初始化数据区
int main(int argc, char *argv[ ])
{
int b; // b为局部变量,所以存储于栈区
char s[] = "abc"; // s为数组局部变量,存在于栈区
char *p2; // p2为局部变量,所以存储于栈区
char *p3 = "123456"; // 123456\0在常量区(已初始化数据区),p3在栈上。
static int c =0; // c存储于静态数据区(静态数据区和全局初始化区同在一个区域)
p1 = (char *)malloc(10); // 系统动态分配得来的10和20字节的区域就在堆区
p2 = (char *)malloc(20);
free(p1);
free(p2);
return 0;
}
2、内存的分配(内存的申请)
1) 申请方式
¨ Stack(静态分配):静态对象是有名字的变量,可以直接对其进行操作。由系统自动分配内存。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间。
¨ Heap(动态分配):动态对象是没有名字的变量,需要通过指针间接地对它进行操作。需要程序员自己申请内存,并指明大小。
在C中malloc函数,如p1 = (char *)malloc(10);
在C++中用new运算符,如p2 = new char[20]; //(char *)malloc(20);
分配堆空间之后,p1、p2得到所分配堆空间首地址,将会指向堆空间。
2) 申请后系统的响应
¨ 栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出
¨ 堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
其次,大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。
此外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
3) 申请大小的限制
栈:在Windows下,栈是高地址向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的。
在WINDOWS下,栈的大小是2M(也有的说是1M,总之它是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是低地址向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。
堆的大小受限于计算机系统中有效的虚拟内存,由此可见,堆获得的空间很灵活。
但由程序员操作的过程中容易发生内存泄露,也极易产生内存空间的不连续,即内存碎片(频繁使用malloc和free(new和delete)的结果)。
4) 申请效率的比较
¨ 栈由系统自动分配,速度较快,但程序员是无法控制的。
¨ 堆是由malloc或者new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
5) 分配效率的比较
栈是机器提供的数据结构,机器会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令。
堆则是C函数库提供的,它的机制很复杂。首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
此外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
可见,堆得存取效率和栈比较起来要低得多。
3、下面再给出一个数据存储区域的实例
#include <stdio.h>
#include <malloc.h>
#include <unistd.h>
#include <alloca.h>
extern void afunc(void); //声明afunc()函数
extern etext, edata, end; //声明三个外部变量
int bss_var; // 未初始化全局数据存储在BSS区
int data_var = 42; // 初始化全局数据存储在数据区
// 定义了一个宏,用于打印地址
#define SHW_ADR(ID, I) printf("the %8s\t is at adr:%8x\n", ID, &I);
int main(int argc, char *argv[])
{
char *p, *b, *nb;
printf("Adr etext:%8x\t Adr edata%8x\t Adr end %8x\t\n", &etext, &edata,&end);
printf("\ntext Location:\n");
SHW_ADR("main", main); // 查看代码段main函数位置
SHW_ADR("afunc", afunc); // 查看代码段afunc函数位置
printf("\NBSS Location:\n");
SHW_ADR("bss_var", bss_var); // 查看BSS段变量位置
printf("\NDATA Location:\n");
SHW_ADR("data_var", data_var); // 查看数据段变量位置
printf("\nSTACK Location:\n");
afunc();
p = (char *)alloca(32); // 从栈中分配空间
if(p != NULL){
SHW_ADR("start", p); // 打印栈空间的起始位置
SHW_ADR("end", p+31); // 打印栈空间的结束位置
}
b = (char *)malloc(32*sizeof(char)); // 从堆中分配空间
nb = (char *)malloc(16*sizeof(char)); // 从堆中分配空间
printf("\NHEAP Location:\n");
printf("the heap start: %p\n", b); // 打印堆起始位置
printf("the heap end: %p\n", (nb+16*sizeof(char))); // 打印堆结束位置
printf("\nb and nb in Stack\n");
SHW_ADR("b", b); // 打印栈中字符指针变量b的存储位置
SHW_ADR("nb", nb); // 打印栈中字符指针变量nb的存储位置
free(b); // 释放申请的堆空间
free(nb); // 释放申请的堆空间
return 0;
}
void afunc(void)
{
static int long level = 0; // 静态数据存储在数据段中
int stack_var; // 局部变量,存储在栈区
if(++level == 5){
return;
}
printf("stack_var is at:%p\n", &stack_var); // 打印局部变量的地址
afunc(); // 递归执行afunc函数
}
4、介绍几个内存管理函数
1) Malloc/free函数
原型:extern void *malloc(size_t num_bytes);
头文件:include <stdlib.h>
功能:分配长度为num_bytes字节的内存块
返回值:如果分配成功则返回指向被分配内存首地址的指针,否则返回空指针NULL。
说明:
¨ 该函数返回为void型指针,因此必要时要进行类型转换。
¨ 当内存不再使用时,应使用free()函数将内存块释放。
问题:为什么不再使用时,要用free()释放掉所申请的内存?
¨ 由于内存区域总是有限的,不能无限制地分配下去。
¨ 程序应该尽可能地去节省资源,当申请的堆空间不再使用时,应该释放掉,交由其它进程来使用。
注意:不能用free()来释放非malloc(), calloc(), realloc()函数所创建的堆空间,否则会发生错误。
2)new/delete(在C++中)
使用new/delete运算符实现内存管理比malloc/free函数更有优越性。它们的定义如下:
Static void* operator new(size_t sz);
Static void* operator delete(void* p);
先看一段C++代码:
void test(void)
{
//申请一个sizeof(obj)大小的一块动态内存,并把头指针赋值给obj类型的指针变量a
obj *a = new obj;
delete a; //清除并且释放所申请的内存
}
下面通过一段代码具体介绍一下new/delete的用法:
Class A
{
Public:
A() { count << “A is here!” << endl; } //构造函数
~A() { count << “A is here!” << endl; } //析构函数
Private:
Int I;
};
A* pA = new A; // 调用new运算符申请空间
delete pA; // 删除pA
其中,语句new A完成了一下两个功能:
A. 调用new运算符,在堆上分配一个sizeof(A)大小的内存空间。
B. 调用构造函数A(),在所分配的内存空间上初始化对象。
语句delete pA完成的是相反的两件事:
A. 调用析构函数~A(),销毁对象。
B. 调用运算符delete,释放内存。
注意:
¨ 使用new比使用malloc()有以下优点
A. New 自动计算要分配给对象的内存空间大小,不使用sizeof运算符,这样一来简单,而来可以避免错误。
B. 自动地返回正确的指针类型,不用进行强制类型转换。
C. 用构造函数给分配的对象进行初始化。
¨ 使用malloc函数和new分配内存的时候,本身并没有对这块内存空间做清零等任何工作。因此,申请内存空间后,其返回的新分配的空间是没有用零填充的,程序员须使用memset()函数来初始化内存。
3) realloc函数(更改已经配置的内存空间)
头文件:include <stdlib.h>
函数定义:
void *realloc(void *ptr, size_t size)
参数ptr为先前由malloc、calloc和realloc所返回的内存指针,而参数size为新配置的内存大小。
realloc函数用来从堆上分配内存,当需要扩大一块内存空间时,realloc()试图直接从堆上当前内存段后面的字节中获得更多的内存空间:
¨ 如果能够分配成功,则返回指向这块新内存空间的首地址,而将原来的指针(realloc函数的参数指针)指向的空间释放掉;
¨ 如果当前内存段后面的空闲字节不够,那么就使用堆上第一个能够满足这一要求的内存块,将目前的数据复制到新的位置,而将原来的数据块释放掉;
¨ 如果内存不足,重新申请空间失败,则返回NULL,此时原来的指针(realloc函数的参数指针)仍有效。
#include <stdio.h>
#include <stdlib.h>
int main (int argc, char* argv[], char* envp[]) //主函数
{
int input;
int n;
int *numbers1;
int *numbers2;
numbers1 = NULL;
if((numbers2 = (int *)malloc(5*sizeof(int))) == NULL) //numbers2指针申请空间
{
printf("malloc memory unsuccessful");
//free(numbers2);
//numbers2=NULL;
exit(1);
}
for (n=0; n<5; n++) //初始化(0-4)
{
*(numbers2+n)=n;
printf("numbers2's data: %d\n", *(numbers2+n)); //把0-4打印出来
}
printf("Enter an integer value you want to remalloc ( enter 0 to stop)\n"); //新申请空间大小
scanf ("%d", &input);
numbers1 = (int *)realloc(numbers2, (input+5)*sizeof(int)); // 重新申请空间
if (numbers1 == NULL)
{
printf("Error (re)allocating memory");
exit (1);
}
for(n=0;n<5;n++) // 这5个数是从numbers2(原指针空间)拷贝而来
{
printf("the numbers1s's data copy from numbers2: %d\n", *(numbers1+n));
}
for(n=0; n<input; n++) // 新数据初始化(0-input)
{
*(numbers1+5+n) = n*2;
printf ("nummber1's new data: %d\n", *(numbers1+5+n)); // numbers1++;
}
printf("\n");
free(numbers1); // 释放numbers1
numbers1 = NULL;
// free(numbers2); // 不能再释放numbers2,因为number2早已被系统自动释放
return 0;
}