这个算是我今年最有成就感的一件事了。
以下是期间的一些日志和杂谈,简单分享。

十分尴尬,部分的图片已经丢失了,因为本来想整理这篇博客的,但是一直拖到了26.3.2才发

如果感兴趣可以看看我们的os代码:SubsToKernel,里面有“官方”版本的日志

2025.6.30

今天看了宽宽的博客,之前就听他说要多写博客,不过还是没有养成这个习惯,因为在我看来,博客可能会比较专业,能学到东西是真的,不过按照我的性格,我一般只有完全了解一个东西背后的逻辑之后才会分享出来,以及背后的背后的背后的东西,所以我总是写不下去,以及我的博客说实话还没搭好,我是飞舞,不过今天看了宽宽的博客之后发现其实博客也没那么严肃,之后一定要多写写,毕竟输出才是自己的。

7.1

Plan:

搭点博客,复习rcore,尤其是地址空间部分,不懂真的太折磨了。

从今天开始坚持写博客

Get:

空分复用

  • 空分复用是指在不同的时间,不同的用户或进程可以使用相同的物理资源(如内存空间)。在内存管理中,空分复用可以让多个进程共享有限的物理内存。
  • 共享页面:多个进程可以共享一些相同的物理页面。例如,操作系统中的某些库函数(如C标准库中的printf函数)是被多个进程调用的。这些库函数的代码存储在物理内存的某些页面中。通过过页表的设置,不同进程的虚拟地址空间可以映射到这些相同的物理页面。这样,物理内存中的这些页面就可以被多个进程复用。

Chore:

git 查看远程:git remote -v

git 添加远程:git remote add "name" "url"

难绷,hexo deploy总是失败,调了一个小时的ssh问题,结果最后是因为校园网不行,我真服。最后用V6就可以了

7.2

Plan:

同7.1+207环境搭建+树莓派报告

Get:

内存空间

我们打算重构操作系统的进程管理和内存管理部分,所以我又重新去看了一遍rcore的文档,重新整理了一下思路。

每个用户有一个自己的管理地址空间的东西,也就是memory set,在memory set里,有自己的page table,page table是vpn to ppn 的映射关系,vpn都是从零开始,但是ppn是所有用户都不能有重复。

默认情况下 MMU 未被使能(启用),此时无论 CPU 位于哪个特权级,访存的地址都会作为一个物理地址交给对应的内存控制单元来直接访问物理内存。**我们可以通过修改 S 特权级的一个名为 satp 的 CSR 来启用分页模式,**在这之后 S 和 U 特权级的访存地址会被视为一个虚拟地址,它需要经过 MMU 的地址转换变为一个物理地址,再通过它来访问物理内存

alt text

1751461960663

关于stap寄存器的结构:

1751462062201

MODE 设置为 0 的时候,代表所有访存都被视为物理地址;而设置为 8 的时候,SV39 分页机制被启用,所有 S/U 特权级的访存被视为一个 39 位的虚拟地址,它们需要先经过 MMU 的地址转换流程,如果顺利的话,则会变成一个 56 位的物理地址来访问物理内存;

在一个多任务系统中,可能同时存在多个任务处于运行/就绪状态,它们各自的多级页表在内存中共存,那么 MMU 应该如何知道当前做地址转换的时候要查哪一个页表呢?回到 satp CSR 的布局 ,其中的 PPN 字段指的就是多级页表根节点所在的物理页号。

vpn和ppn的格式组成:

1751462218892

页面大小为4K,所以需要低12位(二进制12位最多能表示4K)作为页内偏移。

虚地址的高27位为vpn

物理地址的高44位位ppn

虚地址只有39位?

SV39分页模式规定64位的虚地址的[63:39]这25位必须和第38位相同(同为1或同为0),否则MMU会认为其是不合法的虚拟地址。所以,虚拟地址的范围是低256G(同为0)和高256G(同为1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
impl PhysAddr {
/// Get the (floor) physical page number
pub fn floor(&self) -> PhysPageNum {
PhysPageNum(self.0 / PAGE_SIZE)
}
/// Get the (ceil) physical page number
pub fn ceil(&self) -> PhysPageNum {
PhysPageNum((self.0 - 1 + PAGE_SIZE) / PAGE_SIZE)
}
/// Get the page offset of physical address
pub fn page_offset(&self) -> usize {
self.0 & (PAGE_SIZE - 1)
}
/// Check if the physical address is aligned by page size
pub fn aligned(&self) -> bool {
self.page_offset() == 0
}
}

如果一个地址是aligned的,那floor和ceil是同一个页号。如果不对齐,floor是当前页号,ceil是floor+1。

pte结构:

1751463730278

  • V(Valid):仅当位 V 为 1 时,页表项才是合法的;
  • R(Read)/W(Write)/X(eXecute):分别控制索引到这个页表项的对应虚拟页面是否允许读/写/执行;
  • U(User):控制索引到这个页表项的对应虚拟页面是否在 CPU 处于 U 特权级的情况下是否被允许访问;
  • G:暂且不理会;
  • A(Accessed):处理器记录自从页表项上的这一位被清零之后,页表项的对应虚拟页面是否被访问过;
  • D(Dirty):处理器记录自从页表项上的这一位被清零之后,页表项的对应虚拟页面是否被修改过。
  • V 为 0 的时候,代表当前指针是一个空指针,无法走向下一级节点,即该页表项对应的虚拟地址范围是无效的;
  • 只有当 V 为1 且 R/W/X 均为 0 时,表示是一个合法的页目录表项,其包含的指针会指向下一级的页表;
  • 注意: 当 V 为1 且 R/W/X 不全为 0 时,表示是一个合法的页表项,其包含了虚地址对应的物理页号。

SV39 中的 R/W/X 组合的含义:

1751464361878

地址转换过程:

1751464574191

具体来说,假设我们有虚拟地址VA :

  • 我们首先会记录装载「当前所用的一级页表的物理页」的页号到 satp 寄存器中;
  • 把VPN0作为偏移在一级页表的物理页中找到二级页表的物理页号;
  • 把VPN1作为偏移在二级页表的物理页中找到三级页表的物理页号;
  • 把VPN2作为偏移在三级页表的物理页中找到要访问位置的物理页号;
  • 物理页号对应的物理页基址(即物理页号左移12位)加上 就是虚拟地址对应的物理地址。

os/src/linker.ld 中,我们用符号 ekernel 指明了内核数据的终止物理地址,在它之后的物理内存都是可用的。

在Qemu模拟的 virt 硬件平台上,物理内存的起始物理地址为 0x80000000 ,如果end kernel设置为0x80800000,这意味着我们将可用内存大小设置为8MB 。实际上在 Qemu 模拟器上可以通过设置使用更大的物理内存,但这里我们希望它和真实硬件 K210 的配置保持一致,因此设置为仅使用8MB。——rCore

FrameTracker本质上是一个可用的物理页号,通过frame_alloc()获得

1
2
3
4
pub struct PageTable {
root_ppn: PhysPageNum,
frames: Vec<FrameTracker>,
}

这里面的root_ppn应该就是上图satp所存的ppn

Chore:

搭了一会博客,设置头像设置半天无敌了,头像放在public和.deploy_git目录下都不行,得放在themes目录下。而且头像的渲染顺序太乱了,只好直接把themes下面的config.yml直接注释掉了。终于感觉勉强能用了。

7.3

Plan:

继续重构地址空间,树莓派报告,看一看207的配置如果有时间的话

Get:

所以,对于虚拟的地址空间来说,内核和用户是相互独立的,各自都可以设置自己的高256G和低256G。

我们将起始地址 BASE_ADDRESS 设置为0x10000(我们这里并不设置为0x0,因为它一般代表空指针)。——rCore

Trap

特权级切换的核心是对Trap的管理。这主要涉及到如下一些内容:

  • 应用程序通过 ecall 进入到内核状态时,操作系统保存被打断的应用程序的 Trap 上下文;
  • 操作系统根据Trap相关的CSR寄存器内容,完成系统调用服务的分发与处理;
  • 操作系统完成系统调用服务后,需要恢复被打断的应用程序的Trap 上下文,并通 sret 让应用程序继续执行。
  • 当一个应用 Trap 到内核时,sscratch 已指向该应用的内核栈栈顶,我们用一条指令即可从用户栈切换到内核栈.

sscratch CSR 的用途

在特权级切换的时候,我们需要将 Trap 上下文保存在内核栈上,因此需要一个寄存器暂存内核栈地址,并以它作为基地址指针来依次保存 Trap 上下文的内容。但是所有的通用寄存器都不能够用作基地址指针,因为它们都需要被保存,如果覆盖掉它们,就会影响后续应用控制流的执行。

事实上我们缺少了一个重要的中转寄存器,而 sscratch CSR 正是为此而生。从上面的汇编代码中可以看出,在保存 Trap 上下文的时候,它起到了两个作用:首先是保存了内核栈的地址,其次它可作为一个中转站让 sp (目前指向的用户栈的地址)的值可以暂时保存在 sscratch 。这样仅需一条 csrrw sp, sscratch, sp 指令(交换对 spsscratch 两个寄存器内容)就完成了从用户栈到内核栈的切换,这是一种极其精巧的实现。

  • 注意到,在 trap_handler 的开头还调用 set_kernel_trap_entrystvec 修改为同模块下另一个函数 trap_from_kernel 的地址。这就是说,一旦进入内核后再次触发到 S态 Trap,则硬件在设置一些 CSR 寄存器之后,会跳过对通用寄存器的保存过程,直接跳转到 trap_from_kernel 函数,在这里直接 panic 退出。这是因为内核和应用的地址空间分离之后,U态 –> S态 与 S态 –> S态 的 Trap 上下文保存与恢复实现方式/Trap 处理逻辑有很大差别。这里为了简单起见,弱化了 S态 –> S态的 Trap 处理过程:直接 panic

RISCV寄存器功能

1751528967659

一条汇编指令的解读

1
2
3
__alltraps:
csrrw sp, sscratch, sp
# now sp->kernel stack, sscratch->user stack
  1. 指令功能
  • csrrw 指令
    • csrrw 是 RISC-V 指令集中的一个指令,全称是 Control Status Register Read and Write
    • 它的功能是读取一个控制状态寄存器的值到一个通用寄存器中,同时将该通用寄存器的值写入到该控制状态寄存器。
  • sp 寄存器
    • sp 是栈指针寄存器(Stack Pointer),通常用于指向当前栈顶的位置。
  • sscratch 寄存器
    • sscratch 是一个控制状态寄存器,通常用于保存临时数据,特别是在上下文切换时。
  1. 指令解析
  • csrrw sp, sscratch, sp
    • 这条指令的作用是:
      1. sscratch 寄存器的值读取到 sp 寄存器中。
      2. sp 寄存器的值写入到 sscratch 寄存器中。
    • 简单来说,它交换了 spsscratch 寄存器的值。

Chore:

我去?发现了新大陆,edge浏览器的工作区。

1751523142673

这里可以创建一个工作区,然后就是下面这个样

1751523339093

这个工作区里面的标签页可以保存,即使退出了浏览器,下次使用的时候只需要再次打开之前的工作区就会恢复之前放在工作区中的所有标签页。

1751523392871

还能邀请别人加入工作区,不过这个功能倒是还没使用过

1751523486252

7.4

Plan:

做些操作系统

Get:

Chore:

7.5

Plan:

Get:

龙芯地址空间:

龙芯(LoongArch)架构下用户虚拟地址空间和内核虚拟地址空间的范围如下:

用户虚拟地址空间范围

  • LA32架构
    • 用户虚拟地址空间范围为 0x000000000x7FFFFFFF,大小为 2GB。
  • LA64架构
    • 用户虚拟地址空间范围为 0x00000000000000000x3FFFFFFFFFFFFFFF,大小为 4TB。

内核虚拟地址空间范围

  • LA32架构
    • 内核虚拟地址空间包括:
      • 0x800000000x9FFFFFFF:直接映射,非缓存。
      • 0xA00000000xBFFFFFFF:直接映射,可缓存。
      • 0xC00000000xFFFFFFFF:分页映射,可缓存。
  • LA64架构
    • 内核虚拟地址空间包括:
      • 0x90000000000000000xBFFFFFFFFFFFFFFF:直接映射,可缓存或非缓存。
      • 0xC0000000000000000xFFFFFFFFFFFFFFFF:分页映射,可缓存。

说明

  • 用户态(PLV3)只能访问用户虚拟地址空间(UVRANGEXUVRANGE),而内核态(PLV0)可以访问整个虚拟地址空间。

trap处理

在用户发生trap时,stvec存储着trap处理的入口地址,也就是到stvec的地址执行trap处理函数,一般存储着一段汇编代码的入口地址,然后通过汇编操作,进行一些重要信息的保存

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
__alltraps:
csrrw sp, sscratch, sp
# now sp->*TrapContext in user space, sscratch->user stack
# save other general purpose registers
sd x1, 1*8(sp)
# skip sp(x2), we will save it later
sd x3, 3*8(sp)
# skip tp(x4), application does not use it
# save x5~x31
.set n, 5
.rept 27
SAVE_GP %n
.set n, n+1
.endr
# we can use t0/t1/t2 freely, because they have been saved in TrapContext
csrr t0, sstatus
csrr t1, sepc
sd t0, 32*8(sp)
sd t1, 33*8(sp)
# read user stack from sscratch and save it in TrapContext
csrr t2, sscratch
sd t2, 2*8(sp)
# load kernel_satp into t0
ld t0, 34*8(sp)
# load trap_handler into t1
ld t1, 36*8(sp)
# move to kernel_sp
ld sp, 35*8(sp)
# switch to kernel space
csrw satp, t0
sfence.vma
# jump to trap_handler
jr t1
  • 当应用 Trap 进入内核的时候,硬件会设置一些 CSR 并在 S 特权级下跳转到 __alltraps 保存 Trap 上下文。此时 sp 寄存器仍指向用户栈,但 sscratch 则被设置为指向应用地址空间中存放 Trap 上下文的位置。随后,就像之前一样,我们 csrrw 交换 sp 和 sscratch ,并基于指向 Trap 上下文位置的 sp 开始保存通用寄存器和一些 CSR ,这个过程在第 28 行结束。到这里,我们就全程在应用地址空间中完成了保存 Trap 上下文的工作。

  • 接下来该考虑切换到内核地址空间并跳转到 trap handler 了。

    • 第 30 行将内核地址空间的 token 载入到 t0 寄存器中;
    • 第 32 行将 trap handler 入口点的虚拟地址载入到 t1 寄存器中;
    • 第 34 行直接将 sp 修改为应用内核栈顶的地址;

    注:这三条信息均是内核在初始化该应用的时候就已经设置好的。

    • 第 36~37 行将 satp 修改为内核地址空间的 token 并使用 sfence.vma 刷新快表,这就切换到了内核地址空间;
    • 第 39 行 最后通过 jr 指令跳转到 t1 寄存器所保存的trap handler 入口点的地址。

地址空间:

经过这几天的重新学习,对地址空间有了进一步的认识。由于要改的部分需要队友的一些实现,但是同时改会出现冲突,所以决定,等队友改完进程管理部分,我再进行地址空间的调整和改善。避免改的地方相同出现冲突。现在我先去做其他test的支持

地址空间的设置本质上就是把不同的内容放到不同的位置,避免冲突,目前的想法是这样的

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
/*
//用户空间只用低256G
-----------------------user_space_top
trap of all threads
--------------------
stack of all threads
------------------
...
...
...


heap_bottom
----------------
elf data
----------------------user_space_bottom


//内核地址空间使用高位地址
------------------------------kernel_space_top
kernel_stack of all users
------------------------
...
...
...


---------------------kernel_base
*/
  • 地址空间的切换本质是页表root_ppn的切换,保存在satp寄存器中。
  • sscratch寄存器里是当前用户的trap context的地址,其余寄存器的操作是根据这个来的

Chore:

妈妈总是能在我不想学的时候监督我前行。

7.6

Plan:

推进os

Get:

不知道为什么glibc总是运行不起来,今天debug发现是在fork子进程的时候对地址空间的处理有问题,参考的代码中,fork的子进程没有完全复制父进程的地址空间,而是将stack和trap空间单独处理,他是给子进程重新分配了trap和stack页面,但是却copy了父进程的数据,而不是直接的copy父进程的地址空间。。。。发现问题,进程和线程的设计有问题,目前的困境是,在子进程fork时线程的new需要trap信息,但是如上面所说,trap在开始时不设置而是单独处理的,但是这个单独处理需要子进程的fork,,造成了死循坏,所以跑不起来。

子进程的地址空间

在 Linux 中,当父进程调用 fork() 创建子进程时,子进程的地址空间是通过一种高效的机制设置的。这个过程涉及以下几个关键步骤和机制:

  1. 写时复制(Copy-On-Write,COW)

fork() 的核心机制是 写时复制,它是一种延迟拷贝技术,用于高效地创建子进程。具体来说:

  • 在调用 fork() 时,子进程的地址空间(包括代码段、数据段、堆、栈等)并不会立即被完整复制一份。
  • 相反,父进程和子进程的地址空间在创建时是共享的,但每个地址空间的页面(内存块)被标记为“只读”。
  • 当父进程或子进程试图修改某个共享页面时,操作系统会触发一个“写时复制”事件:
    • 操作系统会为修改的进程创建该页面的独立副本。
    • 修改的进程会继续在新的副本上操作,而另一个进程仍然使用原来的共享页面。
  • 这种机制大大减少了 fork() 的开销,因为只有在实际需要修改内存时,才会进行复制。
  1. 地址空间的初始化

fork() 调用时,子进程的地址空间会按照以下方式初始化:

  • 代码段(Text Segment):代码段通常包含程序的可执行代码,通常是只读的。在 fork() 时,代码段会被标记为共享且只读,因此父进程和子进程可以共享相同的代码段,无需复制。
  • 数据段(Data Segment):数据段包含全局变量和静态变量。在 fork() 时,数据段的页面会被标记为“只读”,并采用写时复制机制。如果父进程或子进程试图修改数据段中的内容,操作系统会为修改的进程创建独立的页面副本。
  • 堆(Heap):堆用于动态内存分配(如通过 malloc() 分配的内存)。在 fork() 时,堆的页面同样被标记为“只读”,并采用写时复制机制。当父进程或子进程试图修改堆中的内容时,操作系统会为修改的进程创建独立的页面副本。
  • 栈(Stack):栈用于函数调用和局部变量存储。在 fork() 时,栈的页面也会被标记为“只读”,并采用写时复制机制。当父进程或子进程试图修改栈中的内容时,操作系统会为修改的进程创建独立的页面副本。
  1. 文件描述符和资源

除了地址空间,子进程还会继承父进程的其他资源:

  • 文件描述符:子进程会继承父进程打开的所有文件描述符。
  • 信号处理设置:子进程会继承父进程的信号处理函数和信号屏蔽集。
  • 环境变量和工作目录:子进程会继承父进程的环境变量和当前工作目录。
  1. 效率优势

写时复制机制使得 fork() 在大多数情况下非常高效。例如:

  • 如果子进程创建后立即调用 exec()(如在常见的“fork + exec”模式中),则子进程的地址空间不会被复制,因为 exec() 会替换掉子进程的地址空间。
  • 如果父进程和子进程在 fork() 后没有修改共享页面,则它们可以共享相同的物理内存,节省资源。

总结

fork() 时,子进程的地址空间是通过写时复制机制初始化的。父进程和子进程的地址空间在创建时是共享的,但每个页面被标记为“只读”。只有在实际需要修改页面时,操作系统才会为修改的进程创建独立的页面副本。这种机制既保证了子进程的独立性,又提高了 fork() 的效率。

Chore:

最近比较火的一句话,“越过山丘,却发现无人等候”。

不知道为什么,今天突然对“人在接近幸福的时候最幸福”这句话有很深的感触,我似乎有些明白了为什么古人喜欢寄情于物,于景,于事。因为对于无形的情来说,我们很难有直接而形象的描述,从而用有形的事物来寄托。我也不知道为什么突然有这种感悟,似乎和前阵子学“听话”有关,体会在话的表面,其背后的意思,话里有话,话里有情,似乎有些触类旁通了。逻辑和推理组成我理性的一部分,文学和感情组成我感性的一部分,或许这才是完整的我,或者不如说,这才是完整的人。

”重要的不是你认识谁,而是谁认识你“

7.7

Plan:

  • 修复一下musl测例

  • 我靠我靠,突然想起来还有207的事

  • 支持glibc

  • 支持静态动态库

Get:

配置vmware虚拟机与主机的ssh

虚拟机下载ssh-server,start ssh,添加ssh公钥到~/.ssh/authorized_keys文件

block流程

当前进程block会调用schedule,切换到processor的控制流,然后由processor再去fetch task and run,在processor中的run task loop中,也是通过切换控制流,切换到fetch到的task的任务上下文

1
2
3
4
5
6
7
8
9
10
11
12
pub struct TaskContext {
/// Ret position after task switching
ra: usize,
/// Stack pointer
sp: usize,
/// s0-11 register, callee saved
#[cfg(target_arch = "riscv64")]
s: [usize; 12],
/// loongArch下需要保存10个s寄存器
#[cfg(target_arch = "loongarch64")]
s: [usize; 10],
}

这里的ra是返回地址,sp是该task对应的内核栈顶的地址,也就是trap上下文。(?内核栈顶是trap上下文位置?这里先存疑)

1
2
3
4
5
6
7
8
9
10
pub fn goto_trap_return(kstack_ptr: usize) -> Self {
Self {
ra: trap_return as usize,
sp: kstack_ptr, //存放了trap上下文后的栈地址,内核栈地址
#[cfg(target_arch = "riscv64")]
s: [0; 12],
#[cfg(target_arch = "loongarch64")]
s: [0; 10],
}
}

一般是通过这个函数来设置的任务上下文,这里的ra存储着trap_return的地址,之前还一直好奇,trap handler里只调用set_kernel_trap_entry();设置了内核trap地址,为什么用户trap的时候能够进入内核,原来是在用户执行之前,先调用了trap return

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pub fn trap_return() -> ! {
//disable_supervisor_interrupt();
set_user_trap_entry();
//debug!("in trap return");
let trap_cx_user_va = current_trap_cx_user_va();
let user_satp = current_user_token();

let restore_va = __restore as usize - __alltraps as usize + TRAMPOLINE;
// trace!("[kernel] trap_return: ..before return");
unsafe {
asm!(
"fence.i",
"jr {restore_va}", // jump to new addr of __restore asm function
restore_va = in(reg) restore_va,
in("a0") trap_cx_user_va, // a0 = virt addr of Trap Context
in("a1") user_satp, // a1 = phy addr of usr page table
options(noreturn)
);
}
}

这里设置了用户的trap entry.

hash

  • 有序表和有序集合(map and set)底层相同,无序表和无序集合底层相同,区别是有无value
  • 有序hash——O(logN),无序——O(1)
  • 有序hash在存储自定义类型是需要提供比较器

Chore:

1751906485321

静态库之前的问题:一直在空转,现在找到了为什么,是因为page fault,但是page fault不知道为什么一直没解决,应该也是地址空间的问题。需要等队友的hal移植成功估计。不过明天应该可以先搞清楚懒分配COW的逻辑。

现在是

  • glibc的问题是地址空间问题

  • musl虽然可以运行,但是test fail的问题暂时还没找到

  • 静态库的空转是因为page fault,感觉也是地址空间,存疑,需要进一步看懒分配COW的逻辑。

plan的完成情况似乎不错,但是没有什么实质性的进展,救命。207也没什么大事,目前算是比较简单,编译已经完成了,等下一步指示,不过估计要看看源码的逻辑,有时间再说。好多测试性能的文件,目前工作应该集中在lua和tcp了,and iozone

1751908242328

666666,lua的问题和静态库一样无敌了

1751908512122

????不对劲,怎么iperf也是这个问题。。。

1751908631419

地址空间这么多问题吗,,,bk了,netperf也是这个问题,这两个甚至都没来得及报no syscall之类的错误。。

7.8

Plan:

  • lua支持
  • tcp支持

Get:

load_page_fault和store_page_fault

在 Rust 中,特别是在 RISC-V 架构的上下文中,LoadPageFaultStorePageFault 是两种不同类型的内存访问异常,它们分别对应于加载(读取)操作和存储(写入)操作时触发的页错误。

LoadPageFault

  • 定义:当程序尝试从一个未映射的虚拟地址加载(读取)数据时,会触发 LoadPageFault
  • 触发场景
    • 访问的虚拟地址没有对应的页表项。
    • 访问的地址权限不足(例如,没有读权限)。
  • 处理方式:操作系统通常会捕获该异常,并根据情况决定是分配新的物理页面、修复页表,还是终止程序。

StorePageFault

  • 定义:当程序尝试向一个未映射的虚拟地址存储(写入)数据时,会触发 StorePageFault
  • 触发场景
    • 访问的虚拟地址没有对应的页表项。
    • 访问的地址权限不足(例如,没有写权限)。
  • 处理方式:与 LoadPageFault 类似,操作系统会捕获该异常,并根据情况决定是分配新的物理页面、修复页表,还是终止程序。

区别

  • 操作类型
    • LoadPageFault 是在读取操作时触发的。
    • StorePageFault 是在写入操作时触发的。
  • 用途
    • LoadPageFault 主要用于处理读取未映射或权限不足的内存。
    • StorePageFault 主要用于处理写入未映射或权限不足的内存。

Chore:

  • 空转是因为page fault,本质是对COW机制的实现有些问题,设置和检查的时机不匹配。
  • 我想我知道为什么了,因为我们参考的代码是实现了懒分配和COW机制的,他们在mmap的时候使用了懒分配机制,也就是说,程序在使用这部分空间的时候会触发页面错误,从而进行懒分配,在懒分配的时候他们会对拥有PTEFlags::W标志位的pte设置COW位,之后如果该进程fork子进程,子进程也想使用这个空间时,就会进行COW位的判断和处理。但是我们的mmap是实时分配,也就是说,进程在使用mmap空间的时候不会触发懒分配的错误处理,也就没有设置COW位,之后子进程复制了父进程的地址空间,在子进程想使用这个空间的时候,触发COW错误,但是由于没有设置COW位,所以判断失败,也就没有正确处理这个错误,以至于page fault没有解决,程序空转。
  • 可以看看计网的内容,看书看书看书
  • 靠,为什么顶会和期刊都是英文,,,救命。upupup!

7.9-7.12

回家一趟

7.13

Plan:

今天仍然有事,随便学点,预计明天正式回归

Get:

Chore:

  • ls -alhf
    • a:显示全部文件
    • l:显示详细信息
    • h:以人类阅读方便的形式显示文件的大小
    • f:文件夹后面会加一个\
  • 文件类型:
    • -: 普通文件
    • d: 目录文件
    • l: 软连接文件
    • c: 字符设备
    • b: 块设备
    • p: 管道文件
    • s: 本地套接字文件
  • 用户类型:文件所有者,文件所属组用户,其他人
  • rwx-:读、写、执行、无权限
  • 对目录有执行权限才能进入目录
  • 目录的大小仅表示目录本身的大小,不代表目录内文件大小
  • mkdir: 创建目录
    • mkdir newdir
    • mkdir parent/children/path -p
    • mkdir a b
  • rmdir:只能删除空目录
  • rm:可以删除目录也可以删除文件,如果删除目录,需要加-r参数
    • -i: 给出提示
    • -f: 强制

7.14

Plan:

  • 推os
  • 学英语
  • shm
  • iperf

Get:

futex

futex(fast userspace mutex)是 Linux 内核提供的一种“绝大多数时间工作在用户态、只有在真正需要阻塞或唤醒时才陷入内核”的同步原语,用来高效实现锁、条件变量等线程同步机制。


  1. 动机:为什么要有 futex?

传统 System V IPC(semget, semop …)或早期 POSIX 信号量/互斥体每次操作都要系统调用,代价高。
futex 把「无竞争时完全在用户态完成」和「有竞争时由内核帮忙挂起/唤醒」结合起来,兼顾正确性性能


  1. 工作原理(两步走)

  2. 用户态 fast-path
    线程在共享内存中放一个 32 位整数(futex word)表示锁状态;
    用原子指令(LDREX/STREX, cmpxchg…)尝试获取锁——成功就直接返回,0 syscall

  3. 内核 slow-path(只在需要时)
    如果尝试失败(发现已被占用):

    • 调用 futex(..., FUTEX_WAIT, expected, ...):内核检查值是否仍为 expected,是则把当前任务挂起;
    • 持有者释放锁后调用 futex(..., FUTEX_WAKE, n, ...):内核唤醒最多 n 个等待者。
  4. 关键系统调用

1
2
3
4
5
6
7
#include <linux/futex.h>
#include <sys/syscall.h>
#include <unistd.h>

long futex(int *uaddr, int op, int val,
const struct timespec *timeout,
int *uaddr2, int val3);
  • op 决定行为:FUTEX_WAIT, FUTEX_WAKE, FUTEX_REQUEUE, FUTEX_WAKE_OP, FUTEX_WAIT_BITSET
  • 返回值:成功返回 0 或唤醒数;失败返回 -1 置 errno。

  1. 内核里发生了什么?
  • 挂起等待futex_wait()futex_wait_setup()schedule() 把 task 置为 TASK_INTERRUPTIBLE,挂入 futex 等待队列。
  • 唤醒futex_wake()wake_up_q()try_to_wake_up() 把等待者状态改为 TASK_RUNNING,放回调度队列。
  • 队列管理:每个 futex 地址在内部哈希表 futex_hash 中对应一个 futex_q 链表,锁粒度很细,避免全局大锁。

  1. 用 futex 构建高层同步器

glibc 的 pthread_mutex_tpthread_cond_tsem_t、C++11 的 std::mutex/std::condition_variable,以及 Rust 的 std::sync::Mutex 等,底层全部用 futex 实现。
示例:pthread_mutex_lock()__lll_lock_wait() 里最终会走到:

1
lll_futex_wait(&mutex->__data.__lock, 2, private);

  1. 小结
维度 说明
作用 用户态/内核态混合的同步原语
性能 无竞争时 ≈ 原子指令;有竞争时 syscall
接口 futex() 系统调用
使用方 glibc、libstdc++、musl、Rust std 等高层库
优点 低开销、可扩展、支持优先级继承/robust 锁等高级特性

因此,futex 是现代 Linux 线程同步的基石

MapPermission-rv/la

RISC-V 64 的 MapPermission

1
2
3
4
5
6
7
8
9
#[cfg(target_arch = "riscv64")]
bitflags! {
pub struct MapPermission: u8 {
const R = 1 << 1; // 可读
const W = 1 << 2; // 可写
const X = 1 << 3; // 可执行
const U = 1 << 4; // 用户态可访问
}
}

🔍 解释

  • RISC-V 的 PTE(页表项)格式中,权限位如下:
    • bit 0: V(Valid,存在位)
    • bit 1: R(Readable)
    • bit 2: W(Writable)
    • bit 3: X(Executable)
    • bit 4: U(User accessible)

LoongArch 64 的 MapPermission

1
2
3
4
5
6
7
8
9
10
11
#[cfg(target_arch = "loongarch64")]
bitflags! {
pub struct MapPermission: usize {
const NX = 1 << 62; // 不可执行(No eXecute)
const NR = 1 << 61; // 不可读(No Read)
const W = 1 << 8; // 可写
const PLVL = 1 << 2; // 特权等级低位
const PLVH = 1 << 3; // 特权等级高位
const RPLV = 1 << 63; // 受限特权等级使能
}
}

🔍 解释

  • LoongArch 的 PTE 格式与 RISC-V 不同,权限位分布更复杂,且采用了“反向权限”设计:
    • NX:禁止执行(为 1 表示不可执行)
    • NR:禁止读取(为 1 表示不可读)
    • W:可写(为 1 表示可写,不是反向)
  • PLVL/PLVH:组合成 2 位的 PLV(Privilege Level),表示该页表项对应的特权等级。
  • RPLV:Restricted PLV,若置位,则只有 PLV 完全匹配的进程才能访问该页。

总结对比表

特性 RISC-V 64 LoongArch 64
可读 R = 1 << 1 !NR(NR=1 表示不可读)
可写 W = 1 << 2 W = 1 << 8
可执行 X = 1 << 3 !NX(NX=1 表示不可执行)
用户态访问 U = 1 << 4 PLVRPLV 控制
特权等级控制 无(由 U 位区分) PLVL/PLVH + RPLV
权限位方向 正向(置位表示允许) 混合(NX/NR 为反向,W 为正向)

Chore:

  • 看了一会儿各个大学的老师研究方向,找了一下操作系统方向,感觉有点少(可能是有这个方向老师的学校我考不上,真bk)

1752474954191

  • musl/lua:就是之前发现的cow时机不对的问题,临时改了一下就可以了

  • 小知识:alt+41400 =「alt+41401=」

  • data_flow!({ *timeoutp }); => *translated_ref()

目前的问题:

1752481777242

  • 这个iperf无敌了,只说不允许和fail,不告诉你哪里有问题😂

1752475043981

  • musl/libctest:也差不多,不过出现了新的需要支持的syscall
  • 98 syscall 写完之后卡住了,因为这个要创建线程,但是我们pcb中的fork线程暂时是空实现。

1752482310659

1752482380529

  • gilbc目前的启动都有问题,,,不针对某个测例。。bk

1752483166807

  • 没看懂这个iozone什么意思

1752483278824

  • netperf和iperf差不多应该。

1752483687490

  • 搜了一下ltp的东西,似乎也和文件系统有一点点关系。
  • 这是一个 “无脑”批量跑 LTP(Linux Test Project)测试用例 的脚本,用来检查 musl-libc 系统里每个 LTP 可执行文件的退出码是否正常;它并不解析测试日志,也不关心用例到底测了什么,只要返回值非 0 就报 FAIL。
  • 结论
    • 测试范围ltp/testcases/bin 目录下所有可执行文件(通常包含几百到上千个 LTP 用例)。
    • 测试深度非常浅——只看 exit code,不解析 TAP、不检查 stdout/stderr、不比对预期结果。
    • 适用场景
      • 快速冒烟:确认 musl 构建出的 LTP 二进制都能“跑起来”而不会立即段错误。
      • CI 里做“回归”:如果某个用例以前返回 0,现在返回非 0,就说明可能出现了 musl 相关的回归缺陷。

1752505666843

1752505699204

  • 测试了一下发现需要新的syscall

1752505788462

1752505823879

  • 这个打不开很神奇,不知道是不是因为第一行

1752505947460

  • 没有任何输出。。

7.15

Plan:

  • 修复一下busybox
  • 支持iperf or netperf

Get:

NUMA

  1. 为什么需要 NUMA 策略?
  2. Linux 到底有哪几类策略?

一、先画一张 NUMA 机器图,建立直觉

假设你有一台 2-路服务器:

1
2
3
4
5
6
7
8
9
CPU 0 ---┐
│ 内存条 A(节点 0) ←─ 本地,延迟 100 ns

CPU 1 ---┘

CPU 2 ---┐
│ 内存条 B(节点 1) ←─ 本地,延迟 100 ns

CPU 3 ---┘
  • 对 CPU 0/1 来说,访问节点 0的内存叫本地访问(100 ns)。
  • 访问节点 1 的内存叫远程访问(可能 300 ns)。

如果所有线程都随机拿内存,就会出现“CPU 0 经常访问节点 1”,性能白白下降。
NUMA 内存策略就是“让程序员告诉内核:请尽量把内存放在××节点”


二、Linux 支持的 6 种策略(一张表背下来)

策略宏 中文名 内核行为规则(白话) 何时用
MPOL_DEFAULT 系统默认 “你在哪个 CPU 运行,就从哪个节点拿内存”。完全不管别的。 默认
MPOL_BIND 绑定 “只能在我指定的节点列表里拿;如果这几个节点全没内存,就失败”。 需要硬隔离,如实时任务
MPOL_PREFERRED 优先 “先尝试我指定的节点;如果它没内存,再去最近的其它节点”。 大多数调优场景
MPOL_LOCAL 强制本地 显式说“我就要本地节点”,优先级高于 DEFAULT,但不如 BIND 硬。 同 DEFAULT,但可覆盖父策略
MPOL_INTERLEAVE 轮询 在多个节点轮流各拿一页(0→1→0→1…)。提高总带宽,牺牲局部性。 大数组/大矩阵并行计算
MPOL_WEIGHTED_INTERLEAVE 加权轮询 上面升级版,节点权重不同,按比例轮询。 节点带宽差异大时

补充:还有一个 MPOL_PREFERRED_MANY,Linux 5.14+ 加入,允许一次指定多个“优先节点”,行为类似 PREFERRED,但候选集合更大。


三、策略可以挂在 3 个“挂载点”

  1. 线程级(thread policy)
    set_mempolicy() 设置,之后该线程所有匿名页、堆、栈都服从它。
    查询:get_mempolicy(flags=0)
  2. 地址区间级(VMA policy)
    mbind() 指定一段 [start, end) 地址。之后只要这段地址发生缺页,就按该策略。
    查询:get_mempolicy(MPOL_F_ADDR, addr)
  3. 共享对象级(shared policy)
    同样用 mbind(),但目标可以是 shmget() 的共享内存、或 mmap() 的 tmpfs 文件。
    所有映射到这块物理页的进程都会受同一策略影响。

Chore:

  • 昨天移植了shm,没出现什么冲突,也暂时没发现有什么用

1752560451097

1752560502574

  • cyclictest:打开这个文件失败了应该是,然后后面的就都报错了,报错相同,地址也相同

  • 修复busybox,mkdir比较神奇,它本身有了一个文件,然后它还想创建,(我也不知道为什么本身会有那个文件),本来返回-1,给改成返回0就success了,不过,之后要查一下为什么那个文件本身就存在了。

  • mv命令也有改名的效果,不过在fstatat中的返回值不对,导致输出了operation not permitted,应该返回no such file or dictionary,然后添加了rename的系统调用,后面的几个就都ok了。

  • 剩下的busybox都和文件系统的布局有点关系。

7.16

Plan:

  • 改一下_start的bug
  • 追一下文件系统相关的几个问题

Get:

  • 管道是单向通信的

Chore:

  • busybox中mkdir的问题是因为之前运行过测例,已经修改过了文件系统镜像,重新加载一个新的文件系统镜像之后,mkdir相关的测例就过了。

1752666383573

  • 用debug信息发现了iozonetest的问题,似乎是指令错误,暂时不知道为什么。不过是和page fault相关,有一行cow page fault输出,怀疑是地址空间问题。

  • 发现了一些之前因为文件系统问题的原因,在我们原来参考的代码中有,只不过因为兼容的问题,移植初期给取消了,今天做了兼容,目前没什么问题。

  • 写了一些socket的支持,但是还没有细看

7.17

Plan:

  • 看socket部分

Get:

Chore:

7.18

Plan:

Get:

Chore:

1752848030700

  • 全新的错误,前面两个问题一个是没有映射kernel space,一个是因为在clone user res的时候有一个config常量拼错了。
  • 这个真没见过。要么是文件系统问题,要么还是地址空间问题

1752852379329

  • 这个fs::init()是可以执行成功的,但是很奇怪open /musl/busybox就不行。。。

1752852491023

  • 难道这就是他们把busybox直接编译进内核的原因?

NotReady可能原因分析

  1. 文件系统未正确挂载
    • 挂载操作未完成或挂载点未激活
    • 超级块(Superblock)初始化失败
    • 根 inode 未正确加载
  2. 文件系统驱动未完全实现
    • 缺少关键操作函数(如 openread
    • 文件系统类型(如 ext2/FAT)的驱动未注册到内核
    • Inode 或 Dentry 缓存未正确建立
  3. 硬件/存储层问题
    • 块设备驱动未就绪(如磁盘读取失败)
    • 文件系统镜像未正确加载到内存

7.19

Plan:

Get:

参考代码使用insert with hint

之前我仔细看参考代码的时候,发现他们alloc user res的时候,使用的是insert with hint当时我还好奇为什么这么麻烦,还得先递归寻找可用页面,再分配,根据tid直接计算分配不是更好,还节省性能。今天知道为什么了,因为在创建子进程的时候,子进程首先使用父进程的地址空间,如果子进程exec其他应用的时候再重新分配stack和trap资源,但是问题是如果直接copy父进程的stack和trap信息的话,创建子进程的时候返回会返回到父进程的trap信息处,这里似乎要返回子进程的trap信息,我们参考的代码是给子进程一个存储trap的空间,然后copy父进程的数据,之后如果exec其他应用的话,再重新给子进程分配一个trap页,所以使用的是insert with hint ,而不能直接根据tid分配,因为tid唯一,做不到给一个进程分配两个trap页。

  • 举例来说,在 fork 子进程并创建子进程的主线程的时候,就不必再分配一次用户栈和 Trap 上下文,因为子进程拷贝了父进程的地址空间,这些内容已经被映射过了。因此这个时候 alloc_user_res 为假。其他情况下则需要进行映射。——rCore

Chore:

7.20

Plan:

  • 解决NotReady问题

Get:

ext4_blockdev

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#[doc = "@brief   Definition of the simple block device."]
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct ext4_blockdev {
#[doc = "@brief Block device interface"]
pub bdif: *mut ext4_blockdev_iface,
#[doc = "@brief Offset in bdif. For multi partition mode."]
pub part_offset: u64,
#[doc = "@brief Part size in bdif. For multi partition mode."]
pub part_size: u64,
#[doc = "@brief Block cache."]
pub bc: *mut ext4_bcache,
#[doc = "@brief Block size (bytes) logical"]
pub lg_bsize: u32,
#[doc = "@brief Block count: logical"]
pub lg_bcnt: u64,
#[doc = "@brief Cache write back mode reference counter"]
pub cache_write_back: u32,
#[doc = "@brief The filesystem this block device belongs to."]
pub fs: *mut ext4_fs,
pub journal: *mut ::core::ffi::c_void,
}

这个结构体 ext4_blockdev 是 ext4 文件系统中用于**抽象块设备(block device)**的核心数据结构。它封装了底层块设备的接口、缓存、分区信息以及与文件系统的关联。下面逐个字段解释:


  1. bdif: *mut ext4_blockdev_iface
  • 作用:指向块设备接口(interface)的指针,定义了底层块设备的操作函数(如读写块、同步等)。
  • 用途:通过此接口,文件系统可以屏蔽不同硬件设备的差异,统一调用标准方法。

  1. part_offset: u64
  • 作用:当前分区在整个物理设备中的起始偏移(以字节为单位)。
  • 场景:当物理设备被划分为多个分区(如 /dev/sda1/dev/sda2)时,此字段表示当前分区从物理设备的哪个字节开始。

  1. part_size: u64
  • 作用:当前分区的总大小(以字节为单位)。
  • 场景:与 part_offset 配合使用,定义了分区的边界。

  1. bc: *mut ext4_bcache
  • 作用:指向块缓存(block cache)的指针,用于缓存磁盘上的块数据以提高性能。
  • 细节:ext4 通过缓存减少直接访问磁盘的次数,支持延迟写回(writeback)或直写(writethrough)策略。

  1. lg_bsize: u32
  • 作用逻辑块大小(logical block size),单位为字节。
  • 注意:这是文件系统层面的块大小(通常为 4KB),可能与物理设备的块大小不同。

  1. lg_bcnt: u64
  • 作用:逻辑块的总数(逻辑块的计数)。
  • 计算:分区大小除以逻辑块大小(part_size / lg_bsize)。

  1. cache_write_back: u32
  • 作用写回模式(writeback)的引用计数
  • 解释
    • 当计数 > 0 时,缓存采用写回模式(数据先写入缓存,延迟刷盘)。
    • 当计数 = 0 时,可能切换为直写模式(数据立即写入磁盘)。
  • 用途:支持动态切换缓存策略(如挂载时指定 syncasync 模式)。

  1. fs: *mut ext4_fs
  • 作用:指向此块设备所属的 ext4 文件系统实例。
  • 关联:通过此指针可以访问文件系统的超级块、inode 表等元数据。

  1. journal: *mut c_void
  • 作用:指向日志(journal)的指针,用于支持 ext4 的日志功能(保证崩溃一致性)。
  • 细节:日志可以是独立的块设备(如外部 journal),也可以是同一设备上的特定区域。c_void 表示底层实现可能为 ext4_journal 或其他类型。

总结:结构体的角色

这个结构体是ext4 文件系统与底层存储设备之间的桥梁,通过它:

  1. 管理分区:通过 part_offsetpart_size 支持多分区。
  2. 统一设备操作:通过 bdif 屏蔽硬件差异。
  3. 优化性能:通过 bc 缓存和 cache_write_back 策略减少磁盘访问。
  4. 支持日志:通过 journal 保证崩溃后的数据一致性。
  5. 关联文件系统:通过 fs 指针与文件系统元数据关联。

简单来说,ext4_blockdev 是 ext4 对块设备的高层抽象,所有对磁盘的读写最终都会经过这个结构体协调。

ext4_blockdev_iface

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
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct ext4_blockdev_iface {
#[doc = "@brief Open device function\n @param bdev block device."]
pub open: ::core::option::Option<
unsafe extern "C" fn(bdev: *mut ext4_blockdev) -> ::core::ffi::c_int,
>,
#[doc = "@brief Block read function.\n @param bdev block device\n @param buf output buffer\n @param blk_id block id\n @param blk_cnt block count"]
pub bread: ::core::option::Option<
unsafe extern "C" fn(
bdev: *mut ext4_blockdev,
buf: *mut ::core::ffi::c_void,
blk_id: u64,
blk_cnt: u32,
) -> ::core::ffi::c_int,
>,
#[doc = "@brief Block write function.\n @param buf input buffer\n @param blk_id block id\n @param blk_cnt block count"]
pub bwrite: ::core::option::Option<
unsafe extern "C" fn(
bdev: *mut ext4_blockdev,
buf: *const ::core::ffi::c_void,
blk_id: u64,
blk_cnt: u32,
) -> ::core::ffi::c_int,
>,
#[doc = "@brief Close device function.\n @param bdev block device."]
pub close: ::core::option::Option<
unsafe extern "C" fn(bdev: *mut ext4_blockdev) -> ::core::ffi::c_int,
>,
#[doc = "@brief Lock block device. Required in multi partition mode\n operations. Not mandatory field.\n @param bdev block device."]
pub lock: ::core::option::Option<
unsafe extern "C" fn(bdev: *mut ext4_blockdev) -> ::core::ffi::c_int,
>,
#[doc = "@brief Unlock block device. Required in multi partition mode\n operations. Not mandatory field.\n @param bdev block device."]
pub unlock: ::core::option::Option<
unsafe extern "C" fn(bdev: *mut ext4_blockdev) -> ::core::ffi::c_int,
>,
#[doc = "@brief Block size (bytes): physical"]
pub ph_bsize: u32,
#[doc = "@brief Block count: physical"]
pub ph_bcnt: u64,
#[doc = "@brief Block size buffer: physical"]
pub ph_bbuf: *mut u8,
#[doc = "@brief Reference counter to block device interface"]
pub ph_refctr: u32,
#[doc = "@brief Physical read counter"]
pub bread_ctr: u32,
#[doc = "@brief Physical write counter"]
pub bwrite_ctr: u32,
#[doc = "@brief User data pointer"]
pub p_user: *mut ::core::ffi::c_void,
}

这个结构体 ext4_blockdev_ifaceext4 文件系统与底层块设备之间的接口抽象层,定义了操作块设备所需的所有函数指针和元数据。它相当于一个 “驱动程序接口”,让 ext4 可以适配不同的硬件设备(如磁盘、SD 卡、虚拟块设备等)。


📌 核心作用

  • 统一设备操作:通过函数指针屏蔽不同硬件的实现差异。
  • 提供设备信息:记录物理块大小、总块数等关键参数。
  • 支持并发控制:通过 lock/unlock 实现多分区模式下的互斥访问。
  • 统计与调试:通过计数器记录读写次数,方便性能分析。

🔍 字段详解

1. 设备操作函数指针(关键部分)

字段 作用
open 初始化设备(如打开文件描述符、申请资源)。
bread 读块数据:从设备读取 blk_cnt 个块(从 blk_id 开始)到 buf
bwrite 写块数据:将 buf 中的数据写入设备的 blk_cnt 个块(从 blk_id 开始)。
close 关闭设备(释放资源、刷新缓存)。
lock 加锁(可选):在多分区模式下防止并发访问冲突(如多线程同时操作 /dev/sda1/dev/sda2)。
unlock 解锁(可选):与 lock 配对使用。

2. 物理设备信息

字段 作用
ph_bsize 物理块大小(如 512B、4KB),由底层硬件决定。
ph_bcnt 物理块总数(整个设备的容量 = ph_bsize * ph_bcnt)。
ph_bbuf 临时缓冲区:用于物理块操作的临时存储(如DMA缓冲区)。

3. 引用与统计

字段 作用
ph_refctr 引用计数:跟踪当前有多少个分区或文件系统实例在使用此接口,防止设备被提前释放。
bread_ctr 物理读计数:统计实际发生的物理读操作次数(可用于性能分析)。
bwrite_ctr 物理写计数:统计实际发生的物理写操作次数。

4. 用户扩展数据

字段 作用
p_user 用户自定义指针:可指向设备特定的私有数据(如 struct my_disk_handle),方便驱动扩展。

🚗 类比理解

可以把 ext4_blockdev_iface 想象成 “块设备的驱动程序手册”

  • open/close 是开关引擎。
  • bread/bwrite 是加油(读)和放油(写)。
  • lock/unlock 是防止多人同时开车(多分区互斥)。
  • ph_bsize/ph_bcnt 是油箱容量和油桶大小。
  • bread_ctr/bwrite_ctr 是里程表(记录读写次数)。

📎 与 ext4_blockdev 的关系

  • ext4_blockdev“设备实例”(如 /dev/sda1)。
  • ext4_blockdev_iface“设备驱动接口”(如 SATA 驱动、NVMe 驱动)。
  • 每个 ext4_blockdev 都有一个 bdif 指针指向对应的 ext4_blockdev_iface,确保操作能正确路由到硬件驱动。

✅ 总结一句话

ext4_blockdev_ifaceext4 操作底层存储设备的“驱动协议”,定义了如何读写、加锁、统计,并提供了设备的关键参数。

  • #[doc = "..."]Rust 的文档注释属性(documentation attribute),作用是给结构体、字段、函数等添加 说明文字,最终会被 rustdoc 工具提取并生成 HTML 文档(类似 Java 的 /** */ 或 Python 的 """ """)。

NotReady

1753012731336

1753012812391

1753012827708

  • 在fs中的create_init_files就可以open成功,但是在syscall中的sys_exec中就open失败,怀疑是权限问题。

根本原因:create_init_files() 是在 init 进程(PID 0)里执行的,它拥有“根文件系统”的完整挂载和缓存;而 sys_exec() 是在 用户进程 里执行的,该进程的文件系统信息(cwd、挂载点、缓存)与 init 进程并不共享,导致 open("/musl/busybox", …) 找不到或设备状态不对,最终返回 RespStatus::NOT_READY(即你看到的 “not ready”)。


🔍 具体差异

create_init_files() sys_exec()
执行上下文 内核初始化阶段(PID 0,root namespace) 任意用户进程(PID > 0)
cwd /(根) 可能是 /musl/glibc/,取决于 sys_exec() 里的逻辑
挂载表 已完整挂载 /musl(busybox 所在分区) 用户进程可能 没有挂载 /musl,或挂载尚未完成
块设备缓存 / 引用计数 open()/musl/busybox,设备处于 Ready 用户进程第一次访问,ext4_blockdev::RespStatus::NOT_READY
错误码 成功 NOT_READY(驱动层返回)

如何验证 & 修复

  1. 确认挂载
    sys_exec() 开头加打印:

    1
    debug!("cwd = {}, mounts = {:?}", inner.fs_info.cwd(), list_mounts());

如果 /musl 没出现在挂载列表,说明用户进程命名空间里根本没挂这块分区。

  1. 确保设备已初始化
    create_init_files() 里把 /musl 对应的块设备引用计数拉高(比如 open("/musl/busybox", …)不立即 close,或显式 bdev_ref_inc()),防止用户进程第一次访问时设备状态为 NOT_READY

  2. 统一 cwd
    sys_exec() 里,如果 path 是相对路径,而 cwd 不是 /,拼接后可能变成 /glibc/../musl/busybox 之类的不存在路径。可以强制:

    1
    let abs_path = get_abs_path("/", &path);

    或确保用户进程把 /musl 正确挂载在根命名空间的 /musl 上。


一句话结论

create_init_files 能打开 busybox,是因为它在“根进程”里,文件系统已完全就绪;sys_exec 打不开,是因为用户进程的文件系统视图或设备状态尚未就绪,返回 NOT_READY。确保 /musl 已挂载且块设备已初始化即可解决。

Chore:

  • 为什么逻辑不同但是能编译过:因为一个是使用current_task.getuid()获取的uid,一个是使用全局变量保存了uid来获取。

1753026545618

1
2
3
4
5
6
7
8
9
10
11

lazy_static! {
pub static ref KEEP_BUSYBOX_ELF: Option<Arc<FileClass>> = {
let fd = open("/musl/busybox", OpenFlags::O_RDONLY, NONE_MODE).unwrap();
Some(Arc::new(fd))
};
}

pub fn get_musl_busybox() -> Arc<FileClass> {
unsafe { KEEP_BUSYBOX_ELF.as_ref().unwrap().clone() }
}
  • 如果只是保存FileClass的话,在sys_exec中就可以open过去,但是read_all会失败。

1753026062207

  • 发现在create file中可以open成功,但是sys_exec中open失败,想了一个取巧的办法是在create file的时候就把busybox的elf数据保存下来
1
2
3
4
5
6
7
8
9
10
11
12
13
lazy_static! {
pub static ref KEEP_BUSYBOX_ELF: Option<Vec<u8>> = {
let app_inode = open("/musl/busybox", OpenFlags::O_RDONLY, NONE_MODE)
.unwrap()
.file()
.unwrap();
Some(app_inode.inode.read_all().unwrap())
};
}

pub fn get_musl_busybox() -> Vec<u8> {
unsafe { KEEP_BUSYBOX_ELF.as_ref().unwrap().clone() }
}
  • 然后做一个可以get到busybox的elf数据的接口,虽然这个方法可以跳过上一个问题,但是新的问题又出现了,我请问呢,为什么这么多错误,这就是内核吗?这就是底层吗?我真的要bk了。。。。
  • 根据目前的现象, 在create中没有问题,但是在sys_exec中有问题,说明底层没问题,文件系统可能有问题,可能是在不知什么地方把对底层的引用释放了,或者是在fork中有问题,总之就是在切换进程的时候把底层的引用给搞丢了,所以底层找不到了,在子进程中访问的时候就访问不到,(底层应该也没有每次访问都初始化的机制,或者说,在子进程的访问机制就不是让底层初始化的),按理说文件系统应该也没什么问题,因为参考的他们都可以,怀疑是引用的polyhal层中某个地方使用roll思想给底层设备给释放了,可以看看polyhal的example代码。
  • 队友说改了processor的run_task代码,看来一眼,发现把参考代码的多核逻辑给搞过来了,且拥有抢占式调度似乎,可能有点关系。

7.21

Plan:

  • 解决notready
  • 解决地址问题

Get:

type Target = PageTable;

关联类型 (associated type) 的语法,出现在 impl Deref for PageTableWrapper 的上下文中。

解释:

在 Rust 中,Deref 是一个 trait,它定义了一个关联类型 Target,表示“解引用之后得到的类型”。

1
2
3
4
pub trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}

所以这里:

1
2
3
4
5
6
impl Deref for PageTableWrapper {
type Target = PageTable; // 告诉编译器:解引用 PageTableWrapper 会得到 PageTable
fn deref(&self) -> &Self::Target {
&self.0
}
}
  • type Target = PageTable; 是在为 Deref trait 的关联类型 Target 指定具体类型。
  • 这意味着:当你对 PageTableWrapper 使用 * 解引用时,比如 *wrapper,你会得到一个 &PageTable

总结一句话:

type Target = PageTable; 不是定义新类型,而是为 trait 的关联类型指定具体类型,让编译器知道解引用后应该得到什么。

Chore:

1753085254167

  • polyhal中的pagetable的change里,satp::write失败。

7.22

Plan:

  • 查看polyhal给出的rcore使用的逻辑

Get:

内核栈:

名称 类型 作用阶段 内容说明
kernel_sp usize(通常在 TaskContextKContext 里) 用户态 → 内核态 切换时 保存该任务下一次进入内核后要用的内核栈顶地址(sp)。
__switchtrap_return 之前把它写到 sscratch,硬件/汇编用。
trap_cx TrapFrame 内核态 → 用户态 返回时 保存 用户态现场(寄存器、sepc、sstatus 等),
从中断/系统调用返回用户态时用它来恢复用户寄存器。

一句话总结:

  • kernel_sp 放在 task_cx 里,是“用户态陷入内核后”要用的内核栈指针;
  • trap_cx(TrapFrame) 只保存“返回用户态”时需要恢复的用户寄存器现场,不含 kernel_sp

Chore:

1753180230368

  • 这个问题应该是在loop里多次获取trap_cx的问题

1753180073131

  • 开始空转了,这个应该是page fault

7.23

Plan:

Get:

Chore:

真的要bk了,每天都在碰到新的bug。。。

1753285654761

  • 注释了一段代码,不知道有什么影响

没办法了,只能和傻逼的example一样,不能自己设置trap信息了。很有可能是这个原因,它不让我自己写trap地址。根本map不来。

似乎是,因为没有使用正确的memory_set??

  • 把ustack的映射放到了from_elf中,但是还是有问题

1753288684482

  • 还是会page fault,但是分配成功了

7.24

Plan:

Get:

Chore:

1753350205962

  • 把ustack放进from_elf中,分配成功了,但是还是页面错误,我怀疑是因为在exec中的memory_set没有activate的原因,但是不知道为什么这个activate不能执行,很奇怪

  • 所以polyhal不需要用token的原因就是它手动的切换了页表,使用activate。而我们原来的代码和参考的代码都是使用token找到对应的页表,再根据找到的页表去翻译vpn。

  • 没招了,我真没招了。

7.25

Plan:

  • 退回mem重构

Get:

exec

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
/*
-----------------------ustack top == sp(init)
.
.
.
------------
env string 2
------------
env string 1
------------------------env_st
地址对齐
--------------------------

.
.
.
-------------
args string 2
-------------
args string 1
---------------------------args_st
地址对齐
----------------------------
aux空间
以两个0为结束标志
----------------------------
0(envp 结束)
---------------------
.
.
.
---------------------
envp2 -> env string 2
---------------------
envp1 -> env string 1
-----------------------------env_base
0(argv 结束)
--------------------------
.
.
.
--------------------------
argv2 -> args string 2
--------------------------
argv1 -> args string 1
--------------------------------argv_base
argc
---------------------------------sp(final)
*/

#!/bin/sh

#!/bin/sh 被称为 shebang(或 hashbang)

作用

告诉操作系统:用哪个解释器来执行本脚本

  • #! 是固定前缀
  • /bin/sh 是解释器的绝对路径,表示“用系统的 POSIX Shell 来运行下面这段脚本”

常见变体

Shebang 解释器
#!/bin/bash Bash
#!/usr/bin/env python3 用环境变量里的 Python 3
#!/usr/bin/perl Perl
#!/usr/bin/env node Node.js

不写会怎样?

如果脚本没有 shebang,系统会用当前用户的默认 Shell 去执行,可能行为不一致。


📌 一句话

#!/bin/sh 就是“这文件是 Shell 脚本,请用 /bin/sh 来跑”。

Chore:

  • 删了user res,不过问题出现了,在fork之后,父进程和子进程都要返回,父进程要自己的trap,子进程也要使用父进程的trap信息,按照目前的逻辑,子进程的trap直接指向父进程的trap,这样有两个可能的问题,一是把父进程的返回值错误的返回0,导致父进程没办法继续执行,二是子进程找不到(不过这种可能比较小)。

  • 子进程要使用父进程的trap信息,父进程先返回,返回之后processor会fetch 到子进程,然后子进程返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pub fn trap_cx_ppn(&self, tid: usize) -> PhysPageNum {
debug!(
"in get trap cx ppn , self tid is : {}, tid is : {}",
self.tid(),
tid
);
let process = self.process.upgrade().unwrap();
let process_inner = process.inner_exclusive_access();
let trap_cx_bottom_va: VirtAddr = trap_cx_bottom_from_tid(tid).into();
process_inner
.memory_set
.translate(trap_cx_bottom_va.into())
.unwrap()
.ppn()
}

1753435690189

1753435747889

  • 为什么这个tid为1的时候就会unwrap失败,但是为0的时候就能unwrap成功?

  • 这个地方虽然使用的是子进程的页表,但是这是fork,子进程复制了父进程的地址空间,所以0可以translate成功,因为0在父进程的地址空间中有映射,而1没有,所以1就translate失败。

  • 突然感觉我这样改没什么实际意义,就是把tid变唯一了,这有什么用?有这时间不如去做一些更有意义的工作。

1753444039481

  • mmap的copy有问题,应该是因为mmap中某些页是懒分配,没有分配实际的物理空间,所以在copy的时候loadpagefault了。

1753445787126

1753445801917

  • 两次都是/glibc/busybox执行,所以有相同的elfmax_end_vpnheap_bottom,不过奇怪的是,为什么musl没有这个问题
  • 对奥,process id都不一样,理论上是有不同的地址空间才对,虽然有相同的va,但是pa应该不同。

1753446657019

  • 在内核态trap了?
  • 将内核栈改大,但是没有解决。不是内核栈过小的问题。
  • 队友在改这个,先去改别的。

basename not found

1753455559226

  • 是因为busybox中没有basename功能。

7.26

Plan:

  • 完善线程相关
  • socket相关

Get:

TCP/UDP

当然可以!UDP 和 TCP 是互联网传输层的两大核心协议,它们都基于 IP 协议,但设计哲学和工作原理截然不同。下面我用通俗的语言和例子帮你理解它们的原理和区别。


1. TCP(传输控制协议):像“打电话”

  • 特点:可靠、有序、面向连接。

  • 核心原理

    • 三次握手建立连接
      客户端和服务器先互相确认“你能听到我吗?”(类似打电话时的“喂?能听到吗?”)。
      例:客户端发送 SYN → 服务器回复 SYN-ACK → 客户端再回 ACK,连接建立。
    • 数据分段与排序
      数据被拆分成多个小包(如发送一张图片),每个包标记顺序号。若某个包丢失(如第3包),接收方会要求重传,最终按顺序组装还原。
    • 流量控制
      根据接收方的处理能力调整发送速度(比如对方手机信号差时,说“慢点说”)。
    • 四次挥手断开连接
      双方确认“我说完了,可以挂了”(如 FINACKFINACK)。
  • 适用场景:网页(HTTP/HTTPS)、邮件(SMTP)、文件传输(FTP)等需要数据完整的应用。


2. UDP(用户数据报协议):像“发短信”

  • 特点:无连接、不可靠、高效。

  • 核心原理

    • 无需握手:直接发数据,不管对方是否在监听。
      例:你直接发短信给朋友,不确认他是否开机。
    • 无状态传输
      每个数据包(称为“数据报”)独立发送,不标记顺序,不追踪是否到达。若丢失(如短信没收到),发送方完全不知道。
    • 无流量控制
      以恒定速度发送,不管网络是否拥堵(类似不停群发消息)。
    • 轻量头部
      仅8字节头部(TCP为20字节以上),传输开销小。
  • 适用场景:视频直播、在线游戏、DNS查询等需要实时性、可容忍少量丢包的应用。


对比表格:TCP vs UDP

特性 TCP UDP
连接方式 面向连接(三次握手) 无连接(直接发)
可靠性 保证数据完整、有序 不保证,可能丢包或乱序
传输效率 较低(需确认、重传、排序) 极高(无额外机制)
流量控制 有(滑动窗口机制)
头部大小 20+字节 8字节
典型应用 网页、邮件、文件传输 视频、语音、游戏、DNS

生活化例子

  • TCP:网购快递(必须签收、按顺序到货,丢失会补发)。
  • UDP:电台广播(信号可能断断续续,但实时性强,错过就错过了)。

总结一句话

TCP是“可靠但慢”的协议,UDP是“快速但不可靠”的协议——选择哪个取决于应用更看重数据完整性还是实时性。

内核视角

下面用**“一张图 + 两条时间线”**的方式,从 内核实现视角UDPTCP 讲清楚。所有细节均来自上面引用的 Linux/UNIX 第一手资料。


🌐 一张总览图(内核视角)

1
2
3
4
5
6
7
8
9
用户进程              内核空间                            网卡
─────────── ────────────────── ──────
socket() ──► struct socket ─┐ │
bind() ──► inet_sock │ │
send() ──► sk_buff ──────┼──► IP层 ──► MAC层 ──► 发送队列
recv() ◄── sk_buff ◄─────┼──◀ IP层 ◀── MAC层 ◀── 接收队列
close() ──► tcp_v4/udp_v4─┘ ▲

内核管理结构:sock → sk_buff → skb_queue → DMA ring

⏱️ UDP 时间线(无连接)

阶段 内核做了什么 关键结构
socket(AF_INET, SOCK_DGRAM) 创建 struct udp_sock,分配一个 UDP 输入队列(固定大小,默认 208 KB,可调 SO_RCVBUF
bind(port) udp_sock 挂到 全局哈希表port -> udp_sock 哈希冲突时启用 SO_REUSEPORT 做负载均衡
sendto(buf,len,addr) 把用户数据拷到单个 sk_buff,直接填 UDP 首部 → 交给 IP 层 无流量控制,不保证到达
recvfrom() 从输入队列弹出一个 完整数据报,拷贝到用户空间;队列空 → EAGAIN 队列溢出直接丢包
close() 释放 udp_sock 和输入队列 无状态,立即回收

UDP 的“输入队列”只负责存完整报文,无顺序、无确认、无重传。


⏱️ TCP 时间线(面向连接)

阶段 内核做了什么 关键结构
socket(AF_INET, SOCK_STREAM) 创建 struct tcp_sock,准备 个队列:发送缓冲区、接收缓冲区
bind(port) + listen() tcp_sock 标为 LISTEN,放入 半连接 hash (SYN queue) 等待三次握手
accept() 阻塞直到 SYN queue 有完成握手的子 socket;返回新的 tcp_sock 子 socket 继承父 socket 的缓冲区大小
send(buf,len) 把用户数据切成 MSS 片,放入发送缓冲区 → 滑动窗口 → ACK 驱动 窗口满时阻塞或返回 EAGAIN
recv() 从接收缓冲区顺序读字节流;无数据时阻塞 乱序包先进入 乱序队列,再合并
close() 发起 四次挥手;维护 TIME_WAIT 定时器 资源在 TIME_WAIT 结束后回收

TCP 的 接收缓冲区 会重排乱序段、做流量控制;发送缓冲区 负责超时重传。


🎯 内核差异速记表

维度 UDP TCP
连接状态 无状态 11 种 TCP 状态机
队列数量 1 个输入队列 发送 + 接收 + 乱序队列
队列管理 完整报文、无顺序 字节流、重排、窗口控制
系统调用差异 sendto/recvfrom 每次带地址 send/recv 已连接
资源消耗 极低 每个连接 ≈ 3–4 kB 内核内存
网络可见 报文边界保留 字节流无边界

🧩 一句话总结

  • UDP 内核实现 = 端口哈希表 + 固定大小输入队列 + 无重传。
  • TCP 内核实现 = 状态机 + 双缓冲区 + 滑动窗口 + 重传定时器 + 四次挥手。

Trap

今天突然怀疑为什么已经有跳板页了,还要我们手动设置trap信息,这两个是不一样的,跳板页是在用户trap到内核的时候执行的汇编代码,而真正的trap信息还保存在我们自己设置的trap位置。

Chore:

1753526079010

  • 目前不太清楚为什么。。。iperf

7.27

Plan:

Get:

Brk

是的,brk 区域的 COW(写时复制)机制fork() 之后的父子进程中行为如下:


简明回答:

父子进程拥有相同的虚拟地址(brk区域)映射,但物理页是共享的(只读)。第一次写入时,内核会为写入方分配新的物理页,并建立独立的映射。


🔍 详细解释:

✅ 1. fork() 后,brk 区域的页表项状态:

  • 虚拟地址相同(如 0x1102000)。
  • 页表项标记为只读(用于触发 COW)。
  • 物理页共享(节省内存)。

✅ 2. 第一次写入时(如 brk()malloc()):

  • 触发 COW 缺页异常cow_page_fault)。
  • 内核:
    • 分配一个新的物理页。
    • 复制原页内容到新页。
    • 更新当前进程的页表项,指向新页,并标记为可写。
  • 此时:
    • 父进程和子进程的虚拟地址相同
    • 物理页不同(不再共享)。

✅ 3. 后续访问:

  • 各自独立,互不干扰。
  • 即使一方释放或扩展堆,不影响另一方。

总结对比表:

阶段 虚拟地址 物理页 是否共享
fork() ✅ 相同 ✅ 共享 ✅ 是(只读)
第一次写入 ✅ 相同 ❌ 复制出新页 ❌ 不再共享
后续访问 ✅ 相同 ❌ 独立物理页 ❌ 否

补充:brk 的 COW 和 MAP_PRIVATE 的 COW 是一样的

  • 无论是 堆(brk) 还是 匿名映射(MAP_PRIVATE),COW 机制都遵循上述逻辑。
  • 只有 MAP_SHARED线程 才会持续共享物理页。

结论:

是的,brk 区域的 COW 机制就是:虚拟地址相同,物理页延迟复制(第一次写入时才分配新页)。

Chore:

1753606690088

  • 先把COW和懒分配机制删了.
  • 在exec中似乎有一个地方没有正确设置user_sp,修改之后这个就解决了.

1753609058943

  • mmap是懒分,正在删除这个机制.
  • 删了mmap的懒分配之后这个地方过了,不过我似乎又回到之前的bug 了

1753609256492

1753614717508

  • 呜呜呜,这句话终于输出了
  • 改了heap的分配方式,按照heap id分配
  • 不过为什么1进程会访问已经退出的2进程的heap空间呢

1753617954606

  • 嘿嘿!过啦!

  • 因为按照linux的标准,COW的机制导致父子进程的对堆空间的映射应该是相同的虚拟地址,即便在exec中也是,因为两次使用相同的elf(也就是/glibc/busybox),所以结束地址相同,heap bottom 相同,但是没人知道为什么我之前使用两个不同的页表映射相同的地址映射不进去….于是我给改成用固定的方式计算,给他们传入相同的计算变量,也就是固定的heap id,神奇的是居然可以映射进去….这就太好了,这样就算是使用巧妙的方法绕过了COW机制,但是满足了其中要求的父子进程的heap空间的虚拟地址相同.

  • 这个应该也解释了为什么之前进程1会跑到进程2的heap地址,因为理论上这两个的heap地址应该是一样的.

  • 这个是因为子进程在copy父进程地址空间的时候应该把一些页面加入到group manager中管理,加上之后就好了.

  • /glibc/basic跑出来了!!

1753621059701

  • 跑了一下glibc的busybox发现里面有个basename的测试,突然想起来[之前musl里有个错误](#basename not found)说basename找不到,去跑了一下musl/busybox,这里面的basename测试成功了呀,很奇怪.

收获满满,glibc基本都跑起来了.

7.28

Plan:

  • 目前还剩线程,信号,la的支持,有些信号操作是支持的,但是没有仔细的看过原理。
  • 这之后就是对test的支持了。
  • 近期先做线程和信号吧

Get:

虚拟机扩展磁盘

参考这里

Chore:

  • ltp想使用basename,但是文件系统中并没有basename,不过busybox中有basename功能,目前先使用特判的方式,如果用户想使用basename,就调用busybox中的basename,之后估计要改成软连接。
  • 目前只支持musl,还没写glibc的特判。

sss

  • 对,还有网络相关

1753705873914

  • FAIL /musl/ltp/testcases/bin/abort01 ,这个ltp先暂存。

7.29

Plan:

Get:

Chore:

  • 刷b站刷到一个博主连续阅读2000多天,每天2个小时(他自称是两个小时),get到一个新的驱动行为的理论,我就用自己的理解去阐述了,当一个行为想要发生时,通常需要三个因素,即动机(为什么想要做),推动力(为什么能做下去),扳手(为什么开始做,这里的扳手是使用了视频里的定义);有意思,可以试试开始阅读。

1753779443048

  • 加上动态链接之后就page fault了。

7.30

出去陪西瓜玩

7.31

Plan:

need to do

  • 动态链接栈问题
  • tid唯一,支持部分信号
  • 网络协议栈

Get:

  • ld-linux-riscv64-lp64d.so.1glibc 的动态链接器;musl 使用的是 ld-musl-riscv64.so.1 等命名方式,两者不兼容,不能混用。

Chore:

8.1

Plan:

Get:

Chore:

  • trap需要保存的信息本质上只有两个,一个是虚拟地址,用于从页表中查取,一个是ppn,用于获取元数据。

  • trap和ustack的分配方式改成和参考一样,不过我感觉他那个也有取巧,不然凭什么只需要start vpn就能从another的area中复制数据,没道理。应该是两个页表映射到了相同的虚拟地址,所以才可以。

  • tid唯一的问题完成。

1754059148564

1754059165424

  • 真尴尬,不是栈问题,这个动态链接,是mmap的地址。父进程在子进程退出后尝试访问子进程的mmap地址。

  • 我怀疑是子进程在clone父进程的areas的时候的问题,可能连物理地址也clone了,所以子进程对地址空间的修改影响了父进程。

  • 似乎应该加上COW机制

8.2

Plan:

  • 动态链接栈问题
  • 信号
  • 8.4去207

Get:

在 RISC-V 中,tp(Thread Pointer)寄存器官方用途是:

存放当前执行线程的“线程局部存储(TLS)块”基地址
—— 也就是 __thread / thread_local 变量所在内存区域的起始指针


✅ 官方 ABI 规定

  • ABI 名称tpx4 寄存器的别名。
  • 调用约定:跨函数调用时 保持不变(callee-saved)。
  • 访问 TLS 时,编译器生成的代码会把 tp 加上一个常量偏移,直接定位变量:
    1
    ld   t0, 0x18(tp)   # 读取 thread_local 变量

✅ 典型用法

场景 代码示例 编译器行为
C/C++ __thread / thread_local __thread int foo; foo 的地址 = tp + 固定偏移
Rust #[thread_local] #[thread_local] static BAR: u32 = 0; 同上
运行时初始化 libc / std 启动代码 把 TLS 块的物理地址写进 tp

⚠️ 注意事项

  • 不是特权寄存器tp 只是一个 通用寄存器;内核/用户态都能读写。
  • 内核不强制语义:如果程序完全不用 TLS,可以把 tp 当作普通 callee-saved 寄存器用(不推荐)。
  • 初始化责任
    • 用户态:libc/链接器负责在 ELF 启动时设定好 tp
    • 内核:切换线程时,需要保存/恢复 tp(ABI 要求用户态自己维护)。

✅ 一句话总结

在 RISC-V 上,tp(x4)就是“线程局部存储基址寄存器”,专供编译器和运行时用来快速定位 thread_local / __thread 变量。

Chore:

1754131068627

1754131133660

  • from_existed_user的时候没有对mmap进行特判,所以在clone area的时候应该是把所有的vpn和对应的ppn都copy过来了,也就是说,子进程和父进程享有相同的ppn,所以子进程在修改ppn的时候把父进程的ppn给改了,导致错误。应该是这个问题。在copy数据的时候,应该给子进程重新分配页。这个和COW的底层原理差不多了

  • 不对,这个clone和mmap没什么关系,主要是from existed user里面的copy。但是这个里面的copy确实重新分配了物理页,这就很奇怪了。

  • 解决了,我的天很神奇,今天和队友讨论了一下,用gdb搞了一下,发现父进程访问的非法地址在tp寄存器里面存着,然后我的队友就说,它改la的时候改了这个tp的保存和恢复,在我的代码里加上之后就运行成功了。。。。就加了四行代码。。。。难绷,看来还是应该多讨论。

测试各个test

1754138464828

  • iozone需要libc.so.6,但是给我们的文件系统镜像里面并没有这个东西。。。好像需要我们自己搞一个过来。

1754138598816

  • 暂时不知道什么问题。

1754138641539

  • iperf也是,有点像。

1754138692475

1754138789248

  • libcbench

1754138837430

  • 这个似乎不能用busybox的sh

1754138936977

  • unixbench和iozone问题差不多

1754138988489

  • netperf同样缺少动态库

1754139060959

  • ltp也是

8.3

Plan:

Get:

  • **POSIX线程(pthread)**明确规定:
    • 信号处理动作是进程共享的(通过 sigaction 设置)。
    • 信号递送是线程定向的(如 pthread_kill 可定向到特定线程),但处理逻辑仍遵循进程级规则。
    • 例如:SIGSEGV 触发时,即使某线程注册了处理器,整个进程仍可能终止(除非使用 sigactionSA_ONSTACK 等标志)。

Futexcmd == 9

命令码 9 对应的正式名称是

FUTEX_WAKE_OP

它不是简单地把某个线程唤醒,而是

  1. 先在 uaddr2 指向的 futex 字上做一次原子运算(加、或、与、异或等),
  2. 再把运算结果跟 val3 的低 24 位(即 FUTEX_OP_ARG3)做一次比较,
  3. 如果比较结果为真,就唤醒 uaddr 上最多 val 个等待者,
    如果比较结果为假,就唤醒 uaddr2 上最多 val 个等待者。

典型用途:把条件变量的“等待-触发”逻辑全部塞进一次 futex 调用里完成,避免竞争。


运算编码

val3 的 32 位拆成三部分:

1
2
3
4
31           28 27        24 23                     0
+--------------+-----------+-------------------------+
| op_arg1 | op | op_arg2 |
+--------------+-----------+-------------------------+
  • op_arg1:对 uaddr2 做的运算
    0 = FUTEX_OP_SET, 1 = FUTEX_OP_ADD, 2 = FUTEX_OP_OR, 3 = FUTEX_OP_ANDN, 4 = FUTEX_OP_XOR
  • op:比较运算
    0 = FUTEX_OP_CMP_EQ, 1 = FUTEX_OP_CMP_NE, 2 = FUTEX_OP_CMP_LT, 3 = FUTEX_OP_CMP_LE, 4 = FUTEX_OP_CMP_GT, 5 = FUTEX_OP_CMP_GE
  • op_arg2:比较用的立即数(24 位有符号)

伪代码(内核视角)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let op_arg1 = ((val3 >> 28) & 0xF) as u8;
let op = ((val3 >> 24) & 0xF) as u8;
let op_arg2 = (val3 & 0x00FF_FFFF) as i32;
let old = *translated_ref(token, uaddr2);
match op_arg1 {
0 => *translated_refmut(token, uaddr2) = op_arg2,
1 => *translated_refmut(token, uaddr2) += op_arg2,
2 => *translated_refmut(token, uaddr2) |= op_arg2,
3 => *translated_refmut(token, uaddr2) &= !op_arg2,
4 => *translated_refmut(token, uaddr2) ^= op_arg2,
_ => return Err(SysErrNo::EINVAL),
}
let cmp_ok = match op {
0 => old == op_arg2,
1 => old != op_arg2,
2 => old < op_arg2,
3 => old <= op_arg2,
4 => old > op_arg2,
5 => old >= op_arg2,
_ => return Err(SysErrNo::EINVAL),
};
let wake_addr = if cmp { uaddr } else { uaddr2 };
futex_wake(wake_addr, val);

总结

当你的 sys_futex 收到 cmd == 9 时,应:

  • 解析 val3FUTEX_WAKE_OP 的运算-比较指令;
  • 原子地修改 uaddr2 指向的 futex 字;
  • 根据比较结果决定唤醒 uaddr 还是 uaddr2 上的等待者。

Chore:

1754211557453

  • 神奇,不知道为什么translated不出来。。

1754233271964

  • 也不是毫无进展

1754235722737

  • ??没有物理页帧了?

8.4

Plan:

Get:

Chore:

111

  • 红色是24V,蓝色是0

  • 内存是64M,flash是32M

8.5

Plan:

  • 网络

Get:

Chore:

8.6

Plan:

Get:

#[inline(always)] 是 Rust 的一个属性(attribute),告诉编译器:请一定把这段函数体直接“粘贴”到每个调用点去,不要生成真正的函数调用指令

为什么这么做?

  1. 减少函数调用开销
    普通函数调用需要压栈参数、跳转、返回,这些有 CPU 周期开销。内联后,这些指令直接展开到调用处,省去了调用/返回的开销。

  2. 优化器更好发挥
    内联后,编译器能看到调用上下文,可能会进一步优化(比如常量折叠、消除边界检查等)。

  3. always 是强制性的
    普通的 #[inline] 只是“建议”,编译器可以拒绝。但 #[inline(always)] 是强制性的(除非遇到特殊情况,如递归或动态分发),编译器必须内联。

在你的代码里

1
2
3
4
#[inline(always)]
pub fn copy_to_user(&self, src: &[u8], dst: usize) -> Result<(), SysErrNo> {
self.get_mut().copy_to_user(src, dst)
}

这个函数只是一个简单的转发(wrapper),本身逻辑很轻,内联后能完全消除这一层抽象的开销,直接变成对 self.get_mut().copy_to_user(...) 的调用。

注意

  • 滥用 #[inline(always)] 可能增大二进制体积(代码膨胀),因为函数体被复制到每个调用点。
  • 只有对极小且频繁调用的函数才建议用 always,比如这种简单转发的 wrapper。

三次握手

222

Chore:

8.8

Plan:

  • iozone的问题应该得用COW或者懒分配解决。

Get:

Chore:

1754663439360

  • 添加完信号之后,iozone就变成了这样。

1754663529803

  • 这个libcbench也是。

1754664797451

  • 改了内存大小之后仍然是这样,说明和内存大小没关系了。
  • 怀疑是没有将子进程转移的问题。之后可以试试。

8.9

Plan:

  • net
  • 子进程转移

Get:

自顶向下

下面用最少、最可落地的 Rust 代码,把“能跑通系统调用”的 TCP/UDP 网络模块自顶向下写出来。
只保留必须的数据结构与函数名,其余全部留白,方便你后面一点点填。

────────────────

  1. 用户视角:只暴露 8 个系统调用
    sys_socket()
    sys_bind()
    sys_listen()
    sys_accept()
    sys_connect()
    sys_sendto() / sys_sendmsg()
    sys_recvfrom() / sys_recvmsg()
    sys_close()

────────────────
2. 自顶向下:代码骨架

2.1 task::fd_table(你已有时可直接复用)

1
2
3
pub struct FdTable {
entries: Vec<Option<Arc<dyn File>>>,
}

2.2 net::Socket(对上层它就是 dyn File

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pub trait File: Send + Sync {
fn read(&self, buf: &mut [u8]) -> Result<usize, Error>;
fn write(&self, buf: &[u8]) -> Result<usize, Error>;
fn poll(&self) -> PollEvents; // 非阻塞用
fn as_socket(&self) -> Option<&Socket>; // downcast
}

pub struct Socket {
domain: u32, // AF_INET
typ: u32, // SOCK_STREAM / DGRAM
protocol: u32, // IPPROTO_TCP / UDP
inner: SocketInner,
}

enum SocketInner {
Tcp(TcpSocket),
Udp(UdpSocket),
}

2.3 传输层最小结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pub struct UdpSocket {
local: Option<SocketAddrV4>, // 已 bind 的地址
peer: Option<SocketAddrV4>, // connect() 后填
rx: VecDeque<Vec<u8>>, // 收到的包
}

pub struct TcpSocket {
state: TcpState, // LISTEN / ESTABLISHED ...
local: SocketAddrV4,
peer: Option<SocketAddrV4>,
rx: VecDeque<u8>, // 顺序字节流
tx: VecDeque<u8>,
}
#[derive(Copy, Clone)]
enum TcpState { Closed, Listen, SynSent, Established }

2.4 网络层最小结构体(只占位)

1
2
3
4
5
#[derive(Copy, Clone)]
pub struct SocketAddrV4 {
ip: Ipv4Addr,
port: u16,
}

地址族

一句话:地址族(Address Family)就是“地址的格式说明书”。

内核里凡是出现 AF_XXX 的常量,都是在告诉你“接下来的 IP/端口/路径该怎么解析、占多少字节”。常见值:

AF_INET → IPv4 地址,4 字节 IP + 2 字节端口
AF_INET6 → IPv6 地址,16 字节 IP + 2 字节端口
AF_UNIX → 本地文件系统路径,可变长字符串
AF_PACKET → 原始链路层帧,以太网头格式

因此:

• 创建 socket 时你必须先选 AF,告诉内核“后面我要给的是哪种地址”。
• 后续所有系统调用(bind、connect、sendto、accept)都把 sockaddr 强转成对应的格式。

没有地址族,内核就无法知道 struct sockaddr 里那 14 个字节到底是 IPv4、IPv6,还是别的。

这些值只是“协议族”常量,在内核/系统库里用整数区分,没有内在算术意义——就像枚举标签。

宏名 典型值(十进制) 含义(地址格式 / 用途)
AF_INET 2 IPv4:4 byte IP + 2 byte port
AF_INET6 10 IPv6:16 byte IP + 2 byte port
AF_UNIX 1 Unix 域套接字:本地文件系统路径

在 Linux 的 include/linux/socket.h 里能看到:

1
2
3
4
#define AF_UNSPEC   0
#define AF_UNIX 1 /* Unix domain sockets */
#define AF_INET 2 /* Internet IP Protocol */
#define AF_INET6 10 /* IP version 6 */

因此:

• 写程序时只认“名字”,不要写死数字。
• 内核收到 domain = 2 就知道后续 sockaddr_in 是 IPv4 格式;收到 domain = 10 就按 sockaddr_in6 解析。

TCP

下面用「一条 TCP 连接从创建到关闭」的时间线串起 TcpInner 的所有字段,说明它们什么时候被写、什么时候被读、如何驱动整个状态机


阶段 0:socket 诞生
TcpInner::new() 初始化
state = Closed
snd_una = snd_nxt = rcv_nxt = 0(真正握手时再填 ISN)
rx/tx/backlog/rtx_queue/ooo_map/… 全部为空
local_addr = peer_addr = None


阶段 1:bind → listen
• 用户调用 bind("0.0.0.0:8080")bound_port = 8080
• 用户调用 listen(128)
state = Listen
backlog = 128
pending_conns(半连接队列)清空
completed_conns(全连接队列)清空


阶段 2:三次握手(SYN 到达)
2.1 收到第一个 SYN
‑ 新建 PendingConnection 塞进 pending_conns
‑ 给这条子连接随机产生 ISN → 写入子连接的 snd_nxt
‑ 发送 SYN-ACK,启动 RTO 定时器 (rto_timer.start())

2.2 收到 ACK(三次握手最后一个包)
‑ 把该连接从 pending_conns 移到 completed_conns
‑ 子连接状态变为 Established
‑ 上层 accept()completed_conns.pop_front() 取走即可


阶段 3:数据传输(ESTABLISHED)
3.1 接收方向
• 网卡收到数据包 →
‑ 若 seq == rcv_nxt:直接追加到 rx(应用可读)
‑ 若 seq > rcv_nxt:把 RecvSegment 插入 ooo_map,等待空洞填补
‑ 触发 pending_ack = true,启动 延迟 ACK 定时器 (delack_timer.start())

• 定时器到期 → 发送 ACK,清空 pending_ackrcv_nxt += 已交付字节

3.2 发送方向
• 用户 write(buf) → 把字节推到 tx
• 发送逻辑循环:
允许发送窗口 = min(peer_wnd, cwnd)
tx 弹出 ≤ MSS 的数据 → 构建 SentSegment
‑ 填入 seq = snd_nxtlen = data.len()
snd_nxt += len
‑ 启动 RTO 定时器
‑ 包放入 rtx_queue → 真正发送

3.3 收到 ACK
• 解析 ack_seq → 把所有 SentSegment.seq + len ≤ ack_seqrtx_queue 删除
• 更新 snd_una = ack_seq
• 根据 RTT 样本更新 rto_timer.rtt_estimator()
• 拥塞控制:
‑ 若处于慢启动:cwnd += MSS
‑ 若到达 ssthresh:进入拥塞避免 cwnd += MSS*MSS/cwnd

3.4 超时重传
rto_timer 触发 → 把 rtx_queue.front() 重新发送
rtx_count += 1;若超过阈值,可 close() 并发送 RST
• 拥塞反应:ssthresh = max(cwnd/2, 2*MSS)cwnd = MSS(TCP Tahoe)


阶段 4:四次挥手(主动/被动关闭)
• 用户 close()state = FinWait1,放入 FIN 段到 tx 并发送
• 收到对端 FIN → 根据当前状态机迁移
FinWait1 → FinWait2 → TimeWait
CloseWait → LastAck
TimeWait 定时器到期后真正释放资源


阶段 5:资源回收
• 所有定时器停止
rx/tx/rtx_queue/ooo_map 清空
state = Closed


一句话总结
TcpInner 像一个“迷你 TCP 端点”:

  • rx/tx 对接用户;
  • snd_* / rcv_* 与序号空间打交道;
  • 定时器 + 队列(rtx_queue / ooo_map / delack_timer)保证可靠、按序、流量/拥塞控制;
  • 状态机字段(state, pending_conns, completed_conns)驱动连接生命周期。

Chore:

8.10

Plan:

Get:

Chore:

  • 移植blueos的net模块

8.11

Plan:

  • 后面实现一下

Get:

  • 所以其实本质上socket不需要一个真正的inode,只需要有一个虚拟的概念,就我似乎给你分配的一个inode,但具体分没分配不关心,主要是为了使用fd来管理这个socket

Chore:

8.12

Plan:

Get:

子进程需要自己的内核栈

父进程在 fork() 返回后还要继续运行,如果子进程和父进程共用同一块内核栈,上下文切换会互相踩栈。因此 fork 时会为子进程新分配一页(或若干页)作为 内核 trap 栈

Chore:

1754990773260

1754990814929

  • 第一个失败了,看了一下源码,应该是要由一部分被跳过,跳过的部分应该是0才对。
  • 但是这个工作似乎应该是用户态来做才对
  • 不过很奇怪,按理说我在每次mmap或者brk的时候都让alloc出来的页面全零配置了,之后如果只是在用户空间跳了一些部分,前面没有被操作过,应该还是全零才对。

1755004698197

  • 全新的指令page fault。在跑决赛的glibc测例的时候。

1755009890952

  • 测例的镜像更新了,跑了一遍发现之前能过的iozone,glibc/*基本都过不了了。本人已经疯了,不过说啥都没用,不如赶进把bug修复了比啥都强。
  • 好在新的镜像多了一些工具。不用再纠结要不要自己编译一个进来了。

1755010274655

  • musl/busybox问题

1755010313897

  • musl/cyclictest问题。

1755010385398

  • musl/iozone

1755010435527

1755010472998

1755010540741

  • netperf/iperf应该要等网络分支那边,还有libctest

1755010656455

1755010736933

  • Imbench还是打不开sh,unixbench也是如此了

1755011494405

  • Imbench原来叫lmbench,尴尬。。

glibc

  • 和remove没关系
  • 和map elf没关系

1755014059314

  • trap信息变了,我有点不理解

8.13

Plan:

Get:

vDso

vDSO = “virtual Dynamic Shared Object” —— 内核在 用户态地址空间映射的一小段只读代码/数据,用来让 libc 调用 “快”且不进入内核 的系统调用(如 gettimeofdayclock_gettimesigreturn 等),同时避免 系统调用号/ABI 差异 带来的兼容性问题。


为什么需要 vDSO?

  1. 加速
    gettimeofday 这类纯读系统信息,不修改内核状态,内核提前把实现 编译成一段机器码 拷到 vDSO 里;
    用户态直接 call vDSO不走 syscall/ecall,省一次上下文切换。

  2. 兼容
    不同架构/内核版本的系统调用号、寄存器约定不一样,libc 只需 链接到 vDSO 提供的符号__vdso_gettimeofday…),内核升级后 vDSO 内容跟着变,用户程序无需重编

  3. 信号返回
    glibc 把 rt_sigreturn 的 stub 放在 vDSO 里;若内核没映射 vDSO,stub 地址就是悬空 → InstructionPageFault(你遇到的情况)。


在系统里的样子

  • 用户用 ldd /bin/ls 常见到一行:

    1
    linux-vdso.so.1 =>  (0x00007ffe6b7fc000)

    这就是 vDSO 被自动映射的页。

  • 内核启动时调用 arch_setup_additional_pages,把 一页 R-X 映射到 随机化后的用户地址(ASLR)。


一句话总结
vDSO 就是 内核送给用户态的“自带库”,让 快系统调用ABI 兼容 同时成立。

Chore:

1755068355950

  • 在连接脚本中加了一页之后,错误地址也加了一页,说明是信号返回时候的错误。

1755068792439

  • musl的相关代码是正确执行了的。

  • ai结论:

  • musl 把 stub 放在 用户 libc → 地址有效,能用。

  • 你用的 glibc 依赖 vDSO 提供 stub,而你的内核 没给用户映射 vDSO,于是返回地址指向“空气”,于是 InstructionPageFault。

1755097169100

  • glibc莫名其妙能跑了。。。救命,我在用寿命debug吗?莫名其妙的错误,莫名其妙的对。

8.14

Plan:

Get:

la-PTE

下面按 LoongArch 64 页表项(PTE)的格式,把 bitflags 里出现的每一个标志位说明清楚。
LoongArch 的 PTE 低 64 位布局如下(官方手册 4.4 节“页表项格式”):

1
2
3
4
 63    62    61    60 ... 12   11 10 9 8 7 6 5 4 3 2 1 0
+----+----+----+---------------+------------------------+
|RPLV| NX | NR | PPN[43:0] |G|P|W|MAT[1:0]|PLV|D|V|
+----+----+----+----------------+------------------------+

bitflags 的常量与上图一一对应:

常量名 含义
V 0 Valid。为 1 表示该页表项有效,硬件会解析;为 0 触发 Page Fault。
D 1 Dirty。为 1 表示该页已被写脏,需要写回磁盘或同步。操作系统在首次写入时置位。
PLVL 2 低 bit:页表项对应的 特权级别 (PLV)。LoongArch 有 4 个级别 0-3。
PLVL=1、PLVH=1 时 PLV=3,最低特权,用户态可访问。
PLVH 3 高 bit:同上。
PLV3 3:2 快捷常量:PLV3 = 0b11 << 2,即 PLV=3,用于用户页。
MAT_SUC 4 Memory Attribute Type 低 2 位编码:
00 = Strongly-ordered Un-cacheable(设备/IO 区域)。
MAT_CC 4 01 = Cache Coherent(普通写回可缓存内存)。
MAT_WUC 4 10 = Weakly-ordered Un-cacheable(部分设备区域)。
G 6 Global。为 1 时该映射对所有 ASID 全局可见,TLB 不区分 ASID。
P 7 Present。为 0 时视为“页不存在”,与 V 位一起决定页是否存在。
W 8 Writable。为 1 允许写;为 0 则写访问触发 Page Fault(只读页)。
NR 61 No-Read。为 1 时禁止读,只能写或执行(极少用)。
NX 62 No-eXecute。为 1 时禁止执行该页,实现 DEP/W^X 保护。
RPLV 63 Rest PLV。为 1 时允许低特权级(PLV>0)访问高特权级(PLV=0)页,用于用户态访问内核共享内存。

使用示例

  • 用户态可读写页:V | D | PLV3 | MAT_CC | W | P
  • 内核代码段:V | PLV0 | MAT_CC | NX
  • 用户态只读共享库:V | PLV3 | MAT_CC | P(无 WD

DA&PG

在 LoongArch 架构中,CRMD 寄存器是 当前模式信息寄存器(Current Mode Information Register),其中的 DAPG 位共同决定了处理器当前的虚实地址翻译模式,具体含义如下:

名称 含义
3 DA 直接地址翻译模式使能位(Direct Address translation mode enable)
4 PG 映射地址翻译模式使能位(Paged address translation mode enable)

**在 LoongArch 架构中,CRMD 寄存器是 当前模式信息寄存器(Current Mode Information Register),其中的 DAPG 位共同决定了处理器当前的虚实地址翻译模式,具体含义如下:

名称 含义
3 DA 直接地址翻译模式使能位(Direct Address translation mode enable)
4 PG 映射地址翻译模式使能位(Paged address translation mode enable)

1. DA(Direct Address)

  • DA=1 表示启用 直接地址翻译模式(Direct Address Translation Mode)。
  • 在此模式下,虚拟地址直接等于物理地址(或低位部分直接映射),不经过页表或TLB转换
  • 处理器复位后默认进入该模式(DA=1, PG=0),此时 PC=0x1C000000 对应的物理地址也是 0x1C000000。

2. PG(Paged)

  • PG=1 表示启用 映射地址翻译模式(Paged Address Translation Mode)。
  • 在此模式下,地址翻译通过 直接映射窗口页表映射(TLB) 完成。
  • 操作系统正常运行时,通常启用该模式(DA=0, PG=1),以支持虚拟内存管理。

两种模式的组合关系

DA PG 模式说明
1 0 直接地址翻译模式(复位默认)
0 1 映射地址翻译模式(启用页表)
其他组合 非法或未定义

内核/固件中的使用场景

  • 启动阶段:固件或内核启动初期使用 直接地址翻译模式(DA=1, PG=0),以简化地址管理。
  • 正常运行:操作系统初始化完成后,切换到 映射地址翻译模式(DA=0, PG=1),启用虚拟内存。
  • 从休眠唤醒:部分固件可能未在唤醒时自动启用 PG 模式,此时内核需手动设置 PG=1 以确保正常运行。

总结

  • DA=1:虚拟地址=物理地址,无地址转换,适合启动或裸机环境。
  • PG=1:启用 页表/TLB机制,支持虚拟内存,适合操作系统正常运行。

Chore:

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
[kernel] Hello, world!
cpu: 0
[ERROR] Logging init success
[DEBUG] heap_start: 90000000002c1d28, heap_size: 1e00000
[ INFO] Heap allocator initialized
frame range: 0x20c4-0x10000
[ INFO] Frame allocator initialized
kernel_trap_entry: 0x900000000024c000
trap init success
[ INFO] PALEN: 48
[ INFO] VALEN: 48
[ INFO] Support MMU-Page :true
[ INFO] Support Read-only :true
[ INFO] Support Execution-Protect :true
[ INFO] Support RPLV: true
[ INFO] Support RVA: false
[ INFO] Support RVAMAX :1
[ INFO] Support Page-Size: 0b111111111111111111000000000000
[ INFO] Support LSPW: true
[ INFO] Have STLB + MTLB
[ INFO] MLTB Entry: 63
[ INFO] SLTB Ways :8
[ INFO] SLTB Entry: 8
[ INFO] SLTB Page-size: 12
[ INFO] PTE-size: 8
[ INFO] TLB-RFill entry_point: 0x9000_0000_0024_d000
[ INFO] TLB-RFill page-size :12
[ INFO] PTE-index-width: 12,9
[ INFO] PGD-index-width: 21,9
[ INFO] PMD-index-width: 30,9
[ INFO] DA: false
[ INFO] PG :true
[ INFO] DATF: CoherentCached
[ INFO] DATM :CoherentCached
[ INFO] MISC: enable_32_in_plv3 :false
[ INFO] dmwo: 0x8000000000000001
[ INFO] dmw1: 0x9000000000000011
[ INFO] PLV: Ring0
Interrupt enable: LineBasedInterrupt(HWI0 | TIMER)
/**** APPS ****
[ INFO] mmconfig_base = 0x9000000020000000
[DEBUG] Enumerating PCI devices...
[DEBUG] alloc_pci: size = 0x1000
[DEBUG] alloc_pci: size = 0x4000
[DEBUG] Device features: BlkFeature(SEG_MAX | GEOMETRY | BLK_SIZE | FLUSH | TOPOLOGY | CONFIG_WCE | DISCARD | WRITE_ZEROES | RING_INDIRECT_DESC | RING_EVENT_IDX | VERSION_1)
[ INFO] config: 0x9000000040006000
[ INFO] found a block device of size 4194304KB
[ INFO] New an Ext4 Block Device
[DEBUG] OPEN Ext4 block device p_user=0x90000000002c2000
[DEBUG] lwext4 mount Okay
[DEBUG] ********************
[DEBUG] ext4_mount_point_stats
[DEBUG] inodes_count = 40000
[DEBUG] free_inodes_count = 3e6dd
[DEBUG] blocks_count = 100000
[DEBUG] free_blocks_count = 29d67
[DEBUG] block_size = 1000
[DEBUG] block_group_count = 20
[DEBUG] blocks_per_group= 8000
[DEBUG] inodes_per_group = 2000
[DEBUG] volume_name = ""
[DEBUG] ********************

[DEBUG] ********************
[DEBUG] ext4 blockdev stats
[DEBUG] bdev->bread_ctr = 6
[DEBUG] bdev->bwrite_ctr = 5
[DEBUG] bcache->ref_blocks = 2
[DEBUG] bcache->max_ref_blocks = 3
[DEBUG] bcache->lru_ctr = 37
[DEBUG] ********************

[dir] .
  • final-test
  • reboot-la

8.15

Plan:

Get:

git工作流程

你的日志显示 Git 正在正常地读取 /home/.gitconfig 文件。它先以只读 (O_RDONLY) 模式打开,然后使用 fstat 系统调用多次检查文件的元数据(O_ASK_SYMLINK),这是程序在处理文件时的标准流程。


Git 配置文件的查找顺序

Git 会按照一个固定的优先级顺序来查找和加载配置文件:

  1. 仓库级 (.git/config):优先级最高,只对当前仓库有效。
  2. 用户级 (~/.gitconfig~/.config/git):优先级次之,对当前用户的所有仓库有效。
  3. 系统级 (/etc/gitconfig):优先级最低,对系统上所有用户的所有仓库有效。

Git 会依次检查这些文件,找到一个就读取它的内容,然后继续检查下一个,如果发现有重复的配置项,优先级高的配置会覆盖优先级低的

Chore:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[ INFO] ##### syscall with id 48 #####
[DEBUG] open file: /, flags: O_RDWR
[DEBUG] the abs_path already has inode
[DEBUG] open file: /, flags: O_RDWR
[DEBUG] in some inode
[ INFO] in sys faccessat , the abs path is : /.gitconfig
[DEBUG] open file: /.gitconfig, flags: O_RDWR
[DEBUG] the abs_path already has inode
[DEBUG] open file: /.gitconfig, flags: O_RDWR
[DEBUG] in some inode
[DEBUG] in tcb inner, get trap cx, trap cx ppn is : 599995
[DEBUG] current task tid is : 5
[DEBUG] tcb, trap cx user va is : 0x4ffff000
[DEBUG] in tcb inner, get trap cx, trap cx ppn is : 599995
[DEBUG] [kernel] trap_handler: trap type: Exception(UserEnvCall), stval: 0x0, sepc: 0x277498
[DEBUG] in tcb inner, get trap cx, trap cx ppn is : 599995
[ INFO] ##### syscall with id 78 #####
[DEBUG] [sys_read_linkat] got path : /musl
[DEBUG] open file: /.gitconfig, flags: O_RDONLY
[DEBUG] the abs_path already has inode
[DEBUG] open file: /.gitconfig, flags: O_RDONLY
[DEBUG] in some inode
[ERROR] ext4_readlink error: rc = 2
[kernel] Panicked at src/syscall/fs.rs:267 called `Result::unwrap()` on an `Err` value: Default
  • 这些只是git在配置之前正常寻找系统中的配置文件罢了,主要原因是unwrap了一个Err,而不是将这个Err信息返回给git。

在内核里建了一个/etc/gitconfig文件

1
2
3
4
#### OS COMP TEST GROUP START git-musl ####
error: opening /home/.gitconfig: Operation not permitted
error: opening /home/.gitconfig: Operation not permitted
error: opening /home/.gitconfig: Operation not permitted
  • 不知道为什么,明明创建了lock文件,读取了/etc/gitconfig,但是就是不创建/home/.gitconfig就想写,太奇怪了。于是我尝试直接创建/home/.gitconfig文件
1
2
3
4
#### OS COMP TEST GROUP START git-musl ####
error: failed to write new configuration file /home/
error: failed to write new configuration file /home/
error: failed to write new configuration file /home/
  • 这个是因为在git想要确认/home/.gitconfig文件是否是一个符号链接的时候返回了错误的错误码,因为这个文件本身是普通文件,所以在git问,“你是一个符号链接吗?”的时候,应该告诉他,“不,我不是”。
1
2
3
4
#### OS COMP TEST GROUP START git-musl ####
error: could not write config file /home/.gitconfig: No such file or directory
error: could not write config file /home/.gitconfig: No such file or directory
error: could not write config file /home/.gitconfig: No such file or directory
1
[DEBUG] old path is : /home/.gitconfig.lock, new path : /home/.gitconfig
  • 它居然是先写/.gitconfig.lock文件,再重命名为/.gitconfig文件,但是我已经创建了/.gitconfig文件,所以会重命名失败。
  • 但是如果我把内核中的/.gitconfig删除,又会回到第一个问题。
  • 估计是权限问题,不过git的权限检查也是离谱,奇奇怪怪。

8.16

1
2
3
4
5
6
7
[ INFO] ##### syscall with id 276 #####
[DEBUG] old path is : /home/.gitconfig.lock, new path : /home/.gitconfig
[DEBUG] open file: /home/.gitconfig.lock, flags: O_RDWR
[DEBUG] the abs_path already has inode
[DEBUG] open file: /home/.gitconfig.lock, flags: O_RDWR
[DEBUG] in some inode
[ERROR] ext4_frename error: rc = 2

8.17

尝试做git的支持。

总是在这三个错误之间徘徊,上层的调用很奇怪,似乎总是在不创建/home/.gitconfig的情况下去读写它。

怀疑是syscall不够标准,又去重新检查了syscall的逻辑和返回值。修改了一部分不规范的返回值之后

1
2
3
4
#### OS COMP TEST GROUP START git-musl ####
error: failed to write new configuration file /home/
error: failed to write new configuration file /home/
usage: git [-v | --version] [-h | --help] [-C <path>] [-c <name>=<value>]

第一个error不见了,但是之后还是和之前类似的问题,很奇怪。

8.18

写文档,做ppt

8.19

1
2
3
4
#### OS COMP TEST GROUP START git-musl ####
error: failed to write new configuration file /home/
error: failed to write new configuration file /home/
usage: git [-v | --version] [-h | --help] [-C <path>] [-c <name>=<value>]
1
2
3
4
5
6
let inode = match open(&abs_path, OpenFlags::O_RDONLY, NONE_MODE, "") {
Ok(i) => i.file().unwrap(),
Err(num) => {
return num as isize;
}
};
  • 在faccessat里的falg不对,应该是O_RDWR,而不是O_RDONLY,改了之后错误变成下面这样了。
1
2
3
4
#### OS COMP TEST GROUP START git-musl ####
fatal: unable to access '/etc/gitconfig': Is a directory
fatal: unable to access '/etc/gitconfig': Is a directory
fatal: unable to access '/etc/gitconfig': Is a directory
  • 这个是在open中被拦截了
1
2
3
4
5
6
7
if writable && inode.is_dir() && !flags.contains(OpenFlags::O_CREATE) {
debug!(
"cannot open directory {} for writing without O_CREATE",
abs_path
);
return Err(SysErrNo::EISDIR);
}
  • 仿照trustos修改了fchmodat–53
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[ INFO] ##### syscall with id 79 #####
[DEBUG] the trans str is : 
[DEBUG] in fstat, the path is : 
[DEBUG] in fstat, abs path is : /home/.gitconfig


[34m[ INFO] ##### syscall with id 222 #####
[DEBUG] [sys_mmap] addr=0x0, len=0x0, port=0x1, flags=0x2, fd=4, off=0x0
[DEBUG] return invalid


[ INFO] ##### syscall with id 57 #####
[DEBUG] in sys close
[DEBUG] in close, pid is :5
[DEBUG] sys close ok


[ INFO] ##### syscall with id 53 #####
[DEBUG] [sys_fchmodat] path is /home/, flags is 2, new mode is 666
[DEBUG] checking if /home is a device file
  • 观察上述syscall顺序,怀疑是在fstat中返回了错误值,所以在mmap中申请了len = 0,返回非法之后将/home/.gitconfig的fd 给close了,之后在fchmod中修改了fd对应的错误的文件描述符的权限。

8.20

1
2
3
libncursesw.so.6.5: 这是实际的库文件,包含了具体的代码和数据。.6.5 是它的“真实版本号”(Real Name),包含了完整的版本信息。

libncursesw.so.6: 这通常是一个符号链接(symbolic link),也叫软链接,它指向一个具体的库文件。在 ncursesw 这个例子中,它指向 libncursesw.so.6.5。.6 是它的“SONAME”(Shared Object Name),或者称为主版本号。
  • 但是这个并不影响对文件的查找和打开。
  • 一直在昨天的几个错误之间徘徊,不过确实最开始的三条命令只有两个error了,第一个的/home/.gitconfig已经正确创建了,但是在第二个git config又出现了第一条出现的问题,就是它尝试修改/home/的权限为0o666,这个是所有人有可读可写,但是目录应该是有执行权限才对。
  • 测试了vim,gcc和rustc,但是都失败了,都是动态链接的问题,但是debug中显示so文件已经正确打开了,不过在后面的mmap请求给so文件分配空间中出现问题,最开始是permission denied,发现是我们打开文件的权限设置的不对,我们并没有给so文件的osinode设置可读标志,但是它应该是可读的。之后是mmap执行过后调用了close syscall,关闭了这个so文件的文件描述符,然后在切换进程之后就LoadPageFault了,pc地址显示在动态链接的地址附近,怀疑错误是so文件的地址映射失败(且大概率是这个问题),但是mmap的debug信息显示mmap成功,我们尝试了找更多的可能,不过很可惜,由于时间有限,最后我们没有找到原因。

8.21

答辩,通过答辩发现了内核赛的本质,和他的名字一样,操作系统设计赛,不仅仅是实现测例,还需要有自己的想法,这是很重要的!

8.22

颁奖典礼,本以为要获得优胜奖或者鼓励奖了,但是可能是今年真的太难了,我们最终获得了国家三等奖。当一个个书本上得知识变成一行行代码,操作系统得大门正在悄然向我打开。

收获:见识了各个大学精英学子的风采,在这个过程中成长很多,日复一日的敲代码,debug,能力的提升是无形的,获奖只是最后的表现形式,重要的是过程,以及在这个过程中收获的团队合作能力,coding能力,对内核的理解进一步加深,在一行行代码中了解内核的运行逻辑,不得不说,这其实是一件很有成就感的事情。

8.23-8.25

南京游玩

8.26

秦皇岛游玩

8.27

休息一天

8.28

寻找下一个小项目,对了,不要忘了给北科改os实验。

或者其实可以开始做cpu了

wsl

你遇到的错误:

无法将磁盘 ext4.vhdx 附加到 WSL2:系统找不到指定的文件。
错误代码:Wsl/Service/CreateInstance/MountDisk/HCS/ERROR_FILE_NOT_FOUND

是由于 WSL2 子系统的虚拟磁盘文件(ext4.vhdx)丢失或损坏 导致的,WSL 无法挂载该磁盘,进而无法启动 Ubuntu 子系统。


解决方案(推荐操作)

方法:注销并重新安装 Ubuntu 子系统(最稳妥,但会清空数据)

⚠️ 注意:此操作会清空原 Ubuntu 子系统中的所有数据,请确认没有重要文件或已提前备份。

  1. 打开 PowerShell(管理员权限),执行以下命令:
1
wsl -l -v

确认 Ubuntu 子系统的名称(如 Ubuntu-22.04Ubuntu)。

  1. 注销损坏的子系统
1
wsl --unregister Ubuntu

如果名称是 Ubuntu-22.04,则替换为 wsl --unregister Ubuntu-22.04

  1. 重新安装 Ubuntu 子系统
1
wsl --install -d Ubuntu

或者从 Microsoft Store 重新安装 Ubuntu。

  1. 首次启动时设置用户名和密码

🔍 可选:检查文件是否真的丢失

你可以手动检查该路径下是否真的不存在 ext4.vhdx 文件:

1
C:\Users\noonering\AppData\Local\Packages\CanonicalGroupLimited.Ubuntu22.04LTS_79rhkp1fndgsc\LocalState
  • 如果文件存在,但仍报错,可能是权限或文件损坏问题,建议也执行注销重装。
  • 如果文件不存在,说明确实被误删或损坏,只能重装。

你遇到的错误:

无法将磁盘 ext4.vhdx 附加到 WSL2:系统找不到指定的文件。
错误代码:Wsl/Service/CreateInstance/MountDisk/HCS/ERROR_FILE_NOT_FOUND

是由于 WSL2 子系统的虚拟磁盘文件(ext4.vhdx)丢失或损坏 导致的,WSL 无法挂载该磁盘,进而无法启动 Ubuntu 子系统。


✅ 解决方案(推荐操作)

方法:注销并重新安装 Ubuntu 子系统(最稳妥,但会清空数据)

⚠️ 注意:此操作会清空原 Ubuntu 子系统中的所有数据,请确认没有重要文件或已提前备份。

  1. 打开 PowerShell(管理员权限),执行以下命令:
1
wsl -l -v

确认 Ubuntu 子系统的名称(如 Ubuntu-22.04Ubuntu)。

  1. 注销损坏的子系统
1
wsl --unregister Ubuntu

如果名称是 Ubuntu-22.04,则替换为 wsl --unregister Ubuntu-22.04

  1. 重新安装 Ubuntu 子系统
1
wsl --install -d Ubuntu

或者从 Microsoft Store 重新安装 Ubuntu。

  1. 首次启动时设置用户名和密码

🔍 可选:检查文件是否真的丢失

你可以手动检查该路径下是否真的不存在 ext4.vhdx 文件:

1
C:\Users\noonering\AppData\Local\Packages\CanonicalGroupLimited.Ubuntu22.04LTS_79rhkp1fndgsc\LocalState
  • 如果文件存在,但仍报错,可能是权限或文件损坏问题,建议也执行注销重装。
  • 如果文件不存在,说明确实被误删或损坏,只能重装。

📌 建议:以后定期备份 WSL 子系统

使用以下命令导出备份:

1
wsl --export Ubuntu ubuntu_backup.tar

恢复时使用:

1
wsl --import Ubuntu C:\WSL\Ubuntu ubuntu_backup.tar

如你有重要数据未备份,建议先尝试使用数据恢复工具扫描该路径下的 ext4.vhdx 文件是否可恢复,再决定是否重装。需要我帮你写恢复流程也可以继续问。

使用以下命令导出备份:

1
wsl --export Ubuntu ubuntu_backup.tar

恢复时使用:

1
wsl --import Ubuntu C:\WSL\Ubuntu ubuntu_backup.tar

如你有重要数据未备份,建议先尝试使用数据恢复工具扫描该路径下的 ext4.vhdx 文件是否可恢复,再决定是否重装。需要我帮你写恢复流程也可以继续问。

  • bk,本来说搞个自动驾驶os那个,他似乎需要有GUI的linux,于是我想搞到vmware上,最后的结果是,,,vmware崩了,得重新搞一个,,,

  • ssh密钥的名字不要随便设置,不然就要在.ssh/config中设置

1
2
3
4
5
Host github.com
HostName ssh.github.com
Port 443
User git
IdentityFile ~/.ssh/id_rsa_github
  • 今天发现做的这个东西居然是让linux支持rust,还不知道他的原理,但是应该挺有用的。rust for linux
1
2
3
4
5
6
make x86_64_defconfig
make LLVM=1 menuconfig
#set the following config to yes
General setup
---> [*] Rust support
make LLVM=1 -j$(nproc)

这些命令是在 Linux 内核源码目录中执行的,目的是使用 LLVM/Clang 工具链构建一个支持 Rust 的内核。下面我逐条解释它们的含义和作用:


✅ 1. make x86_64_defconfig

作用
生成一个适用于 x86_64 架构 的默认内核配置文件 .config

  • x86_64_defconfig 是 Linux 源码中预定义的一个默认配置,适合大多数 64 位 x86 系统。
  • 这一步不会打开任何实验性功能(比如 Rust),只是一个“起点”配置。

✅ 2. make LLVM=1 menuconfig

作用
使用 LLVM/Clang 工具链(而不是默认的 GCC)打开一个基于终端的图形化配置界面,让你手动启用 Rust 支持。

  • LLVM=1 告诉内核构建系统使用 Clang 而不是 GCC。
  • menuconfig 是一个基于 ncurses 的菜单界面,允许你浏览和修改内核配置选项。

✅ 3. 设置以下选项:

1
2
General setup
---> [*] Rust support

作用
在内核中启用 Rust 编程语言支持

  • 这是一个实验性功能(截至 2025 年),允许你用 Rust 编写内核模块或驱动。
  • 启用后,构建系统会检查你是否安装了 Rust 工具链(如 rustccargobindgen 等)。

✅ 4. make LLVM=1 -j$(nproc)

作用
使用 Clang 构建整个内核,启用所有 CPU 核心并行编译。

  • LLVM=1 再次确保使用 Clang。
  • -j$(nproc) 会自动检测你机器的 CPU 核心数,并并行编译,加快构建速度。

⚠️ 注意事项

  • Rust 支持是实验性的,可能会不稳定。
  • 你需要预先安装:
    • LLVM/Clang(版本需与内核要求匹配)
    • Rust 工具链(包括 rust-srcbindgen
  • 某些内核版本可能还需要启用 CONFIG_RUST 的依赖项,比如 CONFIG_DEBUG_INFO_BTF.

✅ 总结一句话

这些命令的目的是:用 Clang 构建一个支持 Rust 的 Linux 内核,适用于 x86_64 架构

8.29-8.31

9.1

linux config

1
2
3
4
5
Link Time Optimization (LTO)
> 1. None (LTO_NONE)
2. Clang Full LTO (EXPERIMENTAL) (LTO_CLANG_FULL) (NEW)
3. Clang ThinLTO (EXPERIMENTAL) (LTO_CLANG_THIN) (NEW)
choice[1-3?]:

这个界面是 内核重编时 make menuconfig(或 make oldconfig)自动弹出的新选项——
“Link Time Optimization(LTO)” 让你决定 最终链接内核/模块时是否启用 LLVM 的 LTO 优化

选哪个?

选项 含义 建议
1. None (LTO_NONE) 完全不用 LTO,最稳、编译最快 第一次做实验/赶作业,直接选 1
2. Clang Full LTO 把整个内核一次性做全局优化,耗内存、时间长 想测极限性能、机器内存 ≥ 8 GB 可试
3. Clang ThinLTO 折中方案,分阶段并行优化,内存占用比 Full LTO 低 想体验 LTO 又怕机器吃不消,可选 3

怎么答?

  • 交互界面里直接敲数字 1 然后回车即可。
  • 如果想以后不再被问,可以在 .config 里加一行:
    1
    CONFIG_LTO_NONE=y

一句话:
先选 1(None)最省事,能编过再说;LTO 不是作业要求,只是新内核引入的可选优化。


1
2
3
4
5
6
7
8
9
Locking event counts collection (LOCK_EVENT_COUNTS) [N/y/?] n
*
* Memory initialization
*
Initialize kernel stack variables at function entry
> 1. no automatic stack variable initialization (weakest) (INIT_STACK_NONE)
2. pattern-init everything (strongest) (INIT_STACK_ALL_PATTERN) (NEW)
3. zero-init everything (strongest and safest) (INIT_STACK_ALL_ZERO) (NEW)
choice[1-3?]:

这是 内核编译时 的又一个 新配置项,用于决定函数栈内局部变量的初始化策略(GCC/Clang 的 -ftrivial-auto-var-init= 功能)。
选哪个,看你对“安全性 vs 性能 vs 调试”的权衡。

三种选项一句话解释

选项 含义 特点
1. no automatic … (INIT_STACK_NONE) 不额外初始化,保持现状 编译最快、运行最快,但可能残留旧数据导致信息泄露或不稳定
2. pattern-init … (INIT_STACK_ALL_PATTERN) 全部填充固定魔数(如 0xAA) 运行期仍能发现“用了未初始化变量”的 bug(魔数易识别),性能开销中等
3. zero-init … (INIT_STACK_ALL_ZERO) 全部清零 最保险、最干净,但清零有额外指令,性能略降

如果你是第一次做实验 / 赶作业

  • 直接选 1(None),最省事、编译最快,先保证能跑起来。
    交互界面里敲 1 回车即可。

  • 以后想追求安全性(尤其做 fuzz/安全研究),再考虑选 3(zero-init)

一句话总结

先选 1(None)跳过,不影响作业;想防未初始化变量泄露再换 3(zero-init)


1
KMSAN: detector of uninitialized values use (KMSAN) [N/y/?] (NEW)

这是 Linux 内核在开启 Clang 编译器时才会出现的配置选项:

1
KMSAN: detector of uninitialized values use (KMSAN) [N/y/?] (NEW)

✅ 一句话解释

KMSANKernelMemorySanitizer 的缩写。
它是一个 运行时检测器,专门用来 捕捉内核里“使用了未初始化内存”的 bug(比如读取了刚 kmalloc 出来、还没写入的数据)。


🧪 原理(非常简略)

  • 基于 Clang 的 MemorySanitizer 技术。
  • 在每次内存读写时插入额外检查代码。
  • 一旦发现“读之前没写过”,立即打印详细堆栈。

代价

  • 内核体积变大(插桩代码)。
  • 运行速度显著下降(2–5 倍)。
  • 只支持 Clang,GCC 无法启用。
  • 目前仍是 实验性,可能误报或漏报。

✅ 选哪个?

场景 建议
第一次编译 / 赶作业 直接按回车(保持 N),不启用。
做内核安全研究 / 调试驱动 y,但只在调试环境用,别上生产机。

🎯 一句话总结

KMSAN 是 Clang 专用的“未初始化内存检测器”,调试神器但开销大;第一次编内核保持 N 即可。

kconfig

这段文件就是 Linux Kconfig 语言写的一个“菜单脚本”,专门负责在 make menuconfig 里生成 Rust samples 这一整张子菜单。
逐条拆开说明:


1️⃣ 主菜单入口

1
2
3
4
5
6
menuconfig SAMPLES_RUST
bool "Rust samples"
depends on RUST
help
You can build sample Rust kernel code here.
If unsure, say N.
  • menuconfig 关键字:
    menuconfig 里会出现一个可勾选的 顶级菜单项 “Rust samples”。
  • bool:只能选 YN,没有 M
  • depends on RUST:只有你在 General setup → Rust support 里选了 Y/M 才会显示。
  • 选中它以后,后面 if SAMPLES_RUST … endif 之间的 所有子条目才会出现
    没选中就全部隐藏。

2️⃣ 子条目示例

1
2
3
4
config SAMPLE_RUST_MINIMAL
tristate "Minimal"
help

  • config SAMPLE_RUST_MINIMAL
    生成一个配置项 CONFIG_SAMPLE_RUST_MINIMAL
  • tristate:可选 Y(编进内核)、M(编成模块)、N(不编)。
  • help 文本:告诉你编译后会得到模块 rust_minimal.ko

其他条目(PRINT、SYNC、CHRDEV…)完全同理,只是名字不同、功能不同。


3️⃣ 特殊写法

  • depends on NETFILTER(只在 SAMPLE_RUST_NETFILTER 里出现):
    只有内核启用了 CONFIG_NETFILTER 才让你选这个例子。
  • menuconfig + if … endif 组合:
    典型的 Kconfig 套路——先有一个总开关,再展开子菜单,避免界面太乱。

4️⃣ 结果是什么?

  • 你在 make menuconfig 里把 SAMPLES_RUST 勾成 Y 后,会看到十几行可单独开关的示例。

  • 把任意一个示例设成 M,编译后就会在源码树里得到对应的 .ko 模块,例如:

    1
    2
    3
    samples/rust/rust_minimal.ko
    samples/rust/rust_chrdev.ko

  • 你可以 insmod rust_xxx.ko 直接加载到 QEMU 或真机里做实验。


✅ 一句话总结

这是内核源码里的 Kconfig 菜单脚本,用来在 make menuconfig 中生成 “Rust samples” 子菜单;勾选后就能把官方提供的 Rust 内核示例代码编译成模块或编进内核。

9.2

ASIC

ASIC 方式是指使用 专用集成电路(Application-Specific Integrated Circuit) 来实现一个电子系统。简单来说,就是为某个特定的应用或功能,专门设计并制造一个唯一的、定制化的芯片

ASIC方式的特点

  1. 高度定制化:芯片的电路布局和功能是完全根据你的需求来设计的,不具备通用性。它只做一件事,而且做得非常好。
  2. 性能极高:由于ASIC是为特定任务优化的,它能够达到最高的运行速度和效率。没有通用处理器中不必要的电路,所以功耗也更低。
  3. 批量生产成本低:虽然前期的设计和制造(称为“流片”)成本极高,但一旦完成,大批量生产的单个芯片成本会非常低。
  4. 开发周期长,风险高:ASIC的开发是一个漫长而复杂的过程,从设计、验证到最终制造可能需要数月甚至数年。如果设计出现错误,整个批次的芯片都可能报废。

ASIC与其他方式的对比

理解ASIC的最好方式是将其与另外两种常见的硬件实现方式进行比较:

  • 与通用处理器(如CPU、GPU)对比
    • 通用处理器:像你的电脑CPU一样,可以运行各种不同的软件程序。它的灵活性极高,但因为需要支持多种功能,所以执行某个特定任务时,效率不如ASIC。
    • ASIC硬件即软件。它的功能在制造时就已经固化在电路上,无法改变。但正因为如此,它在执行特定任务时速度更快、功耗更低。例如,比特币矿机就是一种典型的ASIC,它只做一件事:快速计算哈希值。
  • 与FPGA(现场可编程门阵列)对比
    • FPGA:是一种可重复编程的芯片。你可以用它来快速实现一个数字电路原型。它的灵活性介于通用处理器和ASIC之间。
    • ASIC:一旦制造完成,其功能就无法改变。但在性能、功耗和最终成本上,ASIC通常优于FPGA。FPGA常用于原型验证或小批量生产,而ASIC则用于大规模、高要求的应用。

总结

ASIC方式是一种以性能和效率为核心的硬件开发策略。它牺牲了灵活性和前期成本,以换取在特定应用领域无与伦比的性能和更低的批量成本。因此,ASIC常用于那些需要极致性能、高吞吐量或低功耗的大规模应用,比如手机处理器、AI加速芯片、网络交换机、加密货币矿机等。

9.3

KL散度 (Kullback-Leibler Divergence):
KL散度衡量的是用一个近似分布 Q QQ 来”描述”或”模拟”一个真实分布 P PP 时,所损失的信息量。

直观理解:如果 Q 与 P 越接近,KL散度越小,说明用 Q 近似 P 损失信息很少;如果差别大,KL散度大,说明用 Q 做近似不靠谱。

熵 (Entropy, H):
信息熵是描述”信息不确定性”的度量,是一个随机变量所有可能取值对应概率分布下平均信息量的总和。

原文链接:https://blog.csdn.net/Galen_xia/article/details/150109731


强化学习(Reinforcement Learning, RL)的核心思想:一个智能体(Agent)环境(Environment)中通过行动(Action)来最大化累积奖励(Reward)。智能体根据当前状态(State)选择行动,环境则根据行动给出奖励和新的状态。这个过程周而复始,智能体通过不断试错来学习一个最优的策略(Policy),这个策略就是告诉智能体在任何给定状态下应该采取什么行动的“行为准则”。

图文并茂,彻底讲明白PPO(Proximal Policy Optimization)算法。全文16000+字。 - 知乎


超参数就是“写死”在代码里、训练前由你手动设定、训练过程中模型自己改不了的那几个数。


PPO

基于策略函数,使用策略梯度方法进行探索,可以理解为从一个初始状态下,根据每次的探索的结果给予奖励或惩罚,从而更新到下一个期望状态。

最核心的创新在于一个裁剪机制,避免因为某次的偶然选择导致奖励过多或惩罚过多。假设一次行为的奖励为100,但是我限制的奖励范围是±50,(这个限制是一个超参数,由我们提前设定),取限制和实际奖励的min,防止一次的步子迈的太大。

同时使用熵奖励,鼓励探索,避免陷于局部最优。

如果策略的熵太低,意味着策略变得过于“确定”,它总是选择相同的动作,即使这些动作可能不是全局最优的。这会导致智能体陷入局部最优,无法发现更好的策略。这就像一个餐馆老板,一旦他发现一道菜受欢迎,他就只买这道菜,不再尝试其他新菜品,最终可能错失做大做强的机会。

通过在目标函数中添加一个正的熵项(因为我们是最大化目标函数,所以是加号),PPO鼓励策略保持一定的随机性,从而促进探索。这就像给餐馆老板一个“创新奖励”,鼓励他尝试新的食材和烹饪方法,推出新菜品,即使这些尝试不一定每次都成功

优点在于稳健,简单(使用人为限制的方式,避免复杂的计算),数据可重用,探索和利用的平衡(熵奖励)。

缺点在于超参数敏感,难以处理极端稀疏的奖励。

奖励非常稀疏:即智能体只有在完成任务的最后才获得奖励,中间过程没有任何反馈

9.4-9.5

nothing, because forget

9.6

配置miniob的环境

9.7

阅读miniob的教程,和os有些相关的地方,不得不说,有了os的开发经验,阅读miniob的文档还是蛮有好处的,至少一些涉及os的内容可以轻松理解。

找lc要了pku的编译原理实验minic,我是打算自己补编译原理的课的,不知道为什么北科的物联网不教编译原理,而是教嵌入式,可能想培养嵌入式方向的工程师吧,而不是软件工程师。

想做的和能做的东西感觉挺多的,但是已经大三了,实话讲有点没时间了,而且我还是那种,同时有很多事做,我就什么都不想做的人。。。

9.8

LSM-Tree

LSM-Tree(Log-Structured Merge Tree)是一种面向磁盘、写友好的分层有序存储结构,核心思想只有一句话:

“先把所有写操作变成顺序追加,再定期归并整理,用后台 I/O 换前端性能。”


一、为什么会出现

  • B+ 树每次写入都要寻道、随机写页,磁盘随机 I/O 是瓶颈
  • 大数据/日志/时序场景写多读少,需要高吞吐、低延迟写入结构。
  • LSM-Tree 把“随机写”变成“顺序写”,写入性能比 B+ 树高 5–10 倍

二、核心组件与数据流向

  1. MemTable(内存)

    • 跳表/红黑树等有序结构
    • 写操作先追加到 WAL(预写日志),再插 MemTable,崩溃可恢复
  2. Immutable MemTable

    • MemTable 满后被冻结,只读
    • 后台线程把它顺序刷盘成 SSTable 文件(Level 0)。
  3. SSTable(磁盘)

    • 按键有序、不可变
    • 分层管理:L0 → L1 → … → Ln,容量逐层放大 10 倍
    • 每层内部键范围不重叠(L0 除外)。
  4. Compaction(合并)

    • 定期将相邻层 SSTable 归并
    • 消除旧版本/删除标记,控制空间放大与读放大
    • 策略:Leveling(每层一个文件,读快写放大) vs Tiering(每层多文件,写快读放大)。

三、典型工作流程
① 写 → 追加 WAL + 插入 MemTable
② MemTable 满 → 冻结 → 刷成 L0 SSTable(顺序写)
③ 后台 Compaction → 将 L0 合并到 L1,依此类推
④ 读 → 先 MemTable → Immutable → L0 → L1…,Bloom Filter 快速跳过不存在文件


四、优势 vs 代价
优势

  • 写入近乎内存速度,磁盘顺序落盘;
  • 易于压缩/去重,空间利用率可调;
  • 并发简单(SSTable 只读,无就地更新锁)。

代价

  • 读放大:可能要查多层文件;需靠 Bloom Filter、索引块、缓存缓解。
  • 合并放大:后台 Compaction 占用 I/O;需限速/调度策略。
  • 空间放大:旧版本未回收前暂时冗余。

五、常见系统

  • LevelDB / RocksDB(C++)
  • Cassandra / HBase / BigTable(Java)
  • TiDB、InfluxDB、Doris、ScyllaDB 等均内置 LSM-Tree 引擎。

一句话总结
LSM-Tree = “内存顺序树 + 磁盘顺序文件 + 后台归并” 三层模型;
顺序写随机写,用后台合并前台性能,成为现代写密集存储系统的事实标准。


下面用一个快递仓库的比喻,把 LSM-Tree 的写入、读取、合并全流程串起来——全程无术语,看完就能讲给别人听。


场景设定
你开了个爆款快递仓库,只收“”过来的包裹(订单),顾客偶尔来查“我的包裹在吗?”。

目标:卸货要快(写入吞吐量),查货不能太慢(读延迟),仓库不能爆炸(空间控制)。


  1. 卸货区 = MemTable
  • 货车一到,工人直接把包裹往卸货区桌子上一扔(顺序写)。
  • 桌子自带魔法排序传送带,包裹瞬间按收件人姓名排好序。
  • 为了防断电,旁边小本子先记一笔“今天来了哪些包裹”——这就是 WAL

特点:卸货速度 = 纯内存速度,磁盘零寻道。


  1. 桌子满了 → 冻结成 Immutable 区
  • 卸货区桌子只能放 1000 件,满了就贴封条,工人换张新桌子继续收。
  • 封条区只读,后台小弟把它整桌端走,做成一本有序花名册(L0 SSTable),一次性倒进远处货架——顺序写,无随机搬动。

  1. 远处货架 = 分层 SSTable
  • L0 层:刚倒进来的花名册,可能重叠(同名包裹在不同册子里)。
  • L1/L2 … 层:更大货架,保证同名包裹只出现一次(归并后)。
  • 规则:每层货架比前一层大 10 倍,L1 是 10 本册子,L2 是 100 本,依此类推。

  1. 合并 = 定期“并册”Compaction
  • 后台小弟每天把 L0 的 10 本小册子L1 的 1 本大册子 一起读出来,按姓名重新排序合并,生成新的 L1 大册子
  • 重复包裹只留最新;已取消(删除)的包裹直接扔掉。
  • 写新册→删旧册,全程顺序读写,不堵前台卸货。

  1. 顾客查件 = 读流程
    顾客:“有我的包裹吗?”
    工人从近到远依次找:
  2. 当前卸货区桌子 → 2. 封条区桌子 → 3. L0 小册子 → 4. L1/L2 大册子
  • 每本册子封面贴个**“ Bloom 便利贴”:名字不在封面就直接跳过**,不用翻册子
  • 一旦在某层找到,立即返回;找不到就继续往下。

  1. 代价与权衡(用比喻就能懂)
    问题 比喻场景 LSM 做法
    读慢 要翻很多册子 封面便利贴 + 大册子缓存
    合并占人力 小弟天天并册 后台限速、分层调度
    空间膨胀 册子多份副本 合并后删除旧册

一句话总结(快递版)
LSM-Tree = “卸货区顺序堆 + 封条区顺序搬 + 后台定期并册”
顺序劳动前台速度,再用并册把空间、读速度慢慢赚回来——
所以卸货飞快,查件稍慢但可接受,仓库永不爆炸


在 LSM-Tree 里**没有“原地修改”这个概念;
任何更新、删除、修改都等价于“再写一条新记录”,随后靠
合并(Compaction)**把旧版本清理掉。
下面用“快递册子”比喻 + 代码级细节双轨讲清:


  1. 更新(Update)= 写新包裹
  • 顾客改地址 → 不翻旧册子,直接往当前卸货区桌子写一条“最新包裹”(相同 key,新版本)。
  • 查件时先遇到新版本就返回,旧版本永远看不到。
  1. 删除(Delete)= 写“墓碑”
  • 顾客退单 → 写一条 特殊包裹:key = 原 key,value = 墓碑标记(tombstone)
  • 查件遇到墓碑立即返回“不存在”;合并时遇到墓碑 + 旧包裹 → 一起扔掉
  1. 合并时真正“抹掉”旧记录
    后台并册逻辑(伪代码):
1
2
3
4
5
for each key in {L0_old, L1_old}:
if 遇到墓碑:
扔掉墓碑 + 之前所有同 key 记录
else
只保留最新版本,旧版本丢弃

结果:新 L1 册子里没有 tombstone,也没有旧版本


  1. 代码级例子(RocksDB 风格)
1
2
3
4
5
6
7
8
9
10
// 原始记录
db.Put("user:42", "Alice");

// 更新 → 写新记录,同一 key
db.Put("user:42", "Bob"); // 旧值仍在文件,但不可见

// 删除 → 写墓碑
db.Delete("user:42"); // 查 user:42 返回 NotFound

// 后台 compact 后,旧值 + tombstone 物理消失

  1. 一致性保证
  • 同 key 多条记录:按**序列号(sequence number)**从大到小排序,最新先看到
  • 崩溃恢复:WAL 里也有同 key 多条日志,重启后重放即可。

一句话总结
在 LSM-Tree 里,“改” = 追加一条新版本,“删” = 追加一条墓碑
真正的旧数据清理发生在后台合并阶段,既保证写性能,又保证语义正确。

9.25

看了学长的保研贴,感觉可以向英语+算法+科研实习+一个项目+论文阅读方向努力

具体的重要程度(个人认为):科研实习> 项目>英语>论文阅读>算法(算法主要是机试考察)

目前规划是这个学期搞英语和项目+每周阅读论文+算法,寒假开始科研实习

寒假大概是一月初放到3月份,寒假开始应该算早了

英语这个学期主要是六级和阅读论文和分析论文

算法是打算参加蓝桥杯(以赛促学吧)

项目就是系统能力大赛,争取搞个二等或者一等,当然特等也想过,但是可能有难度。

感觉要一天24小时工作了,争取计算所和软件所,不过看今年的保研趋势和保研经历,可以争南大,华科这种,因为前年有学长到南大和华科了,当然今年有学长去了清华,这个感觉还是太难了,rk太低了,低rk只能争取不看重rk(当然rk也不能太低,要是没有保研名额就寄了,所以同时还得好好学习,好好考试),看能力的老师,我觉得各个学校应该都有。