操作系统内存分配

来源:互联网 发布:淘宝买家信誉有什么用 编辑:程序博客网 时间:2024/06/05 10:25

内存分配原理

 

  林  2011-1-12

 

W问:“K知道,stl的list直接clear后怎样可以释放内存没!

K答:“stl list clear 方法会自己释放自己分配的节点内存,但如果数据是用户用 new 产生的,那用户需要负责释放。”

W说:“我说的就是list自己分配的节点内存如何强行释放,它没有自己释放。刚才找了下,3.3以前的gcc会自己释放,3.4以后的就不会释放!”

K说:“有这事?它会释放它自己分配的节点内存。这点是STL标准,不会变的。我先查下。

W说:“恩!至少我看我代码中clear是不释放了,帮我找找怎么让他强行释放!”

 

 

从标准来说,clear肯定会释放它自己分配的内存的。为了说服他,就开始查找相关的标准。

http://www.cplusplus.com/reference/stl/list/clear/

All the elements in the list container are dropped: their destructors are called, and then they are removed from the list container, leaving it with a size of 0.

  

写测试程序:

 

#include <iostream>

#include <list>

 

int main()

{

   std::list<int> lstData;

   sleep(10);

   for(int i = 0; i < 500000; i++)

   {

       std::cout << i << std::endl;

       lstData.push_back(i);

   }

   std::cout << "clear:" << lstData.size() << std::endl;

   lstData.clear();

   std::cout << "end:" << lstData.size() << std::endl;

   sleep(10);

   while(1)

     std::cout << "ok" << std::endl;

 

}

 

环境:XP,VS2005,运行,一切正常,内存被释放。

 

看客:“你怎么查看内存的?”

“……,哪里凉快你就去哪里歇着吧!”

 

 Windows下一切正常,再在linux下运行下看是否正常。环境:

操作系统:

Linux 2.6.16.60-0.21-smp #1 SMP Tue May 6 12:41:02 UTC 2008 x86_64 x86_64 x86_64 GNU/Linux

 

g++ 版本:

Target: x86_64-suse-linux

Configured with: ../configure --enable-threads=posix --prefix=/usr --with-local-prefix=/usr/local --infodir=/usr/share/info --mandir=/usr/share/man --libdir=/usr/lib64 --libexecdir=/usr/lib64 --enable-languages=c,c++,objc,fortran,obj-c++,java,ada --enable-checking=release --with-gxx-include-dir=/usr/include/c++/4.1.2 --enable-ssp --disable-libssp --disable-libgcj --with-slibdir=/lib64 --with-system-zlib --enable-shared --enable-__cxa_atexit --enable-libstdcxx-allocator=new --program-suffix= --enable-version-specific-runtime-libs --without-system-libunwind --with-cpu=generic --host=x86_64-suse-linux

Thread model: posix

gcc version 4.1.2 20070115 (SUSE Linux)

 

运行。发现top查看到的内存果然如W所说没有释放!怎么会这样呢?就开始分析内存分配过程。查看STL的代码。

 

看客:“STL代码那么多,你如何知道看哪个文件?”

K答:“查看代码的方法有很多种,我常用的是调试跟踪法。”

看客:“没用过。”

K答:“那你先去了解下gdb的调试方法,再回来看。”

 

/**

       *  Erases all the elements.  Note that this function only erases

       *  the elements, and that if the elements themselves are

       *  pointers, the pointed-to memory is not touched in any way.

       *  Managing the pointer is the user's responsibilty.

       */

      void

      clear()

      {

        _Base::_M_clear();

        _Base::_M_init();

      }

注释写得很清楚:它会释放自己分配的内存,但用户自己分配的数据内存要用户自己处理。我们继续看它的实现:

   template<typename _Tp, typename _Alloc>

    void

    _List_base<_Tp, _Alloc>::

    _M_clear()

    {

      typedef _List_node<_Tp>  _Node;

      _Node* __cur = static_cast<_Node*>(this->_M_impl._M_node._M_next);

      while (__cur != &this->_M_impl._M_node)

    {

      _Node* __tmp = __cur;

      __cur = static_cast<_Node*>(__cur->_M_next);

      _M_get_Tp_allocator().destroy(&__tmp->_M_data);

      _M_put_node(__tmp);

    }

    }

 

    这里运用了策略模式进行内存的分配管理。由于标准STL默认的内存分配策略是直接调用 delete。所以这里没有进行缓存(你仔细想想,确实也没有缓存的必要)。但是如果用户想要STL进行缓存内存的话,他可以根据自己的需求定义内存分配策略。

继续跟踪,确实STL默认的内存分配策略是直接调用 delete

//     __p is not permitted to be a null pointer.

      void

      deallocate(pointer __p, size_type)

      { ::operator delete(__p); }

 

到此,可以证明:标准STL会释放自己分配的节点内存,并且不会进行缓存。

那么为什么与实验结果不符呢?我们把目光放到了 delete 上,继续分析:Windows下运行正常,linux下确实没有释放。是不是操作系统内存分配策略上不同?从操作系统原理可知,是由操作系统内存分配置策略不同引起的,具体的请参《操作系统原理》考清华大学。或者:http://blog.163.com/prevBlogPerma.do?host=holyrain1314&srl=1001141352010103011524719&mode=prev

继续我们linux下的分析……

C++ delete 操作最终调用了 C库中的 glibc 中的 malloc 函数。下面是我环境的依赖关系:

libstdc++.so.6 => /usr/lib64/libstdc++.so.6 (0x00002b893a169000)

        libm.so.6 => /lib64/libm.so.6 (0x00002b893a367000)

        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00002b893a4bc000)

        libc.so.6 => /lib64/libc.so.6 (0x00002b893a5ca000)

        /lib64/ld-linux-x86-64.so.2 (0x00002b893a04d000)

newdelete 是在 libstdc++中实现的。声明在 #include <new>

 

从标准C库中的内存分配库中可以得到些malloc分配的信息。详细参见:#include <malloc.h>

修改上面的代码,把这些信息打印出来。

#include <iostream>

#include <list>

#include <malloc.h>

 

 

int main()

{

   std::list<int> lstData;

    sleep(15);

    std::cout << "start" << std::endl;

    malloc_stats();

    for(int i = 0; i < 50000000; i++)

    {

        //      std::cout << i << std::endl;

        lstData.push_back(i);

    }

    std::cout << "clear:" << lstData.size() << std::endl;

    malloc_stats();

    lstData.clear();

    malloc_stats();

    std::cout << "end:" << lstData.size() << std::endl;

   sleep(10);

   while(1)

     std::cout << "ok" << std::endl;

 

}

 

运行结果:

Arena 0:

system bytes     =          0

in use bytes     =          0

Total (incl. mmap):

system bytes     =          0

in use bytes     =          0

max mmap regions =          0

max mmap bytes   =          0

clear:50000000

Arena 0:

system bytes     = 1600118784

in use bytes     = 1600000000

Total (incl. mmap):

system bytes     = 1600118784

in use bytes     = 1600000000

max mmap regions =          0

max mmap bytes   =          0

Arena 0:

system bytes     = 1600118784

in use bytes     =          0

Total (incl. mmap):

system bytes     = 1600118784

in use bytes     =          0

max mmap regions =          0

max mmap bytes   =          0

 

从上面可以看出 in use bytes  = 0system bytes = 1600118784系统没有释放。看来内存管理是在 glibc 中做的。我们继续跟踪代码……

由于现有glibc库不为调试版本,所以前面跟踪法用不了了。我采用了逆向跟踪法。继续分析……

我又写了一个测试程序:

 

void main()

{  

std::cout << "new" << std::endl;

    char *p = new char;

    if(p)

{

std::cout << "delete" << std::endl;

        delete p;

}

}

 

这个程序很简单,目的就是为了跟踪低层的系统调用。

write(1, "new/n", 4new

)                    = 4

brk(0)                                  = 0x503000

brk(0x524000)                           = 0x524000

write(1, "delete/n", 7delete

)                 = 7

当分配置的内存比较小时,调用了系统调用 brk

 

char *p = new char;改成char *p = new char[1024*1024;得到下面结果:

 

write(1, "new/n", 4new

)                    = 4

mmap(NULL, 1052672, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x2ac861bb1000

write(1, "delete/n", 7delete

)                 = 7

当需要分配的内存较大时,直接调用了系统调用 mmap。而delete时没有调用系统调用。这正好与malloc的使用说明中的注释一致。

Normally, malloc() allocates memory from the heap, and adjusts the  size  of  the       heap  as  required,  using sbrk(2).  When allocating blocks of memory larger than        MMAP_THRESHOLD bytes, the glibc malloc() implementation allocates the memory as a       private  anonymous  mapping  using mmap(2).  MMAP_THRESHOLD is 128 kB by default,       but is adjustable using mallopt(3).   Allocations  performed  using  mmap(2)  are       unaffected by the RLIMIT_DATA resource limit (see getrlimit(2)).

 

从这两次实验可以得知new最后调用的系统调用是 brk mmap。下载一份 glibc 的源码。进行分析……

mmap <—— GC_unix_get_mem <—— GET_MEM<——GC_scratch_alloc <——alloc_hdr<——GC_init_headers<——GC_init_inner<—— GC_alloc_large <——GC_generic_malloc <——GENERAL_MALLOC <——GC_malloc <——GC_MALLOC<——operator new

GC_mallo<——REDIRECT_MALLOC<——malloc

 

GC_generic_malloc判断是申请内存是否大值,如果是大于则按上面的线路调系统调用 mmap。如果小于,则最终会调用brk

有人说new实际上是调用了glibcmalloc。但是从这个版本中的情况看不是这样的。在这个版本中,newmalloc都调用了宏,然后再由宏定义到GC_malloc。(也许以前的版本是直接调用malloc的吧。这个是我推测的。但现在这个版本的扩展性更好,更加合理。)准确的讲,现在的newmalloc没有关系,一个是glibc提供给C++的接口,一个是提供给C的接口。但它两的具体实现是一样的(都是通过调用内部接口GC_malloc实现的)。

 

 

munmap<——GC_unmap_gap<——GC_merge_unmapped<——GC_alloc_large<——GC_generic_malloc  ……    GC_freehblk <——GC_free<——REDIRECT_FREE<——free

 

brksbrk主要的工作是实现虚拟内存到内存的映射.GNU C,内存分配是这样的:
      
每个进程可访问的虚拟内存空间为3G,但在程序编译时,不可能也没必要为程序分配这么大的空间,只分配并不大的数据段空间,程序中动态分配的空间就是从这一块分配的。如果这块空间不够,malloc函数族(realloccalloc等)就调用sbrk函数将数据段的下界移动,sbrk函数在内核的管理下将虚拟地址空间映射到内存,供malloc函数使用。(参见linux内核情景分析)

Brk系统调用原码分析http://wenku.baidu.com/view/3928babd960590c69ec37600.html

linux中的物理地址和虚拟地址http://blog.163.com/prevBlogPerma.do?host=hujianjust&srl=72455072201042795435529&mode=prev

 

 

 

#include <iostream>

#include <list>

#include <malloc.h>

int main()

{

    long n = 30240;

    for(long i = 0;i < n;i++)

    {

        char *p = new char[1024*1024];

        if(!p)

            std::cout << "fail new" << std::endl;

    }

while(1)

    {

        std::string a;

        std::cout << "enter:";

        std::cin >> a;

        malloc_stats();

    }

}

这段程序可以正常运行,成功分配了所需要的内存。

 

#include <iostream>

#include <list>

#include <malloc.h>

int main()

{

    long n = 30240;

    for(long i = 0;i < n;i++)

    {

        char *p = new char[1024*1024];

        if(!p)

            std::cout << "fail new" << std::endl;

        else

        {

            for(long j = 0; j < 1024*1024;j++)

                p[j] ='a';

        }

    }

       while(1)

    {

        std::string a;

        std::cout << "enter:";

        std::cin >> a;

        malloc_stats();

    }

}

加了上面程序段后,当内存消耗到快接近物理内存大小时,程序就被操作系统给kill了。

 

从上面这两段程序运行的结果可以看出:mmap向内核申请内存时,并没有真正分配,只有当写入时,才真正进行虚拟内存到物理内存的映射操作。