C学习笔记——malloc内存分配

鉴于上次领导告诉一个解决方案,让我把它写成文档,结果自己脑子里知道如何操作和解决,但就是不知道如何用语言文字把它给描述出来。决定以后多写一些笔记和微博来锻炼自己的文字功底和培养逻辑思维,不然只会是一个敲代码的,永远到不了管理的层面。

把《C程序设计语言》细读了一遍后,到第8章UNIX系统接口的最后两节——“目录列表”和“存储分配程序”,看了一遍都没看懂。智商不过高啊。把存储分配重新看了一遍,才有了眉头。这两天还要找时间把目录列表再看一遍,确保掌握。(前几章节有些地方看了很多遍也糊里糊涂,就找谷歌百度帮忙了)

这里的存储分配程序,讲的就是标准库中malloc函数的实现原理。首先要了解针对malloc的内存存储结构。malloc不像全局变量一样,不是在编译器编译的时候就会分配内存空间,而是在调用到malloc函数时才会分配空间。有时还会中途调用free函数释放空间出来。所以:

1、malloc在第一次被调用时,从系统中获取最小为一个单元的空闲空间(eg:最小单元为1024个最受限单元块。当x<=1024,获取1024个,否则获取x个),再进行分配;

2、malloc所剩下的空闲空间一般都不是连续的,而是分散的。这样也提高了空间的利用率。

为了管理malloc的空闲空间,每一个独立块的最前面都包含了一个“头部”信息:一个指向下一个空闲块的指针、一个本身独立块的长度(书上说还有一个指向自身存储空间的指针,但每个存储空间都有自身的指针,为什么还要这个呢。后看英语版原著,这么写的:Each
block contains a size, a pointer to nextblock, and the space itself.)。下一个空闲块是按存储地址升序排列,离本空闲块最近的一个空闲块,若本空闲块在最后,则指向最前的空闲块。这样所有属于malloc的空闲空间都被串在了一起。如下图所示:

注:中文版的此图解释翻译搞错了,如下图:

因为后面的free函数已经把相邻的空闲链表给整合成一块了,所以我的图没有出现相邻的空闲链表。

每块由malloc控制的空间都包含一个“头部”信息,为了方便管理,每块的空间大小都是头部大小的整数倍。而头部长度=指向下一个空闲块的指针的长度+自身空间大小的unsigned长度。但为了确保由malloc函数返回的存储空间满足将要保存的对象的对齐要求。每个机器都有一个最受限的类型:如果最受限的类型可以存储在某一个特定的地址中,则其他所有的类型也可以存放在此地址中。有的是double型,有的是long型,甚至还有的是int型。因此,头部结构将与最受限的类型进行联合,来确保对齐。

因为有头部信息,头部信息里的本块空间size也是包括头部的大小,所以每次申请malloc空闲块的时候,都要加上一个单元,最后返回给用户的时候,再去掉头部。

每次调用malloc申请空间时,malloc有一个专门指向当前空闲块链表的静态指针freep。从当前开始扫描剩下的空闲块链表,直到扫到一个足够大的空闲块。此算法成为“首次适应”(first fit),与之相对的是“最佳适应”(best fit):它将扫描出满足条件最小的块。这里的代码是“首次适应”算法。结果将出现三种情况:

1)找到一块刚好合适的空闲块,则此块空间从链表中移走并将此块的地址返回给用户,并把静态指针freep指向前一空闲块地址;

2)找到一块比需求大的空闲块,则从此空闲块中的后部取一块与需求一样的空间给用户,前部改变空闲块大小便可;

注(直到写本博文才发现自己错了):

①一直都认为返回给用户地址前一单元的头部,应该把空间退还给前面的空闲块,不然就闲着了。其实,里面记录了一个重要信息:空间块大小(包括头部和返回给用户的单元大小),在free释放空间的时候,就必须用到此头部的信息;

②后面的free程序以为是专供系统申请空间后插入空闲块链表用的,其实它就是我们平常用malloc、realloc、或calloc申请空间后,再释放的程序。

3)如果扫描了一遍,都没有找到足够大的空闲块,则向系统再申请一块新的空间。

上面都是在malloc已经有了空闲块的前提下,但第一次申请的时候,malloc是没有空闲块空间的。因此,在预编译时,就建立了一个单元的空闲块链表base来当做空闲链表的入口。当第一次调用malloc时,空闲链表的静态指针freep为NULL,那将它指向base,大小设为0(这样这块base空间将一直存在,且不被申请,确保了之后freep一直指向有效的空闲块链表),且指向它自己,同时向系统申请空闲空间(每次向系统申请的空间都是一块连续的空闲块)。

typedef long Align;	/*按照long类型的边界对齐,即以long作为最受限类型*/
union header{	/*头部信息*/
	struct {
		union header *ptr;	/*指向下一个空闲块*/
		unsigned size;		/*本空闲块大小*/
	}s;
	Align x;	/*强制对齐*/
};
typedef union header Header;

static Header base;	/*第一次调用malloc的空闲块链表入口,大小为0的空链表(按照上面逻辑的来说,这里的size应该为1)*/
static Header *freep = NULL;	/*静态的空闲块链表指针,初始化为NULL。第一次申请后才会指向base*/

/*malloc函数:通用存储分配函数*/
void *malloc(unsigned nbytes)
{
	Header *p,*prevp;	/*定义一个当前空闲块指针变量,和前一个空闲块指针变量*/
	Header *morecore(unsigned);	/*用于向系统申请空闲空间函数*/
	unsigned nunits;	/*需要申请的实际单元大小,即上面图中的z*/

	nunits = (nbytes + sizeof(Header) - 1)/sizeof(Header) + 1;	/*与上图对应,把字节大小转换为单元大小,向上取整,并加上一个单元(头部)*/
	if((prevp = freep) == NULL){	/*没有空闲链表,第一次申请*/
		base.s.ptr = prevp = freep = &base;	/*freep指向base,base的下一个空闲块指针指向自己*/
		base.s.size =0;	/*设置大小为0*/
	}
	for(p = prevp->s.ptr;;prevp = p, p = p->s.ptr){
		if(p->s.size >= nunits){
			if(p->s.size == nunits)	/*大小刚好合适*/
				prevp->s.ptr = p->s.ptr;	/*移走此块空闲区域*/
			else{					/*比实际需求大,从空闲块尾部分配*/
				p->s.size -= nunits;	/*缩小空闲块大小*/
				p += p->s.size;	/*指针指向被申请的空间的头部*/
				p->s.size = nunits;		/*设置被申请的空闲块大小*/
			}
			freep =prevp;	/*当前静态指针指向前一空闲块,如果当前块还有空闲区域,下次将继续从此处开始扫描,节省时间*/
			return (void *)(p+1);	/*返回去头部单元的空闲空间*/
		}
		if(p == freep)	/*闭环的空闲链表,第一次调用malloc申请,或扫描一遍,未发现足够大的空间*/
			if((p = morecore(nunits)) == NULL)	/*向系统申请空间*/
				return NULL;	/*未申请成功,*/
	}
}

通过下面的morecore()和free()函数的程序分析可知,在向系统成功申请空间后,p将指向有足够空间的空闲块。但在此代码中,进入下一此空闲块扫描前,p将指向下一块不足的空闲块,导致多扫描了一遍。个人觉得,如果空间足够,可以多申请一个静态指针beforefreep,指向freep的前一个空闲块。这样上面代码可添加一句,提高效率:

if(p == freep){ /*这样要添加大括号*/
if((p = morecore(nunits)) == NULL)
		return NULL;
	p = beforefreep;
}
或
if(p == freep) /*这里可以不用加大括号,else与最近的if匹配*/
if((p = morecore(nunits)) == NULL)
		return NULL;
	else
p = beforefreep;

向系统申请空间的时,不是按需分配,而是有一个最小申请单元数。让您足够用,这次用不完可以留着下次用,不用每次都向系统申请,又不会系统浪费空间。

真正向系统申请空间,还需调用系统调用sbrk(n)(UNIX下),若申请成功,该指针返回指向n个字节的存储空间;若申请失败,返回-1(不是NULL)。返回的指针类型是char *(应该是最小的存储空间单元)。

#define NALLOC 1024	/*最小申请单元数*/

/*morecore函数:向系统申请更多的存储空间*/
static Header *morecore(unsigned nu)	/*返回的是静态空闲块链表指针*/
{
	char *cp, *sbrk(int);
	Header *up;

	if(nu < NALLOC)
		nu =NALLOC;
	cp = sbrk(nu * sizeof(Header));	/*调用系统调用申请系统空间*/
	if(cp == (char *) -1)
		return NULL;	/*申请失败,没有空间*/
	up = (Header *)cp;	/*转换为Header*指针类型*/
	up->s.size = nu;	/*设置此空间块的大小*/
	free((void *)(up +1));	/*释放空间*/
	return freep;
}

这里的返回的freep,在free中更新了,才返回的。当初也想过既然freep都是静态全局变量了,那这里为什么还要返回一个静态变量呢,直接在函数里赋值就好了。其实这里有成功与失败,所以程序来需要判断申请结果,而且返回的freep是与申请最相关东西。

free(void *ap)函数就是释放指针ap所指的空间,具体要释放的大小在ap前一个指针,即头部信息里。释放主要就是为了把此空间插入到空闲块链表中。所以要找到此空间块两边的空闲块(也有可能只有一块空闲块,即入口base)。然后判断是否与前一块相连,与后一块相连,相连的话,合并成一块,否则直接在中间插入一个新的空闲块链表。

/*free函数:释放ap,将ap块放入空闲链表中*/
void free(void *ap)
{
	Header *p, *bp;

	bp =(Header *)ap -1;	/*指向ap块的头部*/
	for(p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr)	/*找到bp所在空闲链表中的位置*/
		if(p >= p->s.ptr && (bp > p || bp < p->s.ptr))	/*判断是否在链表的开头或末尾*/
			break;

	if(bp + bp->s.size == p->s.ptr){	/*先判断能否与高地址的空闲块合并,即与后一块合并*/
		bp->s.size += p->s.ptr->s.size;
		bp->s.ptr = p->s.ptr->s.ptr;
	}
	else
		bp->s.ptr = p->s.ptr;	/*不能合并,bp指向后一块地址*/

	if(p + p->s.size == bp){	/*再判断能否与地地址的空闲块合并,即与前一块合并*/
		p->s.size += bp->s.size;
		p->s.ptr = bp->s.ptr;
	}
	else
		p->s.ptr =bp;	/*不能合并,p指向bp地址*/
	freep =p;
}

注:中文版翻译的又有歧义了,原著分别是“join to upper nbr”和“jointo lower nbr”。

这个free程序,处理的太妙了。一般思维,先与前一块合并,再与下一块合并,如下面的程序(显然比我的好多了):

	if(p + p->s.size == bp){	/*与前一块相连?*/
		if(bp + bp->s.size == p->s.ptr){	/*与后一块相连?*/
			p->s.size += bp->s.size + p->s.ptr->s.size;
			p->s.ptr = p->s.ptr->s.ptr;
		}else
			p->s.size += bp->s.size;
	}else{
		if(bp + bp->s.size == p->s.ptr){	/*与后一块相连?*/
			bp->s.size += p->s.ptr->s.size;
			bp->s.ptr = p->s.ptr->s.ptr;
			p->s.ptr = bp;
		}else	/*不与任何一块相连*/
			bp->s.ptr = p->s.ptr;
			p->s.ptr = bp;
	}

从这里可以看出,通过malloc申请后的空间,并没有初始化,所以在使用前记得初始化,不小心当做右值使用,出错的概率很大。

《C程序设计语言》(第2版·新版)不愧是经典,值得细读和巩固。感谢作者!

写了很久才写完,有错误或不好的地方,欢迎各位指正和批评!也感谢您花时间阅读!

时间: 2024-10-06 01:33:18

C学习笔记——malloc内存分配的相关文章

C语言学习笔记--动态内存分配

1. 动态内存分配的意义 (1)C 语言中的一切操作都是基于内存的. (2)变量和数组都是内存的别名. ①内存分配由编译器在编译期间决定 ②定义数组的时候必须指定数组长度 ③数组长度是在编译期就必须确定的 (3)但是程序运行的过程中,可能需要使用一些额外的内存空间 2. malloc 和 free 函数 (1)malloc 和 free 用于执行动态内存分配的释放 (2)malloc 所分配的是一块连续的内存 (3)malloc 以字节为单位,并且返回值不带任何的类型信息:void* mallo

Linux System Programming 学习笔记(九) 内存管理

1. 进程地址空间 Linux中,进程并不是直接操作物理内存地址,而是每个进程关联一个虚拟地址空间 内存页是memory management unit (MMU) 可以管理的最小地址单元 机器的体系结构决定了内存页大小,32位系统通常是 4KB, 64位系统通常是 8KB 内存页分为 valid or invalid: A valid page is associated with an actual page of data,例如RAM或者磁盘上的文件 An invalid page is

linux kernel学习笔记-5内存管理(转)

http://blog.sina.com.cn/s/blog_65373f1401019dtz.htmllinux kernel学习笔记-5 内存管理1. 相关的数据结构 相比用户空间而言,在内核中分配内存往往受到更多的限制,比如内核中很多情况下不能睡眠,此外处理内存分配失败也不像用户空间那么容易.内核使用了页和区两种数据结构来管理内存: 1.1 页 内核把物理页作为内存管理的基本单位.尽管CPU的最小可寻址单位通常为字(甚至字节),但是MMU(内存管理单元,管理内存并把虚拟地址转换为物理地址的

java学习-----jvm的内存分配及运行机制

VM运行时数据区域: 根据<Java虚拟机规范(第二版)>的规定,JVM包括下列几个运行时区域: 我们思考几个问题: 1.jVM是怎么运行的? 2.JVM运行时内存是怎么分配的? 3.我们写的java代码(类,对象,方法,常量,变量等等)最终存放在哪个区? VM运行时数据区域: 1.程序计数器(program Counter Register):   是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器.在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的

内存充足时,malloc内存分配失败问题

如题,为什么内存充足时,malloc内存分配失败? 首先,在32位平台下,系统最多可管理4G内存,其中2G系统自用,剩下2G可供用户使用,然而在实际分配时,用户可用空间始终小于2G(若有童鞋在如上条件下分配空间 等于或大于2G可以联系我哦,以让我纠正自己的错误).64位则可以管理好多,有兴趣的童鞋可以自己算下哦! 那么,这些跟malloc内存分配失败有什么关系呢有什么关系呢?哈哈,问题就在这里,32位平台下就不多说,相信上面的叙述可以解决童鞋们的问题.在64位平台下,童鞋们是不是发现依旧只能分配

RHCA442学习笔记-Unit10内存地址及分配

Unit 10 Memory Addressing and Allocation 内存地址及分配 学习目标: A. 虚拟地址与物理地 B. 调整内存地址分配 C. 解析内存溢出 10.1 Overview of memory addressing 内存地址概述 A. 虚拟地址空间 a. 每个进程都有自己线性连续的地址空间 b. 地址空间范围从0到最大地址空间值: X86: 2^32=4G X86_64: 2^64=1TiB (redhat 最大支持256GiB) B. 物理地址空间 a. 虚拟地

memcached学习——memcached的内存分配机制Slab Allocation、内存使用机制LRU、常用监控记录(四)

内存分配机制Slab Allocation 本文参考博客:https://my.oschina.net/bieber/blog/505458 Memcached的内存分配是以slabs为单位的,会根据初始chunk大小.增长因子.存储数据的大小实际划分出多个不同的slabs class,slab class中包含若干个等大小的trunk和一个固定48byte的item信息.trunk是按页存储的,每一页成为一个page(默认1M). 1.slabs.slab class.page三者关系: sl

RHCA442学习笔记-Unit11内存回收

Unit 12 Memory Reclamation 内存回收            学习目标: A. 了解和调整内存回收 B.   调整内存溢出 C. 调整虚拟内存(swap)的使用 12.1        Characterizing page status各种页面状态的特征 A. .Free 空闲页 页面可以马上分配给进程 B Inactive Clean 干净页 a. 页面内容已写入磁盘,或者 b. 数据从磁盘读入内存后未作修改 c. 这种页面可以分配 C. Inactive Dirty

Linux内核学习笔记——内核内存管理方式

一 页 内核把物理页作为内存管理的基本单位:内存管理单元(MMU)把虚拟地址转换为物理 地址,通常以页为单位进行处理.MMU以页大小为单位来管理系统中的也表. 32位系统:页大小4KB 64位系统:页大小8KB 内核用相应的数据结构表示系统中的每个物理页: <linux/mm_types.h> struct page {} 内核通过这样的数据结构管理系统中所有的页,因此内核判断一个页是否空闲,谁有拥有这个页 ,拥有者可能是:用户空间进程.动态分配的内核数据.静态内核代码.页高速缓存…… 系统中