当前位置:首页 > 技术分析 > 正文内容

Segment Fault!段错误的来龙去脉你知道吗?

ruisui884周前 (04-05)技术分析14

本文主要根据 “Operating Systems: Three Easy Pieces” 第16章总结而来。

在本头条号的上一篇文章中,我们知道通过base/bounds 寄存器,操作系统可以把进程放到可用的物理地址内,让进程认为自己是独享的内存,而且操作系统还能保证进程间互不干扰。进程的地址空间示意图如下所示:

但是我们看到,上面的示意图中,堆和栈之间有很大一块空白区域,就算那块区域永远不会被进程用到,它也不会分配给别的进程。这样造成的问题首先是内存资源的浪费,其次,如果一个进程占用的内存太大,它甚至都找不到一块足够大的内存空间放置它。

为了解决这些问题,内存虚拟化引入了内存分段的技术。本节就具体来看这个技术的细节。

内存分段:base/bounds的推广

通过上面我们看到如果把进程的代码、堆、栈当做一个整体放入内存中,需要一对 base/bounds寄存器。现在我们不希望浪费堆、栈之间的内存区域,那么为什么不多加几个 base/bounds寄存器呢?事实上内存分段就是这样做的,因为进程的地址空间天然就被分成了三段,所以只要在MMU里面放置3对base/bounds寄存器,就能把代码段、堆、栈分成三个独立的段,每个段都有对应的base/bounds寄存器。示意图如下图所示:

可以看到,通过内存分段技术,没有内存空间被浪费了,而且内存地址翻译的方法也跟以前一样,只是说现在对应不同的段,有不同的基地址和界地址。我们平常写程序可能碰到“segmentation fault”这样的错误,一般指的就是访问的地址非法(比如超出了界地址的范围)。就算现在很多硬件不是使用这样的技术,这个错误信息还是被保存了下来并沿用至今。

但是上面的示意图的分段技术也引入了两个新的问题:

A. 如何判断需要翻译的虚拟地址是哪个段的?

我们说过,进程中的地址都是虚拟地址,虚拟地址要经过硬件被翻译成物理地址。由于内存被分段了,给定一个虚拟地址,硬件怎么知道该虚拟地址是哪个段的呢?这决定了用哪个段的base/bounds寄存器做地址翻译。 这里面有两种方法可供选择:

  1. 常用的是显式的方法:可以使用虚拟地址的前两位当做段的标志位。比如对于一个16位的地址,用最开头的 00 表示代码段,01 表示堆,11 表示栈,(注意这种假设里10没被用到),后面的14位地址当做偏移值。程序写起来也非常简单,示意代码如下所示:

  2. 隐式的方法:通过检测虚拟地址的生成方式。比如地址是PC指针,一般是代码段的地址,如果是SP栈指针,一般是栈地址,其他的就是堆地址。这种方法不太常用。

B. 怎么处理地址反向增长的问题?

通过内存分段的示意图我们看到,栈地址是反向增长的,偏移值越大它变的越来越小。对于这种情况,操作系统首先需要硬件MMU的帮助。在MMU中除了base/bounds寄存器,还有一个地方标记了地址增长的方向(比如一个标志位,存储这一段是正向增长还是反向增长)。然后做地址翻译的时候,如果是逆向增长,就用偏移量减去段空间的最大值,得到真正的负向偏移。界寄存器检查负向偏移的绝对值是否在范围之内。

代码段共享

随着计算机的发展,操作系统人员发现使用了分段技术以后,不同进程可以共享某些内存段,比如最常见的代码段。

当然了,为了支持代码段共享,操作系统也需要得到硬件的支持,主要是内存保护的标志位。为了让代码段共享变得安全,系统需要有一个标志位,标示该段是只读的,还是可读可写的。通过标记代码段是只读的,不同进程就可以安全地共享同一个代码段,而不用担心代码段被其他进程修改。虽然物理内存是共享的,但是对于单个进程来说,还是像它们独占了那个代码段一样。

操作系统算法也需要加入一段逻辑,即在修改内存区域值的时候,需要先判断该内存是否是只读的。

操作系统的作用

目前为止,我们已经看到了分段技术的基本原理,以及硬件在里面起到的作用。那么操作系统有什么问题需要解决呢?

  1. 首先是我们曾经讨论过的一个古老的问题:进程上下文切换。由于引入了分段,在进行上下文切换的时候,操作系统需要保存3对base/bounds寄存器的值,还需要保存地址增长方向的标志位,以及内存保护的标志位。

  2. 第二个是更重要也是更难的问题,可用的内存空间列表怎么维护。由于把内存分成了三段,每段的大小又不一样,当一个新进程开始运行的时候,操作系统需要寻找三块足够大小的内存区域放置这些段。随着进程越来越多,整个物理内存就会被分成大大小小很多段,每段之间会有一些空余的“洞”。对比与不分段进程内部的碎片,内存分段产生的“洞”被叫做外部碎片。

对于外部碎片的问题,有很多方法被用来尝试解决它。比如操作系统可以定时通过“压缩”整理外部碎片,操作系统把所有进程“停”下来,把它们的数据拷贝到一块连续的区域去,并同时改变它们的base/bounds地址。通过这种方式,操作系统可以有连续的更大的内存供分配使用。但是这种方式的代价比较大,需要让进程暂时停止运行。

一种更简单的方式是系统通过维护一个可用内存的列表,在列表中找可用的内存用于分配。查找的方法有很多,比如“best-fit”的方式是查找与要分配的内存大小最接近的内存块,“first-fit”是用列表中第一个找到的足够大小的内存块,其他的还有”worst-fit” 或者更复杂的比如 buddy 算法。

然而,正如有很多方法可以用,没有一个方法能完美解决外部碎片的问题,这些方法也只是一定程度缓解碎片化的问题。

总结

内存分段技术能帮助操作系统解决进程内空间浪费的问题,同时也让代码段共享成为可能,但是它也引入了外部碎片的问题。

另外内存分段技术还是不够灵活,它还有一个非常重要的问题没有解决。就是当有一大片内存,被用到的部分其实很少,但是它还是必须整个都放到物理内存中,这样也是一种浪费。而且,当进程虚拟空间的大小大于物理内存的时候,分段也放不下进程的全部大小,这时候分段技术也起不了作用了。

这些是后面我们会继续关注的内容!欢迎大家订阅我的头条号,第一时间收到更新,谢谢!

扫描二维码推送至手机访问。

版权声明:本文由ruisui88发布,如需转载请注明出处。

本文链接:http://www.ruisui88.com/post/3305.html

分享给朋友:

“Segment Fault!段错误的来龙去脉你知道吗?” 的相关文章

最古老的Linux发行版刚刚进行了重大更新

Slackware 15.0 带来了全新的 KDE Plasma 5 桌面体验。Slackware Linux(仍然维护的最古老的Linux发行版)的制造商刚刚发布了Linux发行版的15.0版本。Slackware Linux于1993年出现,创始人Patrick Volderding今天继续维护...

如何在GitLab上回退指定版本的代码?GitLab回退指定版本问题分析

在Git中,回退到指定版本并不是删除或撤销之前的提交,而是创建一个新的提交,该提交包含指定版本的内容。这意味着您需要将当前代码更改与指定版本之间的差异进行比较,并将其合并到一个新的提交中。如果您没有更新本地代码,并且您希望将 GitLab 仓库回退到指定版本,您可以使用以下命令:git fetchg...

面试被逼疯:聊聊Python Import System?

面试官一个小时逼疯面试者:聊聊Python Import System?对于每一位Python开发者来说,import这个关键字是再熟悉不过了,无论是我们引用官方库还是三方库,都可以通过import xxx的形式来导入。可能很多人认为这只是Python的一个最基础的常识之一,似乎没有可以扩展的点了,...

JS数组过滤元素的方法

引言JavaScript 作为前端开发的核心技术之一,在现代 Web 开发中扮演着举足轻重的角色。随着 Web 应用越来越复杂,高效处理数据集合的需求日益凸显。本文旨在介绍 JavaScript 中数组过滤的基础知识及其在实际项目中的应用技巧。技术概述定义数组过滤是 JavaScript 提供的一种...

vue 开发规范

项目运行指南(#项目运行指南)开发本地环境(#开发本地环境)开发相关插件/工具(#开发相关插件工具)开发规范(#开发规范)vue(#vue)【数据流向】(#数据流向)【慎用全局注册】(#慎用全局注册)【组件名称】(#组件名称)【组件中的 CSS】(#组件中的-css)【统一标签顺序】(#统一标签顺序...

千智云低代码平台 v2.0.6发布「平台升级」

【平台简介】千智云低代码应用平台是一款低代码开发+低代码PaaS+SaaS应用中台为一体的应用平台。平台提供了多种应用场景功能及应用组件,满足各种应用的基本实现,可以使用低代码开发的方式,定制化的开发软件项目,并使用平台提供的各种功能,提供了大多数业务场景的支持。也可以将开发的应用发布到平台,成为S...