在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
总结一下C语言中基本的内存分配,加深对内存管理的印象,一步一步走山路~~~~~~~~
1. 程序和进程 问题:程序和进程各是什么? 程序 只是一段可以执行的代码文件,通俗讲在 linux 上就是一个可执行文件。当一个程序运 行时就被称为进程,即进程是运行状态的程序。
程序存储了一系列文件信息,这些信息描述了如何在运行时创建一个进程,包含了下面的内容: 二进制格式标识: 描述可执行文件的元信息,内核利用该信息解释文件中的其他信息 机器语言指令: 对程序进行编码 程序入口地址: 标示程序开始执行的起始指令位置 数据: 程序所包含变量的初始值和程序所使用的字面常量值 符号表和重定位表: 描述程序中函数和变量的位置,重定位表记录要修改的符号引用的位置, 以及如何修改 共享库和动态链接信息:列出程序运行时需要使用的共享库以及加载共享库的动态链接器的路径名 其他信息: 描述如何创建进程
内核在加载程序的时候会为其分配一个唯一标识符即进程号,linux 内核限制进程号需要小于等于 32767每当创建一个进程的时候,内核就会顺序将下一个可用进程分配给其使用, 当进程号大于 32767时,内核会重置进程号计数器,然后开始重新分配。 因为内核会运行一些守护进程和系统 进程,所有一般会预留一些进程号给这些程序使用,所以一般从300开始重置, 类似于端口号 1-1024为系统占用。
2. 内存管理基本概念&进程的内存布局
2.1 进程内存布局 1) 代码区(Text egment): 代码区指令根据程序设计流程依次执行,对于顺序指令,则只会执行一次(每个进程),如果反复, 则需要使用跳转指令,如果进行递归,则需要借助栈来实现。代码段: 代码段(code segment/text segment )通常是指用来存放程序执行代码的一块内存区域。 这部分区域的大小在程序运行前就已 经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序 在代码段中, 也有可能包含一些只读的常数变量,例如字符串常量等。 代码区的指令中包括操作码和要操作的对 象(或对象地址引用)。如果是立即数(即具体的 数值,如5),将直接包含在代码中;如果是局部 数据,将在栈区分配空间,然后引用该数据地址;如果是BSS区和数据区 在代码中同样将引用该数 据地址。另外,代码段还规划了局部数据所申请的内存空间信息。 2) 全局初始化数据区/静态数据区(Data Segment) 只初始化一次。数据段: 数据段(data segment )通常是指用来存放程序中已初始化的全局变量的 一块内存区域。数据段属于静态内存分配。data段中的静态数据区存放的是程序中已初始化的全局变 量、静态变量和常量。 3) 未初始化数据区(BSS): 在运行时改变其值。BSS 段: BSS 段(bss segment )通常是指用来存放程序中未初始化的全局变 量的一块内存区域。BSS 是英文Block Started by Symbol 的简称。BSS 段属于静态内存分配, 即程 序一开始就将其清零了。一般在初始化时BSS段部分将会清零。 4) 堆区(heap): 用于动态内存分配。堆在内存中位于bss区和栈区之间。一般由程序员分配和释放,若程序员不释放, 程序结束时有可能由OS回收。堆(heap): 堆是用于存放进程运行中被动态分配的内存段,它的大小 并不固定,可动态扩张或缩减。当进程调用malloc 等函数分配内存时,新分配的内存就被动态添加到 堆上(堆被扩张);当利用free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。在将 应用程序加载到内存空间执行时,操作系统负责代码段、数据段和BSS段的加载, 并将在内存中为这 些段分配空间。栈段亦由操作系统分配和管理,而不需要程序员显示地管理;堆段由程序员自己管理, 即显式地申请和释放空间。 5) 栈区(stack): 由编译器自动分配释放,存放函数的参数值、局部变量的值等。存放函数的参数值、 局部变量的值, 以及在进行任务切换时存放当前任务的上下文内容。其操作方式类似于数据结构中的栈。 每当一个函 数被调用,该函数返回地址和一些关于调用的信息,比如某些寄存器的内容,被存储到栈区。 然后这 个被调用的函数再为它的自动变量和临时变量在栈区上分配空间, 这就是C实现函数递归调用的方法。 每执行一次递归函数调用,一个新的栈框架就会被使用, 这样这个新实例栈里的变量就不会和该函数 的另一个实例栈里面的变量混淆。栈(stack) :栈又称堆栈, 是用户存放程序临时创建的局部变量, 也就是说我们函数括弧"{}"中定义的变量 (但不包括static 声明的变量,static 意味着在数据段中存放 变量)。除此以外,在函数被调用时, 其参数也会被压入发起调用的进程栈中,并且待到调用结束后, 函数的返回值也会被存放 回栈中。由于栈的先进先出特点, 所以栈特别方便用来保存/ 恢复调用现场。 从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
2.2 为什么要这么多的分区? 之所以分这么多区域,主要是因为: 1) 一个进程在运行过程中,代码是根据流程依次执行的,只需要访问一次, 当然跳转和递归有可能使 代码执行多次,而数据一般都需要访问多次,因此单独开辟空间以方便访问和节约空间. 2) 临时数据及需要再次使用的代码在运行时放入栈区中,生命周期短。 全局数据和静态数据有可能在 整个程序执行过程中都需要访问,因此单独存储管理。 3) 堆区由用户自由分配,以便管理。
上图我们看到的地址都是虚拟地址 linux 32 位操作系统中,每个进程理论上有4G的独立内存空间,进程 的内存空间按照text,data, bss, heap, stack由低到高分配,但是只增值到0xC0000000, 最后1G留给了 内核。0~3G属于用户空间,3~4G为内核空间以上我们描述了一个运行的可执行程序 即一个进程的内 存分布。下面我们比较一下经过编译后的可执行程序(不运行)内存分布。 首先我们编译生成一个可执 行程序(mem),用命令size 查看这个可执行二进制文件结构情况 text: 代码区 data: 全局初始化区/静态数据区 bss: 未初始化区 dec: 十进制总和 hex: 十六进制总和 filename:文件名 可以看到,可执行程序在存储时(没有调入到内存)只有代码区(text), 数据区(data), 和未初始化数据 区(bss) 3个部分。下面贴上一图,对比可执行代码存储结构和运行时(进程)的内存结构对照(图片是 盗用^_^,鸣谢时贴出原创~)
3. 内存分配的方式 3.1 静态和动态分配 在C语言中,可以静态或动态的为一个数据分配内存空间。 静态分配:编译器在处理程序的源代码的时候由编译器分配。 动态分配:程序在执行过程中由程序员自己分配,堆内存调用malloc库函数申请分配,栈内存使用alloca申 请分配 3.2 静态和动态分配的区别 1)静态对象是有名字的变量,可以直接对其进行操作;动态对象是没有名字的变量,需要通过指针间接的对 它进行操作。 2)静态对象的分配与释放由编译器自动处理;动态对象的分配和释放必须由程序员自己显示的管理, 通过 malloc 和free 两个函数来完成。
4. 内存管理函数 4.1 malloc/free 函数 malloc()函数用来在堆中申请内存空间,free()函数释放原先申请的内存空间。malloc()函数是在内存的动态存 储区中分配一个长度为size字节的连续空间。其参数是一个无符号整型数,返回一个指向所分配的连续存储域 的起始地址的指针。当函数未能成功分配存储空间时(如内存不足)则返回一个NULL指针。 由于内存区域总是有限的,不能无限制地分配下去,而且程序应尽量节省资源 所以当分配的内存区域不用时, 则要释放它,以便其他的变量或程序使用。 #include <stdlib.h> void *malloc(size_t size); void free(void (ptr); 示例: int *p1,*p2; malloc()函数返回值赋给p1,又把p1的值赋给p2,所以此时p1,p2都可作为free函数的参数。使用free()函数时, 需要特别注意下面几点: 1)调用free()释放内存后,不能再去访问被释放的内存空间。内存被释放后, 很有可能该指针仍然指向该内存 单元,但这块内存已经不再属于原来的应用程序,此时的指针为悬挂指针(可以赋值为NULL)。 2)不能两次释放相同的指针。因为释放内存空间后,该空间就交给了内存分配子程序,再次释放内存空间会导 致错误。也不能用free来释放非malloc()、calloc()和realloc()函数创建的指针空间,在编程时,也不要将指针进 行自加操作,使其指向动态分配的内存空间中间的某个位置,然后直接释放,这样也有可能引起错误。 3)在进行C语言程序开发中,malloc/free是配套使用的,即不需要的内存空间都需要释放回收。
4.2 realloc 函数 realloc()函数用来从堆上分配内存,当需要扩大一块内存空间时,realloc()试图直接从堆上当前内存段后面的字 节中获得更多的内存空间,如果能够满足,则返回原指针;如果当前内存段后面的空闲字节不够,那么就使用堆 上第一个能够满足这一要求的内存块,将目前的数据复制到新的位置,而将原来的数据块释放掉 如果内存不足, 重新申请空间失败,则返回NULL。此函数定义如下: #include <stdlib.h> 当调用realloc()函数重新分配内存时,如果申请失败,将返回NULL,此时原来指针仍然有效,因此在程序编写时 需要进行判断,如果调用成功,realloc()函数会重新分配一块新内存,并将原来的数据拷贝到新位置 返回新内存 的指针,而释放掉原来指针(realloc()函数的参数指针)指向的空间, 原来的指针变为不可用(即不需要再释放, 也不能再释放)因此,一般不使用以下语句: ptr=realloc(ptr,new_amount)
4.3 calloc 函数 calloc是malloc函数的简单包装,它的主要优点是把动态分配的内存进行初始化,全部清零。其操作及语法类似 malloc()函数。 #include <stdlib.h> /* * 在堆内存上动态分配 nmemb个连续的单元,每个单元大小为size * 与malloc的区别:calloc 在动态分配内存后,自动初始化该内存空间为0,而malloc不初始化,里面数据是随机的垃圾值 */ // 函数原型 void *calloc(size_t nmemb, size_t size); // 函数实现 void * calloc(size_t nmemb, size_t size) { void *p; size_t total; total = nmemb *size; p = malloc(total); /* 申请空间 */ if(p != NULL) memset(p,'\0',total); return p; }
4.4 alloca 函数 alloca()函数用来在栈中动态分配size个字节的内存空间,因此函数返回时会自动释放掉空间。alloca函数原型及 库头文件如下: #include <alloca.h>
void *alloca(size_t size);
alloca 与 malloc 的区别主要在于:alloca是向栈申请内存,无需释放,malloc申请的内存位于堆中, 最终需 要函数free来释放。 malloc函数并没有初始化申请的内存空间,因此调用malloc()函数之后,还需调用函数 memset初始化这部分内存空间;alloca则将初始化这部分内存空间为0。
5. 堆和栈的区别 前面已经介绍过,栈是由编译器在需要时分配的,不需要时自动清除的变量存储区。里面的变量通常是局部变量、 函数参数等。堆是由malloc()函数(C++语言为new运算符)分配的内存块,内存释放由程序员手动控制,在C语 言为free函数完成(C++中为delete)栈和堆的主要区别有以下几点: 1)管理方式不同 栈编译器自动管理,无需程序员手工控制;而堆空间的申请释放工作由程序员控制,容易产生内存泄漏。 2)空间大小不同 栈是向低地址扩展的数据结构,是一块连续的内存区域。 这句话的意思是栈顶的地址和栈的最大容量是系统预 先规定好的,当申请的空间超过栈的剩余空间时,将提示溢出。因此,用户能从栈获得的空间较小。 堆是向高地址扩展的数据结构,是不连续的内存区域。因为系统是用链表来存储空闲内存地址的,且链表的遍历 方向是由低地址向高地址。由此可见,堆获得的空间较灵活,也较大。栈中元素都是一一对应的, 不会存在一个 内存块从栈中间弹出的情况。 3)是否产生碎片 对于堆来讲,频繁的malloc/free(new/delete)势必会造成内存空间的不连续,从而造成大量的碎片, 使程序效 率降低(虽然程序在退出后操作系统会对内存进行回收管理)。对于栈来讲,则不会存在这个问题。 4)增长方向不同 堆的增长方向是向上的,即向着内存地址增加的方向;栈的增长方向是向下的,即向着内存地址减小的方向。 5)分配方式不同 堆都是程序中由malloc()函数动态申请分配并由free()函数释放的;栈的分配和释放是由编译器完成的,栈的动态 分配由alloca()函数完成,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行申请和释放的,无需手 工实现 6)分配效率不同 栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有 专门的指令执行。堆则是C函数库提供的,它的机制很复杂,例如为了分配一块内存, 库函数会按照一定的算法 (具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大的空间,如果没有足够大的空间(可能 是由于内存碎片太多),就有需要操作系统来重新整理内存空间,这样就有机会分到足够大小的内存, 然后返回。 显然,堆的效率比栈要低得多。
6. 数据存储区域 Demo mem_add.c #include <stdio.h> #include <stdlib.h> extern int etext, edata, end; /* 用户进程相关的虚拟地址为int 类型*/ int bss_var; /* 未初始化全局数据存储在BSS区 */ int data_var = 42; /* 初始化的全局数据域存储在数据区 */ void afunc(); /* 打印地址 */ #define SHW_ADDR(ID, I) \ int main(int argc, char *argv[]) { char *p, *b, *nb; /* p,b,nb 都在栈上 */ printf("length of p is %d\n", sizeof(p)); /* 指针的长度为4个字节 */ printf("the p add is [%p]\n", &p); printf("the b add is [%p]\n", &b); printf("the nb add is [%p]\n", &nb); printf("\nAddr etext:%p\t Addr edata:%p\t Addr end:%p\t\n", &etext, &edata, &end); /* text 段地址 */ printf("\ntext Location:\n"); SHW_ADDR("main", main); /* 查看代码段main函数位置 */ SHW_ADDR("afunc", afunc); /* 查看代码段afunc函数位置 */ /* BSS 段地址 */ printf("\nbss Location:\n"); SHW_ADDR("bass_var", bss_var); /* 查看BSS段变量的位置 */ /* data 段地址 */ printf("\ndata Location:\n"); SHW_ADDR("data_var", data_var); /* 查看data段变量的位置 */ /* 栈上地址 */ printf("\nStack Location:\n"); afunc(); p = (char *)alloca(32); /* 从栈中动态分配空间用alloca函数 */ if(p != NULL) { SHW_ADDR("start", p); /* 栈上第一个变量地址 */ SHW_ADDR("end", p+31); } /* 堆地址 */ b = (char *)malloc(32*sizeof(char)); /* malloc动态从堆中分配空间 */ nb = (char *)malloc(16*sizeof(char)); /* malloc动态从堆中分配空间 */ 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_ADDR("b", b); /* 显示栈中数据b的位置 */ SHW_ADDR("nb", nb); /* 显示栈中数据nb的位置 */ free(b); b = NULL; /* b 赋为NULL,防止成为野指针 */ free(nb); nb = NULL; /* nb 赋为NULL,防止成为野指针 */ return 0; } void afunc() { static int level = 0; /* 静态数据存储在数据段中 */ int stack_var; /* 局部变量存储在栈区 */ if(++level == 5) return; /* 栈内存区 */ printf("stack_var%d is at: %p\n", level, &stack_var); SHW_ADDR("stack_var in stack section", stack_var); /* 静态变量数据区 */ SHW_ADDR("level in data section", level); afunc(); } Makefile 1 mem : mem_add.c 2 gcc -W -Wall -o $@ $^ 3 clean: 4 rm -fr mem 运行: [root@zhao Memory]# ./mem 各地址对应内存区间:
8. 后记 九阴真经 - 总纲-1
天之道,损有余而补不足,是故虚胜实,不足胜有余。 其意博,其理奥,其趣深,天地之象分,阴阳之候列, 变化之由表,死生之兆彰,不谋而遗迹自同,勿约而幽明斯契, 稽其言有微,验之事不忒,诚可谓至道之宗,奉生之始矣。 假若天机迅发,妙识玄通,成谋虽属乎生知,标格亦资于治训,未尝有行不由送, 出不由产者亦。 然刻意研精,探微索隐,或识契真要,则目牛无全,故动则有成, 犹鬼神幽赞, 而命世奇杰,时时间出焉。 |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论