分段
复习
- 怎样处理死锁:预防、避免、检测和恢复的取舍
- 地址空间:每个进程都以为自己独占一片连续内存
- 地址重定位与内存保护:程序不知道实际装在哪里时怎样访问数据
TL;DR
- 分段:按用途把内存分成若干段,比如代码段、数据段、栈段
- 每个段有自己的基址和长度,段号加段内偏移就得到地址
- 分段符合程序的天然结构,也便于分别保护
- 缺点是会产生外部碎片:段与段之间的空隙难以利用
正文
上一章的“一整段连续内存”,把所有东西混在一起。可程序自己本来是分块的:代码一块、数据一块、栈一块,它们的用途、大小、增长方向都不一样。硬把三者塞进同一段,既别扭,也不好保护。
于是有了分段(segmentation):干脆顺着程序的结构,把它分成好几段。
按结构切分
一个进程的内存通常被分成这样几段:
- 代码段:存放指令,通常是只读的
- 数据段:存放全局变量和常量
- 栈段:存放函数调用的栈帧
- (还可能有权堆段等)
每一段在物理内存里各自占据一块地方,不必彼此相邻。
段表与地址转换
既然段各自散落,就得有一张表来记录它们的位置。这张表叫段表(segment table),每个表项记着:
- 段的基址:这一段在物理内存里的起始位置
- 段的长度:这一段有多长
程序里的地址,也相应变成“段号 + 段内偏移”:
虚拟地址 = [段号] [段内偏移]
查段表:
若 偏移 >= 该段长度 → 越界,异常
否则 物理地址 = 该段基址 + 偏移
比如“第 1 段(数据段)、偏移 20”,就能唯一确定一个物理地址。
分段的好处
- 贴合程序结构:代码、数据、栈各归各段,符合人对程序的理解
- 保护灵活:可以给不同段设不同权限,比如代码段只读、栈段可读写
- 共享方便:多个进程可以共享只读的代码段,而不必各存一份
外部碎片
分段也带来一个麻烦。因为每段长度不一,在内存里反复地分配和回收之后,整块内存会被切得七零八落:段与段之间留下许多小空隙。
这些小空隙加起来可能很多,但每一处都太小,塞不下一个新段——这叫外部碎片(external fragmentation)。就像停车场里零散地停着几辆车,看着空位不少,却停不进一辆大车。
要缓解,就得“整理”内存:把各段挪到一起、腾出连续空间。可搬动又在运行中的段代价不小。能不能从一开始就避免外部碎片? 下一章的分页,给出了一个很漂亮的答案。
思考题
为什么“代码段只读”这样的保护,在分段里很容易实现,而在“一整段连续内存”的模型里却很难?
小结
知识点
- 分段:按代码、数据、栈等用途划分内存
- 段表记录每段的基址与长度
- 虚拟地址 = 段号 + 段内偏移
- 分段贴合程序结构、便于保护,但有外部碎片
参考资料
- Wikipedia(zh):分段:segmentation
- Wikipedia(zh):外部碎片:fragmentation
思考题答案(仅供参考)
因为分段给每一段单独记录了基址和长度,硬件在每次访问时都知道“这次访问落在哪一段”,于是就能按段设置权限:代码段只读、数据段可读写、栈段可读写。而在“一整段连续内存”的模型里,代码、数据、栈混在同一段,硬件只看到一个大范围,分不清哪部分是指令、哪部分是数据,自然没法只给代码那部分加“只读”。能分别管理,才能分别保护——这正是分段的价值。
协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
封面图
设计师 | 南国微雪