OS_lab2课下实验随笔
LAB2-1
我们的操作系统结构:
/*
o 4G -----------> +----------------------------+------------0x100000000
o | ... | kseg3
o +----------------------------+------------0xe000 0000
o | ... | kseg2
o +----------------------------+------------0xc000 0000
o | Interrupts & Exception | kseg1
o +----------------------------+------------0xa000 0000
o | Invalid memory | /|\
o +----------------------------+----|-------Physics Memory Max
o | ... | kseg0
o VPT,KSTACKTOP-----> +----------------------------+----|-------0x8040 0000-------end
o | Kernel Stack | | KSTKSIZE /|\
o +----------------------------+----|------ |
o | Kernel Text | | PDMAP
o KERNBASE -----> +----------------------------+----|-------0x8001 0000 |
o | Interrupts & Exception | \|/ \|/
o ULIM -----> +----------------------------+------------0x8000 0000-------
o | User VPT | PDMAP /|\
o UVPT -----> +----------------------------+------------0x7fc0 0000 |
o | PAGES | PDMAP |
o UPAGES -----> +----------------------------+------------0x7f80 0000 |
o | ENVS | PDMAP |
o UTOP,UENVS -----> +----------------------------+------------0x7f40 0000 |
o UXSTACKTOP -/ | user exception stack | BY2PG |
o +----------------------------+------------0x7f3f f000 |
o | Invalid memory | BY2PG |
o USTACKTOP ----> +----------------------------+------------0x7f3f e000 |
o | normal user stack | BY2PG |
o +----------------------------+------------0x7f3f d000 |
a | | |
a ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
a . . |
a . . kuseg
a . . |
a |~~~~~~~~~~~~~~~~~~~~~~~~~~~~| |
a | | |
o UTEXT -----> +----------------------------+ |
o | | 2 * PDMAP \|/
a 0 ------------> +----------------------------+ -----------------------------
o
*/
MOS 中 维护 npage
个内存控制块,也就是 Page
结构体。每一个内存控制块对应一页的物理内存,注意并不是这个结构体就是对应的物理内存,而是用这个结构体来管理这块内存的分配。
//获取Page pp的页号
page2ppn(struct Page *pp)
{
return pp - pages;
}
//获取Page pp的物理实地址
static inline u_long
page2pa(struct Page *pp)
{
return page2ppn(pp) << PGSHIFT;
}
//获取物理页地址为pa的Page pp地址
static inline struct Page *
pa2page(u_long pa)
{
if (PPN(pa) >= npage) {
panic("pa2page called with invalid pa: %x", pa);
}
return &pages[PPN(pa)];
}
//获取Page pp的虚地址
static inline u_long
page2kva(struct Page *pp)
{
return KADDR(page2pa(pp));
}
// page number field of address
#define PPN(va) (((u_long)(va))>>12)
#define VPN(va) PPN(va)
// translates from kernel virtual address to physical address.
#define PADDR(kva) \
({ \
u_long a = (u_long) (kva); \
if (a < ULIM) \
panic("PADDR called with invalid kva %08lx", a);\
a - ULIM; \
})
// translates from physical address to kernel virtual address.
#define KADDR(pa) \
({ \
u_long ppn = PPN(pa); \
if (ppn >= npage) \
panic("KADDR called with invalid pa %08lx", (u_long)pa);\
(pa) + ULIM; \
})
链表宏的定义位于 include/queue.h
,其实现的是 双向链表,下面将对一些主要的宏进行解释:
LIST_HEAD(name, type)
,创建一个名称为name
链表的头部结构体, 包含一个指向type
类型结构体的指针,这个指针可以指向链表的首个元素。
#define LIST_HEAD(name, type) \
struct name { \
struct type *lh_first; /* first element */ \
}
LIST_ENTRY(type)
,作为一个特殊的「类型」出现,它的本质是一个链表项,包括指向下一个元素的指针le_next
,以及指向前一个元素链表项le_next
的指针le_prev
。le_prev
是一个指针的指针,它的作用是当删除一个元素时,更改前一个元素链表项的le_next
。
#define LIST_ENTRY(type) \
struct { \
struct type *le_next; /* next element */ \
struct type **le_prev; /* address of previous next element */ \
}
LIST_EMPTY(head)
,判断头部结构体head
对应的链表是否为空。LIST_FIRST(head)
,将返回头部结构体head
对应的链表的首个元素。LIST_INIT(head)
是将头部结构体head
对应的链表初始化,等价于将首个元素清空。LIST_NEXT(elm, field)
,结构体elm
包含的名为field
的数据,类型是一个链表项LIST_ENTRY(type)
, 返回其指向的下一个元素。下面出现的field
含义均和此相同。LIST_INSERT_AFTER(listelm, elm, field)
,将elm
插到已有元素listelm
之后。
#define LIST_INSERT_AFTER(listelm, elm, field) do { \
LIST_NEXT((elm),field) = LIST_NEXT((listelm),field); \
if (LIST_NEXT((listelm),field) != NULL) { \
LIST_NEXT((listelm),field)->field.le_prev = &LIST_NEXT((elm),field); \
} \
LIST_NEXT((listelm),field) = (elm); \
(elm)->field.le_prev = &LIST_NEXT((listelm),field); \
} while (0)
LIST_INSERT_BEFORE(listelm, elm, field)
,将elm
插到已有元素listelm
之前。
#define LIST_INSERT_BEFORE(listelm, elm, field) do { \
(elm)->field.le_prev = (listelm)->field.le_prev; \
LIST_NEXT((elm), field) = (listelm); \
*(listelm)->field.le_prev = (elm); \
(listelm)->field.le_prev = &LIST_NEXT((elm), field); \
} while (0)
LIST_INSERT_AFTER和LIST_INSERT_BEFORE操作分别如蓝线和红线所示:
//修改字体颜色
$\textcolor{red}{这里输入你要改变颜色的文字} $
<font color = "blue">这里输入你要改变颜色的文字</font>
LIST_INSERT_HEAD(head, elm, field)
,将elm
插到头部结构体head
对应链表的头部。LIST_INSERT_TAIL(head, elm, field)
,将elm
插到头部结构体head
对应链表的尾部。- 注意需要判断list是否为空( 蓝线第二步)
#define LIST_INSERT_TAIL(head, elm, field) do { \
if (LIST_FIRST((head)) == NULL) { \
LIST_INSERT_HEAD((head),(elm),field); \
}else { \
LIST_NEXT((elm),field) = LIST_FIRST((head)); \
/* should save the last listelm */ \
while(LIST_NEXT(LIST_NEXT((elm),field),field) != NULL) { \
LIST_NEXT((elm), field) = LIST_NEXT(LIST_NEXT((elm),field),field); \
} /* now LIST_NEXT((elm),head) iss the lastest listelm */ \
LIST_NEXT(LIST_NEXT((elm), field), field) = (elm); \
(elm)->field.le_prev = &LIST_NEXT(LIST_NEXT((elm),field),field); \
LIST_NEXT((elm),field) = NULL; \
/* aslo can use LIST_INSERT_AFTER((LIST_NEXT((elm), field)),elm,field); */ \
} \
} while (0)
LIST_REMOVE(elm, field)
,将elm
从对应链表中删除。
Page_init:(free_pahe_list页面的初始化)
void page_init(void)
{
/* Step 1: Initialize page_free_list. */
/* Hint: Use macro `LIST_INIT` defined in include/queue.h. */
LIST_INIT(&page_free_list);
/* Step 2: Align `freemem` up to multiple of BY2PG. */
freemem = ROUND(freemem,BY2PG);
struct Page *used,*noused;
/freemem以下部分包括内核代码和Page数组:pages[16k]
/* Step 3: Mark all memory blow `freemem` as used(set `pp_ref` filed to 1) */
for (used = pages;page2kva(used) < freemem;used++) {
used -> pp_ref = 1;
}
/* Step 4: Mark the other memory as free. */
for (noused = pa2page(PADDR(freemem));page2ppn(noused) < npage;noused++) {
//pa2page need pa,so freemem -> PADDR(freemem)
noused -> pp_ref = 0;
LIST_INSERT_HEAD(&page_free_list,noused,pp_link);
}
}
- 首先利用链表相关宏初始化
page_free_list
- 接着将
mips_vm_init()
中用到的空间对应的物理页面的内存控制块的引用次数全部标为1
,使用函数page2kva(used)获取页used对应的虚拟地址 - 将其它页面对应的结构体中的 pp_ref 标为 0,同时使用链表宏 LIST_INSERT_HEAD 将其插入空闲链表。注意细节:
noused = pa2page(PADDR(freemem))
1.PADDR:将虚拟地址freemem映射为实际地址
2.pa2page(pa) = &pages[PPN(pa)]
PPN获取页pa对应的页号(即freemem << 12)这里因为freemem已经页对齐了,所以pa2page(PADDR(freemem))一定可以获取到第一个没被用的页(freemem向下空闲空间形成碎片)
&pages[i]获取第i个Page的地址,因为这里的noused是指针类型
page2ppn(noused) < npage即初始化剩余被未初始化页面,
page2ppn(&pages[i]-pages),即获取i
#include <stdio.h>
int main(int argc, char *argv[]) {
int a[10] = {1,100};
int *b = &a[1];
printf("%d\n",b-a);
}
//output : 1
这是编译器的工作
page_alloc(申请一页空间)
int page_alloc(struct Page **pp)
{
/* Step 1: Get a page from free memory. If fail, return the error code.*/
struct Page *ppage_temp;
if (LIST_EMPTY(&page_free_list)) return -E_NO_MEM;
ppage_temp = LIST_FIRST(&page_free_list);
//REMOVE(elm,field) ,look up struct Page,has a son struct named pp_link
LIST_REMOVE(ppage_temp,pp_link);
/* Step 2: Initialize this page.
* Hint: use `bzero`. */
bzero(page2kva(ppage_temp),BY2PG); //bzero need virtual address
*pp = ppage_temp;
return 0;
}
bzero(page2kva(ppage_temp),BY2PG);是为了清空申请出的空页
****page2kva(ppage_temp)表示获取ppage_temp对应的物理页的虚地址****
page_free(释放一页空间)
void page_free(struct Page *pp)
{
if (pp->pp_ref == 0) {
LIST_INSERT_HEAD(&page_free_list,pp,pp_link);
return;
}else if (pp->pp_ref > 0) {
return;
}
/* Step 1: If there's still virtual address referring to this page, do nothing. */
/* Step 2: If the `pp_ref` reaches 0, mark this page as free and return. */
/* If the value of `pp_ref` is less than 0, some error must occurr before,
* so PANIC !!! */
panic("cgh:pp->pp_ref is less than zero\n");
}
LAB2-2
二级页表
boot_pgdir_walk
static Pte *boot_pgdir_walk(Pde *pgdir, u_long va, int create),它返回一级页表基地址 pgdir 对应的两级页表结构中,va 这个虚拟地址所在的二级页表项,如果 create 不为 0 且对应的二级页表不存在则会使用 alloc 函数分配一页物理内存用于存放。
static Pte *boot_pgdir_walk(Pde *pgdir, u_long va, int create)
{
Pde *pgdir_entryp;
Pte *pgtable, *pgtable_entry;
//*pgdir_en tentryp contains entry os page table,PTE_V,PTE_R
pgdir_entryp = pgdir + PDX(va);
//*pgdir_entryp是pgdir_entryp对应的页表项的内容
//create 不为 0 且对应的二级页表不存在则会使用 alloc 函数分配一页物理内存用于存放
if ((*pgdir_entryp & PTE_V) == 0) { //1 page is null
if (create) {
//申请一页空间,这里需要物理空间,(访问二级页表需要)
*pgdir_entryp = PADDR(alloc(BY2PG,BY2PG,1));
//对应标志位赋值
*pgdir_entryp = *pgdir_entryp | PTE_V | PTE_R;
} else return 0;
//#define PTE_ADDR(p) p & ~0xFFF 就是获取页目录对应的页表项的物理地址
//对页表进行操作时处于内核态,因此使用宏 KADDR 获取对应的虚拟地址
return (Pte *)(KADDR(PTE_ADDR(*pgdir_entryp))) + PTX(va);
}
boot_map_segment
void boot_map_segment(Pde *pgdir, u_long va, u_long size, u_long pa, int perm),它的作用是将一级页表基地址 pgdir 对应的两级页表结构做区间地址映射,将虚拟地址区间 [va, va + size − 1] 映射到物理地址区间 [pa, pa + size − 1],因为是按页映射,要求 size 必须是页面大小的整数倍。同时为相关页表项的权限为设置为 perm。这个函数调用了 boot_pgdir_walk,它也是在内核启动过程中使用的,前面我们提到过它为 Page 结构体和 Env 结构体进行了映射。
void boot_map_segment(Pde *pgdir, u_long va, u_long size, u_long pa, int perm)
{
int i, va_temp;
//二级页表的入口地址
Pte *pgtable_entry;
for (i = 0,size = ROUND(size,BY2PG);i<size;i+=BY2PG) {
pgtable_entry = boot_pgdir_walk(pgdir, va+i, 1);
*pgtable_entry = (pa + i) | perm | PTE_V;
}
/* Step 1: Check if `size` is a multiple of BY2PG. */
/* Step 2: Map virtual address space to physical address. */
/* Hint: Use `boot_pgdir_walk` to get the page table entry of virtual address `va`. */
}
int pgdir_walk
int pgdir_walk(Pde *pgdir, u_long va, int create, Pte **ppte),这个函数是 boot_pgdir_walk 的非启动版本,只不过后者是在内核启动过程,不允许失败,而该函数是在普通运行过程,如果空间不够允许失败,因此将返回值变为 int,若为 0 代表执行成功否则为一个失败码,同时将原本的返回值 Pte * 放到 ppte 所指的空间上。此外,因为该函数的调用在启动之后,create 不为 0 且对应的二级页表不存在时,它使用 page_alloc 而不使用 alloc 进行物理页面的分配。
int pgdir_walk(Pde *pgdir, u_long va, int create, Pte **ppte)
{
Pde *pgdir_entryp;
Pte *pgtable;
struct Page *ppage;
int ret;
/* Step 1: Get the corresponding page directory entry and page table. */
pgdir_entryp = pgdir + PDX(va);
/* Step 2: If the corresponding page table is not exist(valid) and parameter `create`
* is set, create one. And set the correct permission bits for this new page table.
* When creating new page table, maybe out of memory. */
if ((*pgdir_entryp & PTE_V) == 0) {
if (create) {
//如果页面申请失败,返回失败码
//page_alloc(**p),传入指针地址,直接修改指针*p为申请到的页面首地址
if ((ret = page_alloc(&ppage)) < 0) return ret;
//如果申请成功,ppage就是申请页面的首地址
//page2pa(ppage)返回Page *pp对应的物理页首地址
//获取的物理地址填入*pgdir_entryp即可
*pgdir_entryp = (page2pa(ppage)) | PTE_V | PTE_R;
ppage->pp_ref++;
} else {
*ppte = 0;
return 0;
}
}
/* Step 3: Set the page table entry to `*ppte` as return value. */
//将*ppte中填入va对应页表项的虚拟地址(KADDR)
*ppte = (Pte *)(KADDR(PTE_ADDR(*pgdir_entryp))) + PTX(va);
return 0;
}
page_insert
int page_insert(Pde *pgdir, struct Page *pp, u_long va, u_int perm),这个函数的作用是将一级页表基地址 pgdir 对应的两级页表结构中 va 这一虚拟地址映射到内存控制块 pp 对应的物理页面,并将页表项权限为设置为 perm。
int page_insert(Pde *pgdir, struct Page *pp, u_long va, u_int perm)
{
u_int PERM;
Pte *pgtable_entry;
PERM = perm | PTE_V;
int ret;
/* Step 1: Get corresponding page table entry. */
pgdir_walk(pgdir, va, 0, &pgtable_entry);
if (pgtable_entry != 0 && (*pgtable_entry & PTE_V) != 0) {
//检查映射到地址是否为pp对应的的物理页,如果不是,需要取消当前地址映射(替换)
if (pa2page(*pgtable_entry) != pp) {
//if page table is not this element,then remove
page_remove(pgdir, va);
//否则,建立映射关系*pgtable_entry -> Page *pp对应的物理页首地址,设置权限位为PERM
} else {
//使用 tlb_invalidate 函数可以实现删除特定虚拟地址的映射,每当页表被修改,就需要调用该函数以保证下次访问该虚拟地址时诱发 TLB 重填以保证访存的正确性。
tlb_invalidate(pgdir, va);
*pgtable_entry = (page2pa(pp) | PERM);
return 0;
}
}
/* Step 2: Update TLB. */
tlb_invalidate(pgdir, va);
/* hint: use tlb_invalidate function */
//再次使用pgdir_walk,无映射有2种可能,page_remove后无映射,原本无映射,此时可以继续建立映射
/* Step 3: Do check, re-get page table entry to validate the insertion. */
/* Step 3.1 Check if the page can be insert, if can’t return -E_NO_MEM */
if ((ret = pgdir_walk(pgdir,va,1,&pgtable_entry)) < 0) return ret;
/* Step 3.2 Insert page and increment the pp_ref */
*pgtable_entry = page2pa(pp) | PERM;
pp -> pp_ref++;
}
tlb_out
tlb_out该函数根据传入的参数(TLB 的 Key)找到对应的 TLB 表项,并将其清空
先了解有关TLB的相关命令
tlbr:以 Index 寄存器中的值为索引,读出 TLB 中对应的表项到 EntryHi 与 EntryLo。
tlbwi:以 Index 寄存器中的值为索引,将此时 EntryHi 与 EntryLo 的值写到索引指定的 TLB 表项中。
tlbwr:将 EntryHi 与 EntryLo 的数据随机写到一个 TLB 表项中(此处使用 Random 寄存器来“随机”指定表项,Random 寄存器本质上是一个不停运行的循环计数器)。
tlbp:根据 EntryHi 中的 Key(包含 VPN 与 ASID),查找 TLB 中与之对应的表项,并将表项的索引存入 Index 寄存器(若未找到匹配项,则 Index 最高位被置 1)。
#include <asm/regdef.h>
#include <asm/cp0regdef.h>
#include <asm/asm.h>
LEAF(tlb_out)
nop
//首先将CP0_ENTRYHI中原有的值写入k1寄存器
mfc0 k1,CP0_ENTRYHI
//将传入的参数(待清空表项的key)写到CP0_ENTRYHI中
mtc0 a0,CP0_ENTRYHI
nop
//根据 EntryHi 中的 Key,查找 TLB 中与之对应的表项,并将表项的索引存入 Index 寄存器,(若未找到匹配项,则 Index 最高位被置 1)。
tlbp
//防止冲突
nop
nop
nop
nop
//取出CP0_INDEX的值
mfc0 k0,CP0_INDEX
//如果是Index 最高位被置 1,表明没找到,跳到 NOFOUND标签处
//如果找到了,Index 最高位为0,继续执行
bltz k0,NOFOUND
nop
//为清空做准备,CP0_ENTRYHI,CP0_ENTRYLO全部填入0
mtc0 zero,CP0_ENTRYHI
mtc0 zero,CP0_ENTRYLO
nop
//根据找到的Index将此时 EntryHi 与 EntryLo 的值写到索引指定的 TLB 表项中,即完成清空
tlbwi
NOFOUND:
//复原现场(将原来的key写会CP0_ENTRYHI)
mtc0 k1,CP0_ENTRYHI
//跳回被调用的地方
j ra
nop
END(tlb_out)