您的位置:首页 > 运维架构 > Linux

简述动态存储分配及malloc(),free()函数(针对linux)

2014-10-31 09:57 211 查看
首先说一下程序运行是的存储分配:


存储分配

这张是典型的C语言的存储分配图。动态存储分配主要涉及图中的堆区。堆是无结构的连续的存储区域。当调用malloc()函数时,存储分配器从堆中找一块合适大小的连续的内存空间返回给程序。

malloc和free函数的原型如下:

void * malloc(size_t size)

void free(void* ptr)

这两个函数的使用就不在赘述了。

其实说白了,malloc和free函数就是负责对堆区的存储空间进行管理。既然要对堆区进行管理,而堆区又是无结构的,因此需要一个(运行库)自定义的结构来对堆区进行管理。

既然是对存储空间的管理,那么必然要面对两个问题--------存储空间的利用率和运行的效率。

首先是存储空间的利用率,这里面牵扯到一个概念--------碎片(fragmentation)。碎片又分为两种:内碎片和外碎片。

内碎片是指分配给程序的存储空间没有用完,有一部分是闲着的,这部分空间成为内碎片。
外碎片是虽然还有空间可以分配,但是这些空间都是不连续的小块,不能满足程序对连续存储的要求,因此无法分配给程序使用。这些不能满足程序要求的小的可以分配的存储空间成为外碎片。

对内碎片的处理很容易,只要保证分配的空间和使用的空间一样大小就行了。这主要是靠写程序的程序员们估算好自己所需要的空 间。(为了保证分配的空间是块对齐,也就是双字对齐,分配器可能返回比实际申请的空间多出一个字节的空间。但相对于目前庞大的内存空间,这一个字节已经微 不足道了。但是如果程序申请大量的小块内存,这个问题还是要考虑的。。。比如:大量申请3个字节的空间,为了对齐,返回四个字节。这就导致四分之一的空间
浪费。)

对于外碎片,处理的方法说起来很简单--------合并。但说归说,真正合并的时候就很困难。如果空闲的碎片是连续的, 那合并很简答。如果碎片不连续,这就很麻烦了。可以对内存进行紧缩,但这样效率很难保证。操作系统对内存进行管理是,大都使用分页,分段或段页式的管理来 减少外碎片。如果在程序中对堆区的管理再引入复杂的外碎片处理措施,不但运行效率低,而且多此一举。。。所以,当在处理合并的时候,基本都只考虑相邻的空
闲块。

关于效率的问题,后面会详细说明。

如果空间无限大,那么malloc就可以不断的分配新的空间给程序,而free函数则什么都不要做。因此,使用过的空间将直接丢弃而不再重用。这样 malloc和free函数即简单,效率也高。但是!存储空间是有限的。理论上在 32-位 x86 系统上,进程可用的最大空间是4G(由地址宽度决定)。但是不可能直接给程序分配4G的空间。。。因此,程序一开始运行的时候,堆区都有一个默认的大小。 这样,在程序运行的过程中,可能出现堆区中的空间都耗尽的情况(也可能没有耗尽,但都是小的碎片)。因此,就要给程序的堆区扩容,也就是让操
作系统给程序分配更多的空间,术语叫把空间映射到程序的堆区中。在这里有一个概念,叫系统中断点或当前中断点,也就是被映射的内存的边界(最后一个有效地 址),也就是堆顶。

对堆区进行扩容,可调用下面的两个系统调用:

brk:
int brk(void *end_data_segment)
是一个非常简单的系统调用。
brk()
只是简单地将系统中断点设置为end_data_segment,向进程添加内存或者从进程取走内存。
mmap:
mmap()
,或者说是“内存映像”,类似于
brk()
,但是更为灵活。首先,它可以映射任何位置的内存,
而不单单只局限于进程。其次,它不仅可以将虚拟地址映射到物理的 RAM 或者 swap,它还可以将 它们映射到文件和文件位置,这样,读写内存将对文件中的数据进行读写。不过,在这里,我们只关心
mmap
向进程添加被映射的内存的能力。
munmap()
所做的事情与
mmap()
相反。

还有一个叫sbrk的系统调用void *sbrk(intptr_t increment)。sbrk()增加increment字节的内存到进程的堆区。而其返回的是旧的系统中断点,而不是新的。

下面说一下一种叫做隐式空闲链表的管理堆区的方法。堆块的格式如下:

-------------------------------------------------------------------

| int available /*是否空闲*/ |

| int size /*此块的大小*/ |

------------------------------------------------------------------

| | <----malloc返回的指针指向此处

| 实际空闲内存区 |

| |

------------------------------------------------------------------

| 填充区(可选) |

------------------------------------------------------------------

available 表示此块是否可用(被分配)。为1时表示可以分配,为0时表示被占用。

size表示此块内存的大小。

填充区用来进行块对齐(双字对齐)。

当调用malloc进行内存分配的时候,需要从空闲的内存块中选择一块合适的。选择的方法常用的有三种:首次匹配,下一次匹配,最佳匹配。

首次匹配:从头开始搜索表,选择第一个合适的块。

下一次匹配:和首次匹配类似,但是不是每次都从头搜索,而是从上一次搜索结束的位置开始搜索。

最佳匹配:选择所有合适的空闲块中最小的一块。

下一次匹配的运行速度要比首次匹配快,但空间利用率没有首次匹配高。最佳匹配的空间利用率最高,但运行也是最慢的。

当搜索到合适的空闲块后,这个空闲块往往不会整好合适,基本上都会大一下。多余的部分如果也分配给程序,就造成了内碎片。如果不分配,就要对空闲块进行分割。将多余的部分重新组成一个空闲块。这就有可能造成外碎片。因此,到底怎样处理,要看运行库的实现了。

若两个空闲块整好是连着的,那么把这两个块合并成一个块就可以提高空间的利用率。如果当前块的后一个块是空闲块,那么根据当前块的指针和其长度,很容易得到下一个空闲块的位置指针。但当空闲块在当前块的前面时,就很难获得其指针了。Knuth提出了一种聪明而有效的方法,叫做边界标记。就是复制块头到空闲块的尾部,形成一个块尾。这样,只需将当前块的指针向前移动一个块头大小的距离,就可以得到指向前一个块的块尾的指针,这样,前一个块的所有信息就都得到了。

在实际的实现中,往往引入两个特殊的块------序言块和结尾块。

序言块是程度为零且一直标记为不可分配的块,在整个堆区的最下面(堆区的开始位置)。序言块有块头和块尾。

结尾块在堆区的最上面(堆区的结束位置),也是长度为零且一直为不可分配。结尾块只有块头。

这两个块的引入可以减少很多边界的处理,提高程序的简洁性和效率。

下面是一个简单的实现的例子:

采用首次匹配。

1 /*

2 * File: my_alloc.h

3 */

4

5 #ifndef _MY_ALLOC_H

6 #define _MY_ALLOC_H

7

8 #ifdef __cplusplus

9 extern "C"

10 {

11 #endif

12

13 #include <unistd.h>

14 #define INIT_S 10240 //堆区的默认大小

15 int is_initialized = 0; //标记是否已经初始化

16 void *memory_start; //堆区的开始地址

17 void *memory_end; //系统中断点

18

19 /*

20 * 内存控制块

21 * 块头和块尾的结构

22 */

23 typedef struct __mem_control_block

24 {

25 int available; //是否可用

26 int size; //大小

27 } mem_control_block;

28

29 /*

30 * 初始化堆区。

31 * 建立堆区的初始结构,设置序言块,结尾块等。

32 */

33 void my_malloc_init()

34 {

35 /*

36 * 获得系统中断点,也即堆区的堆顶

37 */

38 memory_end = sbrk(0);

39 /*

40 * 堆区的开始位置

41 */

42 memory_start = memory_end;

43 /*

44 * 为堆区预先分配INIT_S个字节。

45 */

46 if (sbrk(INIT_S) <= 0)

47 {

48 printf("Init Error!\n");

49 return;

50 }

51 memory_end += INIT_S;

52

53 mem_control_block *mcb;

54 /*序言块*/

55 /*块头*/

56 mcb = memory_start;

57 mcb -> available = 0;

58 mcb -> size = 0;

59 /*块尾*/

60 mcb = (void*)mcb+ sizeof(mem_control_block);

61 mcb -> available = 0;

62 mcb -> size = 0;

63

64 /*设置控制块, 块头*/

65 mcb = memory_start + 2 * sizeof(mem_control_block);

66 mcb -> available = 1;

67 /*这个块的长度为整个堆区(除去控制块的长度)*/

68 mcb -> size = INIT_S - 5 * sizeof (mem_control_block);

69 /*设置边界标记,块尾*/

70 mcb = memory_end - 2 * sizeof (mem_control_block);

71 mcb -> available = 1;

72 mcb -> size = INIT_S - 5 * sizeof (mem_control_block);

73

74

75 /*结尾块*/

76 mcb = memory_end - sizeof (mem_control_block);

77 mcb -> available = 0;

78 mcb -> size = 0;

79

80 /*

81 * 已经进行了初始化

82 */

83 is_initialized = 1;

84 }

85

86 /*

87 * 模拟malloc函数

88 */

89 void * my_malloc(int size)

90 {

91 if (!is_initialized)

92 {

93 my_malloc_init();

94 }

95

96 /*分配的内存块的地址*/

97 void * mem_location = 0;

98

99 /*内存控制块的指针*/

100 mem_control_block *curr_mcb, *tail;

101

102 /*从头开始遍历*/

103 curr_mcb = memory_start;

104

105 while ((void*)curr_mcb < memory_end)

106 {

107 if (curr_mcb -> available)/*找到一个空闲块*/

108 {

109 if (curr_mcb -> size >= size && curr_mcb -> size < size + 2 * sizeof (mem_control_block))

110 /*大小合适,多余的空间不够进行分割的。也就是剩余的空间无法满足块头和块尾所需要的空间。*/

111 {

112 /*获得返回的内存地址*/

113 /*

114 * 此处必须把curr_mcb转成void*的格式!!

115 * 否则,在加的时候是以sizeof(mem_control_block)的倍数增加,而不是仅仅加一!!

116 */

117 mem_location = (void *) curr_mcb + sizeof (mem_control_block);

118 /*标记已经占用*/

119 curr_mcb -> available = 0;

120

121 /*获得边界标记的指针,块尾*/

122 tail = (void *) curr_mcb + sizeof (mem_control_block) + curr_mcb -> size;

123

124 /*标记已经占用*/

125 tail -> available = 0;

126 break;

127 }

128 else if (curr_mcb -> size > size + 2 * sizeof (mem_control_block))

129 /*进行分割*/

130 {

131 int old_size = curr_mcb -> size;

132 /*获得分配的内存块的地址*/

133 mem_location = (void *) curr_mcb + sizeof (mem_control_block);

134 /*标记已经占用*/

135 curr_mcb -> available = 0;

136 curr_mcb -> size = size;

137 /*获得边界标记的指针,块尾*/

138 tail = (void *) curr_mcb + sizeof (mem_control_block) + size;

139 /*标记已经占用*/

140 tail -> available = 0;

141 tail -> size = size;

142

143 /*将余下的部分分割成新的空闲块*/

144 mem_control_block *hd, *tl; /*新块的块头和块尾*/

145 /*块头*/

146 hd = (void *) tail + sizeof (mem_control_block);

147 hd -> available = 1;

148 hd -> size = old_size - size - 2 * sizeof (mem_control_block);

149 /*块尾*/

150 tl = (void *) hd + hd -> size + sizeof (mem_control_block);

151 tl -> available = 1;

152 tl -> size = hd -> size;

153

154 break;

155 }

156 }

157

158 /*指向下一个块*/

159 curr_mcb = (void*) curr_mcb + curr_mcb -> size + 2 * sizeof (mem_control_block);

160 }

161

162 /*没有找到合适的块,则扩展堆区,分配合适大小的内存加到堆区*/

163 if (!mem_location)

164 {

165 /*申请空间*/

166 if (sbrk(size + 2 * sizeof (mem_control_block)) <= 0)

167 {

168 printf("Sbrk Error!\n");

169 return 0;

170 }

171 /*设置控制块的信息*/

172 curr_mcb = (void*)memory_end - sizeof(mem_control_block);

173 curr_mcb -> available = 0;

174 curr_mcb -> size = size;

175 /*设置边界标记,块尾的信息*/

176 tail = (void*) curr_mcb + curr_mcb -> size + sizeof (mem_control_block);

177 tail -> available = 0;

178 tail -> size = size;

179

180 /*获得分配的内存块的地址*/

181 mem_location = (void*)curr_mcb + sizeof (mem_control_block);

182

183 memory_end = memory_end + size + 2 * sizeof (mem_control_block);

184 /*设置结尾块*/

185 tail = (void*)memory_end - sizeof(mem_control_block);

186 tail -> available = 0;

187 tail -> size = 0;

188 }

189

190 return mem_location;

191 }

192 /*

193 * 模拟free函数

194 */

195 void my_free(void *ptr)

196 {

197 if (ptr <= 0)

198 {

199 return;

200 }

201

202 mem_control_block *curr;

203 /*指向控制块的地址*/

204 curr = ptr - sizeof (mem_control_block);

205

206

207

208 /*标记为空闲,可用*/

209 curr->available = 1;

210 /*

211 * 合并

212 */

213 mem_control_block *pre, *next, *tmp;

214

215 /*获得前一个块的块头地址*/

216 pre = ptr - 2 * sizeof (mem_control_block);/*这条语句获得了前一个块的块尾的地址*/

217 pre = (void *) pre - pre -> size - sizeof (mem_control_block);/*进一步计算块头的地址*/

218

219 /*获得后一个块的块头地址*/

220 next = ptr + curr -> size + sizeof (mem_control_block);

221

222 if (!pre -> available && next -> available)/*只有后一个块空闲*/

223 {

224 curr -> size += (next -> size + 2 * sizeof (mem_control_block));

225 /*设置块尾*/

226 tmp = (void *) curr + curr -> size + sizeof (mem_control_block);

227 tmp -> available = 1;

228 tmp -> size = curr -> size;

229 }

230 else if (pre -> available && !next -> available)/*只有前一个块空闲*/

231 {

232 pre -> size += (curr -> size + 2 * sizeof (mem_control_block));

233 /*设置块尾*/

234 tmp = (void *) pre + pre -> size + sizeof (mem_control_block);

235 tmp -> available = 1;

236 tmp -> size = pre -> size;

237 }

238 else if (pre -> available && next -> available)/*前后都块空闲*/

239 {

240 pre -> size += (curr -> size + 4 * sizeof (mem_control_block) + next -> size);

241 /*设置块尾*/

242 tmp = (void *) pre + pre -> size + sizeof (mem_control_block);

243 tmp -> available = 1;

244 tmp -> size = pre -> size;

245 }

246

247 return;

248

249 }

250

251 void print_info()

252 {

253 printf("printf_info:\n");

254 mem_control_block *curr = memory_start;

255 while ((void*)curr < memory_end)

256 {

257

258 printf("a? %d s: %d %d %d\n", curr ->available, curr ->size, memory_end, curr);

259 curr = (void*) curr + curr -> size + 2 * sizeof (mem_control_block);

260 }

261 }

262

263

264 #ifdef __cplusplus

265 }

266 #endif

267

268 #endif /* _MY_ALLOC_H */

269

270
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐