0. 序
这一部分主要记录main函数如何初始化xv6的虚拟内存,main函数依次调用了kinit(), kvminit(), kvminithart()。
1. kinit()函数
struct run {
struct run *next;
};
struct { //kmem是用于管理物理页面的数据结构,就是一个链表。
struct spinlock lock;
struct run *freelist;
} kmem;
//最终达成的效果是:kmem本身是存放在内核的全局数据段里面,
//然后它的freelist字段保存了第一个页面的物理地址
//而第一个页面的物理地址的前8个字节又保存了第二个页面的物理地址……
void
kinit()
{ // end是内核代码结束后的第一个字节的地址,这以后到PHYSTOP的物理内存
//是可以释放的,PHYSTOP是物理内存RAM的最高地址。
initlock(&kmem.lock, "kmem");
freerange(end, (void*)PHYSTOP);
}
void //释放 pa_start 到 pa_end 这一段的物理页面
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
kfree(p);
}
void //简单地把pa指向的物理页面加入到链表中
kfree(void *pa)
{
struct run *r;
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE);
r = (struct run*)pa;
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}
void * //分配出链表头指向的物理页面,并返回该页面的物理地址。
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
if(r)
kmem.freelist = r->next;
release(&kmem.lock);
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}
因此调用kinit函数后,物理内存就由kmem这个数据结构控制好了。
利用gdb查看end的值,再反汇编查看内核代码数据结束的地址:
可以看到,在disk这个全局数据结构开始的物理位置是0x80023000,考虑到其占的大小,end值为0x80026000是比较合理的。
2. kvminit()函数
pagetable_t kernel_pagetable;
void
kvminit(void)
{
kernel_pagetable = kvmmake();
}
pagetable_t
kvmmake(void)
{
pagetable_t kpgtbl;
kpgtbl = (pagetable_t) kalloc();
memset(kpgtbl, 0, PGSIZE);
// uart registers
kvmmap(kpgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);
// virtio mmio disk interface
kvmmap(kpgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
// PLIC
kvmmap(kpgtbl, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
// map kernel text executable and read-only.
kvmmap(kpgtbl, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
// map kernel data and the physical RAM we'll make use of.
kvmmap(kpgtbl, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
// map the trampoline for trap entry/exit to
// the highest virtual address in the kernel.
kvmmap(kpgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
// map kernel stacks
proc_mapstacks(kpgtbl);
return kpgtbl;
//可以看到,kvmmake建立起了内核虚拟地址空间到物理内存空间的映射,
//包括硬件和RAM,这里的映射都是恒等映射,保证了后面开启虚存机制后
//指令和数据的地址仍能够翻译到正确的位置。例外的是以trampoline地址
//起始的汇编代码映射了两次,一次是内核代码段的恒等映射,同时还把它
//映射到了内核虚拟地址空间的最高处。
//疑问,这里为什么没有建立CLINT硬件映射的代码??怀疑它这里写漏了?
//在lab里面的xv6代码是有建立CLINT硬件映射的代码的。
}
void
kvmmap(pagetable_t kpgtbl, uint64 va, uint64 pa, uint64 sz, int perm);
//该函数在内部调用了mappages函数,效果和mappages相同
//作为一层封装,多了一个错误检查(调用panic)
int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm); //va是虚拟地址,pa是物理地址,页表项的访问权限由perm决定。
//该函数在内部调用了work函数,且alloc=1。
//该函数的作用是根据给出的pagetable,设置好从va开始的大小为size的区域
//对应的页表项,使得它们将会映射到从pa开始,大小为size的物理页面。
pte_t *
walk(pagetable_t pagetable, uint64 va, int alloc);
//在 alloc=0 时,walk函数的作用是模仿mmu,利用给定的页表pagetable
//和虚拟地址va,返回该va在pagetable中对应的页表项指针。
//对应数据结构为pte_t。在 alloc=1 时,如果在翻译地址的过程中发现某级页表
//中应该对应va的表项不存在,则会调用kalloc.c中的kalloc函数分配一页,然后
//填写该表项使其指向分配出的页面(在alloc=0时会简单地报错)。
总之,kvminit函数建立了初始化了kernel_pagetable这个全局页表,建立了内核虚拟地址空间到物理空间的映射。
3. kvminithart()函数
#define SATP_SV39 (8L << 60)
#define MAKE_SATP(pagetable) (SATP_SV39 | (((uint64)pagetable) >> 12))
void //将satp装入页表
kvminithart()
{
w_satp(MAKE_SATP(kernel_pagetable));
sfence_vma(); //清除与地址翻译有关的所有缓存(例如PLB中的条目)
}
下图是satp寄存器的结构,前44位是根页表的物理地址的高44位(因为实际页表位置要求4KB对齐),因此最多支持
2
56
2^{56}
256字节的物理地址空间。
mode字段含义如下:
在start函数中在设置satp的值为0也可以理解了,这样设置了mode字段为0,从而关闭虚拟内存。从代码中可以看到,xv6的mode值为8,即翻译模式为39位地址的翻译模式。查看xv6代码的maxva:
少设置一位是为了避免地址的符号拓展?地址为什么要进行符号拓展?目前还不理解这里 。
update:
以下引自spec中4.4.1节对sv39的描述,因为如果bit38为1的话,要求bit 63-39都为1,为了避免这样的符号拓展的麻烦,所以xv6少使用了一半的地址空间。
Instruction fetch addresses and load and store effective addresses, which are 64 bits, must have bits 63–39 all equal to bit 38, or else a page-fault exception will occur.
4. procinit()函数
#define KSTACK(p) (TRAMPOLINE - ((p)+1)* 2*PGSIZE) //memorylayout.h
#define TRAMPOLINE (MAXVA - PGSIZE)
//这部分代码位于proc.c中,每个进程设置了指向内核栈的指针
struct proc proc[NPROC];
// initialize the proc table at boot time.
void //这是之前在kvmmake函数中调用的,为每个进程分配一个内核栈
proc_mapstacks(pagetable_t kpgtbl) { //并建立好页表的映射
struct proc *p;
for(p = proc; p < &proc[NPROC]; p++) {
char *pa = kalloc();
if(pa == 0)
panic("kalloc");
uint64 va = KSTACK((int) (p - proc));
kvmmap(kpgtbl, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
}
}
void
procinit(void)
{
struct proc *p;
initlock(&pid_lock, "nextpid");
initlock(&wait_lock, "wait_lock");
for(p = proc; p < &proc[NPROC]; p++) {
initlock(&p->lock, "proc");
p->kstack = KSTACK((int) (p - proc)); //设置了指针指向该内核栈
} //注意是指向栈底部,而且是虚拟地址。
}
5. 小结
记录以下函数的作用:kalloc,mappages,walk,freepage,kfree。
疑问:为什么没有映射CLINT?
6. update
感谢灵隐寺未来职工
, qianhao2579
这两位哥们的留言,我发现之前对于CLINT的理解出了问题。
- mtime, mtimecmp是两个内存映射寄存器(而不是集成在cpu上的类似于通用寄存器那种),写入mtimecmp实际上是写入到Core Local Interrupt (CLINT),下面的引用来自riscv的参考手册。
Platforms provide a real-time counter, exposed as a memory-mapped machine-mode read-write register, mtime. mtime must run at constant frequency, and the platform must provide a mechanism for determining the timebase of mtime.
The mtime register has a 64-bit precision on all RV32 and RV64 systems. Platforms provide a 64-bit memory-mapped machine-mode timer compare register (mtimecmp), which causes a timer interrupt to be posted when the mtime register contains a value greater than or equal to the value in the mtimecmp register.
- 上面另外值得注意的是,这俩内存映射的寄存器只能在M态下访问。
- 另外是手册上对satp寄存器的描述
The satp register is an SXLEN-bit read/write register, formatted as shown in Figure 4.11 for SXLEN=32 and Figure 4.12 for SXLEN=64, which controls supervisor-mode address translation and protection.
从描述上看来,应该认为M态是没有mmu翻译的,即地址会被直接认为是物理地址(虽然我并没有查到相应的说明,不过从mmu的定义来看上面的推断是合理的)。
- 现在再回头看riscv-xv6对时钟中断的处理,因为mtimecmp寄存器只能在M态进行修改,因此也就没必要把clint的地址映射到虚拟内存中了。在源码中把mtvec寄存器对应的地址设置为timevec函数(start,c),当触发时钟中断后,处理器进入M态,同时跳转到timevec函数(kernelvec.S),timevec函数修改mtimecmp寄存器后手动触发一个软中断,这样调用mret返回后马上会因为这个软中断进入S态,然后有相应的时钟中断处理。这一切的时钟中断处理逻辑并不需要映射CLINT(因为映射了也没啥用)
- 最后是现在的两个疑问:为什么设计时S态不能访问mtimecmp寄存器,导致用一种这样别扭的方式实现时钟中断?以及CLINT的msip bits是如何与mip寄存器中的msip bit对应上的?