首页 > 系统相关 >内存管理

内存管理

时间:2023-08-28 18:13:14浏览次数:47  
标签:malloc 管理 虚拟地址 回收 页表 虚拟内存 内存

虚拟内存分段分页

为了在多进程环境下,使得进程之间的内存地址不受影响,相互隔离,于是操作系统就为每个进程独立分配一套虚拟地址空间,每个程序只关心自己的虚拟地址就可以,实际上大家的虚拟地址都是一样的,但分布到物理地址内存是不一样的。作为程序,也不用关心物理地址的事情。

那既然有了虚拟地址空间,那必然要把虚拟地址「映射」到物理地址,这个事情通常由操作系统来维护。

那么对于虚拟地址与物理地址的映射关系,可以有分段和分页的方式,同时两者结合都是可以的。

  1. 内存分段是根据程序的逻辑角度,分成了栈段、堆段、数据段、代码段等,这样可以分离出不同属性的段,同时是一块连续的空间。但是每个段的大小都不是统一的,这就会导致外部内存碎片内存交换效率低的问题。
  2. 于是,就出现了内存分页,把虚拟空间和物理空间分成大小固定的页,如在 Linux 系统中,每一页的大小为 4KB。由于分了页后,就不会产生细小的内存碎片,解决了内存分段的外部内存碎片问题。同时在内存交换的时候,写入硬盘也就一个页或几个页,这就大大提高了内存交换的效率。
    • 在分页机制下,虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址,这个基地址与页内偏移的组合就形成了物理内存地址。
  3. 再来,为了解决简单分页产生的页表过大的问题,就有了多级页表,它解决了空间上的问题,但这就会导致 CPU 在寻址的过程中,需要有很多层表参与,加大了时间上的开销。
    • 分了二级表,映射 4GB 地址空间就需要 4KB(一级页表)+ 4MB(二级页表)的内存,这样占用空间不是更大了吗?
    • 对于已分配的页表项,如果存在最近一定时间未访问的页表,在物理内存紧张的情况下,操作系统会将页面换出到硬盘,也就是说不会占用物理内存。
    • 如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表。
    • 在 32 位的环境下,虚拟地址空间共有 4GB,假设一个页的大小是 4KB(2^12),那么就需要大约 100 万 (2^20) 个页,每个「页表项」需要 4 个字节大小来存储,那么整个 4GB 空间的映射在单页表时,就需要有 4MB 的内存来存储页表。
    • 假设只有 20% 的一级页表项被用到了,那么页表占用的内存空间就只有 4KB(一级页表) + 20% * 4MB(二级页表)= 0.804MB,这对比单级页表的 4MB 是不是一个巨大的节约?
  4. 于是根据程序的局部性原理,在 CPU 芯片中加入了 TLB,负责缓存最近常被访问的页表项,大大提高了地址的转换速度。CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表。

 

    

Linux 系统主要采用了分页管理,但是由于 Intel 处理器的发展史,Linux 系统无法避免分段管理。于是 Linux 就把所有段的基地址设为 0,也就意味着所有程序的地址空间都是线性地址空间(虚拟地址),相当于屏蔽了 CPU 逻辑地址的概念,所以段只被用于访问控制和内存保护。

另外,Linux 系统中虚拟空间分布可分为用户态和内核态两部分,其中用户态的分布:代码段、全局变量、BSS、函数栈、堆内存、映射区。

 

malloc 是如何分配内存?

虚拟地址空间的内部又被分为内核空间用户空间两部分

内核空间与用户空间的区别:

  • 进程在用户态时,只能访问用户空间内存;
  • 只有进入内核态后,才可以访问内核空间的内存;

虽然每个进程都各自有独立的虚拟内存,但是每个虚拟内存中的内核地址,其实关联的都是相同的物理内存。

用户空间内存从低到高分别是 6 种不同的内存段:

  • 代码段,包括二进制可执行代码;
  • 数据段,包括已初始化的静态常量和全局变量;
  • BSS 段,包括未初始化的静态变量和全局变量;
  • 堆段,包括动态分配的内存,从低地址开始向上增长;
  • 文件映射段,包括动态库、共享内存等,从低地址开始向上增长(跟硬件和内核版本有关 (opens new window));
  • 栈段,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。当然系统也提供了参数,以便我们自定义大小;

在这 6 个内存段中,文件映射段的内存是动态分配的。比如说,使用 C 标准库的 malloc() 或者 mmap() ,就可以分别在堆和文件映射段动态分配内存。

malloc() 源码里默认定义了一个阈值,malloc() 分配内存:

  • 如果用户分配的内存小于 128 KB,则通过 brk() 系统调用从分配内存;
  • 如果用户分配的内存大于 128 KB,则通过 mmap()系统调用在文件映射区域分配内存;

malloc() 分配的是虚拟内存。

如果分配后的虚拟内存没有被访问的话,虚拟内存是不会映射到物理内存的,这样就不会占用物理内存了。

只有在访问已分配的虚拟地址空间的时候,操作系统通过查找页表,发现虚拟内存对应的页没有在物理内存中,就会触发缺页中断,然后操作系统会建立虚拟内存和物理内存之间的映射关系。

 

  • malloc 通过 brk() 方式申请的内存,free 释放内存的时候,并不会把内存归还给操作系统,而是缓存在 malloc 的内存池中,待下次使用;
  • malloc 通过 mmap() 方式申请的内存,free 释放内存的时候,会把内存归还给操作系统,内存得到真正的释放。

brk() 优缺点:

 

  • malloc 通过 brk() 系统调用在堆空间申请内存的时候,由于堆空间是连续的,所以直接预分配更大的内存来作为内存池(malloc(1) malloc(10) 等实际上都是分配 132kb),当内存释放的时候,就不真正释放而是缓存在内存池中。等下次在申请内存的时候,就直接从内存池取出对应的内存块就行了,而且可能这个内存块的虚拟地址与物理地址的映射关系还存在,这样不仅减少了系统调用的次数(可以直接从内存池取),也减少了缺页中断的次数,这将大大降低 CPU 的消耗。
  • 如果我们连续申请了 10k,20k,30k 这三片内存,如果 10k 和 20k 这两片释放了,变为了空闲内存空间,如果下次申请的内存小于 30k,那么就可以重用这个空闲内存空间。但是如果下次申请的内存大于 30k,没有可用的空闲内存空间,必须向 OS 申请,实际使用内存继续增大。因此,随着系统频繁地 malloc 和 free ,尤其对于小块内存,堆内将产生越来越多不可用的碎片,导致“内存泄露”。而这种“泄露”现象使用 valgrind 是无法检测出来的。

malloc() 优缺点:

与 brk() 相对地,

  • mmap 分配的内存每次释放的时候,都会归还给操作系统,于是每次 mmap 分配的虚拟地址都是缺页状态的,所以在第一次访问该虚拟地址的时候,就会触发缺页中断。也就是说不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致 CPU 消耗较大。
  • 而因为每次 free 都会真正释放内存,没有不可用的内存碎片问题。

free() 函数只传入一个内存地址,为什么能知道要释放多大的内存?

malloc 返回给用户态的内存起始地址比进程的堆空间起始地址多了 16 字节。这个多出来的 16 字节就是保存了该内存块的描述信息,比如有该内存块的大小。

这样当执行 free() 函数时,free 会对传入进来的内存地址向左偏移 16 字节,然后从这个 16 字节的分析出当前的内存块的大小,自然就知道要释放多大的内存了。

 

 

 

内存满了会发生什么?

内核在给应用程序分配物理内存的时候,如果空闲物理内存不够,那么就会进行内存回收的工作,主要有两种方式:

  • 后台内存回收:物理内存紧张的时候,会唤醒 kswapd 内核线程来回收内存,这个回收内存的过程异步的,不会阻塞进程的执行。
  • 直接内存回收:如果后台异步回收跟不上进程内存申请的速度,就会开始直接回收,这个回收内存的过程是同步的,会阻塞进程的执行。
  • OOM 机制:在经历完直接内存回收后,空闲的物理内存大小依然不够,那么就会触发 OOM 机制,OOM killer 选择一个占用物理内存较高的进程,然后将其杀死,以便释放内存资源。如果物理内存依然不足,OOM Killer 会继续杀死占用物理内存较高的进程,直到释放足够的内存位置。

主要有两类内存可以被回收,而且它们的回收方式也不同。

    • 文件页(File-backed Page):内核缓存的磁盘数据(Buffer)和内核缓存的文件数据(Cache)都叫作文件页。大部分文件页,都可以直接释放内存,以后有需要时,再从磁盘重新读取就可以了。而那些被应用程序修改过,并且暂时还没写入磁盘的数据(也就是脏页),就得先写入磁盘,然后才能进行内存释放。所以,回收干净页的方式是直接释放内存,回收脏页的方式是先写回磁盘后再释放内存。

 

Swap 就是把一块磁盘空间或者本地文件,当成内存来使用,它包含换出和换入两个过程:

 

  • 换出(Swap Out) ,是把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存;
  • 换入(Swap In),是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来;

 

文件页和匿名页的回收都是基于 LRU 算法,也就是优先回收不常访问的内存。回收内存的操作基本都会发生磁盘 I/O 的,如果回收内存的操作很频繁,意味着磁盘 I/O 次数会很多,这个过程势必会影响系统的性能。

LRU 回收算法,实际上维护着 active 和 inactive 两个双向链表,其中:

  • active_list 活跃内存页链表,这里存放的是最近被访问过(活跃)的内存页;
  • inactive_list 不活跃内存页链表,这里存放的是很少被访问(非活跃)的内存页;

针对回收内存导致的性能影响,常见的解决方式。

  • 设置 /proc/sys/vm/swappiness,调整文件页和匿名页的回收倾向,尽量倾向于回收文件页;(文件页对于干净页回收是不会发生磁盘 I/O 的,脏页才会发生。因此性能影响相对较小)
  • 设置 /proc/sys/vm/min_free_kbytes,调整 kswapd 内核线程异步回收内存的时机;(尽早的触发「后台内存回收」来避免应用程序进行直接内存回收)
  • 设置 /proc/sys/vm/zone_reclaim_mode,调整 NUMA 架构下内存回收策略,建议设置为 0,这样在回收本地内存之前,会在其他 Node 寻找空闲内存,从而避免在系统还有很多空闲内存的情况下,因本地 Node 的本地内存不足,发生频繁直接内存回收导致性能下降的问题;
  • 可以通过调整进程的 /proc/[pid]/oom_score_adj 值,来降低被 OOM killer 杀掉的概率。

什么条件下才能触发 kswapd 内核线程回收内存呢?

内核定义了三个内存阈值(watermark,也称为水位),用来衡量当前剩余内存(pages_free)是否充裕或者紧张:

  • 图中橙色部分:kswapd0 会执行后台异步内存回收,直到剩余内存大于高阈值(pages_high)为止。

  • 图中红色部分:说明用户可用内存都耗尽了,此时就会触发直接同步内存回收,这时应用程序就会被阻塞。

min_free_kbytes 虽然设置的是页最小阈值(pages_min),但是页高阈值(pages_high)和页低阈值(pages_low)都是根据页最小阈值(pages_min)计算生成的,它们之间的计算关系如

  • pages_min = min_free_kbytes
  • pages_low = pages_min*5/4
  • pages_high = pages_min*3/2

 

最终总结:虚拟内存有什么作用?

  • 第一,虚拟内存可以使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。
  • 第二,由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题
  • 第三,页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。

标签:malloc,管理,虚拟地址,回收,页表,虚拟内存,内存
From: https://www.cnblogs.com/suBlog/p/17663061.html

相关文章

  • C++对象内存模型
    根据前面讲过的知识,C++的对象内存模型主要包含了以下几个方面的内容:如果没有虚函数也没有虚继承,那么对象内存模型中只有成员变量。如果类包含了虚函数,那么会额外添加一个虚函数表,并在对象内存中插入一个指针,指向这个虚函数表。如果类包含了虚继承,那么会额外添加一个虚基类表,......
  • 视频汇聚/视频云存储/视频监控管理平台EasyCVR安全检查的相关问题及解决方法
    安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台可拓展性强、视频能力灵活、部署轻快,可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等,以及支持厂家私有协议与SDK接入,包括海康Ehome、海大宇等设备的SDK等。平台既具备传统安防视频监控的能力,也具备接入AI智能分析的......
  • 智慧矿山2.0:煤矿智能化综合管理AI大数据监管平台建设方案设计
    一、行业背景能源与煤矿是我国国民经济的重要物质生产部门和支柱产业之一,同时也是一个安全事故多发的高危行业,施工阶段的现场管理对工程成本、进度、质量及安全等至关重要。煤矿智能化既是未来趋势,更是产业发展需求,建设智慧煤矿已经成为矿业安全生产的必经之路,是推动行业提质增效......
  • 项目管理思考-0828
    1收集需求是项目成功中的一个重要因素,需求越多,做的越多出错的可能性越多。2收集需求沟通技巧WHATWHYHOW。客户考虑是是做什么,技术考虑是怎么做,项目经理更应该考虑客户为什么这么做。举个例子,客户想要更快的一匹马,若是技术人员研究方法变成了一直研究马更快的事情,若是与客户......
  • 龙芯LoongArch架构2K0500开发板应用于车辆管理和控制系统解决方案
         迅为iTOP-LS2K0500开发采用龙芯LS2K0500处理器,基于龙芯自主指令系统(LoongArch®)架构,片内集成64位LA264处理器核、32位DDR3控制器、2DGPU、DVO显示接口、两路PCIe2.0、两路SATA2.0、四路USB2.0、一路USB3.0、两路GMAC、PCI总线、彩色黑白打印接口、HDA及其他常用接口。......
  • 44基于java的汽车销售管理系统设计与实现(可参考做毕业设计)
    本章节给大家带来一个基于java的汽车销售管理系统设计与实现,车辆4S店管理系统,基于java汽车销售交易网站,针对汽车销售提供客户信息、车辆信息、订单信息、销售人员管理、财务报表等功能,提供经理和销售两种角色进行管理。引言实现一个汽车销售管理系统,汽车销售管理系统是一个大型......
  • 使用Python对HTTPS域名证书管理与验证
    随着业务的发展,很多域名都需要使用HTTPS。这就带来了一个新的问题:如何监控HTTPS域名证书的有效性。虽然证书不是一刹那过期的,但是也需要对其进行监控。了解其有效时间,并在过期前进行报警监控。要完成这些功能,所限就是要对证书进行解析。对证书解析可以使用python的OpenSSL库,以下为......
  • RabbitMQ 管理页面该如何查看有哪些连接
    用浏览器访问  http://192.168.1.100:15672   默认用户名:admin   密码: admin 登陆后显示 在Connections页中查看所有连接    ......
  • 实用指令_实操作_RPM包管理
    RPM和YUMRPM包的管理一种用于互联网下载包的打包及安装工具,它包含在某些Linux分发版中,它生成具有.rpm扩展名的文件。PRM是RedHatPackageManager(RedHat软件包管理工具)的缩写,类似window的setup.exe。这一文件格式名称虽然打上了RedHat的标志但理念是通用的。Linux的分布版本......
  • 实用指令_实操作_进程服务管理
    服务(service)管理服务本质就是进程,但是是运行在后台的,通常都会监听某个端口,等待其他程序的请求,比如(mysql,sshd防火墙等),因此我们又称为守护进程,是linux中非常重要的知识点service管理指令service服务名[start|stop|restart|reload|status]systemctlCento7以后基础......