Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

分段

复习

  • 怎样处理死锁:预防、避免、检测和恢复的取舍
  • 地址空间:每个进程都以为自己独占一片连续内存
  • 地址重定位与内存保护:程序不知道实际装在哪里时怎样访问数据

TL;DR

  • 分段:按用途把内存分成若干段,比如代码段、数据段、栈段
  • 每个段有自己的基址和长度,段号加段内偏移就得到地址
  • 分段符合程序的天然结构,也便于分别保护
  • 缺点是会产生外部碎片:段与段之间的空隙难以利用

正文

  上一章的“一整段连续内存”,把所有东西混在一起。可程序自己本来是分块的:代码一块、数据一块、栈一块,它们的用途、大小、增长方向都不一样。硬把三者塞进同一段,既别扭,也不好保护。

  于是有了分段(segmentation):干脆顺着程序的结构,把它分成好几段。

按结构切分

  一个进程的内存通常被分成这样几段:

  • 代码段:存放指令,通常是只读的
  • 数据段:存放全局变量和常量
  • 栈段:存放函数调用的栈帧
  • (还可能有权堆段等)

  每一段在物理内存里各自占据一块地方,不必彼此相邻。

段表与地址转换

  既然段各自散落,就得有一张表来记录它们的位置。这张表叫段表(segment table),每个表项记着:

  • 段的基址:这一段在物理内存里的起始位置
  • 段的长度:这一段有多长

  程序里的地址,也相应变成“段号 + 段内偏移”:

虚拟地址 = [段号] [段内偏移]

查段表:
  若 偏移 >= 该段长度  → 越界,异常
  否则 物理地址 = 该段基址 + 偏移

  比如“第 1 段(数据段)、偏移 20”,就能唯一确定一个物理地址。

分段的好处

  • 贴合程序结构:代码、数据、栈各归各段,符合人对程序的理解
  • 保护灵活:可以给不同段设不同权限,比如代码段只读、栈段可读写
  • 共享方便:多个进程可以共享只读的代码段,而不必各存一份

外部碎片

  分段也带来一个麻烦。因为每段长度不一,在内存里反复地分配和回收之后,整块内存会被切得七零八落:段与段之间留下许多小空隙。

  这些小空隙加起来可能很多,但每一处都太小,塞不下一个新段——这叫外部碎片(external fragmentation)。就像停车场里零散地停着几辆车,看着空位不少,却停不进一辆大车。

  要缓解,就得“整理”内存:把各段挪到一起、腾出连续空间。可搬动又在运行中的段代价不小。能不能从一开始就避免外部碎片? 下一章的分页,给出了一个很漂亮的答案。

思考题

  为什么“代码段只读”这样的保护,在分段里很容易实现,而在“一整段连续内存”的模型里却很难?

小结

知识点

  • 分段:按代码、数据、栈等用途划分内存
  • 段表记录每段的基址与长度
  • 虚拟地址 = 段号 + 段内偏移
  • 分段贴合程序结构、便于保护,但有外部碎片

参考资料

  1. Wikipedia(zh):分段:segmentation
  2. Wikipedia(zh):外部碎片:fragmentation

思考题答案(仅供参考)

  因为分段给每一段单独记录了基址和长度,硬件在每次访问时都知道“这次访问落在哪一段”,于是就能按段设置权限:代码段只读、数据段可读写、栈段可读写。而在“一整段连续内存”的模型里,代码、数据、栈混在同一段,硬件只看到一个大范围,分不清哪部分是指令、哪部分是数据,自然没法只给代码那部分加“只读”。能分别管理,才能分别保护——这正是分段的价值。

协议

  本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

封面图

设计师 | 南国微雪