自学内容网 自学内容网

[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)

当一个用户进程需要使用内核的服务(例如读取文件、分配内存等),它会发起系统调用。系统调用是用户程序与内核之间的接口,它允许用户程序请求内核执行某些特权操作,例如:

  • 文件操作(打开、读取、写入文件)
  • 进程管理(创建、终止进程)
  • 内存管理(分配或释放内存)

系统调用的过程如下:

  1. 进入内核空间:当用户进程发起系统调用时,CPU 会切换到内核模式,并进入内核空间。
  2. 内核执行服务:内核接管控制权,执行请求的服务。
  3. 返回用户空间:服务完成后,内核将控制权返回给用户进程,进程继续在用户空间中执行。

4. 用户空间与内核空间的隔离

用户空间和内核空间的隔离是通过 CPU 提供的硬件保护机制实现的:

  • 用户空间(User Space):用户进程在该空间中运行,权限受限,无法直接访问或修改系统核心资源。
  • 内核空间(Kernel Space):内核在该空间中运行,具有完全的权限,可以直接控制硬件资源。

这种隔离机制确保了每个进程只能访问自己的内存,不能干扰或读取其他进程的内存数据,提高了系统的安全性和稳定性。

总结

  • 内核 是管理系统资源和安全的核心程序,负责处理用户进程的请求。
  • 用户进程 是运行中的程序,每个进程有独立的内存空间。
  • 系统调用 是用户进程请求内核服务的桥梁,触发用户空间和内核空间的切换。
  • 硬件保护机制 确保用户进程只能访问自己的内存空间,实现了用户空间和内核空间的隔离。

学习 xv6 的这种内核-用户分离机制和系统调用接口,可以帮助我们理解现代操作系统是如何确保安全和稳定运行的。

以下是 xv6 中各个系统调用的通俗解释,帮助您了解它们的作用。


进程管理系统调用

  1. fork()

    • 作用:创建一个新的进程。
    • 通俗解释:复制当前进程,生成一个“子进程”,并返回子进程的 ID。
  2. exit(int status)

    • 作用:终止当前进程,返回状态给父进程。
    • 通俗解释:告诉操作系统“我完成任务了”,并让父进程知道任务是否成功。
  3. wait(int *status)

    • 作用:等待一个子进程结束,并获取子进程的退出状态。
    • 通俗解释:父进程等待子进程完成,并查看子进程的任务结果。
  4. kill(int pid)

    • 作用:终止指定进程。
    • 通俗解释:强制让指定的进程“停止运行”。
  5. getpid()

    • 作用:获取当前进程的 ID。
    • 通俗解释:告诉进程它的“身份证号”。
  6. sleep(int n)

    • 作用:暂停进程一段时间(n 个时钟周期)。
    • 通俗解释:让进程“睡一会儿”,过一段时间再继续执行。
  7. exec(char *file, char *argv[])

    • 作用:加载并执行指定的程序,带参数。
    • 通俗解释:运行一个新程序,用指定的参数替换当前进程的内容。
  8. sbrk(int n)

    • 作用:调整进程的内存大小,返回新的内存段起始位置。
    • 通俗解释:增加或减少进程使用的内存空间。

文件管理系统调用

  1. open(char *file, int flags)

    • 作用:打开文件,并返回一个文件描述符。
    • 通俗解释:获取文件的“句柄”以便后续读写操作。
  2. write(int fd, char *buf, int n)

    • 作用:将 n 字节的数据从 buf 写入到文件描述符 fd 指向的文件。
    • 通俗解释:将内容写入指定文件。
  3. read(int fd, char *buf, int n)

    • 作用:从文件描述符 fd 指向的文件中读取 n 字节的数据到 buf
    • 通俗解释:从文件中读取内容放入缓冲区。
  4. close(int fd)

    • 作用:关闭文件描述符 fd
    • 通俗解释:关闭文件,不再访问该文件。
  5. dup(int fd)

    • 作用:创建一个新的文件描述符,指向同一个文件。
    • 通俗解释:复制文件的“句柄”,让多个文件描述符指向同一个文件。
  6. pipe(int p[2])

    • 作用:创建一个管道,返回一对文件描述符(p[0] 用于读,p[1] 用于写)。
    • 通俗解释:创建一个“管道”,让数据可以从一端流向另一端。
  7. fstat(int fd, struct stat *st)

    • 作用:获取文件的状态信息并放入 st 中。
    • 通俗解释:查询文件的详细信息(如大小、类型等)。
  8. stat(char *file, struct stat *st)

    • 作用:获取指定文件的状态信息。
    • 通俗解释:查询指定文件的详细信息。

目录和文件系统管理

  1. chdir(char *dir)

    • 作用:更改当前工作目录。
    • 通俗解释:切换到指定的文件夹,类似于在命令行中输入 cd
  2. mkdir(char *dir)

    • 作用:创建一个新目录。
    • 通俗解释:在文件系统中创建一个文件夹。
  3. mknod(char *file, int major, int minor)

    • 作用:创建一个设备文件。
    • 通俗解释:为设备创建一个“接口文件”,通常用于驱动硬件设备。
  4. link(char *file1, char *file2)

    • 作用:创建一个硬链接,将 file2 链接到 file1
    • 通俗解释:创建一个指向同一文件的新“指针”或别名。
  5. 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)简介

文件描述符是一个小的整数,代表一个由内核管理的对象,进程可以通过该对象进行读取或写入操作。文件描述符实际上是一个接口,它将文件、管道和设备都抽象为字节流,使得程序可以通过相同的方式操作这些对象。

文件描述符的获取方式

一个进程可以通过多种方式获得文件描述符:

  1. 打开文件、目录或设备:例如,通过 open() 系统调用打开一个文件会返回一个文件描述符。
  2. 创建管道:使用 pipe() 创建一个通信管道,返回两个文件描述符,一个用于读,一个用于写。
  3. 复制已有的文件描述符:使用 dup() 可以创建一个现有文件描述符的副本,指向相同的文件对象。

文件描述符的内部工作原理

在 xv6 内核中,文件描述符作为索引用于进程的文件描述符表。每个进程有一个文件描述符表,从 0 开始编号,且每个进程的文件描述符表是独立的。这种设计让进程可以拥有自己的一组文件描述符,避免相互干扰。

readwrite 系统调用

read(fd, buf, n)
  • 作用:从文件描述符 fd 中读取最多 n 个字节的数据,并将它们存储到 buf 中。
  • 返回值:返回读取的字节数;如果到达文件末尾,返回 0
  • 文件偏移量:读取操作从当前偏移位置开始,读取后偏移量自动增加读取的字节数,下次 read 会从新的偏移位置开始读取。
write(fd, buf, n)
  • 作用:将 buf 中的 n 个字节数据写入到文件描述符 fd 指向的文件中。
  • 返回值:返回写入的字节数;如果发生错误,返回的字节数可能小于 n
  • 文件偏移量:写入操作也从当前偏移位置开始,写入后偏移量自动增加写入的字节数,这样下次 write 会从上次写入后的位置开始。

openclosedup 系统调用

  • 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() 打开一个文件时,系统会返回一个文件描述符(编号)。以后程序可以通过这个文件描述符来读写文件,而不需要每次都找文件名。
  • readwrite:读写文件

    • read(fd, buf, n):通过文件描述符 fd 从文件中读取最多 n 个字节的数据到 buf(缓冲区)里。比如,读取一个文档,系统会根据文件描述符找到文件内容并复制到缓冲区中。
    • write(fd, buf, n):通过文件描述符 fdbuf 中的数据写入到文件里。类似于往文档中写入文字。
  • close:关闭文件

    • 当文件操作结束时,调用 close(fd) 关闭文件描述符,相当于归还图书馆的“书架编号”,让这个编号可以分配给其他文件。
  • 每个文件描述符都有一个文件偏移量,可以理解为“光标位置”。
  • 读取:每次 read 都从当前偏移量开始,读取数据后光标会自动往后移动。
  • 写入write 也从当前偏移量开始,写入数据后光标同样自动往后移动。
  • 这样设计的好处是,连续的读或写操作会从上次结束的位置继续,而不是从头开始。
  • dup(fd):创建一个文件描述符的副本,让两个文件描述符指向同一个文件。
  • 举个例子:你有两个编号,都可以指向同一本书。这样,你可以用两个不同的编号来查找同一本书,并且它们会共享同一个光标位置。
  • 打开文档open):你告诉系统“我想打开文件”,系统给你一个编号,比如 3,然后你可以用编号 3 来代表这个文件。

  • 阅读文档read):你用编号 3 读取文件内容,系统会根据“光标位置”给你读出内容。每次读完,光标会自动往后移动,这样下次读取时不会重复之前的内容。

  • 写入文档write):用编号 3 写入内容,系统会在当前光标位置写入数据。写完后光标会自动移动到写入的内容后面。

  • 复制编号dup):假如你用 dup(3) 得到一个新的编号 4,那么 34 都指向同一个文件,而且共享同一个光标位置。用编号 34 读取或写入,都会影响另一个编号的光标位置。

  • 关闭文档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 上,这种架构具有以下特点:

  1. 39 位虚拟地址:在 64 位的虚拟地址中,只使用最低的 39 位(其余高 25 位未使用)。
  2. 页表结构:Sv39 的页表逻辑上是一个包含 2^27(134,217,728)个页表项(PTE)的数组。
  3. 页表项(PTE):每个 PTE 包含一个 44 位的物理页号(PPN)和一些标志位。
地址转换过程

虚拟地址转换成物理地址的过程:

  1. 虚拟地址的前 27 位用于索引页表,找到对应的页表项(PTE)。
  2. 页表项中的 44 位物理页号(PPN)构成物理地址的高 44 位。
  3. 虚拟地址的低 12 位直接复制到物理地址中,形成完整的物理地址。

内存页面(Page):页表将虚拟地址分为 4KB 的页面块(4096 字节)。操作系统可以精确控制每个页面的地址转换,允许按页面级别管理内存。

通俗解释:页表就像是一个“翻译字典”,负责将程序的虚拟地址(类似门牌号)翻译成实际物理内存的地址(类似于仓库位置)。Sv39 页表允许系统将虚拟地址分成 4KB 一页的块来管理,并映射到物理地址空间中。这样不同的进程可以独立运行,不会互相干扰。

总结

  • inode 是文件的核心标识,存储文件的元数据,不依赖文件名。
  • 页表 是一种虚拟内存管理机制,将进程的虚拟地址空间映射到物理地址空间,确保每个进程的内存独立性和安全性。

1. 图解:RISC-V 地址转换细节

这张图展示了 RISC-V 架构下的虚拟地址到物理地址的转换过程。它使用了三级页表结构,分为 L2、L1 和 L0 三级,每一级都起到逐步定位的作用。

左侧:虚拟地址(Virtual Address)
  • 虚拟地址被分成了几部分,从高位到低位依次是:
    • L2L1L0:每一级各 9 位,用来在各级页表中找到对应的页表项(PTE)。
    • Offset:最后 12 位,用来标识具体的页面偏移量。

在这个 39 位的虚拟地址中,最高的 27 位(L2、L1 和 L0)用于索引三级页表,最低的 12 位直接用于偏移,无需转换。

中间:页表层次结构

RISC-V 的页表使用三级结构(L2、L1 和 L0),每级页表的工作如下:

  1. L2 页表

    • 虚拟地址的 L2 部分(9 位)用于在 L2 页表中查找一个页表项(PTE),这个 PTE 包含指向 L1 页表的地址。
  2. L1 页表

    • L1 部分(9 位)用于在 L1 页表中找到下一级的页表项,这个页表项指向 L0 页表的地址。
  3. 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 不直接使用物理地址,而是使用虚拟地址。通过页表,操作系统将虚拟地址转换成物理地址:

  1. 分级查找:虚拟地址中的 L2、L1 和 L0 部分分别对应三级页表的查找。每一级找到一个页表项(PTE),最后一级找到指向物理页的地址。
  2. 偏移合成:最终找到的物理页号(PPN)和虚拟地址中的 Offset 组合,生成完整的物理地址。
  3. 访问控制:页表项中的标志位控制页面的访问权限和状态。如果访问无效页面,CPU 会触发错误,保护系统安全。
比喻理解

可以把这个过程想象成查找仓库货物的过程:

  1. 三级页表:就像是三个不同级别的地图(城市地图、区地图、具体位置图),逐步帮助你找到具体的仓库。
  2. 偏移量:最终找到的仓库位置,再加上偏移(类似于货架的具体层数)确定最终的货物位置。
  3. 标志位:就像仓库的门禁卡,不同权限的门禁卡只能进入特定的区域,确保物品的安全。

从实际需求和问题出发,来通俗地解释为什么需要设计这样复杂的页表结构。

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?

限制地址空间的大小有几个原因:

  1. 减少页表大小:如果使用完整的 64 位地址空间,每个进程的页表会变得非常庞大,浪费内存资源。而使用 39 位地址空间(即 512 GB),页表的大小和管理开销会显著减少。

  2. 硬件复杂性降低:只需要处理 39 位地址空间,硬件设计会更简单,成本更低,转换效率更高。

  3. 现实需求:大多数应用程序并不需要 512 GB 以上的地址空间,提供 512 GB 已经足以满足大部分需求。如果以后需要更大的地址空间,RISC-V 还支持其他模式(如 Sv48,可以提供 48 位的虚拟地址空间)。

Sv39 虚拟地址到物理地址的转换过程

  1. 三级页表结构

    • RISC-V CPU 在 Sv39 模式下,将虚拟地址分为三级来进行转换,每级使用 9 位来定位。
    • 虚拟地址的前三个 9 位(共 27 位)用于三级页表查找,最后 12 位用于页面内的偏移量。
  2. 页表结构

    • 页表是一个三层树状结构,每一层包含一个 4KB 的页表页面。
    • 每个页表页面可以存储 512 个页表项(PTE)。
    • 顶层的页表页面包含指向下一层页表页面的物理地址。通过三级查找可以找到最终的物理页地址。
  3. 地址转换过程

    • 第一步: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 中的存储单元,每个存储单元有一个物理地址
  • 虚拟地址:程序指令使用的地址,经过页表映射后转换为物理地址。
  • 虚拟内存:不是一个实际的物理对象,而是内核用来管理物理内存和虚拟地址的抽象机制。通过虚拟内存,操作系统可以提供更大、更灵活的地址空间,并隔离不同进程的内存。

通俗解释

可以把这个过程想象成一本复杂的“地址翻译字典”:

  1. 三级翻译:虚拟地址的前 27 位像是三级目录索引,逐步帮我们找到目标物理页面。最终的物理地址则通过这个“翻译字典”从虚拟地址转换而来。

  2. 页面权限:每个页表项的标志位就像是“访问控制锁”,决定页面的使用规则。比如某些页面是只读的,某些页面是用户模式禁止访问的,确保安全和稳定。

  3. 页面错误:当我们试图访问一个不存在的页面时,就会触发页面错误。可以理解为“查不到地址”,系统会引发警报,交给操作系统来处理。

为什么要设计这样的结构?

  • 隔离内存空间:通过页表,每个进程都可以有自己的虚拟地址空间,进程间不会相互干扰。
  • 灵活的内存管理:操作系统可以灵活地分配、回收内存,同时支持虚拟内存,让程序可以使用超出物理内存容量的空间。
  • 安全控制:标志位为每个页面设置了严格的访问权限,防止不当的内存操作。
  • 效率:三级结构使得页表只需分配必要的部分,不用占用过多的物理内存。

 


原文地址:https://blog.csdn.net/m0_74331272/article/details/143578112

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!