文文的复习笔记--底层内存管理

内存分配方式:

c++中,内存分成五个区:

堆:new分配执行的内存块,一个new一个delete。

栈:执行函数,局部变量的存储单元在栈上创建,执行结束存储单元自动释放。

自由存储区:由malloc分配的内存块,和堆相似,用free结束自己的生命。

全局/静态存储区:全局变量和静态变量被分配到这一块内存中。

常量存储区:存放常量,不允许修改。

linux中虚拟内存管理:

关键概念:

1.每个进程都有独立的虚拟地址空间 ,进程访问的虚拟地址不是真的物理地址。

2.虚拟地址可以通过每个进程上的页表(也就是在每个进程的内核的虚拟空间)与物理地址进行映射,获得真正的物理地址。

3。如果虚拟地址对应的物理地址不在物理内存中,就会发生缺页中断,真正分配一个物理地址,同时还会更新进程的页表;如果物理内存耗尽了,就会根据内存替换算法,淘汰部分页面至物理磁盘中。

四种典型的页替换算法

1.LRU:算法根据数据的历史访问记录来进行淘汰数据,核心思想是:如果数据最近被访问过,那么将来被访问的几率也会更高。

LRU的实现:用一个双向链表保存缓存的数据,将新数据插入头部,当缓存命中时,命中的数据移动到链表的头部,链表满时,删除链表尾部的数据。

LRU的实现为什么用哈希辅助双向链表?

答:1.用队列:只能做到先进先出,如果重复使用中间的数据时,无法把数据移动到顶端。

        2.用单链表:可以实现新的放在头部,不用的在尾部删除,但是删除的时候,因为单链表只有头指针,需要遍历到尾部。当用到重复使用过的数据时,还需要重新遍历整合链表,确定有没有用过,再遍历的相应的位置来剔除节点,重新放在头部。

        3.单链表匹配哈希表呢?:hashmap可以在单位1的时间内判断value的值是否存在,key直接存储节点对象,直接定位删除对应的节点,并将此节点的前驱节点指向此节点的后继节点。而且单链表不能通过一个节点直接获得前驱节点,双链表可以直接定位到前驱节点,剔除尾节点也非常方便。

Redis的LRU实现:Redis随机取出若干个key,按照访问时间排序后,淘汰掉最不经常使用的。

2.OPT:最佳替换算法,替换下次访问据当前时间最长的页,但是OPT算法需要知道操作系统将来的事件,显示无法实现,所以他是作为衡量其他算法的标准。

3.FIFO:将页面看作一个缓冲区,按照循环方式替换,替换驻留在内存时间最长的页。(一部分程序或者数据在整个程序的生命周期中使用频率很高,会导致频繁反复的换入换出)如果内存有空闲位置,直接替换,否则想象先有一个指针,最开始指向第一个位置,没发生替换时,指针位置不变,每次需要替换时,替换指针所指的位置,然后将指针后移一位,指针如果位于最后,移动到内存第一个位置。

4.Clock:时钟替换算法,给每个页关联一个使用位,当该页第一次装入内存或者被重新访问时,将使用位置置为1,每次需要替换时,查找使用位被置为0的第一个帧进行替换,扫描过程中,如果碰到使用位为1的帧,使用位置为0,再继续扫描。如果所有帧的使用位都为0,则替换第一个帧。

Linux虚拟地址空间分布:

Linux使用虚拟地址空间,增加了进程的寻址空间,由低到高地址分别是:

1.只读段:空间只能读,不能写;包括:代码段,rodata段(C常量字符串和#define定义的常量)

2.数据段:保存全局变量和静态变量的空间。

3.堆:动态内存,malloc和new大部分来源于此,堆顶位置可以通过函数brk和sbrk进行动态调整。

4.文件映射区域:动态库、共享内存等映射物理空间的内存,一般是mmap函数所分配的虚拟地址空间。

5.栈:用于维护函数调用的上下文空间,一般是8M,通过ulimt -s查看。

6.内核虚拟空间:用户代码不可见的内存区域,由内核管理(页表存放在内核虚拟空间)。

32位有4G的地址空间和64位有4G的地址空间:

32: 0x08048000~0xbfffffff 是用户空间,0xc0000000~0xffffffff 是内核空间,包括内核代码和数据、与进程相关的数据结构。%esp 执行栈顶,往低地址方向变化;brk/sbrk 函数控制堆顶_edata往高0地址方向变化。

64:虚拟地址空间划分相较于32位发生了改变。

        1.地址空间大小不是2^32也不是2^64,是2^48。因为2^64太大,过大空间导致资源浪费,48位表示虚拟地址空间,40位表示物理地址。(可通过#cat  /proc/cpuinfo 来查看)

        2.0x0000000000000000~0x00007fffffffffff 表示用户空间, 0xFFFF800000000000~ 0xFFFFFFFFFFFFFFFF 表示内核空间,共提供 256TB(2^48) 的寻址空间。
这两个区间的特点是,第 47 位与 48~63 位相同,若这些位为 0 表示用户空间,否则表示内核空间。

3.用户空间由低地址到高地址仍然是只读段、数据段、堆、文件映射区域、栈。

new和malloc的区别:

1.new是操作符,malloc是函数。

2.new需要调用构造函数,释放时需要调用析构函数;malloc不需要。

3.malloc需要指定调用内存的大小,返回的指针需要强转;new不需要给定内存大小,返回指针void*类型,不需要强转。

4.new可以重载,malloc不行。

5.new分配内存更直接安全。

6.new发生错误抛出异常,malloc返回null。

底层实现:

malloc底层实现:

从操作系统角度看:进程分配内存有两种方式,分别由两个系统调用完成brk和mmap(不考虑共享内存)。

1.brk是将数据段(.data)的最高地址指针_edata向高地址推。

2.mmap是在进程的虚拟地址空间中(堆栈中间,文件映射区域)找一块空闲的虚拟内存。

(都是分配虚拟内存,没有分配物理内存)

在标准c库中:malloc/free函数分配内存,由底层brk,mmap,munmap系统调用实现。

1.当malloc分配小于128k内存时,使用brk分配内存,将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系),如果malloc分配了这块内容,但从不访问,那么它对应的物理页不会被分配。

2.当malloc分配大于128k的内存时,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0)。

3.原因:brk分配的内存需要等高地址内存释放以后才能释放,但是mmap可以单独释放。

4.当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。

5.malloc采用内存池的管理方式,来减少内存碎片,先申请大块内存作为堆区,然后将堆区分为多个内存块,当用户申请内存时,直接从堆区分配一块合适的空闲块,采用隐式链表将所有空闲块中的每一个记录一个未分配的、连续的内存地址。

new底层实现:

1.创建一个新的对象。

2.将构造函数的作用域赋值给这个新的对象。

3.执行构造函数中的代码(为对象添加新属性)。

4.返回新对象。

缺页中断:

查看进程发生缺页中断的次数:用ps -o majflt,minflt -C program命令查看

majflt代表major fault,大错误;minflt代表minor fault,小错误。

两个数值表示一个进程自启动以来发生缺页中断的次数,都是累加值。(在对高性能要求的程序做压力测试时,可以多关注这两个值)

如果一个进程使用了mmap将很大的数据文件映射到进程的虚拟地址空间,我们重点关注majflt的值,相较于minflt,majflt对性能的损害是致命的,随机读取一次磁盘耗时数量级在几个毫秒,而minflt数量很大时才会对性能产生影响。

发生缺页中断后的操作:

发生缺页中断时,会陷入内核态:

1.查找访问的虚拟地址是否合法

2.查找/分配一个物理页

3.填充物理页内容(读取磁盘、直接置0、无操作)

4.建立映射关系(虚拟地址->物理地址)

(如果需要读取磁盘,那么就是majflt)

被测模块在内核态(CPU)高的原因:

  • 每次请求来都malloc一块 2 M的内存,默认情况下,malloc调用 mmap分配内存,请求结束的时候,调用munmap释放内存。
  • 假设每个请求需要 6 个物理页,那么每个请求就会产生 6 个缺页中断,在 2000 的压力下,每秒就产生了 10000 多次缺页中断,这些缺页中断不需要读取磁盘解决,所以叫做minflt;
  • 缺页中断在内核态执行,因此进程的内核态cpu消耗很大。
  • 缺页中断分散在整个请求的处理过程中,所以表现为分配语句耗时( 10 us)相对于整条请求的处理时间( 1000 us)比重很小。

解决办法:

  • 将动态内存改为静态分配,或者启动的时候,用malloc为每个线程分配,然后保存在threaddata里面。但是,由于这个模块的特殊性,静态分配,或者启动时候分配都不可行。另外,Linux下默认栈的大小限制是 10 M,如果在栈上分配几M的内存,有风险。
  • 禁止malloc调用mmap分配内存,禁止内存紧缩。
  • 在进程启动时候,加入以下两行代码:
mallopt(M_MMAP_MAX, 0);         // 禁止malloc调用mmap分配内存
mallopt(M_TRIM_THRESHOLD, -1);  // 禁止内存紧缩

效果:加入这两行代码以后,用ps命令观察,压力稳定以后,majltminflt都为 0 。进程的系统态cpu从 20 降到 10 。

既然堆内内存brksbrk不能直接释放,为什么不全部使用 mmap 来分配,munmap直接释放呢?

既然堆内碎片不能直接释放,导致疑似"内存泄露"问题,为什么 malloc 不全部使用 mmap 来实现呢( mmap分配的内存可以会通过 munmap 进行 free ,实现真正释放)?而是仅仅对于大于 128 k 的大块内存才使用 mmap?
  • 其实,进程向OS申请和释放地址空间的接口 sbrk/mmap/munmap 都是系统调用,频繁调用系统调用都比较消耗系统资源的。
  • 并且, mmap 申请的内存被 munmap 后,重新申请会产生更多的缺页中断。例如使用 mmap 分配 1 M 空间,第一次调用产生了大量缺页中断 ( 1 M/ 4 K 次 ) ,当munmap 后再次分配 1 M 空间,会再次产生大量缺页中断缺页中断是内核行为,会导致内核态CPU消耗较大。
  • 另外,如果使用 mmap 分配小内存,会导致地址空间的分片更多,内核的管理负担更大。
  • 同时堆是一个连续空间,并且堆内碎片由于没有归还 OS ,如果可重用碎片,再次访问该内存很可能不需产生任何系统调用和缺页中断,这将大大降低 CPU 的消耗。
  • 因此, glibcmalloc 实现中,充分考虑了 sbrkmmap 行为上的差异及优缺点,默认分配大块内存 ( 128 k) 才使用 mmap 获得地址空间,也可通过 mallopt(M_MMAP_THRESHOLD, <SIZE>) 来修改这个临界值。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值