|
|
|
|
|
# 内存管理
|
|
|
|
|
|
`segment fault` 这个错误,不知道大家在平时是否见过,这个错误出现的一个很常见的原因就是
|
|
|
在用户空间运行的程序访问了内核空间的内存。
|
|
|
|
|
|
那操作系统是如何实现内核和用户空间的管理的呢?这一节将结合龙芯的结构特点,来介绍。
|
|
|
|
|
|
## 映射窗口
|
|
|
|
|
|
建议先阅读《loongarch 指令集手册》第五章,了解 loongarch 的虚实地址翻
|
|
|
译机制。
|
|
|
|
|
|
阅读完成后,我们讲解一下其中的重点。
|
|
|
在 LoongArch 架构中,MMU 支持两种虚拟地址到物理地址的转换模式:直
|
|
|
接地址转换模式和映射地址转换模式
|
|
|
我们重点关注映射地址转换模式
|
|
|
映射地址转换模式,有两种类型的地址转换模式:直接映射地址转换模式
|
|
|
(直接映射模式)和页表映射地址转换模式(页表映射模式)。
|
|
|
|
|
|
在转换地址时,优
|
|
|
先使用直接映射模式。只有在直接映射模式无法转换地址时,才使用页表映射模
|
|
|
式进行转换。
|
|
|
|
|
|
在 LA64 中,在使用页表映射模式时,虚拟地址空间合法性的规则如下:合
|
|
|
法虚拟地址的 [63:PALEN] 位必须与 [PALEN-1] 位相同,否则将触发地址错误
|
|
|
异常(ADE)。然而,在直接映射模式下,不需要进行此地址合法性检查。
|
|
|
我们看到,在间接映射模式下,页表映射模式的合法规则非常的严格,这就
|
|
|
|
|
|
意味着很多的地址无法使用页表映射模式进行转换。但是,我们可以利用这个特
|
|
|
性,来实现一些特殊的功能。比如在 riscv 版本中,内核和用户空间都是利用页
|
|
|
表来进行地址转换的,而在 loongarch 中,** 我们可以利用直接映射模式来实现内
|
|
|
核空间的地址转换,而利用页表映射模式来实现用户空间的地址转换。 **
|
|
|
|
|
|
前面提到,利用直接映射模式来实现内核空间的地址转换,而利用页表映
|
|
|
射模式来实现用户空间的地址转换,那么我们需要一段无法用页表映射模式
|
|
|
进行地址转换的地址空间,用这一段空间来实现内核空间的地址转换。如果
|
|
|
PALEN 等于 48,且 DMWO 被设置为 0x9000000000000011 1 ,则虚拟地址空间
|
|
|
0x9000000000000000-0x9000FFFFFFFFFFFF 将直接映射到物理地址空间 0x0-
|
|
|
0xFFFFFFFFFFFF
|
|
|
|
|
|
所以我们在 memlayout.h 里面定义
|
|
|
|
|
|
```c
|
|
|
#ifdef __ASSEMBLY__
|
|
|
#define _CONST64_(x) x
|
|
|
#else
|
|
|
#define _CONST64_(x) x ## L
|
|
|
#endif
|
|
|
|
|
|
#define DMW_PABITS 48
|
|
|
|
|
|
#define CSR_DMW1_PLV0 _CONST64_(1 << 0)
|
|
|
#define CSR_DMW1_MAT _CONST64_(1 << 4)
|
|
|
#define CSR_DMW1_VSEG _CONST64_(0x9000)
|
|
|
#define CSR_DMW1_BASE (CSR_DMW1_VSEG << DMW_PABITS)
|
|
|
#define CSR_DMW1_INIT (CSR_DMW1_BASE | CSR_DMW1_MAT | CSR_DMW1_PLV0)
|
|
|
```
|
|
|
|
|
|
这段代码定义了一些宏,用来设置 DWM1 寄存器的值。
|
|
|
|
|
|
• 将物理地址 0x0-0xFFFFFFFFFFFF 映射到虚拟地址 0x9000000000000000-
|
|
|
0x9000FFFFFFFFFFFF
|
|
|
|
|
|
• 将 PALEN 设置为 48
|
|
|
|
|
|
我们现在已经设置了地址转换模式,我们的目的是把它作为内核空间的地
|
|
|
址转换模式,所以我们需要把内核的代码和数据放在这段地址空间中。
|
|
|
我们把这一步目标放在 kernel.ld 文件上
|
|
|
|
|
|
```c
|
|
|
OUTPUT_ARCH ( " loongarch " )
|
|
|
ENTRY( _entry )
|
|
|
SECTIONS
|
|
|
{
|
|
|
/*
|
|
|
* ensure that entry .S / _entry is at 0 x9000000000000000 ,
|
|
|
* where qemu 's -kernel jumps .
|
|
|
*/
|
|
|
. = 0 x9000000000000000 ;
|
|
|
.text : {
|
|
|
...
|
|
|
}
|
|
|
. rodata : {
|
|
|
...
|
|
|
}
|
|
|
.data : {
|
|
|
...
|
|
|
}
|
|
|
.bss : {
|
|
|
...
|
|
|
}
|
|
|
PROVIDE (end = .);
|
|
|
}
|
|
|
```
|
|
|
我们让内核从虚拟地址 0x9000000000000000 开始,这样就可以利用直接映
|
|
|
射模式来进行地址转换了,我们初步的目的就达成了。
|
|
|
|
|
|
UEFI bios 装载内核时,实际跳转到的地址将是 0x0,这也是内核的物理起
|
|
|
始地址。
|
|
|
|
|
|
而我们现在需要设置 DMW1 寄存器,来实现用户空间的地址转换,我们在
|
|
|
entry.S 中设置 DMW1 寄存器
|
|
|
|
|
|
```c
|
|
|
li.d $t0 , CSR_DMW1_INIT
|
|
|
csrwr $t0 , 0x181
|
|
|
```
|
|
|
|
|
|
我们设置直接映射窗口前,没有地址映射,所以 PC 只能直接使用物理地
|
|
|
址,也就是 0x0 0xFFFFFFFFFFFF,而我们设置了直接映射窗口后,PC 就应
|
|
|
该使用虚拟地址,我们应该按照我们设置的规则来设置 PC,也就是让 PC 使用
|
|
|
0x9000000000000000 0x9000FFFFFFFFFFFF 这段地址空间。
|
|
|
|
|
|
我们在 entry.S 中设置 PC
|
|
|
|
|
|
|
|
|
```c
|
|
|
# 计算ertn后一条指令的虚拟地址
|
|
|
li.d $t0, CSR_DMW1_BASE
|
|
|
pcaddi $t1, 0x5 # ------+
|
|
|
add.d $t0, $t0, $t1 # |
|
|
|
# |
|
|
|
# 将虚拟地址写入tlbrera # |
|
|
|
# 并设置tlbrera.IsTLBR # |
|
|
|
ori $t0, $t0, 0x01 # |
|
|
|
csrwr $t0, 0x8a # |
|
|
|
ertn # |
|
|
|
# |
|
|
|
la.abs $sp, stack0+4096 # <-----+
|
|
|
csrrd $t0, 0x20 # CPUID
|
|
|
```
|
|
|
|
|
|
这里首先算出 extn 后一条指令的虚拟地址,也就是 PC+CSR_DMW1_BASE+5,
|
|
|
然后将这个虚拟地址写入 tlbrera 寄存器,也就是在 tlb 重填例外后的返回地址 2 ,
|
|
|
这样 tlb 重填例外返回后,PC 就会跳转到这个虚拟地址,我们也就达到了设置
|
|
|
PC 的目的。
|
|
|
|
|
|
值得一提的是,我们之所以要利用例外来设置 PC,是因为在某些处理器中,
|
|
|
没有提供直接设置 PC 的指令,只能通过 branch 等指令来设置 PC,但是,也
|
|
|
有一些架构支持直接写入 PC,但是 loongarch 不支持,所以我们通过例外来设
|
|
|
置 PC。
|
|
|
|
|
|
## 页表
|
|
|
|
|
|
>需要先阅读龙芯手册 5.4 节内容。
|
|
|
|
|
|
大家在写程序的时候是否考虑过内存地址呢?除了少部分内核专用的地址,我们的进程
|
|
|
好像拥有着整个内存空间,这是如何实现的呢?答案就是页表。
|
|
|
|
|
|
比如进程A在0x114514处写入了一个数据,进程B在0x114514处读取了一个数据,这个时候,进程B读取到的数据是什么呢?是进程A写入的数据吗?答案是不一定。
|
|
|
|
|
|
操作系统通过页表机制实现了对内存空间的控制。页表使得 xv6 能够让不同进程各自的地址空间映射到相同的物理内存上,还能够为不同进程的内存提供保护。
|
|
|
|
|
|
|
|
|
xv6-loongarch 版的页表结构见龙芯手册。
|
|
|
|
|
|
我们定义了虚拟地址共 48 位,在 loongarch 中使用页表时,如果 VA[47] =
|
|
|
0, 使用 CSR.PGDL 作为目录基址。如果 VA[47] = 1, 使用 CSR.PGDH 作为目
|
|
|
录基址。剩下的 47 位用于遍历页表。
|
|
|
|
|
|
**xv6-longarch 只使用了 PGDL 相当于只有 47 位虚拟地址,这 47 位虚
|
|
|
拟地址全留给用户空间使用,内核使用映射窗口。**
|
|
|
|
|
|
这样就把这 47 为虚拟地址分割成了 8 + 9 + 9 + 9 + 12 这种格式,使用的
|
|
|
是四级页表,页大小为 4KB。
|
|
|
|
|
|
看完了页表的结构,我们来根据页表的结构查看和查找页表有关的代码。
|
|
|
```c
|
|
|
pte_t *
|
|
|
walk(pagetable_t pagetable, uint64 va, int alloc)
|
|
|
{
|
|
|
if(va >= MAXVA)
|
|
|
panic("walk");
|
|
|
|
|
|
for(int level = 3; level > 0; level--) {
|
|
|
pte_t *pte = &pagetable[PX(level, va)];
|
|
|
|
|
|
// 检查对应页表项是否有效
|
|
|
if(*pte & PTE_V) {
|
|
|
pagetable = (pagetable_t)(PTE2PA(*pte) | KERNBASE);
|
|
|
} else {
|
|
|
// 如果需要分配新的页表,并且分配失败则返回0
|
|
|
if(!alloc || (pagetable = (pde_t*)allocpage()) == 0)
|
|
|
return 0;
|
|
|
|
|
|
// 将新分配的页表清零
|
|
|
memset(pagetable, 0, PGSIZE);
|
|
|
|
|
|
// 将当前页表项设置为指向新分配的页表,并标记为有效
|
|
|
*pte = PA2PTE(pagetable) | PTE_V;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 返回虚拟地址对应的页表项指针
|
|
|
return &pagetable[PX(0, va)];
|
|
|
}
|
|
|
// Look up a virtual address, return the physical address,
|
|
|
// or 0 if not mapped.
|
|
|
// Can only be used to look up user pages.
|
|
|
uint64
|
|
|
walkaddr(pagetable_t pagetable, uint64 va)
|
|
|
{
|
|
|
pte_t *pte;
|
|
|
uint64 pa;
|
|
|
|
|
|
if(va >= MAXVA)
|
|
|
return 0;
|
|
|
|
|
|
// 调用walk函数,获取对应虚拟地址的页表项指针
|
|
|
pte = walk(pagetable, va, 0);
|
|
|
|
|
|
// 如果页表项指针为空,返回0
|
|
|
if(pte == 0)
|
|
|
return 0;
|
|
|
|
|
|
// 检查页表项是否有效
|
|
|
if((*pte & PTE_V) == 0)
|
|
|
return 0;
|
|
|
|
|
|
// 检查页表项是否为页表项指针
|
|
|
if((*pte & PTE_PLV) == 0)
|
|
|
return 0;
|
|
|
|
|
|
// 获取物理地址
|
|
|
pa = PTE2PA(*pte);
|
|
|
|
|
|
// 返回物理地址
|
|
|
return pa;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
在页表查找中,这两个函数非常重要,walk函数用于查找虚拟地址对应的页表项,walkaddr函数用于查找虚拟地址对应的物理地址。
|
|
|
|
|
|
其他的关于页表的操作,比如创建页表,释放页表,映射页表,都是在vm.c中实现的。
|
|
|
|
|
|
我们再来看一个函数
|
|
|
|
|
|
```c
|
|
|
// Copy from kernel to user.
|
|
|
// Copy len bytes from src to virtual address dstva in a given page table.
|
|
|
// Return 0 on success, -1 on error.
|
|
|
int
|
|
|
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
|
|
|
{
|
|
|
uint64 n, va0, pa0;
|
|
|
|
|
|
while(len > 0){
|
|
|
// 获取虚拟地址所在的页的起始地址
|
|
|
va0 = PGROUNDDOWN(dstva);
|
|
|
|
|
|
// 获取虚拟地址对应的物理地址
|
|
|
pa0 = walkaddr(pagetable, va0);
|
|
|
|
|
|
// 如果物理地址为0,表示无效,返回错误
|
|
|
if(pa0 == 0)
|
|
|
return -1;
|
|
|
|
|
|
// 计算可以复制的数据大小,受到页面边界的限制
|
|
|
n = PGSIZE - (dstva - va0);
|
|
|
|
|
|
// 如果剩余数据长度小于n,则只复制剩余长度
|
|
|
if(n > len)
|
|
|
n = len;
|
|
|
|
|
|
// 将数据从源地址复制到目标物理地址
|
|
|
memmove((void *)((pa0 + (dstva - va0)) | KERNBASE), src, n);
|
|
|
|
|
|
// 更新剩余长度和源地址指针
|
|
|
len -= n;
|
|
|
src += n;
|
|
|
|
|
|
// 更新目标虚拟地址为下一个页的起始地址
|
|
|
dstva = va0 + PGSIZE;
|
|
|
}
|
|
|
|
|
|
// 复制完成,返回成功
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
因为内核地址和用户空间地址不一样,所以当需要从内核读取数据到用户空间时(比如ls时会读取内核中文件的stat结构体),需要使用copyout函数。
|
|
|
|
|
|
## 总结
|
|
|
|
|
|
所以对于内存管理,总的来说,在内核空间,物理地址和虚拟地址之间差一个 `kernel base`,
|
|
|
在用户空间,虚拟地址和用户地址之间没有明确的数学关系,需要通过页表来进行转换。 |
|
|
\ No newline at end of file |