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

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

ruisui883周前 (04-05)技术分析13

本文主要根据 “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!段错误的来龙去脉你知道吗?” 的相关文章

vue项目-父页面数据变化使子页面更新的几种情况

当操作页面时候,特别是增删改操作之后,数据会有所改变,这个时候我们希望组件中的数据要和最新数据一致,就需要重新更新渲染。以下是针对几种不同情况下方式:一.子页面调用接口后重新渲染1.使用ref方式父组件中用ref=“xxx” 来声明子组件,然后通过在父组件值改变的地方来调用子组件中的方法this.$...

Gitlab概览

Gitlab是开源的基于Git的仓库管理系统,也可以管理软件开发的整个生命周期,是项目管理和代码托管平台,支撑着整个DevOps的生命周期。Gitlab很容易选为GitHub,作为公司私有库管理的工具。我们可以用Gitlab Workflow来协同整个团队的软件开发管理过程。软件开发阶段Gitlab...

「Git迁移」三行命令迁移Git包含提交历史,分支,tag标签等信息

问题描述:公司需要将一个git远程服务器的全部已有项目迁移到一台新服务器的Gitlab中,其中需要包含全部的提交纪录,已有的全部分支与全部打tag标签,目前此工作已全部迁移完毕,特此记录一下操作步骤环境描述:1. 要迁移的远程Git:Gitblit2. 迁移目的Git:Gitlab3. 暂存代码的P...

高效使用 Vim 编辑器的 10 个技巧

在 Reverb,我们使用 MacVim 来标准化开发环境,使配对更容易,并提高效率。当我开始使用 Reverb 时,我以前从未使用过 Vim。我花了几个星期才开始感到舒服,但如果没有这样的提示,可能需要几个月的时间。这里有十个技巧可以帮助你在学习使用 Vim 时提高效率。1. 通过提高按键重复率来...

uni-app基于vue开发小程序与标准vue开发新增点

1、路由跳转传参uni.navigateTo({ url: `/pages/transition/spreadTextAction?t=${this.options.t}&rt=${this.options.rt}&l=${this.options.l}`});uni.navigateBack({...

三勾点餐系统java+springboot+vue3,开源系统小程序点餐系统

项目简述前台实现:用户浏览菜单、菜品分类筛选、查看菜品详情、菜品多属性、菜品加料、添加购物车、购物车结算、个人订单查询、门店自提、外卖配送、菜品打包等。后台实现:菜品管理、订单管理、会员管理、系统管理、权限管理等。 项目介绍三勾点餐系统基于java+springboot+element-plus+u...