0%

OS_Lab6_实验报告

PS: 该实验报告作为个人感悟将同步至我的个人博客网站

一. 思考题

Thinking 6.1 如果不关闭自己不会用到的管道端会怎么样?
如果不关闭不使用的管道端,会导致对应文件描述符(fd)的引用计数无法正确清零。因为我们在判断管道一端是否关闭时,依赖的是 pageref(fd) == pageref(pipe)。如果例如读者没有关闭写端,那么即使真正的写进程退出了,管道数据结构中的写端引用计数仍不为0。这会导致读者在读取空管道时,误以为还有写者存活,从而陷入死循环的阻塞等待(syscall_yield)中,永远无法读到 EOF 并退出。

Thinking 6.2 示例代码中,父进程操作管道的写端,子进程操作管道的读端。如果现在想让父进程作为“读者”,代码应当如何修改?
需要将父子进程中关闭端口和读写操作的逻辑对调:

  • 父进程(读者)修改为
1
2
3
4
5
close(fildes[1]); 
read(fildes[0], buf, 100);
printf("parent-process read:%s", buf);
close(fildes[0]);

  • 子进程(写者)修改为
1
2
3
4
close(fildes[0]); 
write(fildes[1], "Hello world\n", 12);
close(fildes[1]);

Thinking 6.3 MOS 中的管道缓冲区大小 PIPE_SIZE 过小有什么影响?缓冲区大小是怎么影响吞吐和切换效率的?

  • 影响:如果 PIPE_SIZE 过小(如 MOS 中仅为 32 Byte),写者会非常容易将缓冲区写满进而被阻塞,读者也会频繁读空缓冲区而阻塞。
  • 对吞吐和切换效率的影响:缓冲区过小会导致父子(读写)进程之间发生极其频繁的进程调度与上下文切换(Context Switch)。由于进程切换本身需要保存和恢复现场,这会带来巨大的时间开销,使得真正用于数据拷贝的 CPU 时间占比下降,最终导致整个系统的吞吐量急剧降低,切换效率低下。

Thinking 6.4 上面这个恒等式 (pageref(rfd) + pageref(wfd) == pageref(pipe)) 在所有时刻都成立吗?请给出你的分析过程,如果不成立,是否会造成误判?

  • 不一定在所有时刻成立
  • 分析过程:修改页面引用计数(map/unmap 操作)并不是一步完成的,而是对 fd 页面和 pipe 页面分别进行系统调用。由于在两次独立的系统调用之间可能会发生时钟中断,导致进程被调度走,因此在这两个操作的间隙,恒等式是不成立的。
  • 造成误判:会。例如,在写端 close 过程中,如果先 unmappipe,此时 pageref(pipe) 减小了,但 pageref(wfd) 还没减。如果此时切换到读进程,读者检查到 pageref(rfd) == pageref(pipe) 恰好成立(实际上写端还没完全关掉),就会误判写端已关闭而提前退出。

Thinking 6.5 上面这种不同步修改 pp_ref 而导致的进程竞争问题在 user/lib/fd.c 中的 dup 函数中也存在。请结合代码模仿上述情景,分析一下我们的 dup 函数中为什么会出现预想之外的情况?

  • dup 函数的作用是复制文件描述符,它需要依次 map 文件描述符页面(fd)和对应的管道数据缓冲区(pipe),这两个操作都会使对应的 pp_ref 增加1。
  • 情景:假设读者进程想要 dup 读端。如果它先 mapfd 页,此时 pageref(fd) 增加了1,但还没来得及 map pipe 页(引用未增加),时钟中断发生,切到了原本阻塞的写者进程。写者在判断读端是否关闭时,发现 pageref(wfd) == pageref(pipe) 居然成立了(因为 pipe 还没加1,导致等式意外左右相等),写者就会误以为读端已经全部关闭,从而错误地退出了。

Thinking 6.6 为什么在我们的 MOS 中,系统调用一定是原子操作呢?如果你觉得不是所有的系统调用都是原子操作,请给出反例。

  • 在我们的 MOS 系统中,以 syscall_ 开头的内核态系统调用函数是原子操作
  • 原因分析:MIPS 架构在触发异常进入内核态(执行 syscall)时,硬件会自动关闭中断(通过修改 CP0 的 STATUS 寄存器禁止中断),或者在陷入内核时的汇编入口中显式屏蔽了时钟中断。这意味着在内核执行这些 syscall_ 函数体时,是不可能被时钟中断打断并发生进程切换的,因此它们相对用户进程而言具备原子性。
  • 反例澄清:对于用户态的库函数(如 pipe_closeforkdup),它们内部包含了多个 syscall_ 调用,这些用户态函数不是原子操作,可能在两次 syscall 之间被打断。

Thinking 6.7 仔细阅读上面这段话,并思考下列问题

  • 控制 pipe_close 中的 unmap 顺序:可以解决。我们保证在 close 时,**先 unmap fd,再 unmap pipe**。这样即使中间被打断,pageref(pipe) 总是大于等于 pageref(fd) 的,此时判断管道关闭的条件 pageref(pipe) == pageref(fd) 绝对不可能意外成立,从而避免了读者的误判。
  • dup 函数中的 map 顺序:同理,会出现类似问题。为了保证在打断时 pageref(pipe) >= pageref(fd),我们在 dup 增加引用计数时,应当**先 map pipe,然后再 map fd**。这样即使在中间态发生切换,pipe 的引用计数已经升上去了,绝不会触发误判关闭的相等条件。

Thinking 6.8 思考以下三个问题。 (关于文件系统,ELF加载与bss段)
elf_load_seg()load_icode_mapper() 确保了 bss 段正确加载。在 ELF 文件中,bss 段不占据实际的物理磁盘空间,其在程序头中的表现为:memsz(在内存中需要的空间)严格大于 filesz(在文件中实际存放的数据大小)。
实现机制上:我们在分配虚拟内存时,根据 memsz 申请足够的内存页。然后从文件中拷贝出大小为 filesz 的数据到内存中,对于剩余的 memsz - filesz 这部分空间(即 bss 段),使用类似 bzero 的操作将其全部显式清零,从而满足了未初始化全局变量初始值为0的要求。

Thinking 6.9 在哪步,0 和 1 被“安排”为标准输入和标准输出?
通过阅读 user/init.c(即 init.b 的源码)可知:进程刚启动时,文件描述符表是空的。进程首先调用 opencons() 函数打开控制台,由于这是进程打开的第一个文件,系统自动分配了当前最小的空闲描述符,即 fd 0(标准输入)。紧接着,代码调用了 dup(0, 1),将描述符 0 映射到了描述符 1 上。至此,0和1都被“安排”好了,分别对应控制台的输入和输出。

Thinking 6.10 在 MOS 中我们用到的 shell 命令是内置命令还是外部命令?为什么 Linux 的 cd 命令是内部命令?

  • 在 MOS 中,我们使用的 lscatecho 都是外部命令(因为系统会为它们在磁盘上找到对应的 .b 文件,并通过 spawn 为其创建一个全新的子进程去执行)。
  • Linux 的 cd 必须是内部命令(内置命令)。因为 cd 的作用是改变当前进程的工作目录。如果把 cd 作为外部命令来 fork 一个子进程执行,子进程确实改变了它自己的工作目录,但在执行完毕退出后,父进程(shell 本身)的工作目录仍然是原来的样子,这完全违背了 cd 命令的设计初衷。

Thinking 6.11 在你的 shell 中输入命令 ls.b | cat.b > motd。观察到几次 spawn,几次销毁?

  • 几次 spawn:观察到 2 次 spawn。分别对应创建 ls.b 进程和 cat.b 进程。
  • 几次进程销毁:观察到 3 次 进程销毁。
  1. ls.b 执行完后自我销毁。
  2. cat.b 执行完后自我销毁。
  3. 因为有管道符号 |,shell 本身会 fork 出一个子 shell 来解析和执行管道右侧的指令。当右侧指令完毕后,这个临时创建的子 shell 也会销毁。

二. 难点分析

在 Lab6 中,最大的难点在于管道中的进程竞争问题以及 Shell 命令解析递归树的构建

  1. 管道竞争机制的脑内推理(Mind Map 替代方案)
    对于 _pipe_is_closed 引起的数据竞争,初看非常反直觉。我们需要在脑海中建立一个“引用计数时间线”。
  • 难点突破点:理解 syscall_ 的原子性与用户态库函数(非原子性)的区别。
  • 解决策略逻辑树:
  • 目标:不触发错误的 pageref(pipe) == pageref(fd) 判定。
  • 情况A (Close减少引用):必须让 fd 跑在前面(更早变小),所以顺序是 unmap(fd) -> unmap(pipe)。间隙状态:pipe > fd(安全)。
  • 情况B (Dup增加引用):必须让 pipe 跑在前面(更早变大),所以顺序是 map(pipe) -> map(fd)。间隙状态:pipe > fd(安全)。
  1. Shell 命令解析 (parsecmd) 的执行流
    parsecmd 的递归与 fork 结合是另一个难点。
  • 当读取到命令 A | B 时:
  1. 父 Shell 构建 pipe
  2. 父 Shell 执行 fork()
  3. 父进程行为:重定向输出到写端 -> runcmd(A) -> spawn(A) -> 等待子 Shell 结束。
  4. 子进程(子 Shell)行为:重定向输入到读端 -> **递归调用 parsecmd(B)**(处理 B 之后可能还挂着重定向符号) -> 最终 runcmd(B) -> 销毁自身。
    理清了这条从左到右、递归解析右子树的逻辑流程图,补全 parsecmd 的空缺代码便水到渠成了。

三. 实验体会

相比于 Lab0 刚接触命令行时的手忙脚乱,到了 Lab6,我感觉自己在系统底层的俯视感越来越强了。

这次实验把前几个 Lab 学到的知识完美地串联了起来:进程的 fork 与调度(Lab3)、共享内存机制的使用(Lab4)、文件系统的底层交互(Lab5),最终在 Lab6 汇聚成了一个肉眼可见、可交互的 Shell 和可以真正通信的管道。特别是用 spawn 读取硬盘上的 ELF 文件并分配内存的过程,让我彻底搞懂了从一行文本命令到可执行进程诞生中间到底发生了什么魔法。

其中最让我痛苦但也最有收获的,莫过于分析管道的竞争条件。作为一个经常在用户态写代码的计科生,以前很少考虑到几行连续的 C 代码中竟然会穿插时钟中断,还会因为执行顺序的微调而导致程序死锁或崩溃。把 mapunmap 的顺序颠倒过来就能化解一次致命的进程竞争,这种精巧的设计令人拍案叫绝。这也为后续面对更复杂的操作系统并发和内存管理问题打下了一剂强心针。总体来说,虽然查 BUG 的过程“苦也苦也”,但看到 MOS 终端跑出绿色的 pipe tests passed 时,成就感无与伦比。

OS_Lab5 实验报告

一、思考题

Thinking 5.1

  • 问题:如果通过 kseg0 读写设备,那么对于设备的写入会缓存到 Cache 中。这是一种错误的行为,在实际编写代码的时候这么做会引发不可预知的问题。请思考:这么做这会引发什么问题?对于不同种类的设备(如我们提到的串口设备和 IDE 磁盘)的操作会有差异吗?可以从缓存的性质和缓存更新的策略来考虑。
  • 解答
  • 引发的问题kseg0 是经过 Cache 缓存的内存段,而设备寄存器(MMIO)的值反映的是硬件的实时状态。如果读写经过 Cache,CPU 写入命令时可能只是修改了 Cache 行(Write-back 策略下),而没有真实写到硬件寄存器上,导致设备无法启动;同理,CPU 读取设备状态时,可能会直接读到 Cache 中的旧脏数据,而无法感知到硬件中断或就绪状态的变化。
  • 设备操作的差异
  • 串口设备:流式设备。通过缓存会导致连续写入的字符被合并,或者延迟发送;读取时如果 Cache 未失效,可能导致同一个字符被读取多次或漏读用户的按键输入。
  • IDE 磁盘设备:块设备。磁盘操作需要极为严格的时序控制(如写入 LBA 寄存器、发送 0x20 读命令、轮询 STATUS 寄存器等待忙碌位清零等)。如果 Cache 的乱序执行或合并写发生,会直接破坏与磁盘握手的状态机,导致驱动程序死循环卡死(如 wait_ide_ready 无限等待)或读写到完全错误的数据块。

Thinking 5.2

  • 问题:查找代码中的相关定义,试回答一个磁盘块中最多能存储多少个文件控制块?一个目录下最多能有多少个文件?我们的文件系统支持的单个文件最大为多大?
  • 解答
    通过查阅 fs.h 中的宏定义:
  1. 一个磁盘块存储的文件控制块数量:块大小 BLOCK_SIZE 为 4096 字节(4KB),文件控制块 struct File 的大小被填充限制为 FILE_STRUCT_SIZE(256 字节)。因此,一个块最多存储 4096/256=164096 / 256 = 16 个文件控制块(即 FILE2BLK 宏的值)。
  2. 单个文件最大大小:在 struct File 中,直接指针数组 f_direct 的大小 NDIRECT 为 10,一级间接指针 f_indirect 指向的一个磁盘块可以存放 4096/4=10244096 / 4 = 1024 个指针(即 NINDIRECT)。因此,单个文件最大支持 10+1024=103410 + 1024 = 1034 个磁盘块。最大大小为 1034×4KB=4,235,2641034 \times 4\text{KB} = 4,235,264 字节(约 4.03 MB)。
  3. 一个目录下最多能有的文件数:目录在文件系统中也是一种文件,因此它同样受限于单个文件的最大大小(1034 个块)。由于每个块能存 16 个控制块,所以一个目录文件最多能记录 1034×16=16,5441034 \times 16 = 16,544 个文件。(注:部分 JOS/MOS 实现中通过逻辑限制使得文件最大严格为 1024 块,此时最大文件数为 16384 个)

Thinking 5.3

  • 问题:请思考,在满足磁盘块缓存的设计的前提下,我们实验使用的内核支持的最大磁盘大小是多少?
  • 解答
    我们实验使用的内核支持的最大磁盘大小为 1 GB
  • 原因分析:在 MOS 的微内核文件系统服务(fs_serv)中,为了实现磁盘块缓存,服务进程会将整个磁盘的内容线性地映射到自身的虚拟内存空间中。根据 user/include/fs.h 中的定义,磁盘映射的虚拟基地址为 DISKMAP (0x10000000),而规定映射区的最大容量上限为 DISKMAX (0x40000000)。
    因为 DISKMAX 的十六进制值 0x40000000 刚好等于 1024×1024×10241024 \times 1024 \times 1024 字节,即 1 GB。由于内存映射的范围不能超过这个限制,因此内核所能支持的物理磁盘最大也不能超过 1 GB。

Thinking 5.4

  • 问题:在本实验中,fs/serv.h、user/include/fs.h 等文件中出现了许多宏定义,试列举你认为较为重要的宏定义,同时进行解释,并描述其主要应用之处
  • 解答
  1. BLOCK_SIZE (4096):定义了文件系统的基本物理和逻辑块大小(4KB)。应用之处:所有与文件偏移、磁盘映射、内存分配(按页对齐)相关的计算中均作为基本单位基数。
  2. FILE_STRUCT_SIZE (256):定义了单个文件控制块(FCB)的字节大小。应用之处:用于结构体占位符 f_pad 的计算,保证控制块按 256 字节对齐,使得一块物理块能完美容纳 16 个控制块。
  3. DISKMAP (0x10000000):文件系统服务进程将 IDE 磁盘块缓存到内存中的起始虚拟地址。应用之处:在 disk_addr(blockno) 函数中,用于将物理块号转换为服务进程的虚拟地址。
  4. PTE_LIBRARY (页表标志位):MOS 专有的共享内存标志。应用之处:在 fork 以及文件描述符映射时,带有此标志的页不会被置为写时复制(COW),而是直接父子共享,是实现文件系统 IPC 零拷贝数据传输和文件状态共享的核心。

Thinking 5.5

  • 问题:那么 fork 前后的父子进程是否会共享文件描述符和定位指针呢?请在完成上述练习的基础上编写一个程序进行验证。
  • 解答
    会共享。 因为 fd 所在的页面被设置了 PTE_LIBRARY 标志,fork 时不会进行写时复制,而是使得父子进程的虚拟地址映射到同一块物理内存。定位指针 fd_offset 存放在该页面中,因此也是共享的。
    验证程序如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <lib.h>
int main() {
int fd, r;
char buf[10];
// 假设 motd 文件内容为 "0123456789"
if ((fd = open("/motd", O_RDONLY)) < 0) {
user_panic("open failed");
}

if ((r = fork()) == 0) { // 子进程
read(fd, buf, 5);
buf[5] = '\0';
debugf("Child read: %s\n", buf); // 应输出 "01234"
exit();
}

wait(r); // 父进程等待子进程执行完毕

read(fd, buf, 5); // 接着读
buf[5] = '\0';
debugf("Parent read: %s\n", buf); // 应输出 "56789"

return 0;
}

如果输出为连贯的 0123456789,说明子进程读取后,父进程的 fd_offset 也跟着后移了,证明两者完全共享描述符和指针。

Thinking 5.6

  • 问题:请解释 File, Fd, Filefd 结构体及其各个域的作用。比如各个结构体会在哪些过程中被使用,是否对应磁盘上的物理实体还是单纯的内存数据等。
  • 解答
  • struct File (文件控制块)既是物理实体也是内存数据。它存放在磁盘的目录数据块中,记录了文件的元数据(文件名 f_name、大小 f_size、类型 f_type 以及直接/间接磁盘块指针 f_direct/f_indirect)。服务端 fs_serv 在进行路径解析 (walk_path) 和物理块查找 (file_block_walk) 时重度使用它。
  • struct Fd (通用文件描述符)纯内存数据。存放在用户进程的 FDTABLE 内存区。包含设备号 fd_dev_id、读写光标 fd_offset 和打开模式 fd_omode。它是 VFS (虚拟文件系统) 的抽象,用户在调用通用的 read/write 接口时,系统通过 Fd 的设备 ID 分发到不同的底层驱动。
  • struct Filefd (磁盘文件描述符)纯内存数据。它是 Fd 的超集,在内存布局的头部“继承”了 Fd,并在其后附带了文件专属的 IPC 标识号 f_fileid 和服务端文件状态副本 f_file。当底层调用 devfile_read 时,会将抽象的 Fd 强转回 Filefd,提取 f_fileid 并通过 IPC 向 fs_serv 发送读盘请求。

Thinking 5.7

  • 问题:图 5.9 中有多种不同形式的箭头,请解释这些不同箭头的差别,并思考我们的操作系统是如何实现对应类型的进程间通信的。
  • 解答
    在微内核文件系统的架构图(图5.9)中,主要有三种不同含义的交互箭头:
  1. 普通系统调用箭头(User -> Kernel):代表用户进程向内核陷入。OS 通过软中断(SYSCALL 汇编指令)触发异常,进入内核的 handle_sys 恢复现场并分发执行(如 sys_yield, sys_mem_alloc 等)。
  2. 进程间通信箭头(User Process <-> FS Server):代表独立的两个用户态进程之间的交互。OS 通过在内存中约定共享通信页(fsipcbuf)来实现。发送方打包请求参数,通过内核 sys_ipc_try_send 将物理页面共享映射到接收方的虚拟地址空间,并传递 FSREQ_* 类型码;接收方通过 sys_ipc_recv 阻塞接收,处理完成后同样通过 IPC 共享页发回数据或状态码。
  3. 硬件设备 I/O 箭头(FS Server <-> IDE Disk):代表服务端与底层硬件的物理交互。因为服务端在用户态,OS 为其提供了专门的特权系统调用 sys_read_devsys_write_dev,在此调用中,内核将虚拟地址通过直接寻址非缓存段(如 MIPS 的 kseg1),向真实的 IDE 寄存器(如 LBA 地址和 DATA 端口)发起 MMIO 读写指令,完成磁盘扇区的数据传输。

二、难点分析

本次 Lab5 的核心是从无到有在微内核环境下构建文件系统,其逻辑跨度涵盖了硬件读写到用户态库函数的封装,包含以下三大难点:

多级间接指针的磁盘块映射
这是文件系统中负责空间分配的最难逻辑。文件内容的逻辑块和磁盘物理块可能并不连续,尤其当逻辑块号超出 NDIRECT (10) 时,需要通过一级间接指针 f_indirect 去磁盘上读取存储指针的额外数据块。在实现这部分代码时,对于“查询但不分配(alloc=0)”和“遇到空洞需要分配(alloc=1)”的分支处理必须极其严谨。特别是在创建间接块本身时,需要连续调用两次分配和内存映射,且期间任何一步失败都需要抛出错误,否则会引发严重的磁盘块泄露或越权写错误。

跨进程 IPC 通信及缓存同步问题 (fsipc 机制)
在 Lab5 中,普通进程并不能直接读写文件,一切读写操作都被抽象成了发送给 fs_serv 进程的 FSREQ 消息。在调试 fsipc_map 和 fsipc_dirty 等函数时,很容易因为 fsipcbuf 共享页的数据封装类型错误导致服务端解析失败。此外,普通进程写完映射到自己内存里的文件数据后,如果在关闭前忘记通过 fsipc_dirty 通知服务端标记脏页,那么当服务端执行 fs_sync 或者关闭文件时,数据将不会被持久化到 IDE 磁盘中。

位图与设备控制的底层细节 (ide_read / ide_write 及 Bitmap 机制)
在最低层的硬件交互中,通过 LBA28 模式寻址 IDE 磁盘要求熟练掌握位运算,将 28 位的扇区号切割塞入四个不同的寄存器中。同时,通过位图来标记这几十万个磁盘块的分配状况时,涉及到大量的 %32 和 /32 的边界判断操作,一旦发生越界便会引起 user_panic。在这部分的编写中需要时刻留意字节、扇区(512B)和磁盘块(4KB)三者之间的尺寸转换公式。

三、实验体会

文件描述符与抽象能力的升华
通过亲手将 struct Filefd 与 struct Fd 的头部强制对齐对文件进行表示,并在 struct Dev 中填入专门的读写函数指针,我深刻理解了 Linux/Unix 系统下“一切皆文件”的设计哲学。这一层精妙的面向对象思想不仅简化了应用层的调用接口,更是将控制台的串口数据和磁盘的永久数据通过微内核系统完美的融为一体。

调试追踪能力的长足进步
文件系统的错误通常非常隐蔽。比如在写文件大小或者清空文件时少释放了一个磁盘块,可能在当时不会发生任何报错,直到系统运行了很久以后调用了 fs_check 测试或者是由于磁盘满导致挂载 Panic,才会让你追悔莫及。在此期间,通过在底层关键入口安插 debugf,以及反复跟踪 block_is_free 的断言报错点,极大锻炼了我从复杂的调用堆栈中锁定源头代码缺陷的能力。

“权限隔离”与微内核的优雅
当整个 Lab 跑通,在模拟器终端敲出字符的一瞬间,最让我震撼的是:所有这些操作磁盘块的复杂逻辑,统统是运行在一个无特权的用户态进程中的。内核没有几万行的庞大文件系统驱动,只负责提供一个可以读写特殊内存地址的 sys_read_dev 接口。这让曾经在课堂上学到的“微内核鲁棒性”变得真实可触——文件系统挂了,重启一下服务进程就好,整个操作系统的核心依然安然无恙。

OS_Lab4_实验报告

一、思考题

Thinking 4.1 思考-系统调用的实现

  • 问:内核在保存现场的时候是如何避免破坏通用寄存器的?系统陷入内核调用后可以直接从当时的 $a0-$a3 参数寄存器中得到用户调用 msyscall 留下的信息吗?我们是怎么做到让 sys 开头的函数“认为”我们提供了和用户调用 msyscall 时同样的参数的?
  • 解答
  1. 避免破坏通用寄存器:内核在保存现场时,利用了 MIPS 约定的两个内核保留寄存器 $k0$k1。陷入异常时,硬件自动将返回地址保存在 $epc 中,汇编代码先利用 $k0 暂存当前的栈指针 $sp 的值,接着将 $sp 切换为异常栈顶指针。因为 $k0$k1 在正常的用户态代码中是被严格保留不用的,所以这个中转过程完美避免了破坏任何用户通用的寄存器环境,随后利用保存好的栈依次将所有寄存器压栈。
  2. 不能直接读取:系统陷入内核后,不能直接从当时的 $a0-$a3 获取信息。因为在陷入内核态执行底层汇编跳转到系统调用分发函数(如 handle_sys)的过程中,可能会发生进一步的 C 语言函数调用,由于 MIPS 的传参规约,这些操作极有可能覆盖覆写 $a0-$a3
  3. “欺骗” sys_ 函数:系统会将异常发生瞬间的用户上下文封装成 Trapframe 并保存在栈中。在系统调用分发代码中,我们直接从栈上的 Trapframe 里提取出 $a0-$a3(即 tf->regs[4]tf->regs[7]),然后再显式地将这些值作为真正的实参去调用内核中 sys_* 对应的 C 函数。通过这种机制,参数被安全传递并伪装成了调用时的原样。

Thinking 4.2 思考-envid2env 的实现

  • 问:思考 envid2env 函数的细节:返回值有什么意义?对 envid 进行了怎样的处理?是否进行了合法性检查?
  • 解答
  1. 返回值意义:返回 0 代表成功在进程块数组中找到了对应的进程并成功返回指针;若返回 < 0(如 -E_BAD_ENV)则说明未能找到合法进程。
  2. 参数处理:如果传入的 envid 等于 0,函数会将其自动视为对当前正在运行的进程(curenv)的调用;若非 0,则通过解析 envid 的低位特征(例如通过宏 ENVX)获取其在 envs 进程数组中的偏移量,取出该进程控制块。
  3. 合法性检查:存在严格的合法性检查。首先会比对通过偏移取出的进程控制块自带的 env_id 是否与传入的 envid 一致;其次判断该进程状态是否处于未分配阶段(ENV_FREE)。并且函数还带有一个 checkperm 参数,如果为 1,系统会强制检查当前操作者是否有权限修改该目标进程(即目标进程必须是当前进程本身或其直接的子进程),防止越权操作。

Thinking 4.3 思考-mkenvid 函数细节

  • 问:mkenvid 函数是如何生成 envid 的?
  • 解答
    mkenvid 函数在创建进程时生成的 env_id 不是简单的数组索引。该 ID 的低位是进程控制块在 envs 数组中的索引偏移,而高位部分则存储了一个系统中不断累加的序列号。由于系统中进程会不断生成与销毁,同一个数组位点可能被反复回收再分配。引入高位序列号后,即使由于索引复用导致低位相同,新进程的全局 env_id 依然与已经销毁的旧进程不同。这防止了别的进程使用旧 env_id 发送 IPC 通信给复用该控制块的新进程,起到了强有力的安全性保障。

Thinking 4.4 思考-fork 的返回结果

  • 问:为什么 fork 在父进程和子进程中的返回值不同?内核是如何实现的?
  • 解答
    在父进程中,系统调用 sys_exofork 会在内核中完整分配出一个子进程控制块,并将父进程当前的上下文原封不动地复制给子进程,随后向父进程返回这个新子进程的 envid 作为函数返回值。
    对于子进程,虽然它的代码段与执行现场(Trapframe)与父进程完全相同,但是内核在创建它时,刻意将子进程结构体 Trapframe 里保存返回值的寄存器 $v0 修改为了 0。于是当子进程被操作系统的进程调度器首次挂载到 CPU 上并恢复现场执行时,硬件从它的 Trapframe 中恢复所有寄存器,此时 $v0 会变成 0。这就实现了同一个上下文分叉,却因为被内核做了不同的定制,使得父进程看到子进程 ID,子进程却看到返回值 0。

Thinking 4.5 思考-用户空间的保护

  • 问:在实现系统调用和页写入等异常时,内核是如何保护用户空间的?
  • 解答
    由于 MOS 系统对物理与虚拟内存做了隔离,在用户进程触发类似于 sys_mem_map 或者内存申请等系统调用时,内核会严格审查传入的虚拟地址参数,保证其不允许超出 UTOP 边界,这就断绝了用户程序跨界修改系统内核区(如 kseg0 等)的可能。此外,无论是申请内存还是父子进程地址映射,系统都会强制过滤标志权限(例如只能设置只读、可写、COW 等),任何不合理、不属于系统允许配置的硬件标志位都会在转换时予以抛弃或报错,彻底保护了用户态环境的纯洁。

Thinking 4.6 思考-vpt 的使用

  • 问:页目录自映射带来了什么好处?用户进程如何使用 vpt 数组?
  • 解答
    MOS 操作系统使用了“自映射页目录”机制,通过在初始化时将页目录的某一项指向自身的物理页,巧妙地将完整的二级页表结构映射到了用户地址空间中的一段虚拟地址(如 UVPT)。
    这样带来的巨大好处是,用户进程无需依赖高开销的系统调用(即不用陷入内核态),即可将 vptvpd 作为两个线性大数组直接查阅自身的页面映射与权限状态。这一特性在实现写时复制时对于父进程遍历 UTOP 之下的整个有效内存页极其方便;同时,因为该地址区段受内存权限的写保护,用户只能看不能写,兼顾了高效和安全。

Thinking 4.7 思考-页写入异常-内核处理

  • 问:当触发 TLB Mod 异常时,内核具体是怎么处理的?
  • 解答
    一旦用户对通过 Fork 设为写时复制(带 PTE_COW 标志且被限制为只读)的页面发起写操作,硬件将拒绝并触发页写入异常(TLB Mod),使得执行流落入内核。
    由于写时复制的分配属于用户进程自身行为,出于微内核设计,内核不对该页进行实际分配,只负责“反射异常”。内核会在当前出错的进程专有的用户异常栈(UXSTACKTOP 区域)上开拓空间,并手动伪造压入一个 Trapframe,记录发生错误一瞬间 CPU 的所有寄存器环境及中断处的异常地址(原 epc)。随后内核修改保存现场中的执行指针 $epc 为该进程之前通过 sys_set_tlb_mod_entry 注册好的用户异常处理程序的入口地址。最后退出异常分发并恢复环境,强行使 CPU 退回用户态,从那个指定的入口程序开始执行补救逻辑。

Thinking 4.8 思考-页写入异常-用户处理-1

  • 问:用户态的写时复制入口函数做了什么工作?
  • 解答
    在用户态异常函数 cow_entry 被唤醒后,它:
  1. 通过读取压在其异常栈上的 Trapframe 信息,拿到了引发页写入异常的真实虚拟内存地址 badvaddr
  2. 使用 sys_mem_alloc 的系统调用,为自己分配出新的一页物理页面,但暂时挂载到一个不冲突的临时地址(如 UCOW)上。
  3. 通过内存拷贝,将由于异常没能成功写下去的那个“旧页面”上的原有数据全部“深拷贝”到 UCOW 映射出的那块新页里。
  4. 利用 sys_mem_map 将带有全新物理页并带有可写标志的虚拟内存覆盖重映射到刚刚的报错地址 badvaddr。这使得那个位置正式解除了 COW 限制,变成了进程私享的可写页,最后解除临时的 UCOW 映射。

Thinking 4.9 思考-页写入异常-用户处理-2

  • 问:如何在完成写时拷贝后安全地退回原先的执行流?
  • 解答
    既然在复制与映射已经做完,就应当去继续执行导致异常的那条写指令。因为之前并非处于普通的函数跳转,无法使用 jr $ra 返回,所以程序必须在此时重新充当“调度器”的角色:通过特殊设计的汇编,将堆栈指针 sp 指向内核之前在异常栈中精心存入的那个 Trapframe。然后依照与系统调用相仿的逻辑,逐一重置全局寄存器,接着取出中断时报错的那条指令地址(epc),最后平滑地跳回该地址继续运行代码。此刻目标页面已被换成了可读写的新页面,指令就能成功写下数据了。

二、难点分析

本次 Lab4 的难度相较于前三次实验有极大的跃升,不仅要求对寄存器、内存架构有细致入微的了解,还涉及大量的用户态/内核态之间的横跳调度。分析得出本次实验的三大难点:

  1. 进程间通信(IPC)机制与挂起状态的同步管理
    在实现 sys_ipc_recvsys_ipc_try_send 时,需要深刻理解系统阻塞一个进程的真谛。接收函数要求我们手动修改进程状态至 ENV_NOT_RUNNABLE 并放弃 CPU 进行调度重分配;发送进程除了判定被发送对象的 recving 标志位之外,还需要注意把目标进程状态及时拉回 ENV_RUNNABLE,并跨进程将页面映射关系写到对方控制块的特定指针区里。由于在测试用例如 pingpong 里收发操作高度异步,如果进程间状态的转换哪怕慢了一步或者遗漏了对调度队列的处理,就会导致死锁。
  2. 写时复制(COW)实现中的内存判断
    利用 fork 时实现完整的 COW 机制极其考验对于操作系统物理页管理和虚拟页保护机制的掌握。在使用 duppage 遍查 vpt 中低于 UTOP 的每一页虚拟内存进行拷贝时,不仅需要绕过无效页(未分配),还需要区分共享页面(PTE_LIBRARY 标志)与数据页。必须慎之又慎地为这两种页面制定不同的保护与分配标志(如读写变只读+COW),并将修改映射反映给原进程和子进程两者,一不留神就会导致父子互相伤害。
  3. 用户态异常栈(UXSTACK)的现场保存及汇编倒带
    内核无法包办 COW 异常恢复的核心原因是进程本身更清楚怎么处理自己的内存。在这个微内核思想中,如何手写一段汇编代码让其通过指针去强行还原 CPU 的寄存器快照是个非常大的门槛。在内核里构造一个位于用户栈指针底部的 Trapframe 并实现跳转,计算字节偏移量错了一次就会引发整个 MIPS 架构的执行流乱跑乃至 TLB 重填死循环。调试这部分汇编是整个实验中时间投入最大的难点。

三、实验体会

  1. 在以前操作系统的理论课堂上,虽然也学习到了诸如“写时复制”、“虚拟内存页表”、“自映射”等概念,但它们就像是云端的海市蜃楼。直到在这次系统调用的实验中,一步一步地编写寄存器的恢复与跳转,手动分配 badvaddr 的新物理页,我才真正理解到操作系统是如何通过一次次的软硬件协同和“障眼法”来维系一个高效安全的计算机底层环境的。
  2. 完成此次实验后,我不禁对操作系统研发先驱们设计的精巧程度感到敬佩。无论是页目录使用自映射,从而省去了每次探查内存分配情况都要通过系统陷入的开销,还是在中断保存时使用专用不透明异常栈的做法,每行底层机制的设计都洋溢着微内核思想中的极简与克制,将安全底线与效率提升拿捏得极其精准。
  3. 本次实验可以说是“写代码半小时,修 Bug 三整天”。由于错误的隐蔽性和发作的滞后性——也许是子进程返回寄存器少了一句修改,也许是页异常恢复的时候少弹出了四个字节,这些错误通常要在代码历经成千上万次 CPU 轮转后才会引发 panic 或断言失败。这段排错经历极大地提升了我对于利用 GDB 的异常向量追踪能力,也让我意识到了 C 语言指针和地址解引在底层系统开发中严格检查边界的极端重要性。整体来看,当跑通测试样例的一瞬间,所有繁复劳动都被巨大的获得感彻底覆盖,非常充实。

OS_Lab3_实验报告

一.思考题

Thinking 3.1

e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_V 实现了页目录自映射
这段代码将进程页目录中的某一个表项(对应 UVPT 虚拟地址段)指向了页目录自身的物理地址。这样一来,无论是在内核态还是用户态,只需要通过访问固定的虚拟地址 UVPT,就能像访问二维数组一样便捷地查阅和修改整个虚拟地址空间的页表映射情况,避免了在内存中额外维护一套复杂的数据结构。

Thinking 3.2

data 参数是一个上下文指针,在本次实验中它实际传入的是 当前进程控制块的指针 (struct Env *e)

不可以没有这个参数。 elf_load_seg 作为一个通用的 ELF 加载函数,它并不知道这些段要加载给哪一个进程。通过 data 参数将进程指针传给回调函数 load_icode_mapper,回调函数内部才能知道应该调用 page_insert 将分配的物理页插入到哪个进程的页目录 (env->env_pgdir) 中。

Thinking 3.3

结合 elf_load_seg 的参数和实现,该函数主要处理了以下几种页面加载情况:

  1. 常规段映射 :段的大小和地址正好是页对齐的,直接按页拷贝。
  2. 非页对齐映射 :段的起始地址或结束地址不在页边界上,需要处理 offset 偏移量。
  3. .bss 段处理(零初始化区) :当分配的内存大小(sg_size)大于实际 ELF 文件中存储的数据大小(bin_size)时,多出来的部分就是 .bss 段。此时 src 指针为 NULL,回调函数只需要分配物理页建立映射,不需要执行拷贝。

Thinking 3.4

env_tf.cp0_epc 存储的是 虚拟地址

因为当进程被调度运行,底层汇编执行 eret 指令从内核态返回用户态时,CPU 会将 PC 指针强制置为 EPC 寄存器中的值。随后 CPU 在用户态下发出的取指地址都会经过 MMU/TLB 的地址翻译,所以这里必须填入 ELF 镜像中指定的程序入口虚拟地址。

Thinking 3.5

kern/traps.c 中,异常处理函数地址被填入了 exception_handlers 数组:

  • 0 号异常 :处理函数为 handle_int,在 kern/genex.S 中通过 NESTED(handle_int, ...) 宏定义实现。
  • 1 号异常 :处理函数为 handle_mod,在 kern/genex.S 中实现(TLB 修改异常)。
  • 2、3 号异常 :处理函数为 handle_tlb,在 kern/genex.S 中通过 BUILD_HANDLER tlb do_tlb_refill 宏展开实现。

Thinking 3.6

  • 开启时机 :在用户态程序运行时是开启的。我们在 env_alloc 中将新进程的 cp0_status 赋予了 STATUS_IESTATUS_IM7。每次进程通过 env_pop_tf 恢复上下文并执行 eret 返回用户态时,硬件中断就会被开启。
  • 关闭时机 :在陷入内核处理异常/中断时是关闭的。当异常发生进入 exc_gen_entry 时,汇编代码通过 and t0, t0, ~(STATUS_UM | STATUS_EXL | STATUS_IE) 显式清除了 IE 位,关闭了中断,防止在内核处理异常时发生嵌套。

Thinking 3.7

操作系统根据时钟中断切换进程的流程如下:

  1. 硬件触发 :硬件 Timer 计时器到达设定值,触发 7 号硬件中断。
  2. 异常分发 :CPU 陷入内核,PC 指向 0x80000180,执行 entry.S 中的 SAVE_ALL 保存当前进程上下文到内核栈。
  3. 识别与调度 :跳转到 handle_int,识别出是时钟中断,调用 kern/sched.c 中的 schedule(0)
  4. 队列轮转schedule 将当前进程时间片减一。若用完,则将其从就绪队列头拔下插到队尾,并选出新的队头进程。
  5. 恢复执行 :调用 env_run,更新当前页目录,调用 env_pop_tf 恢复新进程的上下文,最后执行 eret,CPU 开始运行新进程。

二.难点分析

Lab 3 的难点主要集中在异常体系的构建和软硬件结合的上下文切换。

难点一:异常分发查表中的位运算巧思

难点解析:entry.S 中,我们需要根据 CP0 Cause 寄存器中的 ExcCode 去查 exception_handlers 数组。ExcCode 位于寄存器的第 2~6 位。

精妙之处在于:函数指针在 32 位机器下正好占 4 字节(等于左移 2 位)。因此,我们不需要将 ExcCode 右移回最低位再乘以 4,而是直接使用掩码 0x7C (0111 1100) 按位与,截取出来的数值 天然就是目标函数在数组中的字节偏移量 。这极大地优化了异常处理底层的指令开销。

难点二:进程上下文切换的“偷梁换柱” (env_run)

难点解析: 进程切换并非简单的方法调用。其难点在于深刻理解 KSTACKTOP - 1 的含义:当异常发生时,硬件和底层汇编已将旧进程的寄存器现场压入了内核栈。env_run 必须精确地将这段内存拷贝到旧进程的 PCB->env_tf 中保存;随后,将新进程的页目录基址赋给 cur_pgdir 以切换内存视野;最后通过 env_pop_tf 将新进程的 env_tf 重新装载到硬件寄存器中。

难点三:ELF 加载与 .bss 段的陷阱 (load_icode_mapper)

难点解析: 在使用回调函数映射 ELF 段时,极易忽略物理页指针与虚拟地址的转换(必须使用 page2kva(p) 获取内核虚拟地址才能 memcpy)。此外,.bss 段在磁盘中无实际数据,传入的 srcNULL,此时只需映射清零的物理页,绝不能直接进行 memcpy,否则会引发 Kernel Panic。


三.实验体会

这次 Lab 3 的实验相比于 Lab 2,代码量虽然不多,但逻辑深度有了质的飞跃。一开始面对各种繁杂的汇编宏(如 SAVE_ALLRESTORE_ALL)和进程状态的转换感到十分困惑。尤其是“伪造现场”将 ELF 入口地址强行塞入 cp0_epc,再利用 eret 巧妙启动进程的设计,让我拍案叫绝。

通过在纸上画图推演 TAILQ 双向队列在 schedule 时间片轮转中的变化,并在网上查阅 MIPS 架构 CP0 寄存器的手册,我终于打通了硬件中断触发到软件系统调度的任督二脉。看到终端里两个死循环进程在时钟中断的驱动下完美交替运行,极大地满足了我的成就感!这也为我后续理解 Lab 4 的 IPC 和 fork 写时复制机制打下了坚实的基础。

OS-Lab2-实验报告

思考题

Thinking 2.1

1.虚拟地址 2.虚拟地址

Thinking 2.2

一.

用宏实现提高了数据结构的多态性,通常如果我们不用宏,就需要定义一个包含 void *data 的通用链表节点,然后在存取数据时进行繁琐且不安全的强制类型转换;或者为 StudentProcessFile 每种不同的数据类型都手写一遍对应的链表增删改查函数。

宏在预处理阶段直接展开为对应类型的指针操作(如 struct type *le_next)。这使得编译器可以在编译阶段严格检查指针的类型匹配。保证了严格的类型安全。

二.

链表种类 单向链表 双向链表 循环链表
结构特点 节点只包含一个 next 尾随指针 包含 next 一级指针和精妙的 **le_prev 二级前驱指针 包含普通的一级 nextprev 指针,首尾相接形成一个闭环
插入性能 在头部插入:O(1)O(1)
在已知节点插入:O(1)O(1)
在已知节点插入:O(n)O(n)
均为O(1)O(1) 均为O(1)O(1)
删除性能 O(n)O(n) O(1)O(1) O(1)O(1)

Thinking 2.3

C

Thinking 2.4

一.

在多进程操作系统中,每个进程都有自己的虚拟地址空间,如果TLB中仅仅缓存VA->PA的映射关系,会导致TLB污染和巨大的性能开销

而ASID引入之后,TLB 的每一个表项不仅记录 VA 到 PA 的映射,还会打上一个“标签”。TLB 的查找逻辑变成了:只有当 CPU 发出的虚拟地址以及当前 CPU 寄存器中的 ASID 与 TLB 表项中的 VA 和 ASID 完全匹配时,才算命中。

二.

根据 MIPS32 4Kc 官方手册的硬件规范,在 MIPS32 架构中,管理 TLB 查找和写入的关键协处理器寄存器是 EntryHi 寄存器(CP0 Register 10, Select 0) 。在 EntryHi 寄存器中,包含一个专门用于存放当前进程地址空间标识符的位域,就是ASID字段,这个字段占据了 EntryHi 寄存器的最低8位,可以组合出256种不同的状态

Thinking 2.5

一.

tlb_invalidate是C语言函数,它在内部调用了由汇编语言编写的tlb_out函数

二.

tlb_invalidate 的作用是:在 TLB 中查找与指定虚拟地址和进程标识符相匹配的表项,如果存在,则将其清空,以防止 CPU 使用过期的地址映射。

三.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
LEAF(tlb_out)                 # 定义一个叶子函数名为 tlb_out
.set noreorder # 告诉汇编器不要重排指令
mfc0 t0, CP0_ENTRYHI # 将协处理器 CP0 的 EntryHi 寄存器当前的值读出,保存在通用寄存器 t0 中。
mtc0 a0, CP0_ENTRYHI # 将函数参数 a0(由 tlb_invalidate 构造好的包含目标 VA 和 ASID 的值)写入 CP0_ENTRYHI 寄存器。
nop # 空指令,用于解决 CP0 寄存器写入的硬件延迟。

/* Step 1: Use 'tlbp' to probe TLB entry */
tlbp # TLB Probe 探测指令。硬件会自动在 TLB 中查找与当前 CP0_ENTRYHI 匹配的项。如果找到,将索引写入 CP0_Index;找不到则将 CP0_Index 的最高位(符号位)置 1。
nop # 延迟槽,等待 tlbp 硬件查找完成。

/* Step 2: Fetch the probe result from CP0.Index */
mfc0 t1, CP0_INDEX # 将探测结果从 CP0_INDEX 寄存器读出,存入通用寄存器 t1。
.set reorder # 恢复汇编器的指令自动重排功能。
bltz t1, NO_SUCH_ENTRY # Branch if Less Than Zero。如果 t1 < 0,则跳转到 NO_SUCH_ENTRY 标签。

.set noreorder # 再次禁用指令重排,准备向 TLB 写入数据。
mtc0 zero, CP0_ENTRYHI # 将 CP0_ENTRYHI 寄存器清零。
mtc0 zero, CP0_ENTRYLO0# 将 CP0_ENTRYLO0 寄存器(偶数物理页项)清零,表示该页无效。
mtc0 zero, CP0_ENTRYLO1# 将 CP0_ENTRYLO1 寄存器(奇数物理页项)清零,表示该页无效。
nop # 延迟槽,等待上述写入完成。

/* Step 3: Use 'tlbwi' to write CP0.EntryHi/Lo into TLB at CP0.Index */
tlbwi # TLB Write Indexed 指令。将当前 CP0_ENTRYHI、ENTRYLO0、ENTRYLO1 的值(现在全是0)写入到 CP0_INDEX 指定的 TLB 槽位中。这实质上就完成了将该 TLB 项“清空/失效”的操作。

.set reorder # 恢复指令重排。

NO_SUCH_ENTRY: # 标签:当 TLB 中本来就没有这个表项时,直接跳到这里。
mtc0 t0, CP0_ENTRYHI # 将最开始保存在 t0 中的原 CP0_ENTRYHI 的值写回 CP0。
j ra # 跳转到返回地址,返回调用者 tlb_invalidate。
END(tlb_out) # 标记函数结束。

Thinking 2.6

一.纯硬件流程

当用户程序执行一条访存指令(如 lwsw)时,CPU 会产生一个虚拟地址,这个阶段没有任何OS相关的函数被调用

二.软硬件协同流程

当硬件在TLB中找不到对应的映射时,此时它会触发异常,将控制权交给操作系统,开始了内核函数调用

函数调用和CPU访存之间,CPU 访存依赖 TLB 硬件实现极致的性能;而当我们编写的页表管理函数(page_alloc, page_insert 等)构成了 TLB 的坚实后盾。硬件查不到时,由软件函数去完整的两级页表中去兜底。Lab2 中的 passive_alloc 等函数调用 不是在进程创建时就立刻执行的 。它们是以 CPU 访存失败为契机,被被动且延迟触发的。这种设计极大地节省了物理内存。CPU 硬件访存失败 \rightarrow 触发异常 \rightarrow 执行内核内存管理函数分配物理页 \rightarrow 刷新 TLB \rightarrow CPU 重新访存成功。在这个闭环中, 函数调用是连接“虚拟内存假象”与“物理内存现实”的桥梁

Thinking 2.7

x86架构的内存管理由硬件承担了大量的工作,虚拟地址首先要经过分段机制转换为线性地址,再经过分页机制转换为最终的物理地址。x86的CPU中还有一个专门的硬件单元叫MMU,当CPU访问一个虚拟地址且TLB Miss的时候,MMU会暂停当前指令,顺着CR3寄存器去物理内存中逐级遍历页表,如果找到了,硬件会自动把表项填入 TLB 并继续执行指令,只有当硬件发现页表项无效时,才会抛出 Page Fault 异常交给 OS 处理。

MIPS的设计硬件比较简单,复杂逻辑交给了软件。x86 TLB Miss发生时,硬件 MMU 自动去查页表,操作系统在这一步完全不知情。OS 只需要在进程创建时把页表建好,把基址扔给 CR3 寄存器就行了。而MIPS在TLB Miss发生时,会抛出一个TLB Refill异常,捕获这个异常必须执行实验里写的软件中断处理函数,由 OS 自己的代码一步步查页目录、查二级页表,最后用汇编指令(如 tlbwr)把查到的结果手动塞进 TLB。

x86因为是硬件去查页表,硬件必须知道页表长什么样。MIPS因为是软件去查页表,所以 MIPS 硬件根本不关心你在内存里的页表长什么样、是几级页表、甚至是哈希表都可以 。

x86没有固定的硬件空间划分。内核态和用户态的隔离完全依赖于页表项中的权限位。MIPS硬件直接将 32 位虚拟地址空间“一刀切”硬编码划分成了几个段:kusegkseg0kseg1kseg2。在 kseg0kseg1 中的地址, 根本不需要经过 TLB 和页表转换,直接抹去最高几位就变成了物理地址

难点分析

lab2中,我觉得一共有以下三个难点:

难点一:queue.h**le_prev 二级指针

难点解析 :BSD 双向链表最反直觉的地方在于,它的 prev 指针不是指向前一个“节点实体”,而是指向前一个节点内部的 next 指针本身的物理地址 。

难点二:两级页表寻址过程的位运算拆

难点解析 :在 32 位架构下,如何将一个虚拟地址通过页目录和页表,最终拼凑成物理地址。尤其是在 pmap.h 中通过大量的宏(如 PDX, PTX, PTE_ADDR)进行位运算掩码提取,极易混淆。

难点三:MIPS 软件 TLB 重填与缺页中断

难点解析 :相较于 x86 由硬件 MMU 自动遍历页表,MIPS 架构采用了软件 TLB机制。这要求操作系统必须亲自接管 TLB 缺失异常,并通过代码(tlb_miss_entry -> _do_tlb_refill -> passive_alloc)进行软硬件协同,这也是实验中最复杂的控制流。

实验体会

这次实验相比于lab1来说是难度飙升,exercise的数量和thinking的数量比以前也多,这次lab难点困扰了我很久,一开始没分清页表项个数和所占物理内存大小的区别,不了解二级页表的结构,感觉到非常困惑,随着实验的进行,我在网上查询资料以及翻阅os指导书以后,终于对内存分配有了更深层次的理解,这也为我以后学习os打下了一个非常牢固的基础!

OS_Lab1_实验报告

前置信息

Lab1对于Lab0的跳跃对我来说有点大,因此需要利用AI掌握一些前置信息。

ELF

首先是要了解ELF,先看一段ELF代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/* This file defines standard ELF types, structures, and macros.
Copyright (C) 1995, 1996, 1997, 1998, 1999 Free Software Foundation, Inc.
This file is part of the GNU C Library.
Contributed by Ian Lance Taylor <ian@cygnus.com>.

The GNU C Library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public License as
published by the Free Software Foundation; either version 2 of the
License, or (at your option) any later version.

The GNU C Library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.

You should have received a copy of the GNU Library General Public
License along with the GNU C Library; see the file COPYING.LIB. If not,
write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
Boston, MA 02111-1307, USA. */

#ifndef _ELF_H
#define _ELF_H

#include <stdint.h>

/* ELF defination file from GNU C Library. We simplefied this
* file for our lab, removing definations about ELF64, structs and
* enums which we don't care.
*/

/* Type for a 16-bit quantity. */
typedef uint16_t Elf32_Half;

/* Types for signed and unsigned 32-bit quantities. */
typedef uint32_t Elf32_Word;
typedef int32_t Elf32_Sword;

/* Types for signed and unsigned 64-bit quantities. */
typedef uint64_t Elf32_Xword;
typedef int64_t Elf32_Sxword;

/* Type of addresses. */
typedef uint32_t Elf32_Addr;

/* Type of file offsets. */
typedef uint32_t Elf32_Off;

/* Type for section indices, which are 16-bit quantities. */
typedef uint16_t Elf32_Section;

/* Type of symbol indices. */
typedef uint32_t Elf32_Symndx;

/* Lab 1 Key Code "readelf-struct-def" */

/* The ELF file header. This appears at the start of every ELF file. */

#define EI_NIDENT (16)

typedef struct {
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;

/* Fields in the e_ident array. The EI_* macros are indices into the
array. The macros under each EI_* macro are the values the byte
may have. */

#define EI_MAG0 0 /* File identification byte 0 index */
#define ELFMAG0 0x7f /* Magic number byte 0 */

#define EI_MAG1 1 /* File identification byte 1 index */
#define ELFMAG1 'E' /* Magic number byte 1 */

#define EI_MAG2 2 /* File identification byte 2 index */
#define ELFMAG2 'L' /* Magic number byte 2 */

#define EI_MAG3 3 /* File identification byte 3 index */
#define ELFMAG3 'F' /* Magic number byte 3 */

/* Section segment header. */
typedef struct {
Elf32_Word sh_name; /* Section name */
Elf32_Word sh_type; /* Section type */
Elf32_Word sh_flags; /* Section flags */
Elf32_Addr sh_addr; /* Section addr */
Elf32_Off sh_offset; /* Section offset */
Elf32_Word sh_size; /* Section size */
Elf32_Word sh_link; /* Section link */
Elf32_Word sh_info; /* Section extra info */
Elf32_Word sh_addralign; /* Section alignment */
Elf32_Word sh_entsize; /* Section entry size */
} Elf32_Shdr;

/* Program segment header. */

typedef struct {
Elf32_Word p_type; /* Segment type */
Elf32_Off p_offset; /* Segment file offset */
Elf32_Addr p_vaddr; /* Segment virtual address */
Elf32_Addr p_paddr; /* Segment physical address */
Elf32_Word p_filesz; /* Segment size in file */
Elf32_Word p_memsz; /* Segment size in memory */
Elf32_Word p_flags; /* Segment flags */
Elf32_Word p_align; /* Segment alignment */
} Elf32_Phdr;
/* End of Key Code "readelf-struct-def" */

/* Legal values for p_type (segment type). */

#define PT_NULL 0 /* Program header table entry unused */
#define PT_LOAD 1 /* Loadable program segment */
#define PT_DYNAMIC 2 /* Dynamic linking information */
#define PT_INTERP 3 /* Program interpreter */
#define PT_NOTE 4 /* Auxiliary information */
#define PT_SHLIB 5 /* Reserved */
#define PT_PHDR 6 /* Entry for header table itself */
#define PT_NUM 7 /* Number of defined types. */
#define PT_LOOS 0x60000000 /* Start of OS-specific */
#define PT_HIOS 0x6fffffff /* End of OS-specific */
#define PT_LOPROC 0x70000000 /* Start of processor-specific */
#define PT_HIPROC 0x7fffffff /* End of processor-specific */

/* Legal values for p_flags (segment flags). */

#define PF_X (1 << 0) /* Segment is executable */
#define PF_W (1 << 1) /* Segment is writable */
#define PF_R (1 << 2) /* Segment is readable */
#define PF_MASKPROC 0xf0000000 /* Processor-specific */

#endif /* elf.h */

这是一段C语言代码,指导书的注释非常详实,这段代码主要定义了几个结构体,有Elf32_Ehdr,Elf32_Phdr,Elf32_Shdr。

Ehdr(ELF header):

e_ident:主要用来识别是不是ELF文件

e_machine:目标架构,是给x86跑的还是ARM跑的

e_version:ELF的版本号,通常为1

e_entry:程序入口虚拟地址,操作系统把程序加载到内存后,CPU 第一条执行的指令就在这个地址(通常对应代码里的_start函数,而不是main)。

e_phoff:程序头表偏移量 e_shoff:节头表偏移量 e_flags:特定标志位,一般为0

e_ehsize:Elf32_Ehdr结构体本身的字节数 e_phentsize:程序头表中,每一个表项的大小

e_phnum:程序头表里有几个表项(程序被分成了几个Segment) e_shentsize:节头表中每一个表项的大小

e_shnum:节头表有几个表项(文件被分成了几个Section) e_shstrndx:节名字符串表的索引,如.text,.main存放的位置

Shdr(Section header):

sh_name:节的名称索引 sh_type:节的类型 sh_flags:节的权限标志,是否可写,是否需要在内存中分配空间

sh_addr:虚拟地址,如果这个节在程序运行时需要放到内存里,这里就是它期望被加载到的内存地址。如果不加载到内存(比如调试信息),这里就是 0。

sh_offset:文件内偏移量,这个节的实际数据(比如一堆机器指令)在当前文件里的起始位置。

sh_size:这个节占了多少个字节 sh_link:关联节的索引 sh_info:附加信息

sh_addralign:对齐方式,要求这个节在内存里的起始地址必须是某个数的整数倍

sh_entsize:表项大小,如果这个节里面存的是一张表(比如符号表,里面全是固定大小的条目),这里记录每个条目有多大;如果是一般代码/数据,这里为 0。

Phdr(Program header):

p_type:段的类型,最重要的是 PT_LOAD (1),意思是这个段必须被搬进内存里。其他的还有 PT_DYNAMIC(动态链接信息)等。

p_offset:文件内偏移量 p_vaddr_虚拟内存地址,操作系统应该把这个段的数据复制到内存中的哪个虚拟地址。

p_paddr:物理内存地址 p_filesz:文件大小,这个段在文件里占据了多少字节 p_memsz:内存大小,这个段在内存里需要占据多少字节

p_flags:段的访问权限 p_align:内存页对齐要求,规定这个段在内存中的起始地址 p_vaddr 和文件里的 p_offset 必须对齐到一个页面大小的整数倍(通常是 0x1000,即 4096 字节)。这方便操作系统利用内存页机制进行映射。

OS_Lab0_实验报告

PS:该实验报告作为个人感悟将同步至我的个人博客网站

一.思考题

Thinking 0.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 1. 进入目录并创建 README.txt,输出状态到 Untracked.txt
cd ~/learnGit
touch README.txt
git status > Untracked.txt

# 2. 在文件中添加内容,添加到暂存区,并输出状态到 Stage.txt
echo "这是我的第一次 Git 测试" > README.txt
git add README.txt
git status > Stage.txt

# 3. 提交 README.txt
git commit -m "提交README.txt,学号:24371083"

# 4.
cat Untracked.txt
cat Stage.txt

# 5. 修改 README.txt 文件,输出状态到 Modified.txt
echo "这是新增的第二行内容" >> README.txt
git status > Modified.txt

# 6.
cat Modified.txt

README.txt的不同:

1.用 touch 创建时,README.txt 仅仅存在于硬盘目录中。Git 知道这里多了一个文件,但 Git 尚未接管它的版本控制,因此把它标记为 Untracked files。

2.执行git add后,README.txt 的当前快照被复制到了暂存区。Git 开始追踪这个文件,并将其标记为 new file。

catModified.txt其结果和第一次执行add命令之前的status不一样,第一次显示的是Untracked files,第二次显示的是Changes not staged for commit。这种差异是由Git 的追踪机制决定的。

Thinking 0.2

add the file:git add

stage the file:git add

commit:git commit

Thinking 0.3

1.git checkout – print.c
||git restore print.c

2.git reset HEAD print.c$$
git checkout HEAD – print.c

3.git reset HEAD hello.txt

Thinking 0.4

1.第三次提交的哈希值为836a01eeb8fd9cda9fdbac333fdd91f5d6e196e5

2.git reset --hard HEAD^后只有第一次和第二次的提交

3.git reset --hard <‘hash’>后只剩第二次提交的哈希值

4.再次执行git reset --hard <‘hash’>又回到了原先版本

Thinking 0.5

1.终端上输出first

2.自动在当前目录创建output.txt文件并且往其中加入了second

3.third覆盖了second,output.txt文件中只有third

4.forth追加到了third的下一行

Thinking 0.6

command文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/bash
cat > test << 'EOF'
echo Shell Start...
echo set a = 1
a=1
echo set b = 2
b=2
echo set c = a+b
c=$[$a+$b]
echo c = $c
echo save c to ./file1
echo $c>file1
echo save b to ./file2
echo $b>file2
echo save a to ./file3
echo $a>file3
echo save file1 file2 file3 to file4
cat file1>file4
cat file2>>file4
cat file3>>file4
echo save file4 to ./result
cat file4>>result
EOF
bash test > result

result文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
Shell Start...
set a = 1
set b = 2
set c = a+b
c = 3
save c to ./file1
save b to ./file2
save a to ./file3
save file1 file2 file3 to file4
save file4 to ./result
3
2
1

思考题:

  1. echo echo Shell Start 会在屏幕上输出echo Shell Start
  2. ehco 'echo Shell Start’会输出Shell Start,因为反引号代表命令替换,会在一个子Shell中优先被执行
  3. echo echo $c>file1会将echo $c转移到file1文件中
  4. echo 'echo $c>file1’则会将 $c的值转移到file1中,但是对file1进行echo操作会输出空行,file1的内容被修改为3

二.难点分析

lab0作为OS的起点,课下实验主要和命令行操作有关。难点主要有两个,一是Makefile的构建,二是命令的实现。

作为一个对于shell等命令行0基础的小白,在Makefile的构建部分,会感觉到无从下手,摸不清Makefile写的顺序,难以理解它运行的底层逻辑,但是,后来通过练习发现,其实Makefile主要是以倒序为主,多练还是能大致掌握它的规则,对于命令的实现,主要是很难记得不同指令的格式,像上学期的co一样,需要不断查看指导书,不断使用man指令来学习对于某个指令的使用技巧,还有就是git部分,初学其实感觉很繁琐,因为git尤其是与reset,restore相关的部分,一次操作可能会影响全局,就像下象棋一样,每走一步要想三步,git同样如此,每个操作会带来对后续操作的巨大影响。

三.实验体会

体会与难点对应,对于Makefile的体会,我认为这种功能性的文件,尤其是为便利性服务的(其实也包括bash,shell)都是以练为主,多用自然就能熟悉,就像我的某位大佬室友,将笔记本装成了Linux系统,天天写命令行,这次Lab0不到一个小时就全部写完了,所以这次实验我感觉其实不涉及到什么灵感上的问题,不涉及到智商,主要是看课下作业的完成情况,自己完成课下,那么课上exam部分肯定没问题,对命令行再理解深刻一些,便能完成extra。

还有一个惨痛的教训,指导书上的思考题一定要在第一遍看的时候就思考出答案指导书上说读者课下自行查找的内容一定要去看,因为很可能extra就出这些内容,这一次就是awk指令,我没有自行学习,导致考试时无从下手,翻指导书也看不出来什么。哎!苦也苦也。

寒假

1.18号到家,到今天两天时间,回到家的第一感受就是感觉自己变懒了,两个小时的车程在北京其实还好,但是在家就感觉很长很长。

今天又给我的木吉他换了一套弦,不得不说那套弦有些许反人类,花了好大功夫才装上,接下来寒假我的目标吧,就是学会一首歌,这个一定要定好,不然没有目的的学就会导致很长时间琴技都没有进步,等这两天闲下来就开始学习!不能忘记还要学习一下java!

计组期末复习

来不及怀念物理了,现在登场的是计组理论!

章节

大二上期末

基础物理学

现在正式开始复习基物B,接下来会在这里记录我的复习进度。希望我的复习过程能给你一些感悟!

章节总结

基础物理学B(2)主要研究热学,气体动理论,分子物理,振动,波动,光学,量子物理。其中这些部分都有交集,难度最大的应该是量子物理,可是根据往年题来说,占比并不大。

气体动理论

公式回顾

1.理想气体状态方程

  • PV=mMmolRTPV=\frac{m}{M_{mol}}RT

  • P=nkTP=nkT

  • PMmol=ρRTPM_{mol}=ρRT

第三个公式对于某些题目来说尤为重要,不失为一种二级结论,虽然不是课本上总结的公式,但推导起来是很容易的。

2.理想气体压强公式

  • P=23n(12mv2)P=\frac{2}{3}n(\frac{1}{2}m\overline{v^2})

  • P=23nEkP=\frac{2}{3}n\overline{E_k}

3.温度的统计意义

  • Ek=32kT\overline{E_k}=\frac{3}{2}kT

这个地方的k是物质的量!!!

4.能量均分定理

  • Ek=i2kTE_k=\frac{i}{2}kT

5.理想气体的内能

  • E=mMmoli2RTE=\frac{m}{M_{mol}}\frac{i}{2}RT

6.麦克斯韦速率分布函数

  • f(v)=4π(m2πkT)32emv22kTv2f(v)=4\pi(\frac{m}{2\pi kT})^\frac{3}{2}e^{-\frac{mv^2}{2kT}}v^2

7.三种特征速率

(1) 最概然速率

  • vp=2kTm=2RTMmolv_p=\sqrt{\frac{2kT}{m}}=\sqrt{\frac{2RT}{M_{mol}}}

(2) 平均速率

  • v=8kTπm=8RTπMmol\overline{v}=\sqrt{\frac{8kT}{\pi m}}=\sqrt{\frac{8RT}{\pi M_{mol}}}

(3) 方均根速率

  • v2=3kTm=3RTMmol\sqrt{\overline{v^2}}=\sqrt{\frac{3kT}{m}}=\sqrt{\frac{3RT}{M_{mol}}}

8.玻尔兹曼能量分布律

(1)分子数密度按势能分布

  • n=n0eEpkTn=n_0e^{-\frac{E_p}{kT}}

(2)分子数密度按高度分布

  • n=n0emgzkTn=n_0e^{-\frac{mgz}{kT}}

(3)等温气压公式

  • P=P0eMRTgzP=P_0e^{-\frac{M}{RT}gz}

9.气体分子平均碰撞频率及平均自由程

  • Z=2πd2vn\overline{Z}=\sqrt{2}\pi d^2\overline{v}n

  • λ=12πd2n=kT2πd2P\overline{\lambda}=\frac{1}{\sqrt{2}\pi d^2n}=\frac{kT}{\sqrt{2}\pi d^2P}

总的来说,气体动理论就这些公式,其中值得关注的是一些符号的含义,PVT不用多说,需要注意的是n在这里均是分子数密度,即:

  • n=NVn=\frac{N}{V}

然后就是R,即摩尔气体常量:

  • R=8.31Jmol1K1R=8.31Jmol^{-1}K^{-1}

以及玻尔兹曼常量k:

  • k=1.38×1023J/Kk=1.38×10^{-23}J/K

习题及公式应用

2024-2025

第三题考察对方均根和最概然速率公式的记忆,代入上面的公式,很容易得到第一个空为32\frac{3}{2},第二个空涉及到我提到的关于ρ的理想气体方程,代入可以知道RTMmol=Pρ\frac{RT}{M_{mol}}=\frac{P}{ρ},所以答案为3Pρ\sqrt{\frac{3P}{\rho}}

同样,第四题也是考察对公式的记忆,压强变为一半,体积翻倍,密度变为原值的12\frac{1}{2},平均自由程公式为λ=12πd2n=kT2πd2P\overline{\lambda}=\frac{1}{\sqrt{2}\pi d^2n}=\frac{kT}{\sqrt{2}\pi d^2P},故平均自由程为原始的两倍

2022-2023

这道题与上面的题不一样,考察到了麦克斯韦速率分布函数,第一个空用ϵ\epsilon替代v,有v=2ϵmv=\sqrt{\frac{2\epsilon}{m}},代入得到f(ϵ)dϵ=2π(kT)32eϵkTϵ12dϵf(\epsilon)d\epsilon=\frac{2}{\sqrt{\pi}}(kT)^\frac{-3}{2}e^{-\frac{\epsilon}{kT}}\epsilon^{\frac{1}{2}}d\epsilon,最概然值即是它的极值,对f(ϵ)f(\epsilon)求导等于0,得到ϵp=12kT\epsilon_p=\frac{1}{2}kT,作为计算题的第一道题,计算起来实在是需要一点耐心

补充一个关于麦克斯韦速率分布函数的定义:

  • f(v)=dNNdvf(v)=\frac{dN}{Ndv},是某个速度区间的分子数量占全体分子数量的比例!

2015-2016

补充了上面这个定义,那我们来看看下面这道题,f(v)f(v)是麦克斯韦分布函数,那么f(v)dv=dNNf(v)dv=\frac{dN}{N},既然都是dNN\frac{dN}{N}了,那肯定不是00-∞,是vv+dvv-v+dv,故选A,然后看CD,Nf(v)Nf(v)是速率在这个区间里面的分子数,除以VV,就是单位体积内的分子数,C是对的,选AC,分子数密度的定义就是单位体积内的分子数,别忘了,D也得选上!

好了,由于气体动理论在考试中的占比实在太小,我们暂且复习到这里,接下来开始进入热力学基础。

热力学基础

公式回顾

同气体动理论,先来回忆回忆公式

1.功,热量,内能

(1)准静态过程的功

  • A=V1V2pdVA=\int_{V_1}^{V_2}pdV,即pV图像的面积

(2)热量,内能

  • Q=mMmolT1T2CmdTQ=\frac{m}{M_{mol}}\int_{T_1}^{T_2}C_mdT

(3)内能变化

  • E2E1=mMmoli2R(T2T1)E_2-E_1=\frac{m}{M_{mol}}\frac{i}{2}R(T_2-T_1)

2.热力学第一定律

  • Q=E2E1+AQ=E_2-E_1+A

  • 微分形式: dQ=dE+dAdQ=dE+dA

3.摩尔热容

  • Cm=1vlimΔT0ΔQΔTC_m=\frac{1}{v}\lim_{\Delta T \to 0}\frac{\Delta Q}{\Delta T}

  • 定体摩尔热容:CV,m=i2RC_{V,m}=\frac{i}{2}R

  • 定压摩尔热容:Cp,m=i+22RC_{p,m}=\frac{i+2}{2}R

  • 迈耶公式:Cp,m=CV,m+RC_{p,m}=C_{V,m}+R

  • 比热容比:γ=Cp,mCV,m=i+2i\gamma=\frac{C_{p,m}}{C_{V,m}}=\frac{i+2}{i}

4.循环过程

  • 热机效率: η=AQ1=1Q2Q1\eta=\frac{A}{Q_1}=1-\frac{|Q_2|}{Q_1}

  • 卡诺热机效率: η=1T2T1\eta=1-\frac{T_2}{T_1}

  • 致冷效率: ω=Q2A=Q2Q1Q2\omega=\frac{Q_2}{|A|}=\frac{Q_2}{|Q_1|-Q_2}

  • 卡诺致冷系数 ω=T2T1T2\omega=\frac{T_2}{T_1-T_2}

  • Q1=与高温热源交换的热量Q2=与低温热源交换的热量A=对外界或者外界对系统做的功Q_1=与高温热源交换的热量 Q_2=与低温热源交换的热量 A=对外界或者外界对系统做的功

    T1=高温热源的绝对温度T2=低温热源的绝对温度T_1=高温热源的绝对温度 T_2=低温热源的绝对温度

5.热力学第二定律

  • 开尔文表述:不可能从单一热源吸取热量,使它完全变为有用功而不引起其他变化,即功热转化是不可逆的。
  • 克劳修斯表述:不可能使热量从低温物体传向高温物体而不引起其他变化,即热传递过程是不可逆的。
  • 热力学第二定律的统计意义:自发宏观过程总是沿着系统热力学概率增大的方向进行,或者说自发宏观过程是沿着热运动更无序的方向进行的。

6.熵

  • 玻尔兹曼熵: S=klnΩS=k\ln\Omega
  • 克劳修斯熵: ΔS=S2S1=12dQT\Delta S=S_2-S_1=\int_1^2\frac{dQ}{T}
  • 熵增加原理: 对于孤立系统的任意过程,熵永不减少
    dS0dS\ge 0dS>0dS\gt 0为不可逆过程,dS=0dS= 0为可逆过程

习题及公式应用

依旧来做题
2024-2025

做这道题之前,还要补充一个刚刚没有提到的公式,即理想气体的熵变公式:

  • ΔS=nCvln(T2T1)+nRln(V2V1)\Delta S=nC_v\ln(\frac{T_2}{T_1})+nR\ln(\frac{V_2}{V_1})

有了这个公式之后,第一个空显然更好分析,对于AB,AC,AD三条曲线,它们的V2V1\frac{V_2}{V_1}相等,T2T1\frac{T_2}{T_1}AB最大,因此熵增加最多,AD曲线ΔQ=0\Delta Q=0,熵增加为0

机械振动

机械波

波动光学

近代物理基础