- 作者: 雪山肥鱼
- 时间:20210226 22:05
- 目的:理解 Address Space
# 1.The Abstraction: Address Space 地址空间
## 1.1 早期系统
## 1.2 Multiprogramming and Time Sharing
### 1.2.1 进程在等待IP时,CPU可以切换到其他进程
## 1.3 Address Space 地址空间
## 1.4 目标
# 2. Interlude: Memory API
## 2.1 内存类型
## 2.2 malloc call
## 2.3 free call
## 2.4 mmap call
# 3. Mechanism: Address Translation 地址翻译
## 3.1 Assumption: 相关假设
## 3.2 An Example
## 3.3 Dynmic(Hardware-based) Relocation
## 3.4 重定位中所需的相关硬件
## 3.5 OS的介入(Interposition)
## 3.6 相关问题暴露
# 4. Segmentation
## 4.1 分段:通用的Base/Bounds
## 4.2 如何确定段地址
## 4.3 增长方向与Protection Bit
## 4.4 分段带来的问题 - 内存碎片化
# 5. Free - Space Management 空闲内存的管理
## 5.1 Assumtion
## 5.2 底层机制
## 5.3 基础空闲内存管理策略
1. The Abstraction: Address Space
1.1 早期系统
为什么会有地址空间的出现,那么我们一定要知道早期的操作系统是如何管理内存的。
- 没有地址空间这一抽象概念,对物理内存就是硬上
a) OS was a set of routines(a library) 常驻0 - 64kb(举例)
b) 一个进程会常驻在内存中。(used the rest of memory)
1.2 Multiprogramming and Time Sharing
因为计算机的昂贵,所以人们想提高计算机的利用率,那么Multiprogramming就诞生了。也就是在某一时刻,多道进程处于ready 状态.当运行的程序在等待I/O操作时,OS 将在这些处于ready的多道进程中来回切换。
1.2.1 为什么进程在等待I/O 操作时,CPU可以切换到其他进程呢?
外部设别在读取I/O数据时,是很慢的,CPU 可以去做其他事情。关于CPU与外部设备的关系,微机原理的课程又讲到,有时间会整理一些微机原理的知识,直接上图:
随后,程序员开始对计算机的交互性要求越来会高,批处理的运行方式为人诟病。多个用户也想同时操作一台电脑,并得到及时的响应。
粗糙的实现手段
进程在切换的前,保存所有的进程在内存中的状态,并且将这些状态写在硬盘中。如此反复。
很明显的缺陷,这样的切换实在太慢了-
restoring register-level state
这种方式相对于上面讲进程的所有内存状态保存起来相对快得多。
让多个进程同时驻留在内存中,即使运行的进程将被切换
这张图也理想化了, 在实际的物理内存中,这些程序都是分布在各个角落,对一个程序来说,脚在头上也很正常。
引出下一个问题:isolation 隔离 进程与进程之间互不干涉
1.3 Address Space 地址空间
OStep书中定义:
However, we have to keep those pesky users in mind, and doing so requires the OS to create an easy to use abstraction of physical memory.
We can this abstraction the address space, and it is the running program's view of memory in the system.
Understanding this fundamental OS bastraction of memory is key to understand how memory is virtualized.
这里有两点很关键
- 地址空间是 对 物理内存 的是 抽象
- 地址空间是 程序(同CPU) 所面对的,也就是说 物理内存条对于程序员和CPU是完全透明的,看不见的!得益于硬件层面的 MMU
地址空间有什么?(暂时假设只有这些)
- code
- stack
-
heap
堆栈的方向为为何是相反的,等读到后续再讨论把。
(Multiple threads co-exist in an address sapce, no nice way to divide the address space like this works anymore)
当然对于一个进程来说,他的地址空间是连续的,但是映射在真正的内存本体硬件来说,是任意的地址。loaded at some arbitrary physical address.
引出问题:如何虚拟化内存 并且虚拟出来的地址空间可能会很大。
- 比如说,如figure 13.2 所示
访问A进程的 地址空间里的0地址,那么对应的硬件物理地址就是320kb的地方。
1.4 目标
- 透明性(transparency)
进程本身感知不到物理内存的非连续性,每个进程觉得有属于自己的地址空间。
程序中能打印出来的地址,都是虚拟地址,都是假的。 - 高效性(efficiency)
OS本身解决不了的东西,那就得请硬件出场,如后续会提到的TLB - 保护(protection)
也就是隔离性,一个进程崩溃,不会影响其他进程。
safe from the ravages of other faulty or even malicious processes.
2. Interlude: Memory API
Crux: 如何分配内存
2.1 内存类型
- stack
The first is called stack memory, and allocations and deallocations of it are managed implicitly by the compiler for you , the programmer; it is sometimes called automatic memory.
函数结束就释放了 包括内在的 { }:
if you want some information to live beyond the call invocation, you had better not leave that information on the stack. - heap
It is this need for long lived memory that gets us to the second type of memory, called heap memory, where all allocations and deallocations are explicitly handled by you, the programmer.
2.2 The malloc( ) call
malloc 函数调用的时候,并不一定需要头文件 stdlib.h. 因为C库会默认链接的。
加上头文件只是为了让编译器检查malloc()函数使用是否正确。
- 关于sizeof( )操作符
sizeof 是 compile - time operator, 意思就是编译期间就已经知道其值,比如sizeof(int),在编译期间就已经替换成了4. 所以并不是 function call 函数是调用。 - 关于使用malloc时的类型转换
You might also notice that malloc() returns a pointer to type void. Doing so is just the way in C to pass back and address and let the programmer decide what to do with it. The porgrammer further helps out by using what is called a cast; in our example above, the programmer cast the return type of malloc() to pointer to a double. Casting doesn't really accomplish anything, other than tell the compiler and other programmers who might be reading your code: "yeah, I know what I'm doing"
2.3 The free( ) call
malloc 和 free 搭配使用 毋庸置疑。但malloc传输了sizeof,但free 并没有传入任何大小的参数。
原因会在后续的 free-space management 中阐述
free 和 malloc 仅仅是库函数,而不是系统调用!
是C库 在维护你的虚拟空间。
brk 函数 和 sbrk 函数 才是真正的系统调用。
- brk/sbr:
One such system call is called brk, which is used to change the location of the program's break: the location of the end of heap. It takes one argument(the address of new break), and thus either increases or decreases the size of the heap based on whether the new break is larger or smaller than the current break. An additional call sbrk is passed an increment but otherwise serve a similar purpose.
Note that you should never directly call either brk or sbrk. They are used by the memory-allocation library
2.4 the mmap( ) call
mmap 会为你的程序申请一片匿名内存,与后续要提到的 swap space 有关。当然可以理解为和malloc申请出的heap空间作用是一样的。
充分理解mmap,需要理解OS 的内存管理这套东西。后续再详细说明。
3. Mechanism: Address Translation
地址翻译这种任务如果交给软件层面的OS 去做会相当麻烦,所以需要 hardware supprot.
在这里,我们要引出的是一种最基础的(rudimentary)地址翻译,并不涉及后续要讨论的TLB.
- 软硬的配合
硬件单独去完成虚拟化内存的动作是不可能的,他只是提供底层的快速翻译。OS必须在正确的时机介入,去设置硬件产生correct translation.
所以出现了 内存管理(manage memory),管理哪些空闲内存可以使用,保持对内存的控制权。 - A beautiful illustration
illustration: the program has its own private memory,where its onwn code and data reside.
pyhsical truth:
many programs are actually sharing memory at the same time ,as the CPU(or CPUs) switchs betwwen running one program and the next.
3.1 Assumption: 相关假设
- user's address space must be placed contiguously in physical memory
- size of address space is small
- each address space is exactly the same size.
3.2 An Example
一个简单程序的space address 如下:
// 配合上图服用
void func() {
int x = 3000;
x = x +3;
...
}
128: movl 0x0(%ebx), %eax ; load 0 + ebx into eax address of x has been palced in the register ebx.
132: addl $0x03, %eax ; add 3 to eax register
135: movl %eax, 0x0(%ebx) ; store eax back to mem
注意对应的代码段位置 和 白能量X 所处的位置
- 找到adress 128 (PC Counter)处代码
- 执行该行代码,从15KB位置加载值
- 找到addess 132行代码
- 执行代码(并不涉及访问内存)
- 找到address 135行代码
- 执行指令 (讲计算好的值(exa保存) 存储到 address 15kb处)
上面的代码满足我们的假设
- Address space 足够小
-
那么如果对应的 physical address 是连续的,那么该进程在内存布局如下:
- 同时从上图也能看出,每个进程的 space address 大小是一致的。
3.3 Dynamic(Hardware-based) Relocation
提到 dynamic relocation 就必须有提到 base and bounds
- base register 基址寄存器
- bounds (limited) reigster 界限寄存器
意义:
- allow us to place the address space anywhere where we'd like in physical memory.
- the process can only access its own address space.
Figure 15.2中 该进程的base寄存器可以就定为32KB处
A base register is used to transform virtual addresses(generated by the program) into physical address. 用来dynamic relocation.
A bounds(or limit) register ensures that such addresses are within the confines of address space. 利用 bounds register, MMU 来检查地址是在边界以内。非法地址会引发exception,但并不一定所有的 exception 都会引发 程序终止.
CPU -> 发出virtual address -> mmu(hardware) -> physcial address -> memory system
base 和 bouds 寄存器是在芯片上的,是硬件结构。统称为 memery management unit (MMU)
3.4 重定位中所需的相关硬件
硬件要求 | 相关解释 |
---|---|
特权模式 | 以防止用户模式的进程进行特权操作 |
base/bound 寄存器 | MMU |
转换虚拟地址并检查吃否越界 | 电路来完成转换和界限检查 MMU |
修改base/bound寄存器的特权指令 | 让用户程序运行之前,操作系统必须能够设置这些值 |
中断向量表中处理异常程序的特权指令 | OS能够告诉硬件,如果发生异常,那么执行哪些代码 |
能够出发异常 | 进程试图使用特权指令或越界的内存 |
3.5 OS 的 介入(interposition)
There are a few critical juctures where the OS must get involved to implement our base-and-bounds version of virtual memory.
不要忘记此时的假设
- each address space is smaller than the size of physical memory.
- each address space same size
以上两点导致了,我们可以把物理内存看成n个连续的slot(槽)组成的。
1、创建进程时,在free-list 空闲内存列表中 为新的进程的地址空间找到room
2、进程结束时,需要回收进程内存资源。
3、进程之间的切换,OS 需要保存 寄存器 base/bound 的值。(PCB:process contrl block)
4、OS 需要提供 exception handler.OS 在启动的时候已经安装这些 exception handler. The OS does at boot time to ready the machine for use
3.6 相关问题暴露
1、Figure 15.2 heap 和 stack 有一段空间(因为我们现在假设每个进程空间有相同给的大小),但是并没有使用, a big chunck free space ,这就是internal fragmentation, space inside the allocated unit is not all used and thus wasted. 申请的内存并没有使用完。
2、Thus, we are going to need some sohpisitcated machiery(更精巧的机制), to try to better utilize the physical memory and avoid internal fragmentation.
3、引出 Segementation.
4. Segmentation
围绕的问题时,如何支持一个大地址空间(a lot of free space between the stack and heap)
4.1 分段:通用的Base/Bounds
对地址空间进行分段,每一个段拥有一个Base/Bounds pair. 也就是说 MMU 中有多个Base/Bounds pairs.
MMU 中存储 Base register 基地址寄存器,Bounds register 边界寄存器变成了存储这个段的大小(holds the size of a segment).
Segment | Base | Size(Bounds) |
---|---|---|
Code | 32K | 2K |
Heap | 34K | 2K |
Stack | 28K | 2k |
也就是说随着程序的运行,heap 和 stack 在动态的增长。
产生Segmentation fault 的原因 暂时可以说成 对内存的非法操作。比如base是stack base, 去读offset 超过了 2k的位置,则会出现Segmentation fault.
4.2 如何确定段地址
因为现在我们只认为有3个段 code stack heap,所以取前2位
SEG_MASK = 0x3000, SEG_SHIFT = 12, OFFSET_MASK = 0xFFF
Segment = (VirtualAddress & SEG_MASK) >> SEG_SHIFT
Offset = VirtualAddress & OFFSET_MASK
if( Offset >= Bounds[Segment] )
RaiseException(PROTECTION_FAULT)
else
PhysAddr = Base[Segment] + Offset
Register = AccessMemory(PhysAddr)
当然也有隐式的方法确定使用的哪个段,如果地址由PC (instruct fetch)产生,则属于代码段。如果基于栈或者基址指针,则是栈。其他则在堆。
4.3 增长方向 与 Protection Bit
Segment | Base | Size | Grows Positive | Protection |
---|---|---|---|---|
Code | 32K | 2K | 1 | Read-Execute |
Heap | 34k | 2K | 1 | Read-Write |
Stack | 28 | 2K | 0 | Read-Write |
带有保护位的段寄存器值
Protection bit的涉及可以支持代买段的共享。即同样的代码可以被多个进程共享,而不用担心破坏隔离。
4.4 分段带来的问题 - 内存碎片化
分段策略仍然需要关注的为题
- 上下文切换
-
如何管理空闲内存
分段更会导致内存的外部碎片化,因为上述描述的分段内存的管理方式,base不变size在边会导致内存被砍成个各种奇怪的尺寸,所以满足内存分配可能会变得有点困难。
3、不够灵活,不是很方便的支持sparse address.
例如:若有一个很大但是是sparse address 的堆,在一个逻辑段中,整个堆也要完整的加载到内存中,那么base/bounds的这种分段就不能很灵活的支持这种情况,还是会造成内部碎片化?
不难猜出,Fragment 优化到 Compacted 那是相当耗时的。
所以针对空闲内存,是有专门的管理策略的。但内存的碎片化是不可避免的。
5. Free - Space Management 空闲内存的管理
在没有讨论page之前,我们可以假设内存被分割成了相同大小的固定单元。
相同大小的单元管理起来相对简单,显然不同大小的空闲单元管理起来却很复杂。尤其是OS采用Segmentation的方式申请和释放内存。非常容易造成内存的外部碎片化。
5.1 Assumption
- 基础的接口的使用 malloc( ) 和 free( )
- 空闲的空间被链式管理:free-list
- 在这里只关心内存的外部碎片化
- 内存一旦被申请,只被申请的内存独占
- heap空间大小固定(可以通过sbrk 系统调用修改,再次强调malloc 和 free 不是系统调用函数,而是clib的函数)
5.2 底层机制
5.2.1 Splitting and Coalescing 分割与合并
内存可以被分割成一块一块的,用链表管理起来
当然如果OS发现每一块free-space都相同,就会合并起来。
5.2.2 追踪分配空间的大小
free( ) 函数是没有内存大小的。说明malloc/free 这对函数可以通过某种方式来快速的申请内存,并返回内存给free-list.
-
原因:
most allocators store a little bit of extra information in a header block which is kept in memory, usually just before the handed-out chunck of memory.
typedef struct __header_t {
int size;
int magic;
} header_t;
p = malloc(20);
void free(void * ptr) {
header_t *hptr = (void *)ptr - sizeof(header_t);
assert(hptr->magic == 1234567)
...
}
上述代码中,得到了header的指针后,需要进行magic nunber的检验(这里没太明白,有待学习)。
当然释放空间的大小必须带上sizeof(header).
5.2.3 Free list
Free List 数据结构简单说明:
typedef struct __node_t {
int size;
struct __node_t * next;
} node_t;
//we are assuming that the heap is builtt within some free space acquired via a call to the system call mmap();
node_t * head = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0);
head->size = 4096 - sizeof(node_t);
head->next = NULL;
此时有人申请100个字节的heap, 那么列表有4088个字节空闲,(除去8个字节的头),所以被申请后如图所示:
同理 有三块空间已经被申请:
各种释放后 Free-List 的指向将相当复杂,如图:
如果没有整合(Coalesced)极易碎片化
5.2.4 如何让Heap(堆) 增长
利用系统调用 sbrk 来增长heap. OS 会寻找空闲的物理页,并且将空闲的物理内存映射到发去请求的进程的地址空间去,返回新的the end of new heap.
5.3 基础空闲内存管理策略
- Buddy Allocation
Buddy 算法是直面物理内存的 - 1页空闲的放在一张链表上
- 2页空闲的放在一张链表上
- 4页空闲的放在一张链表上
- 8页空闲的放在一张链表上
...
比如刚刚开机的适合,内存中的normal zone 有16页内存,2^4次方,那么着16页在一张链表上,如果此时有人申请一页,剩余15页
分成了 8页 4页 2 页 1页 的4个链表
不停的进行拆分合并 。
全世界任何一个正整数 都是可以分成2的n次方的。
cat /proc/buddyinfo 看到具体数据 1页空闲有几个 2页空闲的有几个。。。
- 但不可避免的是 不停的申请和释放的适合,会出现内存会很大,连续内存却很小,也会导致内存碎片化
- 问题谁会申请连续物理内存呢?
后续深入内存的章节讨论。(并非Application层!)
- 问题谁会申请连续物理内存呢?
kj