分类: LINUX
2013-04-08 08:57:23
原文地址:MIPS TLB 的结构 作者:ramen.sh
MIPS TLB 的结构
实例分析: MIPS R3000上的一个简单的虚存和分页系统.
Case Study: A simple VM and paging system for the MIPS R3000.
体系结构. 下面是将要分析的实例机器的内存层次结构:
* 一级cache. 64KB, 直接映像. Write allocate, write through. Physically addressed.
* 写缓存. 4个表项.
* 二级cache. 256KB, 直接映像. Write back.
* 物理内存. 64MB. 页大小为4KB.
* 交换设备. 256MB交换空间.
Start with architecture. Here is the memory hierarchy of the machine at hand:
* First Level Cache. 64K bytes, direct mapped. Write allocate, write through. Physically addressed.
* Write Buffer. 4 Entries.
* Second Level Cache. 256K bytes, direct mapped. Write back.
* Physical Memory. 64M bytes. 4K page frames.
* Backing Store Disk. 256M bytes of swap space.
地址格式. 两种模式: 用户模式和内核模式. 最高20位是虚拟地址页号(VPN), 低12位是页内偏移. 处理器内还有6位的当前进程id, 它也是38位虚拟地址的一部分. 所有用户模式的地址最高位都是0. (那么用户空间可以有多大?每个页面有多大?)
Address format. Are two modes: user mode and kernel mode. Top 20 bits are Virtual Page Number, bottom 12 bits are page offset. Machine has a 6 bit current process id; process id is part of 38 bit virtual address. All user mode addresses have a top address bit of 0. How big is potential user address space? How big are pages?
内核模式地址.
* 如果地址以0开头, 使用与当前用户进程相同的映射. 因此用户进程的地址空间是OS地址空间的子集. 叫做kuseg.
* 如果地址以100开头, 将被映射到物理内存的最低512MB, 不通过TLB映射.(cached and unmapped). 叫做kseg0. 被用作存储内核的指令和数据.
* 如果地址以101开头, 也将被映射到物理内存的最低512MB, 但没有cache (uncached, unmapped). 叫做kseg1. 被用作磁盘缓存, IO寄存器和ROM的映射.
* 如果地址以11开头, 即被TLB映射也被缓存. 不同的进程将有不同的映射. 叫做kseg2. 用来存储内核中对每个用户进程都不同的数据结构, 如用户页表等.
Kernel mode addresses.
If address starts with 0, is mapped just like current user process. So, user process address space is subset of OS address space. Called kuseg.
If address starts with 100, translates to bottom 512 Mbytes of physical memory and does not go through TLB. (cached and unmapped). Called kseg0. Used for kernel instructions and data.
If starts with 101, translates to bottom 512 Mbytes of physical memory. Is not cached (uncached, unmapped). Called kseg1. Used for disk buffers, I/O registers, ROM code.
If starts with 11, is mapped and cacheable. Maps differently for each process. Called kseg2. Used for kernel data structures in which there is one per address space - user page tables, etc.
映射过程的说明: 必须把31位的虚拟地址和6位的进程id映射到32位的物理地址.(注:实际应该是32位虚拟地址,只不过对于用户空间最高位总是0). 首先, 把虚拟地址最高的19位和6位进程id映射到一个物理页面, 然后用虚拟地址的低12位作为在物理页面中的偏移, 最后得到要访问的实际物理地址.
Specification of mapping process: must map 31 bit virtual address (top bit is always 0) plus 6 bit process id to 32 bit physical address. Do mapping by first mapping upper 19 bits of virtual address plus 6 bit process id to a physical page frame, then using lower 12 bits of virtual address as offset within the physical page frame.
如何映射虚拟地址的最高19位? 使用一个线性页表,保存在kseg2中. 这个页表会有多大?
注意我们对kseg2也会使用一个线性页表进行分页, 这个页表保存在哪? 答案是保存在 kseg0中, 而且每个进程都会有这样一个页表. 如果保存在kseg2中的用户进程页表占据了大部分已使用的地址空间, 保存在kseg0中的kseg2页表会有多大?
How do we map upper 19 bits of virtual address? Use a linear page table stored in kseg2. How big can this page table be?
Note that we will also be paging kseg2 using a linear page table. Where do we hold the page table for kseg2? There is one for each process, and it is stored in kseg0. If the page table for the user process stored in kseg2 takes up most of the used address space, how big can the page table for kseg2 stored in kseg0 be?
实际上, 我们可以用2级页表. 对于kuseg的一个32位虚拟地址, 通过如下方法得到物理地址:
* 取虚拟地址的最高9位. 用它作为进程保存在kseg0中的kseg2页表的索引. 由于 kseg0的空间没有经过任何的映射, 因此对于这里的访问总是没有问题的. 这样我们就得到保存着kseg2页表的物理页, 如果这个物理页不在内存中, 就要把它从磁盘读进来.
* 再取虚拟地址的中间10位. 用它作为保存在kseg2中的页表的索引, 可以得到一个对应到kuseg中的物理页面. 如果物理页不在内存中, 读入它. 注意确定当前的进程id是正确的, 因为kseg2对于不同的进程的映射是不同的.
* 取虚拟地址的最低12位. 作为刚才找到的物理物理页面的偏移, 得到这个虚拟地址对应的物理地址.
In effect, we have a two level approach. Given a 32 bit virtual address from kuseg, we get the physical address as follows:
Extract top 9 bits of address. Use this as an index into that process's kseg2 page table stored in kseg0. The memory in kseg0 is always there and the reference goes unmapped, so there will be no problem will this lookup. We get the physical page frame that holds the relevant part of the page table in kseg2. If the page is not resident, read it back in from disk.
Extract middle 10 bits of address. This is the amount you need to index one page of 4 byte physical addresses. Use the middle 10 bits to index the page table in kseg2. This lookup yields a physical page frame in kuseg. If the page is not resident, read it back in from disk. Must make sure that we are accessing the correct memory for the current process id since kseg2 maps differently for each process id.
Extract lower 12 bits of address. Use this as an offset into the physical page frame holding the page from kuseg. Read the memory location.
为什么要分为两步才完成地址翻译? 为了能同时对虚拟地址空间和它对应的页表都进行分页. 虚拟地址空间的页表保存在内核地址空间中对于每个进程都有不同映射的部分, 而页表的页表保存在内核空间中没有映射,但是cached的部分.
Why divide the lookup into two stages? So we can page the virtual address space AND page the page table. The page table for virtual address space is stored in the part of kernel address space that is mapped differently for different processes. The page table page table is stored in unmapped but cached kernel memory.
这样看起来效率太差了 -- 一次用户空间的访问需要3次物理内存的访问. 那就用TLB来加速它! 64个表项的全相联TLB. 每个表项将一个虚拟页影射到一个物理页.
Seems inefficient - 3 memory accesses for one user memory access. So, speed it up with a TLB. 64 entry fully associative TLB. Each entry maps one virtual address to one physical page frame.
TLB表项的格式. 每个TLB表项长度为64位.
* 最高20位: VPN.
* 接下来6位: PID.
* 接下来6位: 未使用.
* 接下来20位: 物理页面地址
* 下一位: N bit. =1 该地址使用cache. =0 该地址不使用cache * 下一位: D bit. =1 该地址可写, =0 不可写.
* 下一位: V bit. =1 表示valid.
* 下一位: G bit. =1 TLB翻译地址时不检查PID.
TLB entry format. Each TLB entry is 64 bits long.
Top 20 bits: VPN.
Next 6 bits: PID.
Next 6 bits: unused.
Next 20 bits: Physical page frame.
Next bit: N bit. If set, memory access bypasses the cache. If not set, memory access goes through the cache.
Next bit: D bit. If set, memory is writeable. If not set, memory is not writeable.
Next bit: V bit. If set, entry is valid.
Next bit: G bit. If set, TLB does not check PID for translation.
如何查询? 基本思想: 匹配TLB表项的前半部分, 使用后半部分. 会产生3种不同的TLB miss的异常, 每一种都有各自的异常处理程序.
* UTLB miss - 访问kuseg时, TLB中没有对应的映射.
* TLB miss - 访问kseg0, kseg1, kseg2时, TLB中没有对应的映射. 或者虽然有映射, 但是Valid bit V=0.
* TLB mod - 写操作时, 有映射, 但是Dirty bit =0.
How does lookup work? Basic idea: match on upper half of TLB entry, use lower half of TLB entry. Can generate three different kinds of TLB misses, each with its own exception handler.
UTLB miss - generated when the access is to kuseg and there is no matching mapping loaded into the TLB.
TLB miss - generated when the access is to kseg0, kseg1, or kseg2 and there is no mapping loaded into TLB. Also generated when the mapping is loaded into TLB, but valid bit is not set.
TLB mod - generated when the mapping is loaded, but access is a write and the D bit is not set.
TLB查询算法:
* 在用户模式下访问的地址最高位为1, 产生地址错误异常,
* 匹配VPN. 如果没有, 地址最高位为1则产生TLB miss, 最高位为0则产生UTLB miss,
* 匹配PID或者Global bit=1. 如果错误则产生TLB miss (最高位为1) 或者 UTLB miss (最高位为0),
* 检查Valid bit=1. 如果不是, 产生TLB miss,
* 如果D=0, 操作却是写, 产生TLB mod,
* 如果N=1, 访问内存, 否则访问cache.
Here is the TLB lookup algorithm:
If MSB is 1 and in user mode, generate an address error exception.
Is there a VPN match? If no, generate a TLB miss exception if MSB is 1, otherwise generate a UTLB miss.
Does the PID match or is the global bit set? If no, generate a TLB miss (if MSB is 1) or UTLB miss (if MSB is 0).
Is valid bit set? If no, generate a TLB miss.
If D bit is not set and the access is a write, generate a TLB mod exception.
If N bit is set, access memory, otherwise access cache (which may refer access to memory).
PID的存在使得多个进程可以共享TLB.(注:不用在进程切换的时候flush TLB). 没有PID会怎么样? PID只有6位, 如过有超过64个的用户进程怎么办?
The PID field allows multiple processes to share the TLB. What if there was no PID field? The PID field is only 6 bits long. What if create more than 64 user processes?
TLB的操作. 处理器必须能够装入新的表项到TLB中. 基本机制: 两个32位的TLB寄存器: TLB EntryHi, TLB EntryLow. 这和64位的TLB表项正好吻合. EntryHi保存有PID, 属于虚拟地址的一部分. 另外还有Index寄存器: 它有6位可以由软件设置, 和Random寄存器: 一个6位的每个时钟周期都很递减的寄存器, 并且不会小于8.
通过程序控制可以装载数据进入TLB寄存器, 然后再把这些寄存器的内容存入TLB的某一个表项, index由Index寄存器或者Random寄存器的值决定.
Manipulating TLB entries. Processor must be able to load new entries into TLB. Basic Mechanism: Two 32 bit TLB registers: TLB EntryHi, TLB EntryLo. Bits are the same as for 64 bit TLB entry. EntryHi register holds current PID that is part of all virtual addresses. Also have an Index register: 6 bits that can be set by software, and a Random register: 6 bit register decremented every clock cycle. Constrained not to point to first 8 entries.
Can load into TLB entry registers under program control, then store contents of Entry registers either to TLB entry to which index register points, or to which random register points.
TLB 指令:
* mtc0 - 把通用寄存器的值装入TLB寄存器.
* mfc0 - 从TLB寄存器读取数据到通用寄存器.
* tlbp - 在TLB的表项中查找是否有与EntryHi寄存器匹配的, 如果有则把匹配项的index保存到Index寄存器中, 没有匹配则置Index的最高位为1.
* tlbr - 把Index指向的TLB表项装入EntryHi和EntryLo.
* tlbwi - 把EntryHi和EntryLo的值写入Index指向的TLB表项中.
* tlbwr - 把EntryHi和EntryLo的值写入Random指向的TLB表项中.
TLB instructions:
mtc0 - loads one of TLB registers with contents of a general register.
mfc0 - reads one of TLB registers into a general register.
tlbp - probes the TLB to see if an entry matches EntryHi. If so, loads index register with index of TLB entry that matched. If no match, sets upper bit of index register.
tlbr - loads EntryHi and EntryLow with contents of TLB entry that index register points to.
tlbwi - writes TLB entry that index points to with contents of EntryHi and EntryLo registers.
tlbwr - writes TLB entry that random register points to with contents of EntryHi and EntryLo registers.
当TLB miss或UTLB miss时会怎么样? OS必须重新装入TLB, 然后重新开始出错的进程(注:从出错的指令开始). TLB miss和UTLB miss分别跳转到不同的异常处理程序.
异常发生时的状态:
* EPC寄存器: 指向发生异常的指令, 如果该指令在分支延迟槽中(branch delay slot), 则指向分支指令. 基本思想: 当恢复了异常中的错误, 将返回用户模式, 并跳转到EPC指向的地址.
* Cause寄存器: 指名异常的原因, 并保存中断状态.
* Status寄存器: 包含机器状态信息. 重要的位: Kernel/User mode bit, Interrupt Enable bit. 硬件(注:原文为OS,似乎有误)为这些位维护了一个深度为3的栈, 发生异常时自动移位. 因此异常可以嵌套2层而不需要保存这些位.
* BadVaddr寄存器: 保存发生访问错误的虚拟地址.
* Context寄存器: 高11位由程序控制. 接下来的19位被(处理器自动)设为发生异常的虚拟地址的VPN(忽略最高位). 最后两位永远是0.
What happens when there is a UTLB or TLB miss? OS must reload TLB and restart the faulting process. Note - the UTLB and TLB miss exceptions branch to different handlers.
Machine state for exceptions:
EPC register: points to instruction that caused fault, unless faulting instruction was in branch delay slot. If so, points to branch before branch delay slot instruction. Basic idea: when fix up exception and return to user code, will branch to EPC.
Cause register. Tells what caused exception, and maintains some state about interrupts.
Status register. Contains information about status of machine. Important bits: Kernel/User mode bit, Interrupt Enable bit. OS maintains a 3 deep stack of these bits, shifting them over on an exception. So, can take two exceptions without having to extract and store the bits.
BadVaddr register - stores virtual address that caused last exception.
Context register. Upper 11 bits - set under program control. Next 19 bits - set to VPN of address that caused exception (omits top bit). Last 2 bits - always 0.
发生UTLB miss时处理器做了哪些工作?
* 设置EPC
* 设置Cause
* 设置Status, 把K/U和IE移一位, 并把current K/U和IE置0. 即进入内核模式, 中断关闭.
* 设置BadVaddr, 保存产生异常的虚拟地址.
* 设置Context, 高11位不变, 接下来19位设成发生错误的虚拟地址的VPN(忽略最高位).
* 设置TLB EntryHi, 使它包含出错地址的VPN.
UTLB miss时,OS又做了哪些工作?
* 保存EPC到k1 (软件惯例, k1,k2保留为OS处理异常时使用).
* 读取Context到k0.
* 读取k0所指向的内存中的内容到k0. (k0指向地址空间的哪一部分?)
* 把k0的内容装入到EntryLo.
* 把TLB寄存器中的内容保存到TLB表中(位置由Index或Random决定).
* jr k1; rfe; rfe在分支延迟槽中, 负责弹出Status的栈中的状态.
What does machine do on a UTLB miss?
Sets EPC register.
Sets Cause register.
Sets Status register. Shifts K/U and IE bits over one, and clears current Kernel/User and Interrupt Enable bits. So - processor is in kernel mode with interrupts turned off.
Sets BadVaddr register - stores virtual address that caused exception.
Sets Context register. Upper 11 bits - left alone. Next 19 bits - set to VPN of address that caused exception (omits top bit).
Sets TLB EntryHi register to contain VPN of faulting address.
What does OS do in UTLB handler?
Store EPC register to kt1 register (software convention, OS has two registers reserved for its use).
Load context register into kt0 register.
Load contents of memory address that kt0 points to into kt0. Into what part of address space does kt0 point?
Load kt0 into entry low TLB register.
Load TLB entry registers into TLB entry that random register points to.
JR kt1; rfe instruction in branch delay slot. rfe instruction pops bits in Status Register.
这些到底是在做什么? OS对每个进程都有一个页表, 位于Context寄存器的高11位所指向的地址. 每一个表项是一个TLB表项的低32位. 所以OS只要读取这个TLB表项, 在把它保存到TLB表中, 然后重新开始出错的程序.
Context寄存器的最高2位是什么? 是11, 这就是内核地址空间中对每个进程都有不同影射的区域. 接下来的9位是在mapped, process-specific kernel space中的页表的基地址. 因此每个进程都有自己的页表.
What is going on? OS uses a linear page table for each process, starting at address stored in upper 11 bits of context register. Each page table entry is the 32 lower bits of a TLB entry. So, OS just fetched the TLB entry and stored it into a random location in TLB, then started up the program again.
What are upper two bits of context register? 11 - so, this is kernel memory that is mapped separately for each process. Next 9 bits are base of page table in mapped, process-specific kernel space. So, each process has its own page table.
出错的情况:
* 如果页面不在内存中, OS将使页表项的V=0, 程序将再次运行而产生TLB miss. (注意不是UTLB miss).
* 如果地址超出范围, OS stores nothing above the page table in address space, so will get a TLB miss (NOT a UTLB miss). This generates a double fault that OS will handle in general exception handler. (不明白:-()
* 如果页表没有影射或不在内存中, 将再次产生双重异常.
* 如果出错的指令在延迟槽中, EPC指向分支指令, 因此将再次执行分支指令. 这样不会有任何问题 - 在R3000中, 分支指令可以被重新执行而没有其他影响.
* 如果在内核模式下发生了UTLB miss, 异常处理程序如何知道返回到什么状态? 状态寄存器由处理器自动保存在Status中的栈里, 在发生异常和执行rfe指令时自动更新.
Error cases:
What if page is not in memory? Then OS will store a zero in the valid bit of page table entry. Program will reexecute faulting instruction, generating a TLB miss exception. (NOT a UTLB miss exception).
What if address is out of bounds? OS stores nothing above the page table in address space, so will get a TLB miss (NOT a UTLB miss). This generates a double fault that OS will handle in general exception handler.
What if page table page is not mapped or it is not in memory? Another double fault.
What if faulting instruction was in a branch delay slot? EPC points to branch, so will reexecute branch instruction. No problem - in R3000, all branch instructions are reexecutable with same effect.
What if are inside kernel when take a UTLB miss? How does miss handler know which state to return to? Is stored automatically in Status register, and manipulated by exceptions and rfe instruction.
R3000的设计保证这种UTLB的重新装入是高效的.
* 内核地址空间的一部分对于不同的进程有着不同的映射, 因此每个进程都可以把页表保存在那里.
* TLB entry format laid out so that it matches possible page table entries.
* 所有的分支指令都是可再执行的 - 他们不依赖及其状态.
* UTLB miss异常入口与普通异常入口不同, 可以更加高效, 不用判断异常原因.
* 自动设置Context寄存器.
* 支持3级的K/U和IE状态保存, 允许嵌套2层中断而不需要保存状态. Supports double fault mechanism for fast handling of uncommon cases.
R3000 carefully designed to support this efficient UTLB reload mechanism.
Some kernel addresses are mapped differently for different processes. So, can store per-process page tables there.
TLB entry format laid out so that it matches possible page table entries.
All branches are restartable - they do not depend on machine state.
UTLB miss handler is in a different location than normal exception handler - supports fast code. Don't have to decode cause of exception.
Sets context register appropriately.
Supports three levels of Kernel/User mode and interrupt enable/disable bits, so can take two faults in a row without needing to save state. Supports double fault mechanism for fast handling of uncommon cases.
What must OS do when it switches contexts? Must set EntryHi of TLB to contain current process id. Must also load top 11 bits of context register with page table base.
发生TLB miss时处理器做了哪些工作(与UTLB对比)?
* 设置Cause, 可以是TLB mod或TLB miss. TLB mod发生在对不可写地址的写操作 (D=0).
* 设置BadVaddr.
* 设置EPC.
* Status寄存器中的状态移位.
* 设置Context.
* 设置TLB EntryHi.
* 跳转到普通异常处理程序(与UTLB miss的处理不同).
What happens on a TLB miss (as opposed to a UTLB miss).
Sets cause register - can be TLB mod miss, or TLB miss. TLB mod miss is when TLB entry matches but operation was a store and D bit was not set.
Sets BadVaddr register.
Sets EPC.
Shifts bits in Status register.
Sets context register.
Sets TLB EntryHi register.
Branches to general exception handler (different from UTLB miss handler).
TLB miss时,OS做了哪些工作?
* 决定错误原因.
* 如果是TLB mod miss, 检查进程是否有对该页的写权限. 通常是查看页表项中未使用的某一位. 如果可以写, 则在OS的数据结构中把此物理页面标记为Dirty, 设置TLB表项中的D=1, 重新开始继续运行进程. 注意: 不能使用Random寄存器写回TLB表项, 因为表中已经有匹配当前VPN和PID的项. 必须使用tlbp查询其index, 然后用tlbwi写回新的表项.
* 如果TLB miss是由UTLB miss的处理程序产生, (可以从EPC指向UTLB处理程序内部看出来), 先检查请求的地址是否有效, (通过检查这个地址是否在这个进程的虚拟地址空间内), 然后检查页表是否在内存中. If so, construct mapping for page table page and put it into TLB. Use this mapping to get TLB entry for page. Insert this entry into TLB and return to user program.
* 如果页表页不在内存中(这是对于在进程独立的内核空间中的普通内核TLB miss), 把它从磁盘读入. 然后建立页表项, 设置V=1, 让PFN指向读入的物理页. 设置D=0, 映射页表页到TLB, 然后跟上面一样处理.
* 如果TLB miss是由于内核访问了没有映射的地址, 找到内核地址空间的页表项, 将它装进TLB, (使用Rondom), 然后返回.
* 如果TLB miss是由于对用户空间的无效页面的访问, 从磁盘读入这个页面, 在页表项中设置V=1和PFN. 同时, 设置D=0以确保写操作会产生异常. OS将通过这个异常把物理页面(PFN)设为D=1.
What OS does on TLB miss:
First determine what caused miss.
If TLB mod miss, check to see if process has right to write page. Usually stored in page table entry in one of unused bits. If has can write it, mark physical page as dirty in OS data structures, set D bit in TLB entry and restart process. Note: can't use random register to write TLB entry back in. There is already a TLB entry with the matching VPN and PID. Must use tlbp to load index register with the index of the matching entry, then store new page table entry into Entry Low register, then tlbwi to store new TLB entry back into the TLB.
If TLB caused by double miss from UTLB miss handler, (find this out by seeing of EPC points inside UTLB miss handler), first determine if given address is valid. Determine this by checking if it is within range of process virtual address space. Then determine if page table page is resident. If so, construct mapping for page table page and put it into TLB. Use this mapping to get TLB entry for page. Insert this entry into TLB and return to user program.
If page table page not resident (this is for a normal kernel TLB miss in per-process kernel space), read it in from disk. When page arrives, set up page table entry. Set valid bit, make PFN point to page frame where it was read in. Clear D bit. Map page table page into TLB and proceed as above.
If TLB caused by kernel mode reference not mapped, find the PTE for the kernel address and put it into TLB, using entry hi and lo registers and random register. Return to code that caused miss.
If miss caused by reference to invalid page in user address space, read page in from disk, set valid bit and PFN in page table. Also, be sure to clear D bit so that any write reference will cause a trap. The OS will use this trap to mark the PFN dirty.
其他情况:
* OS可以完全运行在没有映射的地址空间中. 缺点: 无法对OS数据结构进行分页.
* OS可以运行在一个与用户空间独立的低值空间. 缺点: 无法简单的访问用户空间.
* Cache可以有虚拟地址. 缺点: 在同一个进程中无法把同一段内存映射到不同地址. 这样要如何实现Unix的mmap机制? 它需要不同的虚拟地址映射到相同的物理地址! 另外, 如果cache没有PID, 必须在每次的进程切换时flush cache! 开销太大!
* TLB有可能没有PID. 缺点: 每次进程切换必须flush TLB!
* TLB重装入可以是硬件自动完成的. 每一个页表项为32位(即前面TLB表项的低32位), 硬件自动重装入TLB. 进程切换必须读入页表的基地址和大小. 缺点: 这样OS就只能使用一种特定的页表结构了.
Alternatives:
The OS may run completely unmapped. Problem: can't page OS data structures.
The OS may have a separate address space from user. Problem: can't as easily access user space.
Cache may be virtually addressed. Problem: can't map same memory in different addresses in same process. How do implement Unix mmap facility, which demands that different virtual addresses map to same physical address? Also, if cache does not have PID field, must flush cache on context switch (!).
TLB may not have PID field. Problem: must flush TLB on context switch.
TLB reload may be done automatically in hardware. Each page table entry is 32 bits (lower 32 bits of TLB entry above), and hardware will automatically reload TLB. Context switch must load page table base and bounds registers. Problem: locks OS into using a specific data structure for page table.
Permission is granted to copy and distribute this material for educational purposes only, provided that the following credit line is included: "Operating Systems Lecture Notes, Copyright 1997 Martin C. Rinard." Permission is granted to alter and distribute this material provided that the following credit line is included: "Adapted from Operating Systems Lecture Notes, Copyright 1997 Martin C. Rinard."
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------