linux malloc和free解析

时间:2024-04-04 19:34:10

一、简介

malloc函数原型:void*malloc(size_t size);

申请size个字节的虚拟地址空间,并返回指向这块内存的指针;如果申请失败则返回一个空指针free函数原型:void free(void *ptr);

释放ptr指向的虚拟地址空间。问题:为啥只要传递地址,而不用传递释放多大空间?(在后文可以找到答案)

 

使用需要注意的一些地方:

A.申请内存空间后,必须检查是否分配成功

B.当不需要再使用的内存时,记得释放;释放后应该指向这块内存的指针指向NULL,防止程序后面不小心使用它

C.这两个函数应该配对,防止内存泄露

D.虽然malloc函数的类型是(void *),为了明确用途最好在前面强制类型转换。

 

二、uclibc库实现

         主要讲述标准封装库uClibc-0.9.33.2里支持MMU的malloc-standard的malloc和free讲解,在配置里有如下选择

choice

   prompt "Malloc Implementation"

   default MALLOC if ! ARCH_USE_MMU

   default MALLOC_STANDARD if ARCH_USE_MMU

 

1、数据结构

         在标准库里有个专门的malloc数据结构管理申请到的虚拟内存,具体如下:

struct malloc_state {

 

  使用fastbin的阈值 64字节

 size_t  max_fast;   /* low 2 bits used as flags */

 

  /*Fastbins 管理<64字节的malloc申请*/

 mfastbinptr     fastbins[NFASTBINS];

 

  /* 指向最顶端的chunk,当这个chunk空闲的size大于trim_threshold就会brk系统调用释放内存 */

 mchunkptr        top;

 

  /*The remainder from the most recent split of a small request */

 mchunkptr        last_remainder;

 

  /* 该数组管理64字节以上通过brk产生的虚拟内存,mmap则一对一申请释放*/

 mchunkptr        bins[NBINS * 2];

 

  /*Bitmap of bins. Trailing zero map handles cases of largest binned size */

 unsigned int    binmap[BINMAPSIZE+1];

 

  /*Tunable parameters */

 unsigned long     trim_threshold;//触发brk系统调用释放内存= 256K

 size_t  top_pad; //最顶端的brk是否空闲,空闲就brk系统调用释放内存

 size_t  mmap_threshold; //触发mmap系统调用申请的阈值 =256K

 

  /*Memory map support */

 int              n_mmaps;

 int              n_mmaps_max;

 int              max_n_mmaps;

 

  /*Cache malloc_getpagesize */

 unsigned int     pagesize;

 

  /*Track properties of MORECORE */

 unsigned int    morecore_properties;

 

  /*Statistics */

 size_t  mmapped_mem;

 size_t  sbrked_mem;

 size_t  max_sbrked_mem;

 size_t  max_mmapped_mem;

 size_t  max_total_mem;

};

 

使用该结构体主要解决如下问题:

A. 空闲块组织:我们如何记录空闲块

B.放置:我们如何选择一个合适的空闲块来放置一个新分配的块

C.分割:在我们将一个新分配的块放置到某个空闲块之后,我们如何处理这个空闲块中剩余的部分

D.合并:我们如何处理一个刚刚被释放的块

 

2、malloc

         malloc申请时在头上会多申请2个字用于管理该申请块,其结构如下:

An allocated chunk looks like this:

   chunk->+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

          |             Size of previouschunk, if allocated        | |

          +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

          |             Size of chunk, inbytes                  |P|

    mem->+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

          |             User data startshere...                    .

          .                                                   .

          .            (malloc_usable_space() bytes)              .

          .                                                  |

nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

          |             Size of chunk                           |

          +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

第一个字指向前一个chunk的大小,第二个字指向当前chunk的大小,P表示是否使用,mem即使malloc返回的获取到地址。因此,malloc(bytes)申请bytes个字节时,实际申请的nb=bytes + 2*sizeof(size_t)

 

根据源码,申请的情况如下:

A.当nb <= max_fast(64字节)

直接从fastbins数组中分配,直接返回。如果没有,则从bins里分配

B.当nb > max_fast

         从bins数组里查找获取。

如果获取到一个比较大的chunk,则先进行分割,剩余的重新组织管理,直接返回给用户。

如果没有找到大的nb,则会努力对bins的空闲进行合并,如果还是没有则会通过系统调用brk或mmap从kernel申请。

C.系统调用brk或mmap

         malloc管理的fastbins和bins里没有找到需要的虚拟内存时,只能通过系统调用从内核获取。

当nb > mmap_threshold=256K时,使用mmap的方式,set_head(p,size|IS_MMAPPED);会将上面第1个字的bit[1]设置成IS_MMAPPED(释放时判断)

当nb < mmap_threshold=256K时,使用brk的方式,同时调整malloc管理的相关信息。

两者细节见《第三章》

 

3、free

         free后chunk的布局结构如下:

Free chunks are stored in circulardoubly-linked lists, and look like this:

   chunk->+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

          |             Size of previous chunk                    |

          +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

     `head:' |             Size of chunk, in bytes                  |P|

    mem->+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

          |             Forward pointer tonext chunk in list         |

          +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

          |             Back pointer toprevious chunk in list        |

          +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

          |             Unused space (may be0 bytes long)        .

          .                                                  .

          .                                                  |

nextchunk->+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

    `foot:' |             Size of chunk, in bytes                     |

         +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

The P (PREV_INUSE) bit

 

问题:为啥只要传递地址,而不用传递释放多大空间?

答:地址前面预留了2个字,其中一个字就保存了申请到的内存块大小,所以只需要传递地址就行

 

根据源码,释放的情况如下:

A.当size <= max_fast时

         直接释放到fastbins数组里

B.当size > max_fast&& 不是mmap时

         先检查前后项是否属于空闲,如果是则可能进行合并,合并规则如下:

         前面的块和后面的块都已经分配的,则只释放自己

         前面的块是已分配的,后面的块空闲,则和后面的空闲块合并

         前面的块是空闲,后面的块是分配的,则和前面的块合并

         前后块都是空闲的,则进行前后合并

进行上面的合并操作后,判断最顶端top的空闲chunk是否超过trim_threshold,如果超过就进行brk系统调用释放的内存。

注意点:

         结合brk系统调用服务程序(见下面章节),brk是线性向上增长,如果最顶端的brk在使用而brk下面的其他地址释放了,即使超过trim_threshold这时仍无法进行brk系统调用释放,这时就会导致这部分物理内存占着,从而引发内存碎片。

所以一个简单原则:通过brk申请的内存malloc后尽快free(即小内存申请<256K),对应不free的申请使用mmap的方式。

 

 

C.如果是mmap出来的内存

         直接调用系统调用munmap进行内存释放

 

三、brk和mmap系统调用

         在user/kernel为3G/1G比例的情况下,进程的内存分布图(摘自网络)如下:

linux malloc和free解析

这是灵活的内存增长方式,栈stack和mmap区域向下增长,堆heap向上增长。

brk和mmap的系统调用分别对应heap区域和memorymmaping segment区域。通过cat /proc/pid/maps可以看到该进程的详细内容:

00008000-00013000 r-xp00000000 1f:05 2722616    /usr/bin/sysapp

0001a000-0001b000 rw-p0000a000 1f:05 2722616    /usr/bin/sysapp

00a38000-00a39000 rw-p00000000 00:00 0          [heap]

b6cac000-b6caf000r-xp 00000000 1f:04 70        /lib/libdl.so.0

b6caf000-b6cb6000---p 00000000 00:00 0

b6cb6000-b6cb7000rw-p 00002000 1f:04 70        /lib/libdl.so.0

b6cb7000-b6cb8000r-xp 00000000 1f:04 69        /lib/libnsl.so.0

b6cb8000-b6cbf000---p 00000000 00:00 0

b6cbf000-b6cc0000rw-p 00000000 1f:04 69        /lib/libnsl.so.0

b6cc0000-b6d59000 r-xp00000000 1f:04 65         /lib/libc.so.0

b6d59000-b6d60000---p 00000000 00:00 0

b6d60000-b6d62000rw-p 00098000 1f:04 65        /lib/libc.so.0

b6d62000-b6d67000rw-p 00000000 00:00 0

b6d67000-b6d87000r-xp 00000000 1f:04 71        /lib/libgcc_s.so.1

b6d87000-b6d8e000---p 00000000 00:00 0

b6d8e000-b6d8f000rw-p 0001f000 1f:04 71        /lib/libgcc_s.so.1

b6d8f000-b6da0000r-xp 00000000 1f:04 67        /lib/libm.so.0

b6da0000-b6da7000---p 00000000 00:00 0

b6da7000-b6da8000rw-p 00010000 1f:04 67        /lib/libm.so.0

b6da8000-b6e5c000r-xp 00000000 1f:04 79        /lib/libstdc++.so.6

b6e5c000-b6e63000---p 00000000 00:00 0

b6e63000-b6e67000r--p 000b3000 1f:04 79        /lib/libstdc++.so.6

b6e67000-b6e69000rw-p 000b7000 1f:04 79         /lib/libstdc++.so.6

b6e69000-b6e6f000rw-p 00000000 00:00 0

b6e6f000-b6e83000r-xp 00000000 1f:04 74        /lib/libpthread.so.0

b6e83000-b6e8a000---p 00000000 00:00 0

b6e8a000-b6e8b000rw-p 00013000 1f:04 74        /lib/libpthread.so.0

b6e8b000-b6e8d000rw-p 00000000 00:00 0

b6e8d000-b6f03000r-xp 00000000 1f:05 4633696   /usr/lib/liblog4cpp.so.5

b6f03000-b6f0a000---p 00000000 00:00 0

b6f0a000-b6f0d000rw-p 00075000 1f:05 4633696   /usr/lib/liblog4cpp.so.5

b6f0d000-b6f14000r-xp 00000000 1f:04 77        /lib/ld-uClibc.so.0

b6f19000-b6f1b000rw-p 00000000 00:00 0

b6f1b000-b6f1c000rw-p 00006000 1f:04 77        /lib/ld-uClibc.so.0

be912000-be933000rw-p 00000000 00:00 0          [stack]

ffff0000-ffff1000 r-xp00000000 00:00 0          [vectors]

蓝色:代码和数据段

红色:堆

紫色:通过mmap出来的存放共享库文件

绿色:栈

橙色:中断向量表

 

1、brk系统调用

         brk从heap向上简单的线性增长,从上面可以看出start_brk表示当前current task的heap起始地址,brk指向已经分配了的heap。

         申请:将申请的地址和start_brk和brk进行比较,如果合法就修改brk指向地址,并返回告之成功

         释放:修改brk指向,调用do_munmap进行内存释放。

 

2、mmap系统调用

         mmap是把一个文件或posix共享内存区映射到调用进程的地址空间。三个目的:

A.使用普通文件提供内存映射IO

B.使用特殊文件提供匿名映射IO

C.使用shm_open以提供无亲缘关系的进程间posix共享内存区

         其他细节见网上说明

 

malloc使用brk或mmap都是从currenttask的线性地址空间申请一块虚拟地址,并没有相应的物理内存。当task任务运行使用到上面的虚拟地址时,MMU转化虚拟地址,根据页表查找虚拟地址对应的物理内存,如果不存在就会产生缺页异常。缺页异常的处理大致逻辑如下图:

 linux malloc和free解析

如果申请到了,就会将这个虚拟地址和物理内存的映射添加到current task的页表,同时刷新cache,下次访问就直接映射。