近期内存学习的几点心得

最近写代码对于内存分配这一方面遇到了一些现象,下面罗列下这些现象,小部分除了现象和推测还有解释。
1.malloc分配内存结构
    char *p = (char *)malloc(1);
    printf("%d %d %u\n",*(int *)(p - 2 * sizeof(size_t)),*(int *)(p - sizeof(int)),(unsigned)p);
    char *p2 = (char *)malloc(1);
    printf("%d %d %u\n",*(int *)(p2 - 2 * sizeof(size_t)),*(int *)(p2 - sizeof(int)),(unsigned)p2);
    free(p2);
    printf("%d %d %u\n\n\n",*(int *)(p2 - 2 * sizeof(size_t)),*(int *)(p2 - sizeof(int)),(unsigned)p2);

    char *p3 = (char *)malloc(65);
    printf("%d %d %u\n",*(int *)(p3 - 2 * sizeof(size_t)),*(int *)(p3 - sizeof(int)),(unsigned)p3);
    char *p4 = (char *)malloc(65);
    char *p5 = (char *)malloc(65);
    printf("%d %d %u\n",*(int *)(p4 - 2 * sizeof(size_t)),*(int *)(p4 - sizeof(int)),(unsigned)p4);
    free(p3);
    printf("%d %d %u\n",*(int *)(p4 - 2 * sizeof(size_t)),*(int *)(p4 - sizeof(int)),(unsigned)p4);
    free(p4);
    printf("%d %d %u\n",*(int *)(p3 - 2 * sizeof(size_t)),*(int *)(p3 - sizeof(int)),(unsigned)p3);
    free(p5);
    printf("%d %d %u\n",*(int *)(p3 - 2 * sizeof(size_t)),*(int *)(p3 - sizeof(int)),(unsigned)p3);

    结果是:
    0 17 145408008
    0 17 145408024
    0 17 145408024
    
    
    0 73 145408040
    0 73 145408112
    72 72 145408112
    0 145 157237288
    0 135137 137388072
    找答案步骤:
    0.刷stackoverflow,这一步很有帮助,其实已经找到答案了,但是还是得扒源码才有底;
    1.找对应源码,lsof任意一个执行中的程序,发现动态链接glibc so /lib/i386-linux-gnu/libc-2.19.so,可以根据版本去官网下源码。然而在我的ubuntu直接dpkg -S /usr/include/stdlib.h可见libc6-dev,apt-get source libc6下源码。在stdlib.h中声明free的地方用ctags跳到一处weak定义的地方,weak定义是在链接时在其他地方找不到定义兜底用的,并以一眼看上去定义及其简单不会是。无奈之下find+grep代码发现了个__libc_free。任写一个包含free函数的程序打断点 b __libc_free(),还真到了,就认定是此处,当然从free跳到__libc_free的过程 to do(还有基础定义到free的地方都略看了下);
    2.对于1\2行的第一列的两个0:看源码可知,对于一般的malloc返回指针,指针前有两个size_t长度的值,一个存prev_size(前面一块堆内存大小),一个存size(本块大小)。仅有当前面一块内存可用,就是已经被free了或者一个大块的前面部分被分配后面部分还没用到时,prev_size才不为0,可见prev_size是前一个“空块”的大小。所以前两个0正常;
    3.1\2\3行第二列的三个17,这个是当前块大小。当前块有两个size_t(见2,一个prev_size,一个size)= 8,还有malloc(1)一个字节,此外,块大小必须和sizeof(double)=8对齐,所以当前块大小是16。17是这样的,因为块大小必须和double对齐,所以size的低三位肯定是0,这三位就被利用为三个标志位了。分别是PREV_INUSE(0x1),IS_MMAPPED(0x2),NON_MAIN_ARENA(0x4),这个17 = 16(大小) + 1(PREV_INUSE),意思就是说前面一个块不是空的,free的时候可以不考虑合并,不过这一位貌似和prev_size冗余了;
    4.从1\2行第三列145408008,145408024也能看出来差值为16;
    5.第3行说不通了,为啥前一块被释放了prev_size还是0,为啥PREV_INUSE还置位!!!!!!继续扒代码,发现对于小块(实际分配内存小于等于64,64其实是有说法的,不过没看明白)的分配请求,malloc直接就不走检查合并流程,(以下说法不准确)大约是有个叫fastchunk的链表,直接插入,之后要用直接调出来;
    6.4\5\6行对于大于64字节的malloc进行了测试,第6行得到了想要的结果。PREV_INUSE被清空,prev_size被填入。意思是前一块内存可以合并;
    7.free p4后打印p3,发现已经合并了8 + 64 + 8 + 64 + 1 = 145;
    8.p5也释放后发现还剩下135136的一个大块,4096(本机页大小,可以通过getpagesize得到)*33=135168,135168-135136=32是前面p1 p2的大小刚好对上,说明malloc一次分配的远大于所需的堆大小33页。
    总结:
    1.malloc干了不少事,上面没写到的至少还有多线程兼容和牛逼的合并算法、以及大块内存用mmap分配,小块传说中用srbk;
    2.malloc也可以被覆盖掉,方法在stackoverflow上至少看到了四种;
    3.malloc分配的内存和物理内存是两码事,下面会举个例子;
    4.在码代码之前需要先多了解,写这篇博客就是初衷想在new的基础上做一个内存管理的通用mynew,仿照nginx的内存管理,先new一些固定的大块内存,如果有小的需求就从这些内存中分点,用map按剩余空间管理大块内存;大的需求就直接new一个大块直接用。后来看了看,这正是malloc一直在做的,并且做得让人膜拜。



2.内存碎片
    内存池、定制allocator、nginx按照httprequest的ngx_pool_t,按照一般网上随便搜的说法,很大一个原因是为了减少内存碎片。好吧,问一个问题,对于32位用户地址空间3G的应用(64位就极端了)来说,碎片就碎片吧,如果预期内存最多用个300MB,3G(当然得排除一部分代码、常量、栈区等)貌似怎么都够用,还管碎片干嘛?下面试图自问自答下。
    1.感性认识,内存碎片是因为内存管理者代码对于使用场景的无法预估造成的,因为不知道实际使用情况,所以只能依赖一些先验知识和部分后来的学习。操作系统不知道各个进程在将来的内存实际申请情况,导致物理内存的内外部碎片(这个词是为数不多的记得的读书阶段概念,概念觉得不复杂所以就用来装逼了)、导致把最近马上要用到的物理内存swap出去;malloc堆管理不知道调用glibc编程者在将来的内存申请,导致虚拟内存碎片,从而导致物理内存碎片。也因为无法预知已free内存之后的使用,导致不能及时归还物理内存给操作系统(甚至一直赖着不还,靠操作系统通过使用时间swap);某类allocator提供者不知道实际使用场景和释放时间,一次申请了较多的空间来容纳一个自己预估的类数量上限导致虚拟内存浪费,释放时间不一致导致碎片;类的实现者动态申请内存malloc也可能会,比如string,看了一段时间源码后发现,string长度增大时会重新申请更长的连续内存,先申请长的再释放短的,string长度缩短时如果用户不主动resize或shrink_to_fit string也不会主动释放多余的内存(我机器上的实现)。操作系统、语言规范、内存池、用户对内存层层盘剥,都会因为释放区域不连续产生内部碎片或者因为用了多余内存产生外部碎片;
    2.但是个人觉得真正危害大的是虚拟内存碎片导致的物理内存碎片,虚拟内存对很多小程序来说基本是无限的,物理内存是公用的有限的,见例子:
    sleep(20); //1
    printf("first sleep done\n");
    char *p[SIZE];
    printf("begin of sbrk:%x,%u\n",(unsigned)sbrk(0),(unsigned)sbrk(0));
    for(int i = 0;i < SIZE;i++)
        p[i] = (char *)malloc(LEN);
    printf("malloc done\n");
    printf("end of sbrk:%x,%u\n",(unsigned)sbrk(0),(unsigned)sbrk(0));
    sleep(20); //2
    for(int i = 0;i < SIZE;i++)
        free(p[i]);
    printf("free done\n");
    sleep(20); //3
    运行时执行pmap,可以看到实际内存占用详情,1处:
    b753f000    1696     156       0 r-x-- libc-2.19.so
    total kB    2016     320      60
    实际内存四分之一MB左右。
    2处:
    096b1000     660     652     652 rw---   [ anon ]
    b753f000    1696     260       0 r-x-- libc-2.19.so
    total kB    2680    1108     740
    因为malloc动态分配的堆,所以可以预期物理内存也涨了,并且可见是新增了096b1000开始的这一块内存,并且共享内存libc.so也增了点,后者是相对于1多调用了printf和sbrk、malloc libcso中对应的代码页也被加载进来。前者可见对sbrk输出的打印:
    begin of sbrk:96b1000,158011392
    end of sbrk:9756000,158687232
    可见sbrk区域(具体是啥我没弄得十分明白),从096b1000一直涨到了9756000,差值恰好是虚拟内存增量660*1024,sbrk这块内存就是堆的一个来源。
    3处:
    096b1000     132     132     132 rw---   [ anon ]
    total kB    2152     588     220
    free后,sbrk的从660减到了132,总物理内存也从1108减到了588.注意这里的132,在上一条经验第8条解释中提到了一个数字135168,恰好是33页132KB,这说明free在归还内存时还是有所保留,本例中保留33页。
    for(int i = 0;i < SIZE;i++)改为for(int i = 0;i < SIZE - 1;i++),即少free最后一个。这是3处:
    09ffe000     660     652     652 rw---   [ anon ]
    total kB    2680    1108     740
    这时内存占用没有变化,是因为sbrk只能释放区域末端的为占用内存。
    这里多多少少就能体现一点虚拟内存碎片对物理内存的危害了,如下:
    char *p[SIZE];
    ......//对p分配堆,占用了很大的内存
    char *xixi = (char *)malloc(1); //对xixi只分配1字节
    ......//释放了p,xixi则一直保留到程序结束。
    经过测试,物理内存不会得到释放。
    当然这个例子比较极端,条件苛刻。
    3.对2的一个不那么极端的例子
    先看malloc.c内的一段注释For very large requests (>= 128KB by default), it relies on system memory mapping facilities, if supported.就是说对于linux,128KB的malloc会直接用mmap,mmap释放就没有sbrk那么多顾虑了,经测试是直接释放的。
    试想一种场景,一个服务器比如游戏服务器,每晚六点开始迎来流量高峰。每个请求是长链接,有人玩十分钟有人玩五小时,七点开始没有新人加入(因为铁杆粉丝都是六点下班,七点过后的都不是粉丝)。对于每个链接都有一个类UserStatistics用于统计用户信息。作为开发者的我为了少调用一点malloc,并且因为进入退出的人不少,所以就先malloc了些大块内存(c++ allocator估计也成),最后这些大块内存尽管大部分人都下线了只要还有一人上线就没法释放还给操作系统。当然操作系统会统计页的访问情况发现大块内存中的很多页很久没有访问而把它们swap出去,但是也浪费了虚拟内存,并且因为不是主动上缴,操作系统还要有拷贝到硬盘的开销。
    这一段是纯文字纯猜想,时间不够,验证留作自己以后的作业。
    4.页内部碎片
    这个就不太好验证了,说下思路。
        for(int i = 0;i < SIZE;i++)
                p[i] = (char *)malloc(LEN);
    如前分配SIZE次动态分配。模拟程序一开始进行大量处理。
    再对pagesize/LEN - 1个指针进行free,下一个保留,再接着free,希望一页只有一个有用的指针。
    接下来这一步不好模拟,需要系统内存比较紧张,或者rlimit支持物理内存上限(本机不支持)。期望看到所有的p相关内存页都被swap掉。
    假设前一步成立,再来模拟下对所有剩下的指针进行遍历,从而使得所有的swap出去的内存又被重新掉入。从而证明尽管实际只用很少的内存,但是因为页内部碎片太大导致占用大量物理内存,并使得swap的效果变得低下。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值