这儿几个字节，那里几个字节，我们说的是真正的内存
今天的帖子来自于最近的 Go 语言的一次小测试，观察下面的测试基础片段 [1] ：func BenchmarkSortStrings(b *testing.B) { s := []string{"heart", "lungs", "brain", "kidneys", "pancreas"} b.ReportAllocs() for i := 0; i < b.N; i++ { sort.Strings(s) } } sort.Strings 是 sort.StringSlice(s) 的便捷包装器，sort.Strings 在原地对输入进行排序，因此不会分配内存（或至少 43% 回答此问题的 Twitter 用户是这么认为的）。然而，至少在 Go 的最近版本中，基准测试的每次迭代都会导致一次堆分配。为什么会是这种情况？正如所有 Go 程序员应该知道的那样，接口是以 双词结构 实现的。每个接口值包含一个字段，其中保存接口内容的类型，以及指向接口内容的指针。 [2] 在 Go 语言伪代码中，一个接口可能是这样的：type interface struct { // the ordinal number for the type of the value // assigned to the interface type uintptr // (usually) a pointer to the value assigned to // the interface data uintptr } interface.data 可以容纳一个机器字（在大多数情况下为 8 个字节），但一个 []string 却需要 24 个字节：一个字用于指向切片的底层数组；一个字用于存储切片的长度；另一个字用于存储底层数组的剩余容量。那么，Go 是如何将 24 个字节装入个 8 个字节的呢？通过编程中最古老的技巧，即间接引用。一个 []string，即 s，需要 24 个字节；但 *[]string —— 即指向字符串切片的指针，只需要 8 个字节。逃逸到堆为了让示例更加明确，以下是重新编写的基准测试，不使用 sort.Strings 辅助函数：func BenchmarkSortStrings(b *testing.B) { s := []string{"heart", "lungs", "brain", "kidneys", "pancreas"} b.ReportAllocs() for i := 0; i < b.N; i++ { var ss sort.StringSlice = s var si sort.Interface = ss // allocation sort.Sort(si) } } 为了让接口正常运行，编译器将赋值重写为 var si sort.Interface = &ss，即 ss 的地址分配给接口值。 [3] 我们现在有这么一种情况：出现一个持有指向 ss 的指针的接口值。它指向哪里？还有 ss 存储在哪个内存位置？似乎 ss 被移动到了堆上，这也同时导致了基准测试报告中的分配：Total: 296.01MB 296.01MB (flat, cum) 99.66% 8 . . func BenchmarkSortStrings(b *testing.B) { 9 . . s := []string{"heart", "lungs", "brain", "kidneys", "pancreas"} 10 . . b.ReportAllocs() 11 . . for i := 0; i < b.N; i++ { 12 . . var ss sort.StringSlice = s 13 296.01MB 296.01MB var si sort.Interface = ss // allocation 14 . . sort.Sort(si) 15 . . } 16 . . } 发生这种分配是因为编译器当前无法确认 ss 比 si 生存期更长。Go 编译器开发人员对此的普遍态度是，觉得 这个问题改进的余地，不过我们另找时间再议。事实上，ss 就是被分配到了堆上。因此，问题变成了：每次迭代会分配多少个字节？为什么不去询问 testing 包呢？% go test -bench=. sort_test.go goos: darwin goarch: amd64 cpu: Intel(R) Core(TM) i7-5650U CPU @ 2.20GHz BenchmarkSortStrings-4 12591951 91.36 ns/op 24 B/op 1 allocs/op PASS ok command-line-arguments 1.260s 可以看到，在 amd 64 平台的 Go 1.16 beta1 版本上，每次操作会分配 24 字节。 [4] 然而，在同一平台先前的 Go 版本中，每次操作则消耗了 32 字节。% go1.15 test -bench=. sort_test.go goos: darwin goarch: amd64 BenchmarkSortStrings-4 11453016 96.4 ns/op 32 B/op 1 allocs/op PASS ok command-line-arguments 1.225s 这引出了本文的主题，即 Go 1.16 版本中即将推出的一项便利改进。不过在讨论这个内容之前，我需要聊聊 “ 尺寸类别 size class ”。尺寸类别在解释什么是 “ 尺寸类别 size class ” 之前，我们先考虑个问题，理论上的 Go 语言在运行时是如何在其堆上分配 24 字节的。有一个简单的方法：追踪目前为止已分配到的所有内存的动向——利用指向堆上最后分配的字节的指针。分配 24 字节，堆指针就会增加 24，然后将前一个值返回给调用函数。只要写入的请求 24 字节的代码不超出该标记的范围，这种机制就没有额外开销。不过，现实情况下，内存分配器不仅要分配内存，有时还得释放内存。最终，Go 语言程序在运行时将释放这些 24 字节，但从运行的视角来看，它只知道它给调用者的开始地址。它不知道从该地址起始之后又分配了多少字节。为了允许释放内存，我们假设的 Go 语言程序运行时分配器必须记录堆上每个分配的长度值。那么这些长度值的分配存储在何处？当然是在堆上。在我们的设想中，当程序运行需要分配内存的时候，它可以请求稍微多一点，并把它用来存储请求的数量。而对于我们的切片示例而言，当我们请求 24 字节时，实际上会消耗 24 字节加上存储数字 24 的一些开销。这些开销有多大？事实上，实际上的最小开销量是一个字。 [5] 用来记录 24 字节分配的开销将是 8 字节。25% 不是很大，但也不算糟糕，随着分配的大小增加，开销将变得微不足道。然而，如果我们只想在堆上存储一个字节，会发生什么？开销将是请求数据量的 8 倍！是否有一种更高效的方式在堆上分配少量内存？与其在每个分配旁边存储长度，不如将相同大小的内容存储在一起，这个主意如何？如果所有的 24 字节的内容都存储在一起，那么运行时会自动获取它们的大小。运行时所需要的是一个单一的位，指示 24 字节区域是否在使用中。在 Go 语言中，这些区域被称为 Size Classes，因为相同大小的所有内容都会存储在一起（类似学校班级，所有学生都按同一年级分班，而不是 C++ 中的类）。当运行时需要分配少量内存时，它会使用能够容纳该分配的最小的尺寸类别。无限制的尺寸类别现在我们知道尺寸类别是如何工作的了，那么问题又来了，它们存储在哪里？和我们想的一样，尺寸类别的内存来自堆。为了最小化开销，运行时会从堆上分配较大的内存块（通常是系统页面大小的倍数），然后将该空间用于单个大小的分配。不过，这里存在一个问题————将大块区域用于存储同一大小的事物的模式很好用 [6] ，如果分配大小的数量是固定的，最好是少数几个。那么在通用语言中，程序可以要求运行时以任何大小分配内存 [7] 。例如，想象一下向运行时请求 9 字节。9 字节是一个不常见的大小，因此可能需要一个新的尺寸类别来存储 9 字节大小的物品。因为 9 字节大小的物品不常见，所以分配的其余部分（通常为 4KB 或更多）可能会被浪费。由于尺寸类别的集合是固定的，如果没有精确匹配的 size class 可用，分配将并入到下一个尺寸类别。在我们的示例中，9 字节可能会在 12 字节的尺寸类别中分配。未使用的 3 字节的开销要比几乎未使用的整个尺寸类别分配好。总结一下这是谜题的最后一块拼图。Go 1.15 版本没有 24 字节的尺寸类别，因此 ss 的堆分配是在 32 字节的尺寸类别中分配的。由于 Martin Möhrmann 的工作，Go 1.16 版本有一个 24 字节的尺寸类别，非常适合分配给接口的切片值。相关文章我在 Devfest 2017年西伯利亚大会谈 Go 语言如果对齐的内存写操作是原子性的，为什么我们还需要 sync/atomic 包呢？为你的树莓派创建一个真实的串行控制台为什么 Go 语言线程的栈是无限制的？（题图：MJ/01d5fe46-778f-48fe-9481-162f4d0289dc）这不是正确的对排序函数进行基准测试的方式，因为在第一次迭代之后，输入已经排序。但这又是另外一个话题了。 ↩︎此语句的准确性取决于所使用的 Go 版本。例如，Go 1.15 版本添加了直接将一些 整数存储在接口值 中的功能，从而节省了分配和间接性。然而，对于大多数值来说，如果它不是指针类型，它的地址将被取出并存储在接口值中。 ↩︎编译器在接口值的类型字段中跟踪了这种手法，因此它记住了分配给 si 的类型是 sort.StringSlice 而不是 *sort.StringSlice。 ↩︎在 32 位平台上，这个数字减半，但我们不再关注它。 ↩︎如果你准备限制分配为 4G 或者可能是 64KB，你可以使用较少内存来存储分配的尺寸，但实际上使用小于一个字来存储长度标头的节省会受到填充的影响。 ↩︎将相同大小的物品存储在一起也是一种有效的对抗碎片化的策略。 ↩︎这并不是一个不切实际的设想，字符串有各种形状和大小，生成以前未见过的大小的字符串可能就像附加空格一样简单。 ↩︎via: https://dave.cheney.net/2021/01/05/a-few-bytes-here-a-few-there-pretty-soon-youre-talking-real-memory作者：Dave Cheney 选题：lujun9972 译者：Drwhooooo 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
内存 RowHammer 保护措施被新的方法击破2015 年，研究人员发现了一种被称之为 RowHammer 的攻击方式，当一个用户级应用程序反复访问 DDR 内存芯片的特定区域，可以导致比特翻转。 比特翻转 Bitflips 是指储存在电子设备上的个别比特发生翻转的事件，比如从 0 变为 1 或反之。各个内存厂商在后续产品中纷纷加入了抵御 RowHammer 攻击的保护措施，主要方法是限制程序在给定时间内打开和关闭目标芯片区域的次数。现在，研究人员发明了被称之为 RowPress 的新方法，能在部署了最新的 RowHammer 保护措施的 DRAM 中诱发比特翻转。RowPress 不是反复“锤击”选定区域，而是让其保持更长的打开时间。将这一方法和 RowHammer 结合可进一步提高这种攻击的威力。研究人员表示，“这本身并不是一种攻击。它只是表明比特翻转是可能的，而且很多，这很容易成为攻击的基础”。消息来源：Ars Technica老王点评：如果类似的方法层出不穷，那或许是该考虑新的内存技术了。一种新的路灯技术可以避免光污染对天文望远镜的影响光污染对天文学的威胁与日俱增，现代望远镜高度敏感，能感受到距离 50 甚至 200 公里的城市室外照明的影响。一项研究发现，星星正在以平均每年 10% 的速度从天空中“消失”。一项名为 “DarkSkyProtector” 的技术可以“让望远镜看到几乎是一片黑暗的天空”。该方案依赖于三个组件：一个能让 LED 灯以人眼无法察觉的极高频率闪烁的简单装置、一个全球定位系统接收器，以及望远镜摄像头上一个能与 LED 灯同步闪烁的特殊设计的快门。全球定位系统技术引导望远镜的快门只在 LED 灯熄灭的瞬间打开。该技术可以将天文图像中不必要的天空辉光减少 94%。消息来源：Space老王点评：技术的进步可以解决技术带来的副作用。另外，是不是可以顺便把马斯克的星链轨迹也一起消除了。Debian 继续支持 /usr 合并Debian 12 的目标是采用与其他 Linux 发行版类似的合并 /usr 文件系统布局，即 /{bin,sbin,lib}/ 目录成为 /usr/{bin,sbin,lib}/ 的符号链接。但 Debian 技术委员会在今年早些时候决定暂停合并 /usr 文件。不过，随着 Debian 12 正式发布几个月后，这一暂停决定被废止了。Debian 的合并 /usr 动作有望在两年后的 Debian 13 发布时全部完成。消息来源：Phoronix老王点评：这就是历史遗留问题，依我看，其实大可不必这么小心翼翼。
硬核观察 #884 PyTorch 和 Triton 正在打破英伟达 CUDA 的垄断
PyTorch 和 Triton 正在打破英伟达 CUDA 的垄断大部分机器学习软件开发框架严重依赖于英伟达 CUDA，并在英伟达 GPU 上表现最佳。但随着 PyTorch 2.0 和 OpenAI Triton 的到来，英伟达 CUDA 对机器学习的垄断地位正逐渐瓦解。即将到来的 PyTorch 2.0 在英伟达 A100 上的训练性能提升了 86%，在 CPU 上的推理性能提升了 26%。而且这种优势可以扩展到 AMD、英特尔、特斯拉、谷歌、亚马逊、微软等等各个公司生产的 GPU 和 AI 加速器上。而 Triton 能让高级语言达到与使用低级语言相当的性能，提高了可用性。消息来源：Semi Analysis老王点评：又一次证明了开源胜过闭源，无论闭源的护城河有多深。Basecamp 因巨额账单退出云计算Basecamp 的 CTO，也是 Ruby On Rails 的创建者 DHH 介绍了让该公司退出云计算的巨额账单。其 2022 年的费用为 320 万美元，绝大部分都花在了 AWS 上，其中 S3 花费 90 万美元，RDS 47 万美元，OpenSearch 52 万美元，Elasticache 12 万美元。即便如此，也是经过大量工作才减少到这一费用的，该团队不但运行了成本检查计划，还就作为私人协议就长期使用达成了协议。DHH 还用戴尔服务器的三年均摊成本做了对比。消息来源：The Register老王点评：云计算确实有很多好处，但也可能是个让你花钱上瘾的无底洞。使用了 25 年的笔记本内存规范 SO-DIMM 将被替换制定内存标准的组织 JEDEC 正在制定新规范，以取代已经使用了 25 年的 SO-DIMM 规范。新的 CAMM 标准将基于戴尔公司的设计，目标是在 2023 年下半年完成 1.0 规范，到明年推出基于 CAMM 的系统。现有的 SO-DIMM 在 DDR5/6400 时已经遇到了“困境”，CAMM 的主要吸引力在于它可以实现更高的内存密度，同时还可以扩展到更高的时钟速度。消息来源：PC World老王点评：这是不是代表以后笔记本会需要更多内存？
硬核观察 #457 新的“行锤”攻击打破了 DDR4 内存保护
新的“行锤”攻击打破了 DDR4 内存保护“ 行锤 Rowhammer ”攻击是通过每秒数百万次访问（或者说锤打）脆弱芯片内的物理行，导致相邻行的比特翻转，这意味着 1 变成 0，0 变成了 1。一个没有特权的应用程序可以通过每秒数百万次以特定模式访问 DDR 内存的“行锤”来破坏内存中的数据，从而 获得系统特权。这个漏洞已经在 2015 年被发现，对 DDR3 造成了严重影响后，在 DDR4 中有所缓解。以前的“行锤”攻击都是以统一的模式锤击行，这些“攻击者”行被访问的次数相同。而 新发表的研究 中使用了非均匀模式，以不同的频率访问两个或更多的攻击者行。结果是：在一个测试池中，所有 40 个随机选择的 DIMM 都经历了比特翻转。老王点评：如果这个攻击技术被武器化，那所有使用 DDR 内存的计算机都处于被攻击的阴影笼罩之下。英特尔 4004 微处理器推出 50 周年1971 年 11 月 15 日 英特尔推出的 4004 微处理器仅使用了 2,300 个晶体管，一次可在芯片中运行四位数据。这种半英寸长的矩形集成电路的时钟速度为 750 kHz，每秒可进行约 92,000 次运算。英特尔于 1972 年推出有 3,500 个晶体管的八位的 8008；1978 年推出有 29,000 个晶体管的十六位的 8086，每秒可进行 710,000 次运算。IBM 将下一代的英特尔 8088 用于其首款 PC。而如今，苹果新的 M1 Max 处理器有 570 亿个晶体管，每秒可执行 10.4 万亿次浮点运算。这意味着计算机能力在 50 年内至少提高了 10 亿倍。老王点评：50 年对于人类历史来说是短短一瞬间，但是这小小的芯片却整个改变了世界。因文字太难输入，Facebook Messenger 五成语音流量来自柬埔寨2018 年 Facebook 工程师注意到了一个令他们不解的现象：Messenger 近五成语音流量 来自柬埔寨。高棉语有 74 个字符，比大部分文字都多。高棉语在每个键位都有两个字符，需要在两个按键层反复切换。因此在柬埔寨人们大量使用语音，虽然后来高棉语输入法也得到了改善。当然，语音沟通的不便之处就是没法快速搜索，只能根据语音条长短大致记忆。老王点评：还好我们的中文输入法比较好，或者说拼音普及的好，要不中文输入更难。
smem 命令允许你快速查看你的网页应用的内存使用情况。在我的编程工作中，我经常需要了解网页应用的内存使用情况。在深入研究细节和浏览器剖析工具之前，一个粗略的估计通常就足够了。为了了解 Linux 或 macOS 上的内存使用情况，人们通常使用 top 或 htop。我很想看到一个单一的数字：一个进程占用了多少内存。但这些工具所显示的统计数据可能很难理解。对于网页浏览器来说，它甚至更加复杂，因为它们经常运行许多独立的进程。它们在 top 输出中显示为一个长长的列表，每一个都有自己的单独指标。smem 命令幸运的是有 smem，另一个用于查看内存使用统计的命令行工具。用你选择的包管理器安装它，例如：sudo apt install smem 要获得 Firefox 的总内存使用量，请执行：smem -c pss -P firefox -k -t | tail -n 1 这些开关做了什么？-c 开关指定要显示的列。我只对 pss 列感兴趣，它显示一个进程分配的内存。-P 开关过滤进程，只包括那些名字里有 firefox 的进程。-k 开关显示以 MB/GB 为单位的内存使用情况，而不是单纯的字节数。-t 开关显示总数。tail -n 1 过滤器只输出最后一行，也就是总数的地方。输出是非常简单的：$ smem -t -k -c pss -P firefox | tail -n 1 4.9G 开门见山！而且，经过又一天忙碌的工作，打开了 50 多个选项卡，Firefox 仍然只使用 5 GB。看看吧，Google Chrome。用一个脚本更容易为了方便起见，我创建一个名为 memory-use 的小脚本，它将进程名称作为参数。我把所有的脚本都放在 ~/bin 里，所以：echo 'smem -c pss -P "$1" -k -t | tail -n 1' > ~/bin/memory-use && chmod +x ~/bin/memory-use 现在我可以很容易地测量任何应用的内存使用：memory-use firefox memory-use chrome memory-use slack 还有更多！该工具不仅可以显示总的内存使用情况。它甚至可以生成图形输出。比如：smem --pie name -c pss 显示类似这样的内容：关于更多的细节，我建议查看 smem 手册页。你可以在 https://linoxide.com/memory-usage-reporting-smem/ 上找到另一个很棒的教程。希望你喜欢!本文最初发表在作者的博客上，并经许可转载。via: https://opensource.com/article/21/10/memory-stats-linux-smem作者：Tomasz Waraksa 选题：lujun9972 译者：geekpi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
了解有关内存安全和效率的更多信息。C 是一种高级语言，同时具有“ 接近金属 close-to-the-metal ”（LCTT 译注：即“接近人类思维方式”的反义词）的特性，这使得它有时看起来更像是一种可移植的汇编语言，而不像 Java 或 Python 这样的兄弟语言。内存管理作为上述特性之一，涵盖了正在执行的程序对内存的安全和高效使用。本文通过 C 语言代码示例，以及现代 C 语言编译器生成的汇编语言代码段，详细介绍了内存安全性和效率。尽管代码示例是用 C 语言编写的，但安全高效的内存管理指南对于 C++ 是同样适用的。这两种语言在很多细节上有所不同（例如，C++ 具有 C 所缺乏的面向对象特性和泛型），但在内存管理方面面临的挑战是一样的。执行中程序的内存概述对于正在执行的程序（又名 进程 process ），内存被划分为三个区域： 栈 stack 、 堆 heap 和 静态区 static area 。下文会给出每个区域的概述，以及完整的代码示例。作为通用 CPU 寄存器的替补，栈 为代码块（例如函数或循环体）中的局部变量提供暂存器存储。传递给函数的参数在此上下文中也视作局部变量。看一下下面这个简短的示例：void some_func(int a, int b) { int n; ... } 通过 a 和 b 传递的参数以及局部变量 n 的存储会在栈中，除非编译器可以找到通用寄存器。编译器倾向于优先将通用寄存器用作暂存器，因为 CPU 对这些寄存器的访问速度很快（一个时钟周期）。然而，这些寄存器在台式机、笔记本电脑和手持机器的标准架构上很少（大约 16 个）。在只有汇编语言程序员才能看到的实施层面，栈被组织为具有 push（插入）和 pop（删除）操作的 LIFO（后进先出）列表。 top 指针可以作为偏移的基地址；这样，除了 top 之外的栈位置也变得可访问了。例如，表达式 top+16 指向堆栈的 top 指针上方 16 个字节的位置，表达式 top-16 指向 top 指针下方 16 个字节的位置。因此，可以通过 top 指针访问实现了暂存器存储的栈的位置。在标准的 ARM 或 Intel 架构中，栈从高内存地址增长到低内存地址；因此，减小某进程的 top 就是增大其栈规模。使用栈结构就意味着轻松高效地使用内存。编译器（而非程序员）会编写管理栈的代码，管理过程通过分配和释放所需的暂存器存储来实现；程序员声明函数参数和局部变量，将实现过程交给编译器。此外，完全相同的栈存储可以在连续的函数调用和代码块（如循环）中重复使用。精心设计的模块化代码会将栈存储作为暂存器的首选内存选项，同时优化编译器要尽可能使用通用寄存器而不是栈。堆 提供的存储是通过程序员代码显式分配的，堆分配的语法因语言而异。在 C 中，成功调用库函数 malloc（或其变体 calloc 等）会分配指定数量的字节（在 C++ 和 Java 等语言中，new 运算符具有相同的用途）。编程语言在如何释放堆分配的存储方面有着巨大的差异：在 Java、Go、Lisp 和 Python 等语言中，程序员不会显式释放动态分配的堆存储。例如，下面这个 Java 语句为一个字符串分配了堆存储，并将这个堆存储的地址存储在变量 greeting 中：String greeting = new String("Hello, world!"); Java 有一个垃圾回收器，它是一个运行时实用程序，如果进程无法再访问自己分配的堆存储，回收器可以使其自动释放。因此，Java 堆释放是通过垃圾收集器自动进行的。在上面的示例中，垃圾收集器将在变量 greeting 超出作用域后，释放字符串的堆存储。Rust 编译器会编写堆释放代码。这是 Rust 在不依赖垃圾回收器的情况下，使堆释放实现自动化的开创性努力，但这也会带来运行时复杂性和开销。向 Rust 的努力致敬！在 C（和 C++）中，堆释放是程序员的任务。程序员调用 malloc 分配堆存储，然后负责相应地调用库函数 free 来释放该存储空间（在 C++ 中，new 运算符分配堆存储，而 delete 和 delete[] 运算符释放此类存储）。下面是一个 C 语言代码示例：char* greeting = malloc(14); /* 14 heap bytes */ strcpy(greeting, "Hello, world!"); /* copy greeting into bytes */ puts(greeting); /* print greeting */ free(greeting); /* free malloced bytes */ C 语言避免了垃圾回收器的成本和复杂性，但也不过是让程序员承担了堆释放的任务。内存的 静态区 为可执行代码（例如 C 语言函数）、字符串文字（例如“Hello, world!”）和全局变量提供存储空间：int n; /* global variable */ int main() { /* function */ char* msg = "No comment"; /* string literal */ ... } 该区域是静态的，因为它的大小从进程执行开始到结束都固定不变。由于静态区相当于进程固定大小的内存占用，因此经验法则是通过避免使用全局数组等方法来使该区域尽可能小。下文会结合代码示例对本节概述展开进一步讲解。栈存储想象一个有各种连续执行的任务的程序，任务包括了处理每隔几分钟通过网络下载并存储在本地文件中的数字数据。下面的 stack 程序简化了处理流程（仅是将奇数整数值转换为偶数），而将重点放在栈存储的好处上。#include <stdio.h> #include <stdlib.h> #define Infile "incoming.dat" #define Outfile "outgoing.dat" #define IntCount 128000 /* 128,000 */ void other_task1() { /*...*/ } void other_task2() { /*...*/ } void process_data(const char* infile, const char* outfile, const unsigned n) { int nums[n]; FILE* input = fopen(infile, "r"); if (NULL == infile) return; FILE* output = fopen(outfile, "w"); if (NULL == output) { fclose(input); return; } fread(nums, n, sizeof(int), input); /* read input data */ unsigned i; for (i = 0; i < n; i++) { if (1 == (nums[i] & 0x1)) /* odd parity? */ nums[i]--; /* make even */ } fclose(input); /* close input file */ fwrite(nums, n, sizeof(int), output); fclose(output); } int main() { process_data(Infile, Outfile, IntCount); /** now perform other tasks **/ other_task1(); /* automatically released stack storage available */ other_task2(); /* ditto */ return 0; } 底部的 main 函数首先调用 process_data 函数，该函数会创建一个基于栈的数组，其大小由参数 n 给定（当前示例中为 128,000）。因此，该数组占用 128000 * sizeof(int) 个字节，在标准设备上达到了 512,000 字节（int 在这些设备上是四个字节）。然后数据会被读入数组（使用库函数 fread），循环处理，并保存到本地文件 outgoing.dat（使用库函数 fwrite）。当 process_data 函数返回到其调用者 main 函数时，process_data 函数的大约 500MB 栈暂存器可供 stack 程序中的其他函数用作暂存器。在此示例中，main 函数接下来调用存根函数 other_task1 和 other_task2。这三个函数在 main 中依次调用，这意味着所有三个函数都可以使用相同的堆栈存储作为暂存器。因为编写栈管理代码的是编译器而不是程序员，所以这种方法对程序员来说既高效又容易。在 C 语言中，在块（例如函数或循环体）内定义的任何变量默认都有一个 auto 存储类，这意味着该变量是基于栈的。存储类 register 现在已经过时了，因为 C 编译器会主动尝试尽可能使用 CPU 寄存器。只有在块内定义的变量可能是 register，如果没有可用的 CPU 寄存器，编译器会将其更改为 auto。基于栈的编程可能是不错的首选方式，但这种风格确实有一些挑战性。下面的 badStack 程序说明了这点。#include <stdio.h>; const int* get_array(const unsigned n) { int arr[n]; /* stack-based array */ unsigned i; for (i = 0; i < n; i++) arr[i] = 1 + 1; return arr; /** ERROR **/ } int main() { const unsigned n = 16; const int* ptr = get_array(n); unsigned i; for (i = 0; i < n; i++) printf("%i ", ptr[i]); puts("\n"); return 0; } badStack 程序中的控制流程很简单。main 函数使用 16（LCTT 译注：原文为 128，应为作者笔误）作为参数调用函数 get_array，然后被调用函数会使用传入参数来创建对应大小的本地数组。get_array 函数会初始化数组并返回给 main 中的数组标识符 arr。 arr 是一个指针常量，保存数组的第一个 int 元素的地址。当然，本地数组 arr 可以在 get_array 函数中访问，但是一旦 get_array 返回，就不能合法访问该数组。尽管如此，main 函数会尝试使用函数 get_array 返回的堆栈地址 arr 来打印基于栈的数组。现代编译器会警告错误。例如，下面是来自 GNU 编译器的警告：badStack.c: In function 'get_array': badStack.c:9:10: warning: function returns address of local variable [-Wreturn-local-addr] return arr; /** ERROR **/ 一般规则是，如果使用栈存储实现局部变量，应该仅在该变量所在的代码块内，访问这块基于栈的存储（在本例中，数组指针 arr 和循环计数器 i 均为这样的局部变量）。因此，函数永远不应该返回指向基于栈存储的指针。堆存储接下来使用若干代码示例凸显在 C 语言中使用堆存储的优点。在第一个示例中，使用了最优方案分配、使用和释放堆存储。第二个示例（在下一节中）将堆存储嵌套在了其他堆存储中，这会使其释放操作变得复杂。#include <stdio.h> #include <stdlib.h> int* get_heap_array(unsigned n) { int* heap_nums = malloc(sizeof(int) * n); unsigned i; for (i = 0; i < n; i++) heap_nums[i] = i + 1; /* initialize the array */ /* stack storage for variables heap_nums and i released automatically when get_num_array returns */ return heap_nums; /* return (copy of) the pointer */ } int main() { unsigned n = 100, i; int* heap_nums = get_heap_array(n); /* save returned address */ if (NULL == heap_nums) /* malloc failed */ fprintf(stderr, "%s\n", "malloc(...) failed..."); else { for (i = 0; i < n; i++) printf("%i\n", heap_nums[i]); free(heap_nums); /* free the heap storage */ } return 0; } 上面的 heap 程序有两个函数： main 函数使用参数（示例中为 100）调用 get_heap_array 函数，参数用来指定数组应该有多少个 int 元素。因为堆分配可能会失败，main 函数会检查 get_heap_array 是否返回了 NULL；如果是，则表示失败。如果分配成功，main 将打印数组中的 int 值，然后立即调用库函数 free 来对堆存储解除分配。这就是最优的方案。get_heap_array 函数以下列语句开头，该语句值得仔细研究一下：int* heap_nums = malloc(sizeof(int) * n); /* heap allocation */ malloc 库函数及其变体函数针对字节进行操作；因此，malloc 的参数是 n 个 int 类型元素所需的字节数（sizeof(int) 在标准现代设备上是四个字节）。malloc 函数返回所分配字节段的首地址，如果失败则返回 NULL .如果成功调用 malloc，在现代台式机上其返回的地址大小为 64 位。在手持设备和早些时候的台式机上，该地址的大小可能是 32 位，或者甚至更小，具体取决于其年代。堆分配数组中的元素是 int 类型，这是一个四字节的有符号整数。这些堆分配的 int 的地址存储在基于栈的局部变量 heap_nums 中。可以参考下图： heap-based stack-based / \ +----+----+ +----+ heap-nums--->|int1|int2|...|intN| +----+----+ +----+ 一旦 get_heap_array 函数返回，指针变量 heap_nums 的栈存储将自动回收——但动态 int 数组的堆存储仍然存在，这就是 get_heap_array 函数返回这个地址（的副本）给 main 函数的原因：它现在负责在打印数组的整数后，通过调用库函数 free 显式释放堆存储：free(heap_nums); /* free the heap storage */ malloc 函数不会初始化堆分配的存储空间，因此里面是随机值。相比之下，其变体函数 calloc 会将分配的存储初始化为零。这两个函数都返回 NULL 来表示分配失败。在 heap 示例中，main 函数在调用 free 后会立即返回，正在执行的程序会终止，这会让系统回收所有已分配的堆存储。尽管如此，程序员应该养成在不再需要时立即显式释放堆存储的习惯。嵌套堆分配下一个代码示例会更棘手一些。C 语言有很多返回指向堆存储的指针的库函数。下面是一个常见的使用情景：1、C 程序调用一个库函数，该函数返回一个指向基于堆的存储的指针，而指向的存储通常是一个聚合体，如数组或结构体：SomeStructure* ptr = lib_function(); /* returns pointer to heap storage */ 2、 然后程序使用所分配的存储。3、 对于清理而言，问题是对 free 的简单调用是否会清理库函数分配的所有堆分配存储。例如，SomeStructure 实例可能有指向堆分配存储的字段。一个特别麻烦的情况是动态分配的结构体数组，每个结构体有一个指向又一层动态分配的存储的字段。下面的代码示例说明了这个问题，并重点关注了如何设计一个可以安全地为客户端提供堆分配存储的库。#include <stdio.h> #include <stdlib.h> typedef struct { unsigned id; unsigned len; float* heap_nums; } HeapStruct; unsigned structId = 1; HeapStruct* get_heap_struct(unsigned n) { /* Try to allocate a HeapStruct. */ HeapStruct* heap_struct = malloc(sizeof(HeapStruct)); if (NULL == heap_struct) /* failure? */ return NULL; /* if so, return NULL */ /* Try to allocate floating-point aggregate within HeapStruct. */ heap_struct->heap_nums = malloc(sizeof(float) * n); if (NULL == heap_struct->heap_nums) { /* failure? */ free(heap_struct); /* if so, first free the HeapStruct */ return NULL; /* then return NULL */ } /* Success: set fields */ heap_struct->id = structId++; heap_struct->len = n; return heap_struct; /* return pointer to allocated HeapStruct */ } void free_all(HeapStruct* heap_struct) { if (NULL == heap_struct) /* NULL pointer? */ return; /* if so, do nothing */ free(heap_struct->heap_nums); /* first free encapsulated aggregate */ free(heap_struct); /* then free containing structure */ } int main() { const unsigned n = 100; HeapStruct* hs = get_heap_struct(n); /* get structure with N floats */ /* Do some (meaningless) work for demo. */ unsigned i; for (i = 0; i < n; i++) hs->heap_nums[i] = 3.14 + (float) i; for (i = 0; i < n; i += 10) printf("%12f\n", hs->heap_nums[i]); free_all(hs); /* free dynamically allocated storage */ return 0; } 上面的 nestedHeap 程序示例以结构体 HeapStruct 为中心，结构体中又有名为 heap_nums 的指针字段：typedef struct { unsigned id; unsigned len; float* heap_nums; /** pointer **/ } HeapStruct; 函数 get_heap_struct 尝试为 HeapStruct 实例分配堆存储，这需要为字段 heap_nums 指向的若干个 float 变量分配堆存储。如果成功调用 get_heap_struct 函数，并将指向堆分配结构体的指针以 hs 命名，其结果可以描述如下：hs-->HeapStruct instance id len heap_nums-->N contiguous float elements 在 get_heap_struct 函数中，第一个堆分配过程很简单：HeapStruct* heap_struct = malloc(sizeof(HeapStruct)); if (NULL == heap_struct) /* failure? */ return NULL; /* if so, return NULL */ sizeof(HeapStruct) 包括了 heap_nums 字段的字节数（32 位机器上为 4，64 位机器上为 8），heap_nums 字段则是指向动态分配数组中的 float 元素的指针。那么，问题关键在于 malloc 为这个结构体传送了字节空间还是表示失败的 NULL；如果是 NULL，get_heap_struct 函数就也返回 NULL 以通知调用者堆分配失败。第二步尝试堆分配的过程更复杂，因为在这一步，HeapStruct 的堆存储已经分配好了：heap_struct->heap_nums = malloc(sizeof(float) * n); if (NULL == heap_struct->heap_nums) { /* failure? */ free(heap_struct); /* if so, first free the HeapStruct */ return NULL; /* and then return NULL */ } 传递给 get_heap_struct 函数的参数 n 指明动态分配的 heap_nums 数组中应该有多少个 float 元素。如果可以分配所需的若干个 float 元素，则该函数在返回 HeapStruct 的堆地址之前会设置结构的 id 和 len 字段。 但是，如果尝试分配失败，则需要两个步骤来实现最优方案：1、 必须释放 HeapStruct 的存储以避免内存泄漏。对于调用 get_heap_struct 的客户端函数而言，没有动态 heap_nums 数组的 HeapStruct 可能就是没用的；因此，HeapStruct 实例的字节空间应该显式释放，以便系统可以回收这些空间用于未来的堆分配。2、 返回 NULL 以标识失败。如果成功调用 get_heap_struct 函数，那么释放堆存储也很棘手，因为它涉及要以正确顺序进行的两次 free 操作。因此，该程序设计了一个 free_all 函数，而不是要求程序员再去手动实现两步释放操作。回顾一下，free_all 函数是这样的：void free_all(HeapStruct* heap_struct) { if (NULL == heap_struct) /* NULL pointer? */ return; /* if so, do nothing */ free(heap_struct->heap_nums); /* first free encapsulated aggregate */ free(heap_struct); /* then free containing structure */ } 检查完参数 heap_struct 不是 NULL 值后，函数首先释放 heap_nums 数组，这步要求 heap_struct 指针此时仍然是有效的。先释放 heap_struct 的做法是错误的。一旦 heap_nums 被释放，heap_struct 就可以释放了。如果 heap_struct 被释放，但 heap_nums 没有被释放，那么数组中的 float 元素就会泄漏：仍然分配了字节空间，但无法被访问到——因此一定要记得释放 heap_nums。存储泄漏将一直持续，直到 nestedHeap 程序退出，系统回收泄漏的字节时为止。关于 free 库函数的注意事项就是要有顺序。回想一下上面的调用示例：free(heap_struct->heap_nums); /* first free encapsulated aggregate */ free(heap_struct); /* then free containing structure */ 这些调用释放了分配的存储空间——但它们并 不是 将它们的操作参数设置为 NULL（free 函数会获取地址的副本作为参数；因此，将副本更改为 NULL 并不会改变原地址上的参数值）。例如，在成功调用 free 之后，指针 heap_struct 仍然持有一些堆分配字节的堆地址，但是现在使用这个地址将会产生错误，因为对 free 的调用使得系统有权回收然后重用这些分配过的字节。使用 NULL 参数调用 free 没有意义，但也没有什么坏处。而在非 NULL 的地址上重复调用 free 会导致不确定结果的错误：free(heap_struct); /* 1st call: ok */ free(heap_struct); /* 2nd call: ERROR */ 内存泄漏和堆碎片化“内存泄漏”是指动态分配的堆存储变得不再可访问。看一下相关的代码段：float* nums = malloc(sizeof(float) * 10); /* 10 floats */ nums[0] = 3.14f; /* and so on */ nums = malloc(sizeof(float) * 25); /* 25 new floats */ 假如第一个 malloc 成功，第二个 malloc 会再将 nums 指针重置为 NULL（分配失败情况下）或是新分配的 25 个 float 中第一个的地址。最初分配的 10 个 float 元素的堆存储仍然处于被分配状态，但此时已无法再对其访问，因为 nums 指针要么指向别处，要么是 NULL。结果就是造成了 40 个字节（sizeof(float) * 10）的泄漏。在第二次调用 malloc 之前，应该释放最初分配的存储空间：float* nums = malloc(sizeof(float) * 10); /* 10 floats */ nums[0] = 3.14f; /* and so on */ free(nums); /** good **/ nums = malloc(sizeof(float) * 25); /* no leakage */ 即使没有泄漏，堆也会随着时间的推移而碎片化，需要对系统进行碎片整理。例如，假设两个最大的堆块当前的大小分别为 200MB 和 100MB。然而，这两个堆块并不连续，进程 P 此时又需要分配 250MB 的连续堆存储。在进行分配之前，系统可能要对堆进行 碎片整理 以给 P 提供 250MB 连续存储空间。碎片整理很复杂，因此也很耗时。内存泄漏会创建处于已分配状态但不可访问的堆块，从而会加速碎片化。因此，释放不再需要的堆存储是程序员帮助减少碎片整理需求的一种方式。诊断内存泄漏的工具有很多工具可用于分析内存效率和安全性，其中我最喜欢的是 valgrind。为了说明该工具如何处理内存泄漏，这里给出 leaky 示例程序：#include <stdio.h> #include <stdlib.h> int* get_ints(unsigned n) { int* ptr = malloc(n * sizeof(int)); if (ptr != NULL) { unsigned i; for (i = 0; i < n; i++) ptr[i] = i + 1; } return ptr; } void print_ints(int* ptr, unsigned n) { unsigned i; for (i = 0; i < n; i++) printf("%3i\n", ptr[i]); } int main() { const unsigned n = 32; int* arr = get_ints(n); if (arr != NULL) print_ints(arr, n); /** heap storage not yet freed... **/ return 0; } main 函数调用了 get_ints 函数，后者会试着从堆中 malloc 32 个 4 字节的 int，然后初始化动态数组（如果 malloc 成功）。初始化成功后，main 函数会调用 print_ints函数。程序中并没有调用 free 来对应 malloc 操作；因此，内存泄漏了。如果安装了 valgrind 工具箱，下面的命令会检查 leaky 程序是否存在内存泄漏（% 是命令行提示符）：% valgrind --leak-check=full ./leaky 绝大部分输出都在下面给出了。左边的数字 207683 是正在执行的 leaky 程序的进程标识符。这份报告给出了泄漏发生位置的详细信息，本例中位置是在 main 函数所调用的 get_ints 函数中对 malloc 的调用处。==207683== HEAP SUMMARY: ==207683== in use at exit: 128 bytes in 1 blocks ==207683== total heap usage: 2 allocs, 1 frees, 1,152 bytes allocated ==207683== ==207683== 128 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==207683== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==207683== by 0x109186: get_ints (in /home/marty/gc/leaky) ==207683== by 0x109236: main (in /home/marty/gc/leaky) ==207683== ==207683== LEAK SUMMARY: ==207683== definitely lost: 128 bytes in 1 blocks ==207683== indirectly lost: 0 bytes in 0 blocks ==207683== possibly lost: 0 bytes in 0 blocks ==207683== still reachable: 0 bytes in 0 blocks ==207683== suppressed: 0 bytes in 0 blocks 如果把 main 函数改成在对 print_ints 的调用之后，再加上一个对 free 的调用，valgrind 就会对 leaky 程序给出一个干净的内存健康清单：==218462== All heap blocks were freed -- no leaks are possible 静态区存储在正统的 C 语言中，函数必须在所有块之外定义。这是一些 C 编译器支持的特性，杜绝了在另一个函数体内定义一个函数的可能。我举的例子都是在所有块之外定义的函数。这样的函数要么是 static ，即静态的，要么是 extern，即外部的，其中 extern 是默认值。C 语言中，以 static 或 extern 修饰的函数和变量驻留在内存中所谓的 静态区 中，因为在程序执行期间该区域大小是固定不变的。这两个存储类型的语法非常复杂，我们应该回顾一下。在回顾之后，会有一个完整的代码示例来生动展示语法细节。在所有块之外定义的函数或变量默认为 extern；因此，函数和变量要想存储类型为 static ，必须显式指定：/** file1.c: outside all blocks, five definitions **/ int foo(int n) { return n * 2; } /* extern by default */ static int bar(int n) { return n; } /* static */ extern int baz(int n) { return -n; } /* explicitly extern */ int num1; /* extern */ static int num2; /* static */ extern 和 static 的区别在于作用域：extern 修饰的函数或变量可以实现跨文件可见（需要声明）。相比之下，static 修饰的函数仅在 定义 该函数的文件中可见，而 static 修饰的变量仅在 定义 该变量的文件（或文件中的块）中可见：static int n1; /* scope is the file */ void func() { static int n2; /* scope is func's body */ ... } 如果在所有块之外定义了 static 变量，例如上面的 n1，该变量的作用域就是定义变量的文件。无论在何处定义 static 变量，变量的存储都在内存的静态区中。extern 函数或变量在给定文件中的所有块之外定义，但这样定义的函数或变量也可以在其他文件中声明。典型的做法是在头文件中 声明 这样的函数或变量，只要需要就可以包含进来。下面这些简短的例子阐述了这些棘手的问题。假设 extern 函数 foo 在 file1.c 中 定义，有无关键字 extern 效果都一样：/** file1.c **/ int foo(int n) { return n * 2; } /* definition has a body {...} */ 必须在其他文件（或其中的块）中使用显式的 extern 声明 此函数才能使其可见。以下是使 extern 函数 foo 在文件 file2.c 中可见的声明语句：/** file2.c: make function foo visible here **/ extern int foo(int); /* declaration (no body) */ 回想一下，函数声明没有用大括号括起来的主体，而函数定义会有这样的主体。为了便于查看，函数和变量声明通常会放在头文件中。准备好需要声明的源代码文件，然后就可以 #include 相关的头文件。下一节中的 staticProg 程序演示了这种方法。至于 extern 的变量，规则就变得更棘手了（很抱歉增加了难度！）。任何 extern 的对象——无论函数或变量——必须 定义 在所有块之外。此外，在所有块之外定义的变量默认为 extern：/** outside all blocks **/ int n; /* defaults to extern */ 但是，只有在变量的 定义 中显式初始化变量时，extern 才能在变量的 定义 中显式修饰（LCTT 译注：换言之，如果下列代码中的 int n1; 行前加上 extern，该行就由 定义 变成了 声明）：/** file1.c: outside all blocks **/ int n1; /* defaults to extern, initialized by compiler to zero */ extern int n2 = -1; /* ok, initialized explicitly */ int n3 = 9876; /* ok, extern by default and initialized explicitly */ 要使在 file1.c 中定义为 extern 的变量在另一个文件（例如 file2.c）中可见，该变量必须在 file2.c 中显式 声明 为 extern 并且不能初始化（初始化会将声明转换为定义）：/** file2.c **/ extern int n1; /* declaration of n1 defined in file1.c */ 为了避免与 extern 变量混淆，经验是在 声明 中显式使用 extern（必须），但不要在 定义 中使用（非必须且棘手）。对于函数，extern 在定义中是可选使用的，但在声明中是必须使用的。下一节中的 staticProg 示例会把这些点整合到一个完整的程序中。staticProg 示例staticProg 程序由三个文件组成：两个 C 语言源文件（static1.c 和 static2.c）以及一个头文件（static.h），头文件中包含两个声明：/** header file static.h **/ #define NumCount 100 /* macro */ extern int global_nums[NumCount]; /* array declaration */ extern void fill_array(); /* function declaration */ 两个声明中的 extern，一个用于数组，另一个用于函数，强调对象在别处（“外部”）定义：数组 global_nums 在文件 static1.c 中定义（没有显式的 extern），函数 fill_array 在文件 static2.c 中定义（也没有显式的 extern）。每个源文件都包含了头文件 static.h。static1.c 文件定义了两个驻留在内存静态区域中的数组（global_nums 和 more_nums）。第二个数组有 static 修饰，这将其作用域限制为定义数组的文件 (static1.c)。如前所述， extern 修饰的 global_nums 则可以实现在多个文件中可见。/** static1.c **/ #include <stdio.h> #include <stdlib.h> #include "static.h" /* declarations */ int global_nums[NumCount]; /* definition: extern (global) aggregate */ static int more_nums[NumCount]; /* definition: scope limited to this file */ int main() { fill_array(); /** defined in file static2.c **/ unsigned i; for (i = 0; i < NumCount; i++) more_nums[i] = i * -1; /* confirm initialization worked */ for (i = 0; i < NumCount; i += 10) printf("%4i\t%4i\n", global_nums[i], more_nums[i]); return 0; } 下面的 static2.c 文件中定义了 fill_array 函数，该函数由 main（在 static1.c 文件中）调用；fill_array 函数会给名为 global_nums 的 extern 数组中的元素赋值，该数组在文件 static1.c 中定义。使用两个文件的唯一目的是凸显 extern 变量或函数能够跨文件可见。/** static2.c **/ #include "static.h" /** declarations **/ void fill_array() { /** definition **/ unsigned i; for (i = 0; i < NumCount; i++) global_nums[i] = i + 2; } staticProg 程序可以用如下编译：% gcc -o staticProg static1.c static2.c 从汇编语言看更多细节现代 C 编译器能够处理 C 和汇编语言的任意组合。编译 C 源文件时，编译器首先将 C 代码翻译成汇编语言。这是对从上文 static1.c 文件生成的汇编语言进行保存的命令：% gcc -S static1.c 生成的文件就是 static1.s。这是文件顶部的一段代码，额外添加了行号以提高可读性： .file "static1.c" ## line 1 .text ## line 2 .comm global_nums,400,32 ## line 3 .local more_nums ## line 4 .comm more_nums,400,32 ## line 5 .section .rodata ## line 6 .LC0: ## line 7 .string "%4i\t%4i\n" ## line 8 .text ## line 9 .globl main ## line 10 .type main, @function ## line 11 main: ## line 12 ... 诸如 .file（第 1 行）之类的汇编语言指令以句点开头。顾名思义，指令会指导汇编程序将汇编语言翻译成机器代码。.rodata 指令（第 6 行）表示后面是只读对象，包括字符串常量 "%4i\t%4i\n"（第 8 行），main 函数（第 12 行）会使用此字符串常量来实现格式化输出。作为标签引入（通过末尾的冒号实现）的 main 函数（第 12 行），同样也是只读的。在汇编语言中，标签就是地址。标签 main:（第 12 行）标记了 main 函数代码开始的地址，标签 .LC0:（第 7 行）标记了格式化字符串开头所在的地址。global_nums（第 3 行）和 more_nums（第 4 行）数组的定义包含了两个数字：400 是每个数组中的总字节数，32 是每个数组（含 100 个 int 元素）中每个元素的比特数。（第 5 行中的 .comm 指令表示 common name，可以忽略。）两个数组定义的不同之处在于 more_nums 被标记为 .local（第 4 行），这意味着其作用域仅限于其所在文件 static1.s。相比之下，global_nums 数组就能在多个文件中实现可见，包括由 static1.c 和 static2.c 文件翻译成的汇编文件。最后，.text 指令在汇编代码段中出现了两次（第 2 行和第 9 行）。术语“text”表示“只读”，但也会涵盖一些读/写变量，例如两个数组中的元素。尽管本文展示的汇编语言是针对 Intel 架构的，但 Arm6 汇编也非常相似。对于这两种架构，.text 区域中的变量（本例中为两个数组中的元素）会自动初始化为零。总结C 语言中的内存高效和内存安全编程准则很容易说明，但可能会很难遵循，尤其是在调用设计不佳的库的时候。准则如下：尽可能使用栈存储，进而鼓励编译器将通用寄存器用作暂存器，实现优化。栈存储代表了高效的内存使用并促进了代码的整洁和模块化。永远不要返回指向基于栈的存储的指针。小心使用堆存储。C（和 C++）中的重难点是确保动态分配的存储尽快解除分配。良好的编程习惯和工具（如 valgrind）有助于攻关这些重难点。优先选用自身提供释放函数的库，例如 nestedHeap 代码示例中的 free_all 释放函数。谨慎使用静态存储，因为这种存储会自始至终地影响进程的内存占用。特别是尽量避免使用 extern 和 static 数组。本文 C 语言代码示例可在我的网站（https://condor.depaul.edu/mkalin）上找到。via: https://opensource.com/article/21/8/memory-programming-c作者：Marty Kalin 选题：lujun9972 译者：unigeorge 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
硬核观察 #295 推动苹果公司成为万亿公司的一封邮件
推动苹果公司成为万亿公司的一封邮件2007 年，苹果的市值约为 1500 亿美元。今天，它已经超过了 2 万亿美元，主要是基于 iPhone 的成功，而 iPhone 的成功，至少部分是基于 App Store 的成功。而这一切，都始于乔布斯的一封邮件。最初，iPhone 在每台设备上只预装了 16 个应用。乔布斯告诉开发者，如果他们想为 iPhone 创造应用，他们可以制作在 Safari 浏览器中运行的网页应用。用户立即开始想办法越狱，以便在他们的设备上安装应用。所以，苹果只能通过某种官方 SDK 使开发应用程序成为可能。乔布斯给该公司软件工程高级副总裁 Bertrand Serlet 回答到：“当然，只要我们能在 2008 年 1 月 15 日的 Macworld 上把它全部推出来。”2008 年，我们见到了苹果的 SDK 和 App Store。无论主动还是被动，App Store 这种划时代的创举，真正开创了新的智能手机时代。Linux 内核将一直保留前 1MB 内存在一些特定的情况下，前 64kb 的内存会被 BIOS 破坏；在另外一些罕见情况下，前面的内存也会被 BIOS 或 EGA/VGA 帧缓冲区所破坏。因此，与其针对不同情况分别设置内存规避区域，不如将整个前 1MB 内存设置为保留。因此，在 5.13 中将永久保留前 1MB 内存而不使用。现在真是内存富余了，可以随便将 1MB 内存保留不用了。而就在 20 多年前，那时候还是按 KB 计算着使用内存的。Torvalds 继续反对在编译 Linux 内核时使用 -O3 优化标志由于 GCC 的较旧版本可能会在 -O3 编译器优化级别下产生坏的代码，而且有时没有性能上的好处，Linus Torvalds 仍然反对在编译 Linux 内核时使用这种优化标志。Torvalds 总结了他目前对 -O3 的看法：“它在历史上一直有可怕的错误。它已经变好了，但‘变好’并不是一个很高的标准。”WireGuard 项目从开始就一直用 -O3 优化级别构建，也并没有看到因此而产生不良代码的情况，但由于 Torvalds 的指导意见，该项目放弃了这个选项。除了 WireGuard 之外，LZ4 和 Zstd 是仍然在用 -O3 的内核模块。一方面很多厂商转向使用新版本的编译器非常缓慢，另外一方面一些没有那么严谨的代码也并不适合更高级别的优化。理想化的优化措施，在现实情况中往往不能采用。
利用 Volatility 找出应用程序、网络连接、内核模块、文件等方面的情况。计算机的操作系统和应用使用主内存（RAM）来执行不同的任务。这种易失性内存包含大量关于运行应用、网络连接、内核模块、打开的文件以及几乎所有其他的内容信息，但这些信息每次计算机重启的时候都会被清除。 内存取证 Memory forensics 是一种从内存中找到和抽取这些有价值的信息的方式。Volatility 是一种使用插件来处理这类信息的开源工具。但是，存在一个问题：在你处理这些信息前，必须将物理内存转储到一个文件中，而 Volatility 没有这种能力。因此，这篇文章分为两部分：第一部分是处理获取物理内存并将其转储到一个文件中。第二部分使用 Volatility 从这个内存转储中读取并处理这些信息。我在本教程中使用了以下测试系统，不过它可以在任何 Linux 发行版上工作：$ cat /etc/redhat-release Red Hat Enterprise Linux release 8.3 (Ootpa) $ $ uname -r 4.18.0-240.el8.x86_64 $ 注意事项： 部分 1 涉及到编译和加载一个内核模块。不要担心：它并不像听起来那么困难。一些指南：按照以下的步骤。不要在生产系统或你的主要计算机上尝试任何这些步骤。始终使用测试的虚拟机（VM）来尝试，直到你熟悉使用这些工具并理解它们的工作原理为止。安装需要的包在开始之前安装必要的工具。如果你经常使用基于 Debian 的发行版，可以使用 apt-get 命令。这些包大多数提供了需要的内核信息和工具来编译代码：$ yum install kernel-headers kernel-devel gcc elfutils-libelf-devel make git libdwarf-tools python2-devel.x86_64-y 部分 1：使用 LiME 获取内存并将其转储到一个文件中在开始分析内存之前，你需要一个内存转储供你使用。在实际的取证活动中，这可能来自一个被破坏或者被入侵的系统。这些信息通常会被收集和存储来分析入侵是如何发生的及其影响。由于你可能没有可用的内存转储，你可以获取你的测试 VM 的内存转储，并使用它来执行内存取证。 Linux 内存提取器 Linux Memory Extractor （LiME）是一个在 Linux 系统上获取内存很常用的工具。使用以下命令获得 LiME：$ git clone https://github.com/504ensicsLabs/LiME.git $ $ cd LiME/src/ $ $ ls deflate.c disk.c hash.c lime.h main.c Makefile Makefile.sample tcp.c $ 构建 LiME 内核模块在 src 文件夹下运行 make 命令。这会创建一个以 .ko 为扩展名的内核模块。理想情况下，在 make 结束时，lime.ko 文件会使用格式 lime-<your-kernel-version>.ko 被重命名。$ make make -C /lib/modules/4.18.0-240.el8.x86_64/build M="/root/LiME/src" modules make[1]: Entering directory '/usr/src/kernels/4.18.0-240.el8.x86_64' << 删节 >> make[1]: Leaving directory '/usr/src/kernels/4.18.0-240.el8.x86_64' strip --strip-unneeded lime.ko mv lime.ko lime-4.18.0-240.el8.x86_64.ko $ $ $ ls -l lime-4.18.0-240.el8.x86_64.ko -rw-r--r--. 1 root root 25696 Apr 17 14:45 lime-4.18.0-240.el8.x86_64.ko $ $ file lime-4.18.0-240.el8.x86_64.ko lime-4.18.0-240.el8.x86_64.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=1d0b5cf932389000d960a7e6b57c428b8e46c9cf, not stripped $ 加载LiME 内核模块现在是时候加载内核模块来获取系统内存了。insmod 命令会帮助加载内核模块；模块一旦被加载，会在你的系统上读取主内存（RAM）并且将内存的内容转储到命令行所提供的 path 目录下的文件中。另一个重要的参数是 format；保持 lime 的格式，如下所示。在插入内核模块之后，使用 lsmod 命令验证它是否真的被加载。$ lsmod | grep lime $ $ insmod ./lime-4.18.0-240.el8.x86_64.ko "path=../RHEL8.3_64bit.mem format=lime" $ $ lsmod | grep lime lime 16384 0 $ 你应该看到给 path 命令的文件已经创建好了，而且文件大小与你系统的物理内存（RAM）大小相同（并不奇怪）。一旦你有了内存转储，你就可以使用 rmmod 命令删除该内核模块：$ $ ls -l ~/LiME/RHEL8.3_64bit.mem -r--r--r--. 1 root root 4294544480 Apr 17 14:47 /root/LiME/RHEL8.3_64bit.mem $ $ du -sh ~/LiME/RHEL8.3_64bit.mem 4.0G /root/LiME/RHEL8.3_64bit.mem $ $ free -m total used free shared buff/cache available Mem: 3736 220 366 8 3149 3259 Swap: 4059 8 4051 $ $ rmmod lime $ $ lsmod | grep lime $ 内存转储中是什么？这个内存转储文件只是原始数据，就像使用 file 命令可以看到的一样。你不可能通过手动去理解它；是的，在这里边有一些 ASCII 字符，但是你无法用编辑器打开这个文件并把它读出来。hexdump 的输出显示，最初的几个字节是 EmiL；这是因为你的请求格式在上面的命令行中是 lime：$ file ~/LiME/RHEL8.3_64bit.mem /root/LiME/RHEL8.3_64bit.mem: data $ $ hexdump -C ~/LiME/RHEL8.3_64bit.mem | head 00000000 45 4d 69 4c 01 00 00 00 00 10 00 00 00 00 00 00 |EMiL............| 00000010 ff fb 09 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 b8 fe 4c cd 21 44 00 32 20 00 00 2a 2a 2a 2a 2a |..L.!D.2 ..*****| 00000030 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a |****************| 00000040 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 2a 20 00 20 |************* . | 00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00000080 00 00 00 00 00 00 00 00 00 00 00 00 70 78 65 6c |............pxel| 00000090 69 6e 75 78 2e 30 00 00 00 00 00 00 00 00 00 00 |inux.0..........| 000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| $ 部分 2：获得 Volatility 并使用它来分析你的内存转储现在你有了要分析的示例内存转储，使用下面的命令获取 Volatility 软件。Volatility 已经用 Python 3 重写了，但是本教程使用的是用 Python 2 写的原始的 Volatility 包。如果你想用 Volatility 3 进行实验，可以从合适的 Git 仓库下载它，并在以下命令中使用 Python 3 而不是 Python 2：$ git clone https://github.com/volatilityfoundation/volatility.git $ $ cd volatility/ $ $ ls AUTHORS.txt contrib LEGAL.txt Makefile PKG-INFO pyinstaller.spec resources tools vol.py CHANGELOG.txt CREDITS.txt LICENSE.txt MANIFEST.in pyinstaller README.txt setup.py volatility $ Volatility 使用两个 Python 库来实现某些功能，所以使用以下命令来安装它们。否则，在你运行 Volatility 工具时，你可能看到一些导入错误；你可以忽略它们，除非你正在运行的插件需要这些库；这种情况下，工具将会报错：$ pip2 install pycrypto $ pip2 install distorm3 列出 Volatility 的 Linux 配置文件你将要运行的第一个 Volatility 命令列出了可用的 Linux 配置文件，运行 Volatility 命令的主要入口点是 vol.py 脚本。使用 Python 2 解释器调用它并提供 --info 选项。为了缩小输出，查找以 Linux 开头的字符串。正如你所看到的，并没有很多 Linux 配置文件被列出：$ python2 vol.py --info | grep ^Linux Volatility Foundation Volatility Framework 2.6.1 LinuxAMD64PagedMemory - Linux-specific AMD 64-bit address space. $ 构建你自己的 Linux 配置文件Linux 发行版是多种多样的，并且是为不同架构而构建的。这就是为什么配置文件是必要的 —— Volatility 在提取信息前必须知道内存转储是从哪个系统和架构获得的。有一些 Volatility 命令可以找到这些信息；但是这个方法很费时。为了加快速度，可以使用以下命令构建一个自定义的 Linux 配置文件：移动到 Volatility 仓库的 tools/linux目录下，运行 make 命令：$ cd tools/linux/ $ $ pwd /root/volatility/tools/linux $ $ ls kcore Makefile Makefile.enterprise module.c $ $ make make -C //lib/modules/4.18.0-240.el8.x86_64/build CONFIG_DEBUG_INFO=y M="/root/volatility/tools/linux" modules make[1]: Entering directory '/usr/src/kernels/4.18.0-240.el8.x86_64' << 删节 >> make[1]: Leaving directory '/usr/src/kernels/4.18.0-240.el8.x86_64' $ 你应该看到一个新的 module.dwarf 文件。你也需要 /boot 目录下的 System.map 文件，因为它包含了所有与当前运行的内核相关的符号：$ ls kcore Makefile Makefile.enterprise module.c module.dwarf $ $ ls -l module.dwarf -rw-r--r--. 1 root root 3987904 Apr 17 15:17 module.dwarf $ $ ls -l /boot/System.map-4.18.0-240.el8.x86_64 -rw-------. 1 root root 4032815 Sep 23 2020 /boot/System.map-4.18.0-240.el8.x86_64 $ $ 要创建一个自定义配置文件，移动回到 Volatility 目录并且运行下面的命令。第一个参数提供了一个自定义 .zip 文件，文件名是你自己命名的。我经常使用操作系统和内核版本来命名。下一个参数是前边创建的 module.dwarf 文件，最后一个参数是 /boot 目录下的 System.map 文件：$ $ cd volatility/ $ $ zip volatility/plugins/overlays/linux/Redhat8.3_4.18.0-240.zip tools/linux/module.dwarf /boot/System.map-4.18.0-240.el8.x86_64 adding: tools/linux/module.dwarf (deflated 91%) adding: boot/System.map-4.18.0-240.el8.x86_64 (deflated 79%) $ 现在自定义配置文件就准备好了，所以在前边给出的位置检查一下 .zip 文件是否被创建好。如果你想知道 Volatility 是否检测到这个自定义配置文件，再一次运行 --info 命令。现在，你应该可以在下边的列出的内容中看到新的配置文件：$ $ ls -l volatility/plugins/overlays/linux/Redhat8.3_4.18.0-240.zip -rw-r--r--. 1 root root 1190360 Apr 17 15:20 volatility/plugins/overlays/linux/Redhat8.3_4.18.0-240.zip $ $ $ python2 vol.py --info | grep Redhat Volatility Foundation Volatility Framework 2.6.1 LinuxRedhat8_3_4_18_0-240x64 - A Profile for Linux Redhat8.3_4.18.0-240 x64 $ $ 开始使用 Volatility现在你已经准备好去做一些真正的内存取证了。记住，Volatility 是由自定义的插件组成的，你可以针对内存转储来获得信息。命令的通用格式是：python2 vol.py -f <memory-dump-file-taken-by-Lime> <plugin-name> --profile=<name-of-our-custom-profile> 有了这些信息，运行 linux_banner 插件来看看你是否可从内存转储中识别正确的发行版信息：$ python2 vol.py -f ~/LiME/RHEL8.3_64bit.mem linux_banner --profile=LinuxRedhat8_3_4_18_0-240x64 Volatility Foundation Volatility Framework 2.6.1 Linux version 4.18.0-240.el8.x86_64 ([mockbuild@vm09.test.com][4]) (gcc version 8.3.1 20191121 (Red Hat 8.3.1-5) (GCC)) #1 SMP Wed Sep 23 05:13:10 EDT 2020 $ 找到 Linux 插件到现在都很顺利，所以现在你可能对如何找到所有 Linux 插件的名字比较好奇。有一个简单的技巧：运行 --info 命令并抓取 linux_ 字符串。有各种各样的插件可用于不同的用途。这里列出一部分：$ python2 vol.py --info | grep linux_ Volatility Foundation Volatility Framework 2.6.1 linux_apihooks - Checks for userland apihooks linux_arp - Print the ARP table linux_aslr_shift - Automatically detect the Linux ASLR shift << 删节 >> linux_banner - Prints the Linux banner information linux_vma_cache - Gather VMAs from the vm_area_struct cache linux_volshell - Shell in the memory image linux_yarascan - A shell in the Linux memory image $ 使用 linux_psaux 插件检查内存转储时系统上正在运行哪些进程。注意列表中的最后一个命令：它是你在转储之前运行的 insmod 命令。$ python2 vol.py -f ~/LiME/RHEL8.3_64bit.mem linux_psaux --profile=LinuxRedhat8_3_4_18_0-240x64 Volatility Foundation Volatility Framework 2.6.1 Pid Uid Gid Arguments 1 0 0 /usr/lib/systemd/systemd --switched-root --system --deserialize 18 2 0 0 [kthreadd] 3 0 0 [rcu_gp] 4 0 0 [rcu_par_gp] 861 0 0 /usr/libexec/platform-python -Es /usr/sbin/tuned -l -P 869 0 0 /usr/bin/rhsmcertd 875 0 0 /usr/libexec/sssd/sssd_be --domain implicit_files --uid 0 --gid 0 --logger=files 878 0 0 /usr/libexec/sssd/sssd_nss --uid 0 --gid 0 --logger=files << 删节 >> 11064 89 89 qmgr -l -t unix -u 227148 0 0 [kworker/0:0] 227298 0 0 -bash 227374 0 0 [kworker/u2:1] 227375 0 0 [kworker/0:2] 227884 0 0 [kworker/0:3] 228573 0 0 insmod ./lime-4.18.0-240.el8.x86_64.ko path=../RHEL8.3_64bit.mem format=lime 228576 0 0 $ 想要知道系统的网络状态吗？运行 linux_netstat 插件来找到在内存转储期间网络连接的状态：$ python2 vol.py -f ~/LiME/RHEL8.3_64bit.mem linux_netstat --profile=LinuxRedhat8_3_4_18_0-240x64 Volatility Foundation Volatility Framework 2.6.1 UNIX 18113 systemd/1 /run/systemd/private UNIX 11411 systemd/1 /run/systemd/notify UNIX 11413 systemd/1 /run/systemd/cgroups-agent UNIX 11415 systemd/1 UNIX 11416 systemd/1 << 删节 >> $ 接下来，使用 linux_mount 插件来看在内存转储期间哪些文件系统被挂载：$ python2 vol.py -f ~/LiME/RHEL8.3_64bit.mem linux_mount --profile=LinuxRedhat8_3_4_18_0-240x64 Volatility Foundation Volatility Framework 2.6.1 tmpfs /sys/fs/cgroup tmpfs ro,nosuid,nodev,noexec cgroup /sys/fs/cgroup/pids cgroup rw,relatime,nosuid,nodev,noexec systemd-1 /proc/sys/fs/binfmt_misc autofs rw,relatime sunrpc /var/lib/nfs/rpc_pipefs rpc_pipefs rw,relatime /dev/mapper/rhel_kvm--03--guest11-root / xfs rw,relatime tmpfs /dev/shm tmpfs rw,nosuid,nodev selinuxfs /sys/fs/selinux selinuxfs rw,relatime << 删节 >> cgroup /sys/fs/cgroup/net_cls,net_prio cgroup rw,relatime,nosuid,nodev,noexec cgroup /sys/fs/cgroup/cpu,cpuacct cgroup rw,relatime,nosuid,nodev,noexec bpf /sys/fs/bpf bpf rw,relatime,nosuid,nodev,noexec cgroup /sys/fs/cgroup/memory cgroup ro,relatime,nosuid,nodev,noexec cgroup /sys/fs/cgroup/cpuset cgroup rw,relatime,nosuid,nodev,noexec mqueue /dev/mqueue mqueue rw,relatime $ 好奇哪些内核模块被加载了吗？Volatility 也为这个提供了一个插件 linux_lsmod：$ python2 vol.py -f ~/LiME/RHEL8.3_64bit.mem linux_lsmod --profile=LinuxRedhat8_3_4_18_0-240x64 Volatility Foundation Volatility Framework 2.6.1 ffffffffc0535040 lime 20480 ffffffffc0530540 binfmt_misc 20480 ffffffffc05e8040 sunrpc 479232 << 删节 >> ffffffffc04f9540 nfit 65536 ffffffffc0266280 dm_mirror 28672 ffffffffc025e040 dm_region_hash 20480 ffffffffc0258180 dm_log 20480 ffffffffc024bbc0 dm_mod 151552 $ 想知道哪些文件被哪些进程打开了吗？使用 linux_bash 插件可以列出这些信息：$ python2 vol.py -f ~/LiME/RHEL8.3_64bit.mem linux_bash --profile=LinuxRedhat8_3_4_18_0-240x64 -v Volatility Foundation Volatility Framework 2.6.1 Pid Name Command Time Command -------- -------------------- ------------------------------ ------- 227221 bash 2021-04-17 18:38:24 UTC+0000 lsmod 227221 bash 2021-04-17 18:38:24 UTC+0000 rm -f .log 227221 bash 2021-04-17 18:38:24 UTC+0000 ls -l /etc/zzz 227221 bash 2021-04-17 18:38:24 UTC+0000 cat ~/.vimrc 227221 bash 2021-04-17 18:38:24 UTC+0000 ls 227221 bash 2021-04-17 18:38:24 UTC+0000 cat /proc/817/cwd 227221 bash 2021-04-17 18:38:24 UTC+0000 ls -l /proc/817/cwd 227221 bash 2021-04-17 18:38:24 UTC+0000 ls /proc/817/ << 删节 >> 227298 bash 2021-04-17 18:40:30 UTC+0000 gcc prt.c 227298 bash 2021-04-17 18:40:30 UTC+0000 ls 227298 bash 2021-04-17 18:40:30 UTC+0000 ./a.out 227298 bash 2021-04-17 18:40:30 UTC+0000 vim prt.c 227298 bash 2021-04-17 18:40:30 UTC+0000 gcc prt.c 227298 bash 2021-04-17 18:40:30 UTC+0000 ./a.out 227298 bash 2021-04-17 18:40:30 UTC+0000 ls $ 想知道哪些文件被哪些进程打开了吗？使用 linux_lsof 插件可以列出这些信息：$ python2 vol.py -f ~/LiME/RHEL8.3_64bit.mem linux_lsof --profile=LinuxRedhat8_3_4_18_0-240x64 Volatility Foundation Volatility Framework 2.6.1 Offset Name Pid FD Path ------------------ ------------------------------ -------- -------- ---- 0xffff9c83fb1e9f40 rsyslogd 71194 0 /dev/null 0xffff9c83fb1e9f40 rsyslogd 71194 1 /dev/null 0xffff9c83fb1e9f40 rsyslogd 71194 2 /dev/null 0xffff9c83fb1e9f40 rsyslogd 71194 3 /dev/urandom 0xffff9c83fb1e9f40 rsyslogd 71194 4 socket:[83565] 0xffff9c83fb1e9f40 rsyslogd 71194 5 /var/log/messages 0xffff9c83fb1e9f40 rsyslogd 71194 6 anon_inode:[9063] 0xffff9c83fb1e9f40 rsyslogd 71194 7 /var/log/secure << 删节 >> 0xffff9c8365761f40 insmod 228573 0 /dev/pts/0 0xffff9c8365761f40 insmod 228573 1 /dev/pts/0 0xffff9c8365761f40 insmod 228573 2 /dev/pts/0 0xffff9c8365761f40 insmod 228573 3 /root/LiME/src/lime-4.18.0-240.el8.x86_64.ko $ 访问 Linux 插件脚本位置通过读取内存转储和处理这些信息，你可以获得更多的信息。如果你会 Python，并且好奇这些信息是如何被处理的，可以到存储所有插件的目录，选择一个你感兴趣的，并看看 Volatility 是如何获得这些信息的：$ ls volatility/plugins/linux/ apihooks.py common.py kernel_opened_files.py malfind.py psaux.py apihooks.pyc common.pyc kernel_opened_files.pyc malfind.pyc psaux.pyc arp.py cpuinfo.py keyboard_notifiers.py mount_cache.py psenv.py arp.pyc cpuinfo.pyc keyboard_notifiers.pyc mount_cache.pyc psenv.pyc aslr_shift.py dentry_cache.py ld_env.py mount.py pslist_cache.py aslr_shift.pyc dentry_cache.pyc ld_env.pyc mount.pyc pslist_cache.pyc << 删节 >> check_syscall_arm.py __init__.py lsmod.py proc_maps.py tty_check.py check_syscall_arm.pyc __init__.pyc lsmod.pyc proc_maps.pyc tty_check.pyc check_syscall.py iomem.py lsof.py proc_maps_rb.py vma_cache.py check_syscall.pyc iomem.pyc lsof.pyc proc_maps_rb.pyc vma_cache.pyc $ $ 我喜欢 Volatility 的理由是他提供了许多安全插件。这些信息很难手动获取：linux_hidden_modules - Carves memory to find hidden kernel modules linux_malfind - Looks for suspicious process mappings linux_truecrypt_passphrase - Recovers cached Truecrypt passphrases Volatility 也允许你在内存转储中打开一个 shell，所以你可以运行 shell 命令来代替上面所有命令，并获得相同的信息：$ python2 vol.py -f ~/LiME/RHEL8.3_64bit.mem linux_volshell --profile=LinuxRedhat8_3_4_18_0-240x64 -v Volatility Foundation Volatility Framework 2.6.1 Current context: process systemd, pid=1 DTB=0x1042dc000 Welcome to volshell! Current memory image is: file:///root/LiME/RHEL8.3_64bit.mem To get help, type 'hh()' >>> >>> sc() Current context: process systemd, pid=1 DTB=0x1042dc000 >>> 接下来的步骤内存转储是了解 Linux 内部情况的好方法。试一试 Volatility 的所有插件，并详细研究它们的输出。然后思考这些信息如何能够帮助你识别入侵或安全问题。深入了解这些插件的工作原理，甚至尝试改进它们。如果你没有找到你想做的事情的插件，那就写一个并提交给 Volatility，这样其他人也可以使用它。via: https://opensource.com/article/21/4/linux-memory-forensics作者：Gaurav Kamathe 选题：lujun9972 译者：RiaXu 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
英特尔推出了包含 3D Xpoint 内存技术的 Optane 持久内存产品线。英特尔的这个解决方案介乎于 DRAM 和 NAND 中间，以此来提升性能。英特尔在 2019 年 4 月的大规模数据中心活动中正式推出 Optane 持久内存产品线。它已经问世了一段时间，但是目前的 Xeon 服务器处理器还不能充分利用它。而新的 Xeon8200 和 9200 系列可以充分利用 Optane 持久内存的优势。由于 Optane 是英特尔的产品（与美光合作开发），所以意味着 AMD 和 ARM 的服务器处理器不能够支持它。正如我之前所说的，OptaneDC 持久内存采用与美光合作研发的 3D Xpoint 内存技术。3D Xpoint 是一种比 SSD 更快的非易失性内存，速度几乎与 DRAM 相近，而且它具有 NAND 闪存的持久性。第一个 3D Xpoint 产品是被称为英特尔“尺子”的 SSD，因为它们被设计成细长的样子，很像尺子的形状。它们被设计这样是为了适合 1u 的服务器机架。在发布的公告中，英特尔推出了新的利用四芯或者 QLC 3D NAND 内存的英特尔 SSD D5-P4325 尺子 SSD，可以在 1U 的服务器机架上放 1PB 的存储。OptaneDC 持久内存的可用容量最初可以通过使用 128GB 的 DIMM 达到 512GB。英特尔数据中心集团执行副总裁及总经理 Navin Shenoy 说：“OptaneDC 持久内存可达到的容量是 DRAM 的 2 到 4 倍。”他说：“我们希望服务器系统的容量可以扩展到每个插槽 4.5TB 或者 8 个插槽 36TB，这是我们第一代 Xeon 可扩展芯片的 3 倍。”英特尔Optane内存的使用和速度Optane 有两种不同的运行模式：内存模式和应用直连模式。内存模式是将 DRAM 放在 Optane 内存之上，将 DRAM 作为 Optane 内存的缓存。应用直连模式是将 DRAM 和 OptaneDC 持久内存一起作为内存来最大化总容量。并不是每个工作负载都适合这种配置，所以应该在对延迟不敏感的应用程序中使用。正如英特尔推广的那样，Optane 的主要使用情景是内存模式。几年前，当 3D Xpoint 最初发布时，英特尔宣称 Optane 的速度是 NAND 的 1000 倍，耐用是 NAND 的 1000 倍，密度潜力是 DRAM 的 10 倍。这虽然有点夸张，但这些因素确实很令人着迷。在 256B 的连续 4 个缓存行中使用 Optane 内存可以达到 8.3GB/秒的读速度和 3.0GB/秒的写速度。与 SATA SSD 的 500MB/秒左右的读/写速度相比，可以看到性能有很大提升。请记住，Optane 充当内存，所以它会缓存被频繁访问的 SSD 中的内容。这是了解 OptaneDC 的关键。它能将非常大的数据集存储在离内存非常近的位置，因此具有很低延迟的 CPU 可以最小化访问较慢的存储子系统的访问延迟，无论存储是 SSD 还是 HDD。现在，它提供了一种可能性，即把多个 TB 的数据放在非常接近 CPU 的地方，以实现更快的访问。Optane 内存的一个挑战唯一真正的挑战是 Optane 插进内存所在的 DIMM 插槽。现在有些主板的每个 CPU 有多达 16 个 DIMM 插槽，但是这仍然是客户和设备制造商之间需要平衡的电路板空间：Optane 还是内存。有一些 Optane 驱动采用了 PCIe 接口进行连接，可以减轻主板上内存的拥挤。3D Xpoint 由于它写数据的方式，提供了比传统的 NAND 闪存更高的耐用性。英特尔承诺 Optane 提供 5 年保修期，而很多 SSD 只提供 3 年保修期。via: https://www.networkworld.com/article/3387117/intel-formally-launches-optane-for-data-center-memory-caching.html作者：Andy Patrizio 选题：lujun9972 译者：RiaXu 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
Linux 并不像 Windows，你经常不会有图形界面可供使用，特别是在服务器环境中。作为一名 Linux 管理员，知道如何获取当前可用的和已经使用的资源情况，比如内存、CPU、磁盘等，是相当重要的。如果某一应用在你的系统上占用了太多的资源，导致你的系统无法达到最优状态，那么你需要找到并修正它。如果你想找到消耗内存前十名的进程，你需要去阅读这篇文章：如何在 Linux 中找出内存消耗最大的进程。在 Linux 中，命令能做任何事，所以使用相关命令吧。在这篇教程中，我们将会给你展示 8 个有用的命令来即查看在 Linux 系统中内存的使用情况，包括 RAM 和交换分区。创建交换分区在 Linux 系统中是非常重要的，如果你想了解如何创建，可以去阅读这篇文章：在 Linux 系统上创建交换分区。下面的命令可以帮助你以不同的方式查看 Linux 内存使用情况。free 命令/proc/meminfo 文件vmstat 命令ps_mem 命令smem 命令top 命令htop 命令glances 命令1）如何使用 free 命令查看 Linux 内存使用情况free 命令 是被 Linux 管理员广泛使用的主要命令。但是它提供的信息比 /proc/meminfo 文件少。free 命令会分别展示物理内存和交换分区内存中已使用的和未使用的数量，以及内核使用的缓冲区和缓存。这些信息都是从 /proc/meminfo 文件中获取的。# free -m total used free shared buff/cache available Mem: 15867 9199 1702 3315 4965 3039 Swap: 17454 666 16788total：总的内存量used：被当前运行中的进程使用的内存量（used = total – free – buff/cache）free： 未被使用的内存量（free = total – used – buff/cache）shared： 在两个或多个进程之间共享的内存量buffers： 内存中保留用于内核记录进程队列请求的内存量cache： 在 RAM 中存储最近使用过的文件的页缓冲大小buff/cache： 缓冲区和缓存总的使用内存量available： 可用于启动新应用的可用内存量（不含交换分区）2) 如何使用 /proc/meminfo 文件查看 Linux 内存使用情况/proc/meminfo 文件是一个包含了多种内存使用的实时信息的虚拟文件。它展示内存状态单位使用的是 kB，其中大部分属性都难以理解。然而它也包含了内存使用情况的有用信息。# cat /proc/meminfo MemTotal: 16248572 kB MemFree: 1764576 kB MemAvailable: 3136604 kB Buffers: 234132 kB Cached: 4731288 kB SwapCached: 28516 kB Active: 9004412 kB Inactive: 3552416 kB Active(anon): 8094128 kB Inactive(anon): 2896064 kB Active(file): 910284 kB Inactive(file): 656352 kB Unevictable: 80 kB Mlocked: 80 kB SwapTotal: 17873388 kB SwapFree: 17191328 kB Dirty: 252 kB Writeback: 0 kB AnonPages: 7566736 kB Mapped: 3692368 kB Shmem: 3398784 kB Slab: 278976 kB SReclaimable: 125480 kB SUnreclaim: 153496 kB KernelStack: 23936 kB PageTables: 73880 kB NFS_Unstable: 0 kB Bounce: 0 kB WritebackTmp: 0 kB CommitLimit: 25997672 kB Committed_AS: 24816804 kB VmallocTotal: 34359738367 kB VmallocUsed: 0 kB VmallocChunk: 0 kB Percpu: 3392 kB HardwareCorrupted: 0 kB AnonHugePages: 0 kB ShmemHugePages: 0 kB ShmemPmdMapped: 0 kB HugePages_Total: 0 HugePages_Free: 0 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB Hugetlb: 0 kB DirectMap4k: 1059088 kB DirectMap2M: 14493696 kB DirectMap1G: 2097152 kB3) 如何使用 vmstat 命令查看 Linux 内存使用情况vmstat 命令 是另一个报告虚拟内存统计信息的有用工具。vmstat 报告的信息包括：进程、内存、页面映射、块 I/O、陷阱、磁盘和 CPU 特性信息。vmstat 不需要特殊的权限，并且它可以帮助诊断系统瓶颈。# vmstat procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 1 0 682060 1769324 234188 4853500 0 3 25 91 31 16 34 13 52 0 0如果你想详细了解每一项的含义，阅读下面的描述。procs：进程r： 可以运行的进程数目（正在运行或等待运行）b： 处于不可中断睡眠中的进程数目memory：内存swpd： 使用的虚拟内存数量free： 空闲的内存数量buff： 用作缓冲区内存的数量cache： 用作缓存内存的数量inact： 不活动的内存数量（使用 -a 选项）active： 活动的内存数量（使用 -a 选项）Swap：交换分区si： 每秒从磁盘交换的内存数量so： 每秒交换到磁盘的内存数量IO：输入输出bi： 从一个块设备中收到的块（块/秒）bo： 发送到一个块设备的块（块/秒）System：系统in： 每秒的中断次数，包括时钟。cs： 每秒的上下文切换次数。CPU：下面这些是在总的 CPU 时间占的百分比us： 花费在非内核代码上的时间占比（包括用户时间，调度时间）sy： 花费在内核代码上的时间占比 （系统时间）id： 花费在闲置的时间占比。在 Linux 2.5.41 之前，包括 I/O 等待时间wa： 花费在 I/O 等待上的时间占比。在 Linux 2.5.41 之前，包括在空闲时间中st： 被虚拟机偷走的时间占比。在 Linux 2.6.11 之前，这部分称为 unknown运行下面的命令查看详细的信息。# vmstat -s 16248580 K total memory 2210256 K used memory 2311820 K active memory 2153352 K inactive memory 11368812 K free memory 107584 K buffer memory 2561928 K swap cache 17873388 K total swap 0 K used swap 17873388 K free swap 44309 non-nice user cpu ticks 164 nice user cpu ticks 14332 system cpu ticks 382418 idle cpu ticks 1248 IO-wait cpu ticks 1407 IRQ cpu ticks 2147 softirq cpu ticks 0 stolen cpu ticks 1022437 pages paged in 260296 pages paged out 0 pages swapped in 0 pages swapped out 1424838 interrupts 4979524 CPU context switches 1577163147 boot time 3318 forks4) 如何使用 ps\_mem 命令查看 Linux 内存使用情况ps\_mem 是一个用来查看当前内存使用情况的简单的 Python 脚本。该工具可以确定每个程序使用了多少内存（不是每个进程）。该工具采用如下的方法计算每个程序使用内存：总的使用 = 程序进程私有的内存 + 程序进程共享的内存。计算共享内存是存在不足之处的，该工具可以为运行中的内核自动选择最准确的方法。# ps_mem Private + Shared = RAM used Program 180.0 KiB + 30.0 KiB = 210.0 KiB xf86-video-intel-backlight-helper (2) 192.0 KiB + 66.0 KiB = 258.0 KiB cat (2) 312.0 KiB + 38.5 KiB = 350.5 KiB lvmetad 380.0 KiB + 25.5 KiB = 405.5 KiB crond 392.0 KiB + 32.5 KiB = 424.5 KiB rtkit-daemon 852.0 KiB + 117.0 KiB = 969.0 KiB gnome-session-ctl (2) 928.0 KiB + 56.5 KiB = 984.5 KiB gvfs-mtp-volume-monitor 1.0 MiB + 42.5 KiB = 1.0 MiB dconf-service 1.0 MiB + 106.5 KiB = 1.1 MiB gvfs-goa-volume-monitor 1.0 MiB + 180.5 KiB = 1.2 MiB gvfsd . . 5.3 MiB + 3.0 MiB = 8.3 MiB evolution-addressbook-factory 8.5 MiB + 1.2 MiB = 9.7 MiB gnome-session-binary (4) 7.5 MiB + 3.1 MiB = 10.5 MiB polkitd 7.4 MiB + 3.3 MiB = 10.7 MiB pulseaudio (2) 7.0 MiB + 7.0 MiB = 14.0 MiB msm_notifier 12.7 MiB + 2.3 MiB = 15.0 MiB evolution-source-registry 13.3 MiB + 2.5 MiB = 15.8 MiB gnome-terminal-server 15.8 MiB + 1.0 MiB = 16.8 MiB tracker-miner-fs 18.7 MiB + 1.8 MiB = 20.5 MiB python3.7 16.6 MiB + 4.0 MiB = 20.5 MiB evolution-calendar-factory 22.3 MiB + 753.0 KiB = 23.0 MiB gsd-keyboard (2) 22.4 MiB + 832.0 KiB = 23.2 MiB gsd-wacom (2) 20.8 MiB + 2.5 MiB = 23.3 MiB blueman-tray 22.0 MiB + 1.8 MiB = 23.8 MiB blueman-applet 23.1 MiB + 934.0 KiB = 24.0 MiB gsd-xsettings (2) 23.7 MiB + 1.2 MiB = 24.9 MiB gsd-media-keys (2) 23.4 MiB + 1.6 MiB = 25.0 MiB gsd-color (2) 23.9 MiB + 1.2 MiB = 25.1 MiB gsd-power (2) 16.5 MiB + 8.9 MiB = 25.4 MiB evolution-alarm-notify 27.2 MiB + 2.0 MiB = 29.2 MiB systemd-journald 28.7 MiB + 2.8 MiB = 31.5 MiB c 29.6 MiB + 2.2 MiB = 31.8 MiB chrome-gnome-sh (2) 43.9 MiB + 6.8 MiB = 50.7 MiB WebExtensions 46.7 MiB + 6.7 MiB = 53.5 MiB goa-daemon 86.5 MiB + 55.2 MiB = 141.7 MiB Xorg (2) 191.4 MiB + 24.1 MiB = 215.4 MiB notepadqq-bin 306.7 MiB + 29.0 MiB = 335.7 MiB gnome-shell (2) 601.6 MiB + 77.7 MiB = 679.2 MiB firefox 1.0 GiB + 109.7 MiB = 1.1 GiB chrome (15) 2.3 GiB + 123.1 MiB = 2.5 GiB Web Content (8) ---------------------------------- 5.6 GiB ==================================5）如何使用 smem 命令查看 Linux 内存使用情况smem 是一个可以为 Linux 系统提供多种内存使用情况报告的工具。不同于现有的工具，smem 可以报告 比例集大小 Proportional Set Size （PSS）、 唯一集大小 Unique Set Size （USS）和 驻留集大小 Resident Set Size （RSS）。比例集大小（PSS）：库和应用在虚拟内存系统中的使用量。唯一集大小（USS）：其报告的是非共享内存。驻留集大小（RSS）：物理内存（通常多进程共享）使用情况，其通常高于内存使用量。# smem -tk PID User Command Swap USS PSS RSS 3383 daygeek cat 0 92.0K 123.0K 1.7M 3384 daygeek cat 0 100.0K 129.0K 1.7M 1177 daygeek /usr/lib/gnome-session-ctl 0 436.0K 476.0K 4.6M 1171 daygeek /usr/bin/dbus-daemon --conf 0 524.0K 629.0K 3.8M 1238 daygeek /usr/lib/xdg-permission-sto 0 592.0K 681.0K 5.9M 1350 daygeek /usr/lib/gsd-screensaver-pr 0 652.0K 701.0K 5.8M 1135 daygeek /usr/lib/gdm-x-session --ru 0 648.0K 723.0K 6.0M . . 1391 daygeek /usr/lib/evolution-data-ser 0 16.5M 25.2M 63.3M 1416 daygeek caffeine-ng 0 28.7M 31.4M 66.2M 4855 daygeek /opt/google/chrome/chrome - 0 38.3M 46.3M 120.6M 2174 daygeek /usr/lib/firefox/firefox -c 0 44.0M 50.7M 120.3M 1254 daygeek /usr/lib/goa-daemon 0 46.7M 53.3M 80.4M 3416 daygeek /opt/google/chrome/chrome - 0 44.7M 54.2M 103.3M 4782 daygeek /opt/google/chrome/chrome - 0 57.2M 65.8M 142.3M 1137 daygeek /usr/lib/Xorg vt2 -displayf 0 77.2M 129.6M 192.3M 3376 daygeek /opt/google/chrome/chrome 0 117.8M 131.0M 210.9M 4448 daygeek /usr/lib/firefox/firefox -c 0 124.4M 133.8M 224.1M 3558 daygeek /opt/google/chrome/chrome - 0 157.3M 165.7M 242.2M 2310 daygeek /usr/lib/firefox/firefox -c 0 159.6M 169.4M 259.6M 4331 daygeek /usr/lib/firefox/firefox -c 0 176.8M 186.2M 276.8M 4034 daygeek /opt/google/chrome/chrome - 0 179.3M 187.9M 264.6M 3911 daygeek /opt/google/chrome/chrome - 0 183.1M 191.8M 269.4M 3861 daygeek /opt/google/chrome/chrome - 0 199.8M 208.2M 285.2M 2746 daygeek /usr/bin/../lib/notepadqq/n 0 193.5M 217.5M 261.5M 1194 daygeek /usr/bin/gnome-shell 0 203.7M 219.0M 285.1M 2945 daygeek /usr/lib/firefox/firefox -c 0 294.5M 308.0M 410.2M 2786 daygeek /usr/lib/firefox/firefox -c 0 341.2M 354.3M 459.1M 4193 daygeek /usr/lib/firefox/firefox -c 0 417.4M 427.6M 519.3M 2651 daygeek /usr/lib/firefox/firefox -c 0 417.0M 430.1M 535.6M 2114 daygeek /usr/lib/firefox/firefox -c 0 430.6M 473.9M 610.9M 2039 daygeek /usr/lib/firefox/firefox -- 0 601.3M 677.5M 840.6M ------------------------------------------------------------------------------- 90 1 0 4.8G 5.2G 8.0G6) 如何使用 top 命令查看 Linux 内存使用情况top 命令 是一个 Linux 系统的管理员最常使用的用于查看进程的资源使用情况的命令。该命令会展示了系统总的内存量、当前内存使用量、空闲内存量和缓冲区使用的内存总量。此外，该命令还会展示总的交换空间内存量、当前交换空间的内存使用量、空闲的交换空间内存量和缓存使用的内存总量。# top -b | head -10 top - 11:04:39 up 40 min, 1 user, load average: 1.59, 1.42, 1.28 Tasks: 288 total, 2 running, 286 sleeping, 0 stopped, 0 zombie %Cpu(s): 13.3 us, 1.5 sy, 0.0 ni, 84.4 id, 0.0 wa, 0.3 hi, 0.5 si, 0.0 st KiB Mem : 16248572 total, 7755928 free, 4657736 used, 3834908 buff/cache KiB Swap: 17873388 total, 17873388 free, 0 used. 9179772 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 2114 daygeek 20 3182736 616624 328228 R 83.3 3.8 7:09.72 Web Content 2039 daygeek 20 4437952 849616 261364 S 13.3 5.2 7:58.54 firefox 1194 daygeek 20 4046856 291288 165460 S 4.2 1.8 1:57.68 gnome-shell 4034 daygeek 20 808556 273244 88676 S 4.2 1.7 1:44.72 chrome 2945 daygeek 20 3309832 416572 150112 S 3.3 2.6 4:04.60 Web Content 1137 daygeek 20 564316 197292 183380 S 2.5 1.2 2:55.76 Xorg 2651 daygeek 20 3098420 547260 275700 S 1.7 3.4 2:15.96 Web Content 2786 daygeek 20 2957112 463912 240744 S 1.7 2.9 3:22.29 Web Content 1 root 20 182756 10208 7760 S 0.8 0.1 0:04.51 systemd 442 root -51 S 0.8 0:05.02 irq/141-iw+ 1426 daygeek 20 373660 48948 29820 S 0.8 0.3 0:03.55 python3 2174 daygeek 20 2466680 122196 78604 S 0.8 0.8 0:17.75 WebExtensi+7) 如何使用 htop 命令查看 Linux 内存使用情况htop 命令 是一个可交互的 Linux/Unix 系统进程查看器。它是一个文本模式应用，且使用它需要 Hisham 开发的 ncurses 库。该名令的设计目的使用来代替 top 命令。该命令与 top 命令很相似，但是其允许你可以垂直地或者水平地的滚动以便可以查看系统中所有的进程情况。htop 命令拥有不同的颜色，这个额外的优点当你在追踪系统性能情况时十分有用。此外，你可以自由地执行与进程相关的任务，比如杀死进程或者改变进程的优先级而不需要其进程号（PID）。8）如何使用 glances 命令查看 Linux 内存使用情况Glances 是一个 Python 编写的跨平台的系统监视工具。你可以在一个地方查看所有信息，比如：CPU 使用情况、内存使用情况、正在运行的进程、网络接口、磁盘 I/O、RAID、传感器、文件系统信息、Docker、系统信息、运行时间等等。via: https://www.2daygeek.com/linux-commands-check-memory-usage/作者：Magesh Maruthamuthu 选题：lujun9972 译者：萌新阿岩 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出