【编译、链接、装载十四】堆与内存管理

news/2024/5/20 9:35:33 标签: 内存管理, c++,

【编译、链接、装载十四】内存管理

一、内存管理

相对于栈而言, 这片内存面临一个稍微复杂的行为模式: 在任意时刻, 程序可能发出请求, 要么申请一段内存, 要么释放一段已申请过的内存, 而且申请的大小从几个字节到数GB都是有可能的, 我们不能假设程序会一次申请多少空间, 因此, 的管理显得较为复杂。 下面让我们来了解一下的工作原理。

1、什么是

光有栈对于面向过程的程序设计还远远不够, 因为栈上的数据在函数返回的时候就会被释放掉, 所以无法将数据传递至函数外部。 而全局变量没有办法动态地产生, 只能在编译的时候定义, 有很多情况下缺乏表现力。 在这种情况下, (Heap) 是唯一的选择。

是一块巨大的内存空间, 常常占据整个虚拟空间的绝大部分。 在这片空间里, 程序可以请求一块连续内存, 并自由地使用, 这块内存在程序主动放弃之前都会一直保持有效。 下面是一个申请空间最简单的例子。

int main()
{
char * p = (char*)malloc(1000);
/* use p as an array of size 1000*/
free(p);
}

在第3行用malloc申请了1000个字节的空间之后, 程序可以自由地使用这1000个字节, 直到程序用free函数释放它。

那么malloc到底是怎么实现的呢?

有一种做法是, 把进程的内存管理交给操作系统内核去做, 既然内核管理着进程的地址空间, 那么如果它提供一个系统调用, 可以让程序使用这个系统调用申请内存, 不就可以了吗? 当然这是一种理论上可行的做法,但实际上这样做的性能比较差,因为每次程序申请或者释放空间都需要进行系统调用。 我们知道系统调用的性能开销是很大的, 当程序对的操作比较频繁时, 这样做的结果是会严重影响程序的性能的。

比较好的做法就是程序向操作系统申请一块适当大小的空间, 然后由程序自己管理这块空间, 而具体来讲,管理着空间分配的往往是程序的运行库。

运行库相当于是向操作系统“批发”了一块较大的空间, 然后“零售”给程序用。 当全部“售完”或程序有大量的内存需求时, 再根据实际需求向操作系统“进货”。 当然运行库在向程序零售空间时, 必须管理它批发来的空间, 不能把同一块地址出售两次, 导致地址的冲突。 于是运行库需要一个算法来管理空间, 这个算法就是的分配算法。 不过在了解具体的分配算法之前, 我们先来看看运行库是怎么向操作系统批发内存的。

二、Linux进程管理

进程的地址空间中, 除了可执行文件、 共享库和栈之外, 剩余的未分配的空间都可以被用来作为空间。 Linux下的进程管理稍微有些复杂, 因为它提供了两种空间分配的方式, 即两个系统调用: 一个是 brk() 系统调用, 另外一个是 mmap()。

  • linux下的malloc是采用brk实现的,还是mmap实现的?
    glibc的malloc函数是这样处理用户的空间请求的: 对于小于128KB的请求来说, 它会在现有的空间里面, 按照分配算法为它分配一块空间并返回; 对于大于128KB的请求来说, 它会使用 mmap() 函数为它分配一块匿名空间, 然后在这个匿名空间中为用户分配空间。

  • brk
    brk() 的作用实际上就是设置进程数据段的结束地址, 即它可以扩大或者缩小数据段(Linux下数据段和BSS合并在一起统称数据段) 。 如果我们将数据段的结束地址向高地址移动, 那么扩大的那部分空间就可以被我们使用, 把这块空间拿来作为空间是最常见的做法之一 。

可以使用man 命令查看brk的用法。命令为:man brk
我的系统当时还出了问题,删除了/usr/bin下的man文件夹,重新安装后才解决
在这里插入图片描述

Glibc中还有一个函数叫sbrk, 它的功能与brk类似, 只不过参数和返回值略有不同。 sbrk以一个增量(Increment) 作为参数, 即需要增加(负数为减少) 的空间大小, 返回值是增加(或减少) 后数据段结束地址, 这个函数实际上是对brk系统调用的包装, 它是通过 brk() 实现的。

  • mmap
    mmap() 的作用和Windows系统下的VirtualAlloc很相似, 它的作用就是向操作系统申请一段虚拟地址空间, 当然这块虚拟地址空间可以映射到某个文件(这也是这个系统调用的最初的作用) , 当它不将地址空间映射到某个文件时, 我们又称这块空间0为匿名(Anonymous) 空间, 匿名空间就可以拿来作为空间。

man mmap在这里插入图片描述
mmap的前两个参数分别用于指定需要申请的空间的起始地址和长度,如果起始地址设置为0, 那么Linux系统会自动挑选合适的起始地址。prot/flags这两个参数用于设置申请的空间的权限(可读、 可写、 可执行) 以及映射类型(文件映射、 匿名空间等) , 最后两个参数是用于文件映射时指定文件描述符和文件偏移的, 我们在这里并不关心它们。

三、Windows进程管理

  • 每个线程的栈都是独立的, 每个线程默认的栈大小是1MB.

栈的位置则在0x00 030 000和EXE文件后面都有分布, 可能有读者奇怪为什么Windows需要这么多栈呢? 我们知道, 每个线程的栈都是独立的, 所以一个进程中有多少个线程, 就应该有多少个对应的栈, 对于Windows来说, 每个线程默认的栈大小是1MB, 在线程启动时, 系统会为它在进程地址空间中分配相应的空间作为栈, 线程栈的大小可以由创建线程时CreateThread的参数指定。在这里插入图片描述

在分配完上面这些地址以后, Windows的进程地址空间已经是支离破碎了。 当程序向系统申请空间时, 只好从这些剩下的还没有被占用的地址上分配。 Windows系统提供了一个API叫做VirtualAlloc(), 用来向系统申请空间, 它与Linux下的mmap非常相似。 实际上VirtualAlloc()申请的空间不一定只用于, 它仅仅是向系统预留了一块虚拟地址, 应用程序可以按照需要随意使用。

在使用 VirtualAlloc() 函数申请空间时, 系统要求空间大小必须为页的整数倍, 即对于x86系统来说, 必须是4096字节的整数倍。 很明显, 这就是操作系统的“批发”内存的接口函数了, 4096字节起批, 而且只能是4096字节的整数倍, 多了少了都不行。

那么应用程序作为最终的“消费者”, 如果它直接向操作系统申请内存的话, 难免会造成大量的浪费,比如程序只需要4097个字节的空间, 它也必须申请8192字节。当然, 在Windows下我们也可以自己实现一个分配的算法, 首先通过VirtualAlloc向操作系统一次性批发大量空间, 比如10MB, 然后再根据需要分配给程序。 不过这么常用的分配算法已经被各种系统、 库实现了无数遍, 一般情况下我们没有必要再重复发明轮子, 自己再实现一个,用现成的就可以了。 在Windows中, 这个算法的实现位于管理器(Heap Manager) 。

管理器提供了一套与相关的API可以用来创建、 分配、 释放和销毁空间:

  • HeapCreate: 创建一个
  • HeapAlloc: 在一个里分配内存。
  • HeapFree: 释放已经分配的内存。
  • HeapDestroy: 摧毁一个

这四个API的作用很明显,

  • HeapCreate就是创建一个空间, 它会向操作系统批发一块内存空间(它也是通过VirtualAlloc()实现的) ,
  • 而HeapAlloc就是在空间里面分配一块小的空间并返回给用户, 如果空间不足的话, 它还会通过VirtualAlloc向操作系统批发更多的内存直到操作系统也没有空间可以分配为止。
  • HeapFree和HeapDestroy的作用就更不言而喻了

Q&A

  • Q: 我可以重复释放两次里的同一片内存吗?
    A: 不能。 几乎所有的实现里, 都会在重复释放同一片里的内存时产生错误。 glibc甚至能检测出这样的错误, 并给出确切的错误信息。

  • Q: 我在有些书里看到说总是向上增长, 是这样的吗?
    A: 不是, 有些较老的书籍针对当时的系统曾做出过这样的断言, 这在当时可能是正确的。 因为当时的系统多是类unix系统, 它们使用类似于brk的方法来分配空间, 而brk的增长方向是向上的。 但随着Windows的出现, 这个规律被打破了。 在Windows里, 大部分使用HeapCreate产生, 而HeapCreate系列函数却完全不遵照向上增长这个规律。

  • Q: 调用malloc会不会最后调用到系统调用或者API?
    A: 这个取决于当前进程向操作系统批发的那些空间还够不够用, 如果够用了, 那么它可以直接在仓库里取出来卖给用户; 如果不够用了, 它就只能通过系统调用或者API向操作系统再进一批货了。

  • Q: malloc申请的内存, 进程结束以后还会不会存在?
    A: 这是一个很常见的问题, 答案是很明确的: 不会存在。 因为当进程结束以后, 所有与进程相关的资源, 包括进程的地址空间、 物理内存、打开的文件、 网络链接等都被操作系统关闭或者收回, 所以无论malloc申请了多少内存, 进程结束以后都不存在了。

  • Q: malloc申请的空间是不是连续的?
    A: 在分析这个问题之前, 我们首先要分清楚“空间”这个词所指的意思。 如果“空间”是指虚拟空间的话, 那么答案是连续的, 即每一次malloc分配后返回的空间都可以看做是一块连续的地址; 如果空间是指“物理空间”的话, 则答案是不一定连续, 因为一块连续的虚拟地址空间有可能是若干个不连续的物理页拼凑而成的。

参考
1、《程序员的自我修养链接装载与库》


http://www.niftyadmin.cn/n/470809.html

相关文章

【网络编程】网络基础(一)

文章目录 一、计算机网络背景1.网络发展2.认识 "协议" 二、网络协议初识1.协议分层2.OSI七层模型3.TCP/IP五层(或四层)模型 三、网络传输基本流程1.网络传输流程图2.数据包首部(报头)3.数据包封装和分用封装分用 4. 跨局域网主机通信 四、网络…

Multi-class classification without multi-class labels (ICLR 2019)

Multi-class classification without multi-class labels (ICLR 2019) 摘要 这项工作提出了针对多分类的新策略,不需要具体的类别标签,取而代之是利用样本之间的两两相似度,这是一种弱化的标注方式。所提方法称作元分类学习,为两…

测试进阶面试必问12个算法题,洞悉出题思路,拿的就是高薪!

可以明确的一点是,面试算法题目在难度上(尤其是代码难度上)会略低一些,倾向于考察一些基础数据结构与算法,对于高级算法和奇技淫巧一般不作考察。 代码题主要考察编程语言的应用是否熟练,基础是否扎实&…

高速电路设计系列分享-电源噪声分析

文章目录 概要整体架构流程技术名词解释技术细节小结 概要 提示:这里可以添加技术概要 例如: 当今许多应用都要求高速采样模数转换器(ADC)具有12位或以上的分辨率,以便用户能够进行更精确的系统测量。然而,更高分辨率…

在线搭建K8S,kubernetes集群v1.23.9,docker支持的最后一个版本

1. 部署环境主机(条件说明) master 192.168.186.128 CentOS Linux release 7.9.2009 (Core) node1 192.168.186.129 CentOS Linux release 7.9.2009 (Core) node2 192.168.186.130 CentOS Linux release 7.9.2009 (Core)2. 系统初始化-所有节点&am…

计算机中CPU、内存、缓存的关系

CPU(Central Processing Unit,中央处理器) 内存(Random Access Memory,随机存取存储器) 缓存(Cache) CPU、内存和缓存之间有着密切的关系,它们共同构成了计算机系统的核…

【跑实验06】os包的理解?如何提取图片的名称?如何遍历一个文件夹,提取里面的图像名称?如何提取图片名称中的特定部分?代码错误地方修改;

文章目录 一、os包的理解1.1 文件和目录操作1.2 进程管理1.3 环境变量1.4 路径操作 二、如何提取图片的名称?三、遍历一个文件夹,提取里面的图像名称四、如何提取图片名称中的特定部分?五、代码报错修改 一、os包的理解 os 是 Python 中的一…

解决联网时自动打开浏览器转到必应msn网址的问题

现象 开机后或者断网重连之后,系统自动打开默认浏览器(不管是IE还是谷歌,或其他的浏览器)网址为http://go.microsoft.com/fwlink/?LinkID219472&clcid0x409接着转到http://cn.bing.com/ 或者 https://www.msn.com/ 解决方法…