[OS] Prerequisite Knowledge about xv6
xv6 简介
xv6 是一个教学操作系统,模仿了 Ken Thompson 和 Dennis Ritchie 设计的 Unix 操作系统,提供了 Unix 的基本接口和内部设计。xv6 通过实现 Unix 的核心概念和机制,帮助学习者理解操作系统的基本原理和设计思想。
1. Unix 的设计特色
- 简洁的接口:Unix 提供了一个“窄接口”,即只提供少量的基本机制,但这些机制可以很好地组合使用,从而实现广泛的功能。这种设计赋予了 Unix 系统极大的通用性。
- 高效的组合:Unix 的接口设计简单而高效,很多功能可以通过基本机制的组合来实现,这种设计让它在许多不同应用场景中都非常灵活。
2. xv6 的作用
- 模仿 Unix 内部设计:xv6 的设计目标是尽可能地模仿早期 Unix 的内部结构和接口,以便学习者更好地理解 Unix 系统的核心概念和实现细节。
- 教学平台:通过研究和修改 xv6,学习者可以直接观察和理解操作系统如何管理进程、内存、文件系统等核心资源。
3. 为什么学习 xv6?
- 通向现代操作系统的桥梁:现代操作系统(例如 BSD、Linux、macOS、Solaris,甚至部分的 Windows)在接口设计上都受到了 Unix 的深远影响。理解 xv6 的设计和接口,有助于更好地理解这些操作系统。
- 操作系统基础:xv6 是一个简化的操作系统,代码量较小,便于学习者一步步理解操作系统的工作原理和实现方式。它提供了进程管理、内存管理、文件系统等操作系统的基础功能。
图 1.1 展示了 xv6 操作系统的传统内核结构,以及内核与用户进程之间的关系。以下是对图和文本的详细解释:
1. 内核(Kernel)是什么?
在 xv6 中,内核是一个特殊的程序,为运行中的用户程序(即进程)提供核心服务。它管理系统的资源(如 CPU、内存、I/O 设备等),并且确保系统的安全性和稳定性。内核是操作系统的核心,负责与硬件进行直接交互,并为用户进程提供系统调用接口。
2. 用户进程(User Process)
用户进程是一个运行中的程序。在 xv6 中,每个进程都有独立的内存空间,用来存储程序的指令、数据和栈:
- 指令:实现程序的具体操作。
- 数据:存储程序中的变量,用于运算和操作。
- 栈:用于组织程序的函数调用,帮助程序管理局部变量和调用关系。
在同一时间,一个计算机可以运行多个进程,但通常只有一个内核在管理这些进程。
3. 系统调用(System Call)
当一个用户进程需要使用内核的服务(例如读取文件、分配内存等),它会发起系统调用。系统调用是用户程序与内核之间的接口,它允许用户程序请求内核执行某些特权操作,例如:
- 文件操作(打开、读取、写入文件)
- 进程管理(创建、终止进程)
- 内存管理(分配或释放内存)
系统调用的过程如下:
- 进入内核空间:当用户进程发起系统调用时,CPU 会切换到内核模式,并进入内核空间。
- 内核执行服务:内核接管控制权,执行请求的服务。
- 返回用户空间:服务完成后,内核将控制权返回给用户进程,进程继续在用户空间中执行。
4. 用户空间与内核空间的隔离
用户空间和内核空间的隔离是通过 CPU 提供的硬件保护机制实现的:
- 用户空间(User Space):用户进程在该空间中运行,权限受限,无法直接访问或修改系统核心资源。
- 内核空间(Kernel Space):内核在该空间中运行,具有完全的权限,可以直接控制硬件资源。
这种隔离机制确保了每个进程只能访问自己的内存,不能干扰或读取其他进程的内存数据,提高了系统的安全性和稳定性。
总结
- 内核 是管理系统资源和安全的核心程序,负责处理用户进程的请求。
- 用户进程 是运行中的程序,每个进程有独立的内存空间。
- 系统调用 是用户进程请求内核服务的桥梁,触发用户空间和内核空间的切换。
- 硬件保护机制 确保用户进程只能访问自己的内存空间,实现了用户空间和内核空间的隔离。
学习 xv6 的这种内核-用户分离机制和系统调用接口,可以帮助我们理解现代操作系统是如何确保安全和稳定运行的。
以下是 xv6 中各个系统调用的通俗解释,帮助您了解它们的作用。
进程管理系统调用
-
fork()
- 作用:创建一个新的进程。
- 通俗解释:复制当前进程,生成一个“子进程”,并返回子进程的 ID。
-
exit(int status)
- 作用:终止当前进程,返回状态给父进程。
- 通俗解释:告诉操作系统“我完成任务了”,并让父进程知道任务是否成功。
-
wait(int *status)
- 作用:等待一个子进程结束,并获取子进程的退出状态。
- 通俗解释:父进程等待子进程完成,并查看子进程的任务结果。
-
kill(int pid)
- 作用:终止指定进程。
- 通俗解释:强制让指定的进程“停止运行”。
-
getpid()
- 作用:获取当前进程的 ID。
- 通俗解释:告诉进程它的“身份证号”。
-
sleep(int n)
- 作用:暂停进程一段时间(
n
个时钟周期)。 - 通俗解释:让进程“睡一会儿”,过一段时间再继续执行。
- 作用:暂停进程一段时间(
-
exec(char *file, char *argv[])
- 作用:加载并执行指定的程序,带参数。
- 通俗解释:运行一个新程序,用指定的参数替换当前进程的内容。
-
sbrk(int n)
- 作用:调整进程的内存大小,返回新的内存段起始位置。
- 通俗解释:增加或减少进程使用的内存空间。
文件管理系统调用
-
open(char *file, int flags)
- 作用:打开文件,并返回一个文件描述符。
- 通俗解释:获取文件的“句柄”以便后续读写操作。
-
write(int fd, char *buf, int n)
- 作用:将
n
字节的数据从buf
写入到文件描述符fd
指向的文件。 - 通俗解释:将内容写入指定文件。
- 作用:将
-
read(int fd, char *buf, int n)
- 作用:从文件描述符
fd
指向的文件中读取n
字节的数据到buf
。 - 通俗解释:从文件中读取内容放入缓冲区。
- 作用:从文件描述符
-
close(int fd)
- 作用:关闭文件描述符
fd
。 - 通俗解释:关闭文件,不再访问该文件。
- 作用:关闭文件描述符
-
dup(int fd)
- 作用:创建一个新的文件描述符,指向同一个文件。
- 通俗解释:复制文件的“句柄”,让多个文件描述符指向同一个文件。
-
pipe(int p[2])
- 作用:创建一个管道,返回一对文件描述符(
p[0]
用于读,p[1]
用于写)。 - 通俗解释:创建一个“管道”,让数据可以从一端流向另一端。
- 作用:创建一个管道,返回一对文件描述符(
-
fstat(int fd, struct stat *st)
- 作用:获取文件的状态信息并放入
st
中。 - 通俗解释:查询文件的详细信息(如大小、类型等)。
- 作用:获取文件的状态信息并放入
-
stat(char *file, struct stat *st)
- 作用:获取指定文件的状态信息。
- 通俗解释:查询指定文件的详细信息。
目录和文件系统管理
-
chdir(char *dir)
- 作用:更改当前工作目录。
- 通俗解释:切换到指定的文件夹,类似于在命令行中输入
cd
。
-
mkdir(char *dir)
- 作用:创建一个新目录。
- 通俗解释:在文件系统中创建一个文件夹。
-
mknod(char *file, int major, int minor)
- 作用:创建一个设备文件。
- 通俗解释:为设备创建一个“接口文件”,通常用于驱动硬件设备。
-
link(char *file1, char *file2)
- 作用:创建一个硬链接,将
file2
链接到file1
。 - 通俗解释:创建一个指向同一文件的新“指针”或别名。
- 作用:创建一个硬链接,将
-
unlink(char *file)
- 作用:删除一个文件。
- 通俗解释:将文件从文件系统中删除。
Unix 的系统调用接口已经通过 POSIX(Portable Operating System Interface,便携式操作系统接口) 标准进行了规范化。然而,xv6 并不符合 POSIX 标准,因为它缺少许多系统调用(包括一些基本的,例如 lseek
),并且它提供的部分系统调用与 POSIX 标准不同。
xv6 的设计目标
xv6 的主要目标是简单性和清晰性。它提供了一个简单的、类 UNIX 的系统调用接口,但并没有实现所有的 POSIX 标准。这样设计的目的是让 xv6 更易于理解,使其成为一个操作系统学习的良好基础。
xv6 的局限性
由于 xv6 追求简洁,许多现代操作系统中的功能在 xv6 中并不存在。例如:
- 网络支持:xv6 不支持网络连接,而现代操作系统内核通常会内建网络协议栈,以支持网络通信。
- 图形窗口系统:xv6 不提供图形界面或窗口系统,而现代系统提供图形用户界面(GUI)和支持窗口操作的服务。
- 用户级线程:现代操作系统支持用户级线程,允许在同一个进程中运行多个线程,但 xv6 并不支持。
- 设备驱动:现代操作系统内核支持大量不同的硬件设备驱动,而 xv6 仅支持极少数简单设备。
xv6 的扩展
有些人对 xv6 进行了扩展,增加了一些系统调用和简单的 C 库,以便能够运行一些基本的 Unix 程序。但与现代内核相比,xv6 的功能仍然非常有限。
现代内核的功能
现代操作系统内核发展迅速,提供了许多超出 POSIX 标准的功能和服务。例如:
- 网络服务:包括 TCP/IP 协议栈,以支持各种网络通信。
- 图形系统:支持图形界面和窗口操作,满足现代用户的需求。
- 高级并发支持:例如线程、协程,以及多核调度。
- 大量设备支持:支持各种硬件设备,从普通的键盘、鼠标到特殊的摄像头、传感器等。
文件描述符(File Descriptor)简介
文件描述符是一个小的整数,代表一个由内核管理的对象,进程可以通过该对象进行读取或写入操作。文件描述符实际上是一个接口,它将文件、管道和设备都抽象为字节流,使得程序可以通过相同的方式操作这些对象。
文件描述符的获取方式
一个进程可以通过多种方式获得文件描述符:
- 打开文件、目录或设备:例如,通过
open()
系统调用打开一个文件会返回一个文件描述符。 - 创建管道:使用
pipe()
创建一个通信管道,返回两个文件描述符,一个用于读,一个用于写。 - 复制已有的文件描述符:使用
dup()
可以创建一个现有文件描述符的副本,指向相同的文件对象。
文件描述符的内部工作原理
在 xv6 内核中,文件描述符作为索引用于进程的文件描述符表。每个进程有一个文件描述符表,从 0
开始编号,且每个进程的文件描述符表是独立的。这种设计让进程可以拥有自己的一组文件描述符,避免相互干扰。
read
和 write
系统调用
read(fd, buf, n)
- 作用:从文件描述符
fd
中读取最多n
个字节的数据,并将它们存储到buf
中。 - 返回值:返回读取的字节数;如果到达文件末尾,返回
0
。 - 文件偏移量:读取操作从当前偏移位置开始,读取后偏移量自动增加读取的字节数,下次
read
会从新的偏移位置开始读取。
write(fd, buf, n)
- 作用:将
buf
中的n
个字节数据写入到文件描述符fd
指向的文件中。 - 返回值:返回写入的字节数;如果发生错误,返回的字节数可能小于
n
。 - 文件偏移量:写入操作也从当前偏移位置开始,写入后偏移量自动增加写入的字节数,这样下次
write
会从上次写入后的位置开始。
open
、close
、dup
系统调用
open()
:打开文件并返回文件描述符。close(fd)
:释放文件描述符fd
,使其可被其他调用重新使用。dup(fd)
:复制文件描述符fd
,返回一个新的文件描述符,指向相同的文件对象。新旧描述符共享同一个偏移量。
dup
示例
例如:
fd = dup(1);
write(1, "hello ", 6);
write(fd, "world\n", 6);
在这个例子中,dup(1)
创建了文件描述符 fd
,它指向与 1
(标准输出)相同的文件对象。这意味着写入 1
和写入 fd
都会输出到相同的地方,因此输出结果会是 hello world
。
总结
-
什么是文件描述符?
文件描述符可以看作是程序打开的文件、设备或其他对象的“编号”。当一个程序想要操作一个文件、管道或者设备时,系统会给它分配一个编号,这个编号就是文件描述符。程序可以通过这个编号来读写数据。
文件描述符的作用
可以把文件描述符想象成图书馆的书架编号。当你借一本书时,图书馆会告诉你这本书的书架编号。下次你想看这本书时,只需要根据编号找到书架就可以。类似地,当程序想要操作某个文件或设备时,操作系统给它分配一个文件描述符(编号),程序可以通过这个编号来找到文件进行操作。
文件描述符的使用
-
open
:打开文件- 当程序调用
open()
打开一个文件时,系统会返回一个文件描述符(编号)。以后程序可以通过这个文件描述符来读写文件,而不需要每次都找文件名。
- 当程序调用
-
read
和write
:读写文件read(fd, buf, n)
:通过文件描述符fd
从文件中读取最多n
个字节的数据到buf
(缓冲区)里。比如,读取一个文档,系统会根据文件描述符找到文件内容并复制到缓冲区中。write(fd, buf, n)
:通过文件描述符fd
把buf
中的数据写入到文件里。类似于往文档中写入文字。
-
close
:关闭文件- 当文件操作结束时,调用
close(fd)
关闭文件描述符,相当于归还图书馆的“书架编号”,让这个编号可以分配给其他文件。
- 当文件操作结束时,调用
- 每个文件描述符都有一个文件偏移量,可以理解为“光标位置”。
- 读取:每次
read
都从当前偏移量开始,读取数据后光标会自动往后移动。 - 写入:
write
也从当前偏移量开始,写入数据后光标同样自动往后移动。 - 这样设计的好处是,连续的读或写操作会从上次结束的位置继续,而不是从头开始。
dup(fd)
:创建一个文件描述符的副本,让两个文件描述符指向同一个文件。- 举个例子:你有两个编号,都可以指向同一本书。这样,你可以用两个不同的编号来查找同一本书,并且它们会共享同一个光标位置。
-
打开文档(
open
):你告诉系统“我想打开文件”,系统给你一个编号,比如3
,然后你可以用编号3
来代表这个文件。 -
阅读文档(
read
):你用编号3
读取文件内容,系统会根据“光标位置”给你读出内容。每次读完,光标会自动往后移动,这样下次读取时不会重复之前的内容。 -
写入文档(
write
):用编号3
写入内容,系统会在当前光标位置写入数据。写完后光标会自动移动到写入的内容后面。 -
复制编号(
dup
):假如你用dup(3)
得到一个新的编号4
,那么3
和4
都指向同一个文件,而且共享同一个光标位置。用编号3
或4
读取或写入,都会影响另一个编号的光标位置。 -
关闭文档(
close
):你关闭编号3
,系统就会回收这个编号,这个编号可以分配给其他文件。
文件(File)与 inode
在 Unix/Linux 系统中,文件的名字与文件本身是分离的。文件的实际内容和属性是存储在一个称为 inode 的数据结构中,而文件名只是 inode 的一个链接。以下是 inode 的一些重要属性:
- 文件类型:指明文件是普通文件、目录还是设备。
- 文件长度:文件的大小。
- 文件内容在磁盘上的位置:指向文件内容实际存储的磁盘地址。
- 链接数:指向该 inode 的文件名(链接)数量。一个文件可以有多个名字(即硬链接),它们都指向同一个 inode。
Unix/Linux 系统通过 inode 来识别文件,而不是文件名。这意味着即使文件名改变,系统仍然可以通过 inode 识别文件。
通俗解释:可以把 inode 想象成一个“文件的身份证”,记录了文件的所有重要信息(大小、位置、类型等)。文件名只是一个指向这个身份证的“标签”,我们可以给一个文件多个名字,但它们都指向同一个 inode。
页表(Page Table)
页表是一种机制,用于实现每个进程的独立地址空间和内存管理,是现代操作系统中隔离进程内存空间的核心技术。在 xv6 中,页表允许不同的进程拥有各自的虚拟地址空间,同时在物理内存上共存。
虚拟地址和物理地址
- 虚拟地址:进程代码使用的地址,方便进程访问自己独立的内存空间。
- 物理地址:实际内存(RAM)中的地址,用于指向实际存储的数据。
在 RISC-V 架构下,所有指令操作的是虚拟地址,而实际数据存储在物理地址中。页表的作用是将虚拟地址映射到物理地址,实现虚拟内存到物理内存的转换。
Sv39 页表
xv6 运行在 Sv39 RISC-V 上,这种架构具有以下特点:
- 39 位虚拟地址:在 64 位的虚拟地址中,只使用最低的 39 位(其余高 25 位未使用)。
- 页表结构:Sv39 的页表逻辑上是一个包含
2^27
(134,217,728)个页表项(PTE)的数组。 - 页表项(PTE):每个 PTE 包含一个 44 位的物理页号(PPN)和一些标志位。
地址转换过程
虚拟地址转换成物理地址的过程:
- 虚拟地址的前 27 位用于索引页表,找到对应的页表项(PTE)。
- 页表项中的 44 位物理页号(PPN)构成物理地址的高 44 位。
- 虚拟地址的低 12 位直接复制到物理地址中,形成完整的物理地址。
内存页面(Page):页表将虚拟地址分为 4KB 的页面块(4096 字节)。操作系统可以精确控制每个页面的地址转换,允许按页面级别管理内存。
通俗解释:页表就像是一个“翻译字典”,负责将程序的虚拟地址(类似门牌号)翻译成实际物理内存的地址(类似于仓库位置)。Sv39 页表允许系统将虚拟地址分成 4KB 一页的块来管理,并映射到物理地址空间中。这样不同的进程可以独立运行,不会互相干扰。
总结
- inode 是文件的核心标识,存储文件的元数据,不依赖文件名。
- 页表 是一种虚拟内存管理机制,将进程的虚拟地址空间映射到物理地址空间,确保每个进程的内存独立性和安全性。
1. 图解:RISC-V 地址转换细节
这张图展示了 RISC-V 架构下的虚拟地址到物理地址的转换过程。它使用了三级页表结构,分为 L2、L1 和 L0 三级,每一级都起到逐步定位的作用。
左侧:虚拟地址(Virtual Address)
- 虚拟地址被分成了几部分,从高位到低位依次是:
- L2、L1、L0:每一级各 9 位,用来在各级页表中找到对应的页表项(PTE)。
- Offset:最后 12 位,用来标识具体的页面偏移量。
在这个 39 位的虚拟地址中,最高的 27 位(L2、L1 和 L0)用于索引三级页表,最低的 12 位直接用于偏移,无需转换。
中间:页表层次结构
RISC-V 的页表使用三级结构(L2、L1 和 L0),每级页表的工作如下:
-
L2 页表:
- 虚拟地址的 L2 部分(9 位)用于在 L2 页表中查找一个页表项(PTE),这个 PTE 包含指向 L1 页表的地址。
-
L1 页表:
- L1 部分(9 位)用于在 L1 页表中找到下一级的页表项,这个页表项指向 L0 页表的地址。
-
L0 页表:
- L0 部分(9 位)用于在 L0 页表中找到最终的物理页面地址(PPN,Physical Page Number)。
右侧:物理地址(Physical Address)
在 L0 页表中找到的 PTE 包含了物理页号(PPN),这是物理地址的高 44 位。物理地址的低 12 位直接从虚拟地址中的 Offset 复制过来,组成完整的物理地址。
页表项(PTE)结构
每个页表项(PTE)包含以下重要信息:
- Physical Page Number (PPN):存储物理页号(实际的内存地址)。
- Flags(标志位):用于控制访问权限和其他属性,包括:
- V:有效位,表示页表项是否有效。
- R/W/X:可读、可写、可执行位,控制访问权限。
- U:用户位,表示该页面是否可由用户访问。
- A:访问位,表示该页面是否被访问过。
- D:脏位,表示页面是否被修改过。
2. 通俗解释:页表和地址转换
什么是页表?
可以把页表想象成一个大地图,帮助操作系统将程序使用的虚拟地址(类似“门牌号”)转换为物理地址(类似“仓库位置”)。页表记录了虚拟地址到物理地址的映射关系,这样每个程序可以拥有自己的地址空间,而不会干扰其他程序。
为什么需要页表?
- 保护进程的独立性:页表允许操作系统为每个进程分配独立的地址空间,不同进程的内存不会互相干扰。
- 管理内存:页表帮助操作系统按需分配内存,使得程序可以使用比实际物理内存更大的地址空间(虚拟内存)。
- 灵活访问控制:通过页表的标志位,操作系统可以控制页面的访问权限(只读、读写等),防止进程错误修改或访问不应该访问的内存区域。
如何工作?
在运行程序时,CPU 不直接使用物理地址,而是使用虚拟地址。通过页表,操作系统将虚拟地址转换成物理地址:
- 分级查找:虚拟地址中的 L2、L1 和 L0 部分分别对应三级页表的查找。每一级找到一个页表项(PTE),最后一级找到指向物理页的地址。
- 偏移合成:最终找到的物理页号(PPN)和虚拟地址中的 Offset 组合,生成完整的物理地址。
- 访问控制:页表项中的标志位控制页面的访问权限和状态。如果访问无效页面,CPU 会触发错误,保护系统安全。
比喻理解
可以把这个过程想象成查找仓库货物的过程:
- 三级页表:就像是三个不同级别的地图(城市地图、区地图、具体位置图),逐步帮助你找到具体的仓库。
- 偏移量:最终找到的仓库位置,再加上偏移(类似于货架的具体层数)确定最终的货物位置。
- 标志位:就像仓库的门禁卡,不同权限的门禁卡只能进入特定的区域,确保物品的安全。
从实际需求和问题出发,来通俗地解释为什么需要设计这样复杂的页表结构。
1. 进程隔离
每个运行的程序(或称“进程”)都需要自己的一块内存来存储数据和代码。如果没有一个机制来区分不同进程的内存,程序之间会相互干扰,比如一个程序可能会无意中修改另一个程序的数据,这会导致严重的安全问题。
解决方法:通过页表,操作系统可以为每个进程提供一个独立的虚拟地址空间,就像每个程序都在自己“专属的房间”里,无法轻易进入其他程序的“房间”。这样即使多个程序运行在同一台计算机上,它们的内存也不会冲突,保证了程序之间的隔离性。
2. 内存管理的灵活性
不同的程序在运行时需要不同大小的内存,并且内存的需求是动态变化的。如果我们让每个程序都直接访问物理内存,那么我们就需要提前为每个程序预留足够的物理空间,这样会导致资源浪费。
解决方法:通过页表,操作系统可以让每个进程“认为”自己拥有一个连续的大块内存(虚拟地址空间),而实际上这些地址可能并不连续,也不一定全部映射到物理内存。这样操作系统可以灵活地为进程分配和回收物理内存,按需加载,减少内存浪费。
3. 安全控制
在实际使用中,我们需要对内存进行安全控制,比如一些内存区域只允许读取,不能写入;一些区域只能由操作系统访问,不能被普通程序访问。如果没有一个机制来设置这些权限,程序可以随意修改甚至破坏内存数据,可能导致系统崩溃。
解决方法:页表不仅映射虚拟地址到物理地址,还可以为每一块内存设置访问权限(例如“只读”或“可执行”)。这样操作系统就能确保每个进程只能按照预定的权限访问内存,避免非法操作,提高系统的安全性。
4. 支持虚拟内存
很多时候,程序所需的内存比实际的物理内存要大,特别是运行多个程序时,总的内存需求往往会超过计算机的实际内存容量。
解决方法:通过页表,操作系统可以实现虚拟内存机制,让程序使用虚拟地址访问内存,当物理内存不足时,可以将部分不常用的内存数据暂时存放到硬盘上(称为“交换”)。页表负责跟踪这些虚拟地址的状态,当程序需要访问硬盘上的数据时,操作系统会自动将数据从硬盘加载回内存,从而让程序感觉自己拥有了一个大内存空间。
5. 为什么要分多级页表?
随着计算机的发展,内存越来越大,地址空间也越来越大。要表示完整的地址空间,如果每个地址都在页表中占一项,页表就会非常大,占用大量内存。为了节省空间,采用多级页表结构,通过逐级查找,只加载需要的页表部分,节省内存资源。
多级页表的好处:可以节省内存空间,只需要为活跃的地址空间分配页表项,而不需要一次性为整个地址空间分配。多级页表的逐级查找结构相当于一个层级地图,帮助我们高效找到目标地址,而无需占用太多内存。
在 Sv39 RISC-V 架构中,虚拟地址的最高 25 位不用于地址转换,这样的设计足以提供 512GB 的地址空间来满足应用程序的需求。
我们逐步解释这句话的逻辑。
1. 64 位架构的虚拟地址
在 64 位架构中,虚拟地址是 64 位的,这意味着理论上每个程序的地址空间可以有 2642^{64}264 字节,这相当于 16 EB(Exabytes,艾字节)。这个地址空间非常庞大,远远超出了目前计算机内存的实际需求和硬件支持的范围。
2. Sv39 的地址空间限制
在 Sv39 模式下,RISC-V 架构只使用虚拟地址的 最低 39 位 进行地址转换,而最高的 25 位则被忽略(不参与地址转换)。因此,Sv39 的虚拟地址空间限制在 39 位,即 2392^{39}239 字节。
- 2392^{39}239 字节 = 512 GB 的虚拟地址空间。
也就是说,Sv39 模式将每个进程的虚拟地址空间限制在 512 GB,而不是完整的 16 EB。这种设计上的限制是故意的,因为 512 GB 的虚拟地址空间对于目前的应用程序来说已经足够了,绝大部分程序在运行时不需要这么大的地址空间。
3. 为什么限制到 512 GB?
限制地址空间的大小有几个原因:
-
减少页表大小:如果使用完整的 64 位地址空间,每个进程的页表会变得非常庞大,浪费内存资源。而使用 39 位地址空间(即 512 GB),页表的大小和管理开销会显著减少。
-
硬件复杂性降低:只需要处理 39 位地址空间,硬件设计会更简单,成本更低,转换效率更高。
-
现实需求:大多数应用程序并不需要 512 GB 以上的地址空间,提供 512 GB 已经足以满足大部分需求。如果以后需要更大的地址空间,RISC-V 还支持其他模式(如 Sv48,可以提供 48 位的虚拟地址空间)。
Sv39 虚拟地址到物理地址的转换过程
-
三级页表结构
- RISC-V CPU 在 Sv39 模式下,将虚拟地址分为三级来进行转换,每级使用 9 位来定位。
- 虚拟地址的前三个 9 位(共 27 位)用于三级页表查找,最后 12 位用于页面内的偏移量。
-
页表结构
- 页表是一个三层树状结构,每一层包含一个 4KB 的页表页面。
- 每个页表页面可以存储 512 个页表项(PTE)。
- 顶层的页表页面包含指向下一层页表页面的物理地址。通过三级查找可以找到最终的物理页地址。
-
地址转换过程
- 第一步:CPU 使用虚拟地址的最高 9 位(L2)选择顶层页表的一个 PTE。
- 第二步:然后使用中间的 9 位(L1)在下一级页表中选择对应的 PTE。
- 第三步:最后使用底部的 9 位(L0)在第三级页表中找到指向物理页的 PTE。
- 偏移量:找到物理页后,虚拟地址中的最后 12 位用于页面内的偏移量,将该偏移量直接添加到物理地址上即可找到具体的物理存储位置。
页面错误(Page Fault)
如果在这三级查找中的任何一级找不到有效的 PTE(即页表项缺失),硬件会触发页面错误异常(pagefault exception)。这时,操作系统内核会介入处理该异常,可能会加载所需的页面或采取其他措施。
页表项(PTE)的标志位
每个页表项(PTE)包含一些标志位,用于控制该页面的访问权限和行为:
- PTE_V:有效位,表示该 PTE 是否存在。如果未设置,则访问该页面会引发异常。
- PTE_R:读权限,控制是否允许读取页面内容。
- PTE_W:写权限,控制是否允许写入页面内容。
- PTE_X:执行权限,控制是否允许将该页面的内容作为指令执行。
- PTE_U:用户模式访问位,控制是否允许用户模式下的程序访问该页面。如果未设置,仅内核可以访问该页面。
补充说明
- 物理内存:指的是 DRAM 中的存储单元,每个存储单元有一个物理地址。
- 虚拟地址:程序指令使用的地址,经过页表映射后转换为物理地址。
- 虚拟内存:不是一个实际的物理对象,而是内核用来管理物理内存和虚拟地址的抽象机制。通过虚拟内存,操作系统可以提供更大、更灵活的地址空间,并隔离不同进程的内存。
通俗解释
可以把这个过程想象成一本复杂的“地址翻译字典”:
-
三级翻译:虚拟地址的前 27 位像是三级目录索引,逐步帮我们找到目标物理页面。最终的物理地址则通过这个“翻译字典”从虚拟地址转换而来。
-
页面权限:每个页表项的标志位就像是“访问控制锁”,决定页面的使用规则。比如某些页面是只读的,某些页面是用户模式禁止访问的,确保安全和稳定。
-
页面错误:当我们试图访问一个不存在的页面时,就会触发页面错误。可以理解为“查不到地址”,系统会引发警报,交给操作系统来处理。
为什么要设计这样的结构?
- 隔离内存空间:通过页表,每个进程都可以有自己的虚拟地址空间,进程间不会相互干扰。
- 灵活的内存管理:操作系统可以灵活地分配、回收内存,同时支持虚拟内存,让程序可以使用超出物理内存容量的空间。
- 安全控制:标志位为每个页面设置了严格的访问权限,防止不当的内存操作。
- 效率:三级结构使得页表只需分配必要的部分,不用占用过多的物理内存。