Linux性能学习(2.3):内存_为什么分配的内存比申请的内存大16个字节


参考资料:

  1. https://www.gnu.org/software/libc/

在上一篇文章中,探讨了Linux系统对进程以及线程的内存分配问题,然后采用申请1KB内存的方式进行验证,然后发现将第二次申请的内存地址减去第一次申请内存的地址,长度为1040,比我们申请的1024多了16个字节,从而提出一个问题,“在64位系统中,为什么系统分配的内存比实际申请的内存大16个字节?”。

但是上一篇的文章主要是探讨内存分配的,所以对这个问题没有过多追究,提出这个问题也不严谨,因为只是申请了1024个字节,系统分配了1040,多了16个字节,那么如果申请1个字节、2个字节、3个字节等等,系统又是如何分配的,是否还是多分配16字节?64位系统和32位系统是否都是多分配16字节?

所以下面我们先验证下,如果我们申请不同大小的内存,系统是否还是会多申请16字节的内存?

1 验证申请不同内存,系统分配机制

1.1 代码

#include <stdio.h>
#include <stdlib.h>

int main()
{
	int i = 0;
	char* s8New = NULL;
	char* s8Old = malloc(0);
	
	if (NULL == s8Old)
	{
		printf("malloc err\n");
	}
	else
	{
		printf("malloc success, malloc size:%d, usable_size:%d, addr:%p, ", 0, malloc_usable_size(s8Old), s8Old);
	}
		
	for (i = 1; i < 20480; i++)
	{	
		s8New = (char*)malloc(i);
		if (NULL == s8New)
		{
			printf("malloc err\n");
		}
		
		if ((NULL != s8New) && (NULL !=  s8Old))
		{
			printf("addr size:%d\n", s8New - s8Old);
			s8Old = s8New;
			printf("malloc success, malloc size:%d, usable_size:%d, addr:%p, ", i, malloc_usable_size(s8Old), s8Old);
		}
		else
		{
			printf("malloc success, malloc size:%d, usable_size:%d, addr:%p, ", i, malloc_usable_size(s8New), s8New);
		}
	}
	
	return 0;
}

上述代码的功能是从1个字节开始,逐步增加,直到申请2MB的内存,查看系统分配情况,会有三个主要参数的打印:“malloc size”表示我们申请的内存,“usable_size”是使用malloc_usable_size来获取系统实际分配的大小,“addr size”为下一个申请的内存地址减去当前申请内存的地址,即为当前申请内存的大小。

1.2 测试

在Ubuntu 64位系统下测试,结果如下:
在这里插入图片描述

在32位系统下测试,结果如下:
在这里插入图片描述

上面的数据,只是部分数据,经过综合,得出如下数据:
64位系统
在这里插入图片描述

32位系统
在这里插入图片描述

1.3 结论

通过上面的表格,我们可以得出如下结论:

  • A.在我们申请内存的时候,系统可能会申请多的内存给到我们,具体规则是:
    32位系统下:N8+4,N为1~无穷大,(N8+4)的值为最接近与我们申请的内存的值;
    64位系统下:N16+8,N为1~无穷大,(N16+8)的值为最接近与我们申请的内存的值。
  • B.申请的内存的地址相减会比实际申请的内存usable_size还要大,在32位系统下大4个字节,在64位系统下大8个字节。即实际分配的内存地址范围大小为:
    32位系统下:N8+8;
    64位系统下:N
    16+16。

2 为什么会多分配内存

针对上面的结论A,我们进行分析查找原因。

在上面我们使用malloc来申请内存,那么这些问题就跟malloc有关了,我们查看下malloc相关的代码,看看有啥收获。

源码可通过上面参考链接进行下载,在malloc.c/_int_malloc函数中,我们看到了checked_request2size函数,它的作用是将我们需要申请的字节大小转换为内部的大小,通过字节对齐等方式进行转换,来获取最小MINSIZE(最小可分配大小)的大小。
在这里插入图片描述

在这里,我们得到了一个信息MINSIZE,我们进到checked_request2size函数里面进行查看,
在这里插入图片描述

在checked_request2size中的request2size中我们可以看到,如果 (req) + SIZE_SZ + MALLOC_ALIGN_MASK的大小小于MINSIZE,那么就返回MINSIZE,如果大于,则进行对齐操作,再返回。
现在我们获取到结果宏参数:SIZE_SZ、MALLOC_ALIGN_MASK、MINSIZE

SIZE_SZ的大小通过代码追踪就是unsigned int的长度,在32位系统中就是4个字节;
MALLOC_ALIGN_MASK相关的定义如下:

/* The corresponding bit mask value.  */
#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)
/* MALLOC_ALIGNMENT is the minimum alignment for malloc'ed chunks.  It
   must be a power of two at least 2 * SIZE_SZ, even on machines for
   which smaller alignments would suffice. It may be defined as larger
   than this though. Note however that code and data structures are
   optimized for the case of 8-byte alignment.  */
#define MALLOC_ALIGNMENT (2 * SIZE_SZ < __alignof__ (long double) \
			  ? __alignof__ (long double) : 2 * SIZE_SZ)
			  

可以得出,MALLOC_ALIGNMENT在32位系统中的长度为2*SIZE_SZ,即长度为8,那么MALLOC_ALIGN_MASK的长度就为7了。
MINSIZE的相关定义如下:

#define MINSIZE  \
  (unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK))
#define MIN_CHUNK_SIZE        (offsetof(struct malloc_chunk, fd_nextsize))

MIN_CHUNK_SIZE 展开如下:

#define MIN_CHUNK_SIZE        (offsetof(struct malloc_chunk, fd_nextsize))
# define offsetof(type,ident) ((size_t)&(((type*)0)->ident))
===》等价于
#define MIN_CHUNK_SIZE (size_t) & ((struct malloc_chunk*)NULL) -> fd_nextsize)

struct malloc_chunk的定义如下:

struct malloc_chunk {

  INTERNAL_SIZE_T      mchunk_prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      mchunk_size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;

  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};

所以上面代码的大致意思是,取malloc_chunk中fd_nextsize的地址,这样得到的地址就是这个成员在结构体中的首地址。所以这个结构体中,必须需要的是前4个,后面两个仅用于large blocks,所以在32位的系统中,这个结构体的大小为44=16字节,在64位上为84=32位,或者4+4+8+8=24位。
由此,可以得到:

#define MINSIZE =(((16+7) & ~7))=16。

至此,我们可以得出如下信息,在32位系统中,一些参数值如下:

SIZE_SZ 4
MALLOC_ALIGNMENT 2*4=8
MALLOC_ALIGN_MASK 8-1=7
MIN_CHUNK_SIZE 16
MINSIZE 16

那么request2size宏定义可以换算如下:

#define request2size(req)       (((req) + 4 + 7 < 16)  ? 16 :                                                       ((req) + 4 + 7) & ~7)

将上面1.2章节中32位申请的内存对照表和上面的request2size中进行对照,结论一致。

同理,在64位系统中,一些参数值如下:

SIZE_SZ 8
MALLOC_ALIGNMENT 2*8=16
MALLOC_ALIGN_MASK 16-1=15
MIN_CHUNK_SIZE 32
MINSIZE 32

至此,我们可以得出结论,当我们申请内存时候,系统会根据自身的机制分配大于我们申请的内存的大小,具体分配大小参考request2size进行确认。

3 为什么会有4字节不可使用

针对问题B进行分析,在上面,我们看到在32位系统中,addr size比实际可使用的内存大小usable_size大4个字节,为什么会有这4个字节的浪费,或者说不能使用?

在malloc.c中有如下解释:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

上面的解释大致如下:
我们申请的chunk主要由Size of previous chunk + Size of chunk+user data等几部分构成的,chunk指针指向chunk开始的地方,mem指针是提供给用户的指针地址,从这个这个地址可以使用,进行读写等操作。
在这里插入图片描述

空闲的chunk是存储是双向循环链表中的,结构体是由 Size of previous chunk + Size of chunk+fd+bk等4部分组成的,参考下图:
在这里插入图片描述

上面的A/M/P三个参数:

  • P:如果P为0,表示前一个chunk为空闲,则prev_size中的值才有效,表示上一个空闲chunk的大小;如果P为1,则前一个chunk正在使用,prev_size无效。
  • M:如果为1,则是通过mmap方式分配的内存;如果为0,则是通过heap方式分配的内存。
  • A:为0表示主分区分配的内存;为1表示非主分配区分配的内存。

通过上图可以看到,一个chunk有head和foot,都是表示当前chunk大小,但是foot已经在next chunk了,即next chunk的Size of previous chunk ,同时为了提高chunk的有效载荷数据,Size of previous chunk 这个数据段也会用来存储数据,所以一个chunk可以由head+mem两部组成。而head的长度为SIZE_SZ的长度,即4个字节。

所以可以理解为,在32位系统下,linux申请的内存减去4个字节的长度,剩下的长度均为有效数据长度,即我们可以使用的长度。

同理,在64位系统下,linux申请的内存减去8个字节的chunk size字段,剩下的便是可以使用的数据长度。

猜你喜欢

转载自blog.csdn.net/u011003120/article/details/128286487