操作系统内核的学习大概从2019.2.2开始,之后春节近一个月进展缓慢没做什么具体工作(期间帮张jm做项目浪费了不少时间),从大概3.3开始进行jos实验。
内存原理篇的核心思想是虚拟内存:通过这种机制,操作系统将内存的容量扩增至无限大、将内存访问速度大幅提高
make //从源代码构建出可执行的boot loader和kernel文件,即obj/kern/kernel.img
//(看作硬盘里的系统文件,它里面包含了boot loader和kernel)
make qemu //运行虚拟机,它会执行引导程序,从硬盘载入kernel并进入监视状态。
//会运行内核,执行所有检查函数并输出检查结果
kerninfo //此时可以查看kernel程序的一些基本信息,例如_start, entry,..., end等链接符号对应的内存地址值
objdump -f obj/kern/kernel //查看kernel文件的信息,如program header,sections,symbol table等
管理物理内存的机制是将物理内存按页分成很多页,每个页对应一个node,将这些node串起来构成一个list。从这个list中剔除掉那些已经被使用过了的node,剩下的就是一个所谓的“page_free_list”。当以后每次需要分配内存的时候,就从这个list的头部开始取用,用掉了的就标记为已经被占用并从page_free_list中剔除出去。
这些节点的结构体连续存放在一片内存空间(pages)中,因此我们可以根据某个节点自身所在的物理地址推测出这个节点所代表的物理页的起始地址。即page node与物理页起始地址的对应关系不是显式指定的,而是隐式地存在的。
首先,我们看一下pmap.c中最重要的函数mem_init(),大致回顾一下内存初始化的过程。
void mem_init(){
//检查内存还有多少
i386_detect_memory();
//分配一个物理页作为页目录表
//boot_alloc只是暂时的页分配器,等我们建立好页目录后,将使用page_alloc函数来
kern_pgdir = (pde_t *)boot_alloc(PGSIZE);
//添加第一个页目录表项
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;
//分配一块内存用于存放struct PageInfo数组,用于追踪所有内存物理页的使用情况
pages = (struct PageInfo *)boot_alloc(npages*sizeof(struct PageInfo));
//分配一块内存用于存放struct Env数组,用于追踪所有进程情况
envs = (struct Env *)boot_alloc(NENV*sizeof(struct Env));
//初始化pages数组,初始化pages_free_list链表
page_init();
//下面构建虚拟地址空间
//函数原型:boot_map_region(pde_t* pgdir, uintptr_t va, size_t size, phyaddr_t pa, int perm)
//功能是:把虚拟地址空间范围[va, va+size]映射到物理空间[pa, pa+size]的映射关系加入到页表pgdir中
boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U);
boot_map_region(kern_pgdir, UENVS, PTSIZE, PADDR(envs), PTE_U);
boot_map_region(kern_pgdir, KSTACKTOP-KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);
boot_map_region(kern_pgdir, KERNBASE, 0xffffffff-KERNBASE, 0, PTE_W);
lcr3(PADDR(kern_pgdir));
}
函数总结:
void page_init();
功能:
初始化pages数组和page_free_list。
其中pages用于追踪每个物理页的使用状态,page_free_list用于记录空闲物理页。
执行完这个函数,我们就清楚了当前物理内存到底哪些页是已经被占用了,哪些是空闲的。
在建立好空闲页的list(即执行完“page_init()”)之后,建立起正式的映射关系之前,会先执行三个检查的函数:
check_page_free_list(); //检查位于page_free_list上的页都是合理的
check_page_alloc(); //检查物理页分配器(page_alloc(), page_free(), page_init())
check_page(); //检查page_insert(), page_remove()
struct PageInfo* page_alloc(int alloc_flags);
功能:
从page_free_list中取下头部节点,这个节点对应的page将被后续使用,返回这个节点的指针。
当alloc_flags为1的时候,将返回节点对应的物理页内容置零。
注意!:在这个函数中不会增加返回节点的pp_ref。
这应该交给它的调用者来执行,要么是显式地执行,要么是交给page_insert()来执行。
void page_free(struct PageInfo* pp);
功能:
将pp节点对应的物理页还给page_free_list(即释放某个物理页),释放之前先检查确保pp_ref==0,pp_link==NULL。
这个页对应节点会被添加到list头部,成为新的头。
说明:
page_alloc()和page_free()函数中都没有修改节点pp_ref值的相关操作。
因为同一个物理地址可能被多个虚拟地址映射,所以这些操作统一被交给构建映射关系的程序来执行,
所以不需要(也不能)在这两个函数中修改pp_ref。
void page_decref(struct PageInfo* pp);
功能:
将被引用次数pp_ref减一,若减至零,则释放该页回page_free_list。
被page_remove()调用。
int page_insert(pde_t* pgdir, struct PageInfo* pp, void* va, int perm);
功能:
把一个虚拟地址va与物理页pp(节点)建立映射关系,即写入一个新的页表项(写入pp对应的物理地址)
将节点的pp_ref加一,将页表项的permission设置为perm|PTE_P,若执行成功则返回0
注意!:这个函数应配合page_alloc()来使用,先由page_alloc()返回一个节点(空闲页),
然后将这个节点作参数再执行page_insert()。由调用者page_insert()来将pp_ref加一。
说明:
1)多个虚拟地址可映射到同一物理地址,一个虚拟地址只能映射到一个物理地址。
所以如果在页表中查询到va已经映射到另一个物理地址,而现在我们想要将它映射到一个新地址,就必须先取消旧的映射关系。
2)page_insert()函数应该配合page_alloc()使用,而不能直接操作page_free_list中的节点!
因为如果直接传入一个page_free_list中的节点指针作参数,将会导致page_free_list中节点的pp_ref不为零!
3)另外,在执行这个函数时可能遇到一种特殊情况,即在页表项中留下的老记录本就是将虚拟地址va映射到pp的物理地址。
由于这种情况的存在,我们不能先执行page_remove(pgdir, va),再建立新映射,再执行pp->pp_ref++。
因为如果按照这种顺序执行的话,在消除页表项中的旧映射关系时,在page_remove()中会将pp引用次数减一,
此时pp物理页被释放回page_free_list。而我们之前说过,page_insert不能直接使用
page_free_list中的节点作参数。所以必须把pp->pp_ref++这个操作放到最前面。
这个函数在lab2中可以完全不使用,只是为后续实验做的准备。
而且这个函数的设计逻辑我也没弄得很明白,留作以后补充。
void page_remove(pde_t* pgdir, void* va);
功能:
取消虚拟地址va及其映射的物理页之间的映射关系,即从页表中找到va对应的页表项及page节点,
将节点引用次数减一(调用page_decref),将页表项内容置零
调用关系和设计思想:
void boot_map_region(pde_t* pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm);
功能:
将虚拟地址[va, va+size)映射到物理地址[pa, pa+size),size是PGSIZE的整数倍,va和pa都是页对齐的。
这个函数仅用于映射UTOP以上的虚拟空间,这些空间到物理空间是“静态”映射,故不需要修改pp_ref的值。
//关于虚拟地址的操作及它们调用关于物理地址操作函数的调用关系
void boot_map_region(pgdir, va, size, pa, perm)->pte_t* pgdir_walk(pgdir, va)
int page_insert()->pte_t* pgdir_walk()->struct PageInfo* page_alloc()
void page_remove() ->struct PageInfo* page_lookup()
void page_decref()->void page_free()
三段映射:
1)boot_map_region(kern_pgdir, UPAGES, PGSIZE, PADDR(pages), PTE_U);
2) boot_map_region(kern_pgdir, KSTACKTOP-KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);
3) boot_map_region(kern_pgdir, KERNBASE, 0xffffffff-KERNBASE, 0, PTE_W);
这三段映射均不改变pp_ref,是静态映射。
GDT:
地址翻译分为了segmentaion translation和page translation两部分,关于这两部分的宏操作和数据结构等定义在mmu.h中。
全局符号表最早是在boot/boot.S中install的(专用在bootstrap程序中的GDT),但在当时,它实质上是没有segmentation translation的功能的。因此,虚地址中的selector部分没有起到作用,线性地址就等于虚拟地址中的offset部分。实际上此时属于保护模式,物理地址=GDT表项中段基址+偏移地址,而实际上指针的值通常就是偏移地址。如果此时查看GDT表内容,会发现有两个有效表项,分别标识代码段和数据段,且段基址都为0.
在进入内核后,会加载新的GDT表,使代码段和数据段的段基址都为0x10000000.
等到在lab3设置中断/异常处理机制部分,为了设置优先级,也会涉及到跟segmentation相关的东西。
使用命令:
info gdt
可查看GDT表的内容。
gdt: # Bootstrap GDT, 包含了三段
SEG_NULL # null seg
SEG(STA_X|STA_R, 0x0, 0xffffffff) # code seg
SEG(STA_W, 0x0, 0xffffffff) # data seg
上面语句是使用了宏定义来设置全局符号表的表项
其中SEG_NULL是个宏定义,等价于:
.word 0, 0;
.byte 0, 0, 0, 0
而SEG(type, base, lim)也是一个宏,等价于:
.word ();
.byte ()
lgdt gdtdesc #载入全局描述符表。把代码中标识符’gdtdesc‘的值发给‘GDTR’寄存器
gdtdesc是一个包含了GDT表的地址及长度的标识符,其中desc是descriptor的简写:
gdtdesc:
.word. 0x17 # sizeof(gdt)-1
.long gdt # address gdt
在env.c中为了实现优先级切换,建立了全局描述符表,该表为内核模式和用户模式分别设置segments。
struct Segdesc{
unsigned
}