mit6.s081 一些函数的解析 备查
本文主要是对xv6中一些常用函数的解释,备查,毕竟有很多函数你不去读源码你是看不懂的。当然也要结合手册和授课内容。
参考的相关链接
[]: http://xv6.dgs.zone/tranlate_books/book-riscv-rev1/c2/s1.html “xv6教材中文”
[]: https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081 “授课内容”
关于页表相关操作函数
其实大多跟虚拟内存的函数都定义在了vm.c中 感兴趣的可以看一下,这里介绍一些常用的。
walk函数
1 | walk(pagetable_t pagetable, uint64 va, int alloc) |
从教材中我们可以看到这个是三级页表。
它的主要作用是查找虚拟地址 va
对应的页表项(PTE)
参数说明:pagetable 页表的起始地址
va 需要查找的虚拟地址。
alloc 分配的标志位 如果walk函数没有找到,且为1 那么会给他分配一个新的页表
1 | va >= MAXVA |
MAXVA ?
1 | #define MAXVA (1L << (9 + 9 + 9 + 12 - 1)) |
查阅资料得知:
在 RISC-V 架构中,虚拟地址由三级页表管理,每级页表有 9 位索引,加上页内偏移的 12 位,总共 39 位地址空间。具体计算如下:
每级页表有 9 位索引:9+9+9=27
页内偏移有 12 位:27+12=39
然而,虚拟地址空间的最高位是符号位,用于表示正负地址。因此,实际可用的地址位数是 38 位,而不是 39 位。
PX
我们看一下定义:
1 |
PX 的作用是从虚拟地址va中提取特定级别的页表索引
其中PGSHIFT是12 。个人理解是这样的
从图中我们可以看出offset是12 也同时对应了一个页的大小是12 做偏移用。一个leave 对应 9 所以看你是几级 level 然后*9 右移之后 对0x1ff 取与 操作 ,就是去出对应级数的索引。
去出对应的索引后 我们得到他的地址 对应是哪一个PTE。因为2的次方是512 ,对应512个PTE .
找到对应的PTE后 我们对标志位取 与操作,判断这个pte是否有效。 如果有效 ,则通过PTE2PA函数实现相关操作。
PTE2PA
PTE2PA 是一个宏,用于从页表项(PTE)中提取物理地址。页表项不仅包含物理地址,还包含一些标志位(如有效位、权限位等)。PTE2PA 宏的作用是从页表项中提取出物理地址部分。以下是 PTE2PA 的定义:
有效代表到了叶子节点,这个时候我们就可以从虚拟地址映射到物理地址了。
1 | #define PTE2PA(pte) (((pte) >> 10) << 12) |
我们可以看到 先往右位移10位,然后左移12位。
就是舍去低的10位flag 然后左移12位。
关于位移12位?看了下上课的内容。
Frans教授:我们不会加上虚拟地址中的offset,这里只是使用了12bit的0。所以我们用44bit的PPN,再加上12bit的0,这样就得到了下一级page directory的56bit物理地址。这里要求每个page directory都与物理page对齐(也就是page directory的起始地址就是某个page的起始地址,所以低12bit都为0)。
貌似是这样解释的。
walkaddr
1 | uint64 |
我们观看源码可以得知 walk 函数的调用。
walkaddr的作用
walkaddr
函数用于将虚拟地址转换为物理地址。
主要就是通过walk函数的调用得到 一个pte 然后 进行相对应的判断。
1.检查虚拟地址是否有效:如果虚拟地址超出最大值 MAXVA,则返回 0。
2.查找页表项:使用 walk 函数查找页表项。如果页表项不存在或无效,则返回 0。
3.检查页表项的有效性:如果页表项无效(即没有设置 PTE_V 位)或不可用户访问(即没有设置 PTE_U 位),则返回 0。
4.返回物理地址:如果页表项有效且用户可访问,则返回对应的物理地址。
kvmmap
1 | void |
通过查看kvmmap 函数的实现,我们可以知道 ,他主要调用mappages函数,他将物理地址映射到虚拟地址 ,我们再看看mappages 函数即可。
mappages
1 | int |
我们首先看看他的形参
函数参数
pagetable
(pagetable_t): 页表指针。va
(uint64): 起始虚拟地址。size
(uint64): 映射的大小(以字节为单位)。pa
(uint64): 起始物理地址。perm
(int): 权限标志,定义了映射页面的访问权限(例如读、写、执行权限
首先a被初始化向下取整到页面的边界值
last 被初始化va+size-1 向下取整到页面边界值。
使用walk函数 查找对应页表项,如果不存在 则创建页表项。如果walk返回0 表示创建失败,函数返回-1。
之后:判断是否有效 如果已经有效 则 panic。
否则继续执行下去 将物理地址对应的页表项 设置相应的权限,并给V赋值。
判断a == last 是因为 如果你从某地址开始映射的地方,它对应的对齐内存,跟你要映射的大小所对应的边界如果是一样,那么就映射完了。
否则加一个PGSIZE 大小的页面。然后继续循环。
kvmpa
1 | uint64 |
kvmpa
函数在 xv6 操作系统中用于将内核虚拟地址转换为物理地址。
off 是计算 虚拟地址的偏移量。
然后用walk函数,找va对应的页表项。
如果不存在 则panic “kvmpa”
如果页表项无效 也调用panic。
之后利用PTE2PA 提取ppn
之后pa + off 最终的物理地址。
uvmunmap
1 | void |
uvmunmap
函数用于取消映射一段虚拟地址空间,并可选择性地释放对应的物理内存。这在进程终止或释放内存时非常重要,以确保内存资源的正确管理和回收。
下面来解释这段流程。
函数参数
pagetable
(pagetable_t): 页表指针。va
(uint64): 起始虚拟地址。npages
(uint64): 要取消映射的页面数量。do_free
(int): 是否释放物理内存的标志。如果为非零值,则释放物理内存。
首先判断 va % PGSIZE 是否是对齐的。
不是则 panic
然后开始 for循环 每次一个页面。
a = va a < va + npages*PGSIZE a += PGSIZE
进入第一个 if 找第一个pte 如果 为 0 表示页表项不存在。那么 就panic
第二个if 就是 判断pte_V 标志位 如果 无效 则panic
第三个if 我们注意到 有一个 PTE_FLAGS
1 |
定义如上。
因为0x3ff 是 11 1111 1111 刚好10 位 对应pte 的10个符号位。 将pte 只保留低位的10位 如果 刚好 等于PTE_V 那么表示他不是叶子结点。触发panic
第四个if 判断是否要进行 释放对应的内存操作
如果等于1 表示确定。 PTE2PA 将pte 转换到对应的物理地址。 然后通过kfree 释放。
kfree
1 | void |
1 | if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP) |
这一段主要判断 地址是否合法。
之后 acquire 加锁。
然后进行减少引用 –ref.ref.count 如果大于0 表示这个页面还在被使用。
再release 释放锁。
填充垃圾数据:
memset(pa, 1, PGSIZE);
将页面填充为 1,以捕捉悬空引用(dangling references)。
将页面添加到空闲列表:
r = (struct run*)pa;
将页面地址转换为 struct run 类型。
acquire(&kmem.lock);
获取内存管理锁。
r->next = kmem.freelist;
将页面添加到空闲列表的头部。
kmem.freelist = r;
更新空闲列表头指针。
release(&kmem.lock);
释放内存管理锁。
1 | extern char end[]; // first address after kernel. |
extern char end[];
是一个外部变量声明,用于指示内核代码和数据段的结束地址。这个变量在 kernel.ld
链接脚本中定义。
主要用途
- 内存管理初始化:
- 在内核启动时,
end
用于确定内核占用的内存范围,从而知道从哪里开始分配空闲内存。例如,在kinit
函数中,freerange
函数使用end
作为起始地址来初始化空闲内存列表。
- 在内核启动时,
- 内存分配:
end
变量帮助内核知道哪些内存区域是可用的,哪些是已经被内核代码和数据占用的。这对于内存分配器(如kalloc
和kfree
)非常重要。
在 kernel.ld 链接脚本中,end 通常定义为内核代码和数据段的结束地址。
kalloc
1 | void * |
大致逻辑跟kfree是一样的 都是 acquire 然后release 。
copyin
1 | int |
copyin 是将数据从用户空间复制到内核空间。
函数参数
pagetable
(pagetable_t): 页表指针,用于转换虚拟地址到物理地址。dst
(char*): 目标地址,内核空间中的缓冲区。srcva
(uint64): 源地址,用户空间中的虚拟地址。len
(uint64): 要复制的数据长度。
主要步骤
- 计算页面对齐地址:
va0 = PGROUNDDOWN(srcva);
- 将源虚拟地址
srcva
向下取整到页面边界。
- 转换虚拟地址到物理地址:
pa0 = walkaddr(pagetable, va0);
- 使用
walkaddr
函数将虚拟地址va0
转换为物理地址pa0
。 - 如果转换失败(
pa0 == 0
),返回 -1。
- 计算要复制的字节数:
n = PGSIZE - (srcva - va0);
- 计算当前页面内剩余的字节数。
if(n > len) n = len;
- 如果剩余字节数大于要复制的长度,则只复制需要的长度。
- 执行内存复制:
memmove(dst, (void *)(pa0 + (srcva - va0)), n);
- 将数据从物理地址
pa0 + (srcva - va0)
复制到目标地址dst
。
- 更新指针和长度:
len -= n;
- 减少剩余要复制的长度。
dst += n;
- 更新目标地址指针。
srcva = va0 + PGSIZE;
- 更新源虚拟地址到下一个页面。
- 循环直到复制完成:
- 重复上述步骤,直到所有数据都被复制。
题外话: 向下取整和向上取整
在操作系统和内存管理中,向下取整和向上取整是常见的操作,主要用于对齐地址。以下是它们的区别和使用场景:向下取整(Round Down):
将地址向下取整到最近的页面边界。使用场景:通常用于计算页面的起始地址。例如,在 copyin 函数中,PGROUNDDOWN(srcva) 将虚拟地址 srcva 向下取整到页面边界,以确保从页面的起始位置开始操作。
示例:PGROUNDDOWN(0x1234) 结果是 0x1000。
向上取整(Round Up):
将地址向上取整到最近的页面边界。
使用场景:通常用于分配内存时,确保分配的内存大小是页面大小的整数倍。例如,在分配内存时,如果需要分配 3000 字节,向上取整到页面大小(通常是 4096 字节)以确保分配足够的空间。
示例:PGROUNDUP(0x1234) 结果是 0x2000。栈的生长方向
在大多数计算机系统中,栈是从高地址向低地址生长的。这意味着栈顶的地址会随着数据的压入而减小。低地址:内存地址较小的部分。
高地址:内存地址较大的部分。
内存布局示意
高地址
+——————+
| |
| 栈 |
| | | 堆 | | | —————— | 数据段 | —————— | 代码段 | —————— 低地址 在这个布局中,栈从高地址向低地址生长,而堆从低地址向高地址生长。