永久Memories
type: channel
link: https://blog.aqhours.cn/
items: Arch Linux 引导修复实录：一次硬盘故障后的恢复过程
items: Project rCore：在一个RISC-V机器上运行自己实现的内核
items: Project rCore：进程抽象
items: 记2023年秋冬季开源操作系统训练营一二阶段
items: rCore ch1 从一个 Bare Metal APP 开始
items: 对于虚拟内存 Virtual Memory的理解
items: Linux 下的输入法⌨️解决方案— fcitx5 + rime-ice
items: 信号量机制
items: 记安装 Pytorch 环境 (GPU 版本)
items: 算法题解：给表达式树添加合适的括号

Arch Linux 引导修复实录：一次硬盘故障后的恢复过程
type: item
date: 2023-11-26T22:14:00+08:00
description: 起因 如图所示，我的硬盘出现了故障。开机后系统报错提示 3F0：Operating System Not Found，显然是启动盘已经损坏。进一步检查后确认，一块 SSD 硬盘损坏，导致无法引导系统。值得幸运的是，我的 Arch Linux 系统本身并不在这块硬盘上。但遗憾的是，GRUB 引导程序是安装在这块出问题的硬盘上的。更换硬盘后，虽然系统数据仍在，但失去了引导项，自然也就无法启动 Arch Linux。修复思路本质上，这是一个典型的引导损坏问题。解决方法是通过 Live 系统重新安装 GRUB，并生成引导配置。以下是完整的修复过程，包含了很多网上教程略过的细节，尤其是涉及 btrfs 文件系统的挂载方式。修复步骤1. 进入 Arch Linux Live 环境使用 Arch Linux 安装镜像制作启动 U 盘，进入 Live 系统。2. 挂载原系统分区我的 Arch Linux 安装在 btrfs 文件系统中，因此挂载方式与 ext4 略有不同：mount -t btrfs -o subvol=@ /dev/nvme1n1p3 /mnt # 挂载根子卷（@ 为子卷名） mount /dev/nvme0n1p1 /mnt/boot # 挂载 boot 分区确保 /mnt 和 /mnt/boot 分别挂载到系统根目录和引导分区。3. 切换到原系统环境使用 arch-chroot 切换到已挂载的系统环境：arch-chroot /mnt此时已获得对原系统的完全控制权限。4. 重装 GRUB 引导程序执行以下命令将 GRUB 安装到 EFI 分区：grub-install --target=x86_64-efi --efi-directory-id=ARCH5. 安装微码与内核镜像（关键步骤）很多教程忽略了这一点：如果是全新硬盘或曾经清理过 /boot，需要重新安装微码和内核镜像：pacman -S intel-ucode pacman -S linux建议使用 iPhone 或其他智能手机通过 USB 开启热点，使系统在 Live 环境下能够联网，避免手动配置网络。6. 生成 GRUB 配置文件grub-mkconfig -o /boot/grub/grub.cfg生成日志中应能看到已检测到内核镜像文件。此时如果系统存在双系统（如 Windows 安装在另一块硬盘上），但未被识别，不用担心，等会重复生成 GRUB 配置即可。处理 Windows 启动项未识别问题首次生成 GRUB 配置时，如未识别 Windows 启动项，可在图形界面环境下重新生成一次：确保 /boot 或 /boot/efi 正确挂载；再次执行：sudo grub-mkconfig -o /boot/grub/grub.cfg这里我是没有找到 Windows 的，因为我的 Windows 装在另一块硬盘上，没有检测到，我们通过引导程序进入 Linux 图形化界面之后再引导一遍即可。当然需要先将已经挂载的盘符解除挂载 umount 再重启。为了再次引导并且能够找到 Windows 启动程序，我们需要再次把 boot 分区挂载到/mnt/efi，再执行，结果如下。再重启就可以看到熟悉的 grub 引导界面了，可以成功引导进入 Arch Linux 和 Windows。此时应能识别到 Windows 启动项，重启后即可在 GRUB 界面中看到熟悉的双系统启动菜单。经验总结我之前对于挂载 mount 其实一直不太理解，现在学了操作系统的文件系统的设计和实现之后，然后现在又操作了一波，mount 就是把某个盘符，挂载到某个目录下，我们通过被挂载的目录可以直接访问被挂载的目录。Arch Linux 出意外了可以通过一块 Arch Linux 启动盘，通过挂载系统盘再 arch-chroot，获取到对 Arch Linux 的控制权。

Project rCore：在一个RISC-V机器上运行自己实现的内核
type: item
date: 2023-11-16T12:36:00+08:00
description: 参考文章：扩展阅读：RISC-V 架构与内核启动 · GitBook我们在 rCore 的第一步是能够在没有任何操作系统的裸机上运行我们的内核，并能够把控制权转交给 Rust 方便我们编写内核源代码，下面一起来看看为了达到这个目的需要做些什么吧。启动 QemurCore 编译内核源代码时设置的目标平台是 riscv64gc-unknown-none-elf，我们想要启动我们的内核的话，需要一台 RISC-V 架构的机器来运行我们的内核，我们可以使用 Qemu 来模拟一台。启动的时候需要给定一些启动参数，可以指定我们这台模拟的机器有些什么东西，比如下面是一条启动 qemu 并加载我们的内核的指令。qemu-system-riscv64 \ -machine virt \ -nographic \ -bios ../bootloader/rustsbi-qemu.bin \ -device loader,file=target/riscv64gc-unknown-none-elf/release/os.bin,addr=0x80200000这里介绍一些启动参数：-nographic 表示模拟器不需要提供图形界面，而只需要对外输出字符流。-bios 设置 Qemu 模拟器开机时用来初始化的引导加载程序（bootloader），它是运行在 M 态的程序，负责初始化以及启动内核（指会跳转到内核所在地址），默认会被加载到内存中 0x80000000 这个地址。默认情况下， Qemu 会使用 OpenSBI 启动，但 rCore-Tutorial 在这里指定了使用 RustSBI 启动。-device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA) 指定了内核加载的位置是 KERNEL_ENTRY_PA，即 0x80200000，内核镜像会被放置在内存的这个位置。内核启动过程Mret: 从 M 态返回到特权级更低的模式。Ecall 陷入特权级更高的模式。这里解释一下 Qemu 模拟的机器启动以后到底做了些什么事情：第一阶段，将必要的文件载入到 Qemu 物理内存之后，Qemu CPU 的程序计数器（PC, Program Counter）会被初始化为 0x1000， CPU 从物理地址 0x1000 开始执行一段硬件（ Qemu 模拟出的）上的引导代码，此时 CPU 位于 M 态。第二阶段，Qemu 的第一阶段固定跳转到 0x80000000 ，RustSBI 已经被加载到这里，开始执行 RustSBI 的初始化代码，此时 CPU 位于 M 态。第三阶段，通过 mret 跳转到 0x80200000 执行内核的第一条代码，同时 CPU 切换到 S 态。0x80200000 这个地址是直接编码在 SBI 里的，会固定跳转到该地址。我们已经编写好的内核的第一条代码是位于 os/src/entry.asm 中的 _start 符号所在的位置。不过连接器默认的内存布局不能按照我们的想法把第一条指令加载到正确的地方，我们可以通过 链接脚本 (Linker Script) 调整链接器的行为，使得最终生成的可执行文件的内存布局符合 Qemu 的预期，即内核第一条指令的地址应该位于 0x80200000 。// os/src/entry.asm .section .text.entry .globl _start _start: la sp, boot_stack_top call rust_main .section .bss.stack .globl boot_stack_lower_bound boot_stack_lower_bound: .space 4096 * 16 .globl boot_stack_top boot_stack_top:第 2 行表明我们希望将第 2 行后面的内容全部放到一个名为 .text.entry 的段中，将其命名为 .text.entry 从而区别于其他 .text 的目的在于我们想要确保该段被放置在相比任何其他代码段更低的地址上。这样，作为内核的入口点，这段指令才能被最先执行。下面是链接脚本，第 12 行我们将包含内核第一条指令的 .text.entry 段放在最终的 .text 段的最开头，同时注意到在最终内存布局中代码段 .text 又是先于任何其他段的。因为所有的段都从 BASE_ADDRESS 也即 0x80200000 开始放置，这就能够保证内核的第一条指令正好放在 0x80200000 从而能够正确对接到 Qemu 上。OUTPUT_ARCH(riscv) ENTRY(_start) BASE_ADDRESS = 0x80200000; SECTIO…

Project rCore：进程抽象
type: item
date: 2023-11-11T17:30:00+08:00
description: 进程概念在rCore关于进程这一部分之前，我们有 任务 的概念，即 正在执行的程序 ，主角是程序。而相比于 任务 ， 进程 (Process) 的含义是 在操作系统管理下的程序的一次执行过程。打一个比方，可执行文件本身可以看成一张编译器解析源代码之后总结出的一张记载如何利用各种硬件资源进行一轮生产流程的 蓝图 。而内核的一大功能便是作为一个硬件资源管理器，它可以随时启动一轮生产流程（即执行任意一个应用），这需要选中一张蓝图（此时确定执行哪个可执行文件），接下来就需要内核按照蓝图上所记载的对资源的需求来对应的将各类资源分配给它，让这轮生产流程得以顺利进行。当按照蓝图上的记载生产流程完成（应用退出）之后，内核还需要将对应的硬件资源回收以便后续的重复利用。因此，进程就是操作系统选取某个可执行文件并对其进行一次动态执行的过程。相比可执行文件，它的动态性主要体现在：1.它是一个过程，从时间上来看有开始也有结束；2.在该过程中对于可执行文件中给出的需求要相应对 硬件/虚拟资源 进行 动态绑定和解绑 。数据结构进程标识符同一时间存在的所有进程都有一个唯一的进程标识符，它们是互不相同的整数，这样才能表示表示进程的唯一性。这里我们使用 RAII 的思想，将其抽象为一个 PidHandle 类型，当它的生命周期结束后对应的整数会被编译器自动回收。当然我们也需要一个使用简单栈式分配策略的进程标识符分配器 PidAllocator。pub struct PidHandle(pub usize); struct PidAllocator { current: usize, recycled: Vec<usize>, }内核栈就像之前为每个 task 提供一个内核栈一样，我们也需要为每个进程提供一个内核栈。可以想想内核地址空间布局，有一片连续的空间用于放置每个进程的内核栈。在没有进程之前我们将每个应用的内核栈按照人为给定的应用编号从小到大的顺序将它们作为逻辑段从高地址到低地址放在内核地址空间中，且两两之间保留一个守护页面使得我们能够尽可能早的发现内核栈溢出问题。现在引入了进程的概念后我们将应用编号替换为进程标识符决定每个进程内核栈在地址空间中的位置。这里的实现关键是实现 kstack_alloc() 用于申请内核栈空间，在 new 或 fork 创建新进程的时候会用到，其中需要实现 kernel_stack_position() 去获取该进程对应内核栈在内核地址空间中的位置，获取到位置后需要在内核地址空间中建立映射。/// Kernel stack for a process(task) pub struct KernelStack(pub usize); /// allocate a new kernel stack pub fn kstack_alloc() -> KernelStack { let kstack_id = KSTACK_ALLOCATOR.exclusive_access().alloc(); let (kstack_bottom, kstack_top) = kernel_stack_position(kstack_id); KERNEL_SPACE.exclusive_access().insert_framed_area( kstack_bottom.into(), kstack_top.into(), MapPermission::R | MapPermission::W, ); KernelStack(kstack_id) } /// Return (bottom, top) of a kernel stack in kernel space. /// 根据进程标识符计算内核栈在内核地址空间中的位置 pub fn kernel_stack_position(app_id: usize) -> (usize, usize) { let top = TRAMPOLINE - app_id * (KERNEL_STACK_SIZE + PAGE_SIZE); let bottom = top - KERNEL_STACK_SIZE; (bottom, top) } Task Control Block在初始化之后就不再变化的元数据：直接放在任务控制块中。在运行过程中可能发生变化的元数据：则放在 TaskControlBlockInner 中，将它再包裹上一层 UPSafeCell<T> 放在任务控制块中。保证外层只能获取任务控制块的不可变引用，若想修改里面的部分内容的话这需要 UPSafeCell<T> 所提供的内部可变性。进程控制块的本体是被放到内核堆上面的, 子进程的进程控制块并不会被直接放到父进程控制块中，因为子进程完全有…

记2023年秋冬季开源操作系统训练营一二阶段
type: item
date: 2023-11-06T16:07:00+08:00
description: 背景我是一名计算机专业的学生，现在进入大三上学期，学校已经安排了操作系统课程，但老师教授的偏理论性缺少实践的过程。我本来已经在学习MIT 6.S081操作系统课程，朋友推荐了rCore，采用Rust语言编写的操作系统课程，觉得非常棒交流环境很好而且是国产然后配套资源也很全，就报名学习了。一起学rCore吧！我也认为在本科阶段，就需要认真学习理解计算机核心课程包括操作系统，应该学习计算机底层知识去了解探索计算机科学领域的魅力和故事，打下扎实基础，毕竟是计算机专业的本科生，大学毕业后可能没有足够的时间与精力去做这些事情。收获我觉得rCore tutorial最赞的方面就是每一章都实现了一个操作系统，并且按照操作系统的发展历史由简单到负责，非常符合认知水平，一章章地看下去让我对于操作系统的发展和实现过程有了很深刻的印象。我大概陈述一下我对每个操作系统的认识吧：从把操作系统放在qemu上运行开始，并提供一些printf和shutdown的函数调用，这时候的操作系统以函数库的形式存在。这里关键实现是需要支持函数调用操作，需要维护函数调用上下文。这样好麻烦，每次都要输入一整个程序到计算机然后运行，再计算机等待输入程序再运行。批处理系统出来了，可以一次性将多个程序打包到一起输入计算机。而当一个程序运行结束后，计算机会自动加载下一个程序到内存并开始执行。这里开始有了操作系统的概念。这里关键是AppManager的实现，管理所有的应用程序。但这么多应用一起执行，我们不希望相互影响，更不能影响内核代码，也不希望一个应用崩溃了就整个系统崩溃了，就引入了特权级机制，给os和应用程序不同的特权级，这里需要硬件软件一起实现，硬件控制User特权级下不能执行内核的代码，软件调整特权级需要通过_alltrap保存trapContext，同时找到内核态的traphandle的入口点，开始执行内核态的代码，之后再通过_restore恢复trapContext。但是这些应用是逐个执行的，还是独占了计算机资源，它们在进行IO操作的时候CPU是空闲的，就引入了多道批处理系统，一次性把多个应用程序加载进入内存，在计算机中以task的形式存在，这里关键是TaskManager的实现管理所有的task，同时为每一个task实现一个TaskControlBlock，存储控制信息。这里也需要顺便实现调度算法调度各个task。这个时候task一旦被调度开始执行，os只能等待操作系统主动放弃cpu，但是有些应用不自觉会长时间占着，也对我们的task不公平，这时候需要计算机来来控制，分时系统的概念形成了。这里的实现关键是定时发送时间中断使用户程序陷入trap交出CPU。最后为了进一步提升操作系统的隔离性和防御性引入了地址空间和进程的抽象。实验总结lab11.sys_taskinfo目标是查找每个 task 的各个系统调用的次数和距离 task 第一次被调用的时间差。这个很好实现，第一眼就应该想到显然需要把这两个值放到 task 对应的 TCB 里。Task 第一次被调度的时候记录时间并保存下来。Task 每次进入 trap_handle 的时候更新 syscall 次数。写值的时候直接在 unsafe 块内向指针指向的地方写就行。lab21.兼容 sys_taskinfo 和 sys_time因为引入了地址空间，我们在内核态看到的用户程序传来的虚拟地址在内核的地址空间中没有被映射，所以我们需要去通过当前 task 的地址空间的 memory set 去获取到具体的物理地址再写入。2.sys_mmap需要页面对齐！！！指 len 长度应当修改为 4096 的整数倍。可以直接调用 insert_framed_area 建立映射，这里也会一起申请好 pagetable。判断地址有没有被建立映射，可以去遍历当前地址空间的 VPNRange。3.sys_munmap这里要 len 正确才行！我们不对齐 len，len 错误的话直接返回错误。物理方面需要去移除输入的页面的 areas。软件方面需要取消建立的映射。lab31.兼容之前的代码之前 task 的功能被拆分到了 task 和 process，稍微改动即可。2.syscall spawn参考 fork 和 exec，很容易实现，关键是要创建新的进程并加入调度。3.stride 调度算法一种优先级调度算法，维护一个优先级和步长就行。每次调度的时候不再从 task 队首选择，而是选择当前步长最小的进行调度，并更新步幅。总结与展望前前后后花了几个礼拜的时间从学习rust开始，到配置qemu和docker环境，开始看rcore文档，开始写lab，痛苦debug，对所有权冲突焦虑不解，对概念理解不透彻。但还是一步一步实现了从0到1，最重要最后看到自…

rCore ch1 从一个 Bare Metal APP 开始
type: item
date: 2023-10-29T15:16:00+08:00
description: 🌲长青 #OS #Rust这里是 rCore-Tutorial-Book-v3 3.6.0-alpha.1 文档的阅读笔记。解释一下文章标题: Bare Metai (裸机平台)，在裸机平台上的软件没有传统操作系统支持。我们的目标是在这样一个裸机平台上编写一个应用程序，功能是打印出 HelloWorld！应用程序执行环境与平台支持编程工作能够如此方便简洁并不是理所当然的，实际上有着多层硬件和软件工具和支撑环境隐藏在它背后，才让我们不必付出那么多努力就能够创造出功能强大的应用程序。生成应用程序二进制执行代码所依赖的是以 编译器 为主的 开发环境 ；运行应用程序执行码所依赖的是以 操作系统 为主的 执行环境 。本章主要是讲解如何设计和实现建立在裸机上的执行环境，并让应用程序能够在这样的执行环境中运行。从而让同学能够对应用程序和它所依赖的执行环境有一个全面和深入的理解。本章设计实现了一个支持显示字符串应用的简单操作系统–“三叶虫”操作系统 – LibOS，它的形态就是一个函数库，给应用程序提供了显示字符串的函数。库操作系统（Library OS，LibOS）LibOS 以函数库的形式存在，为应用程序提供操作系统的基本功能。它最早来源于 MIT PDOS 研究小组在1996年左右的 Exokernel（外核）操作系统结构研究。Frans Kaashoek 教授的博士生 Dawson Engler 提出了一种与以往操作系统架构大相径庭的 Exokernel（外核）架构设计 1 ，即把传统的单体内核分为两部分，一部分以库操作系统的形式（即 LibOS）与应用程序紧耦合以实现传统的操作系统抽象，并进行面向应用的裁剪与优化；另外一部分（即外核）仅专注在最基本的安全复用物理硬件的机制上，来给 LibOS 提供基本的硬件访问服务。这样的设计思路可以针对应用程序的特征定制 LibOS ，达到高性能的目标。这种操作系统架构的设计思路比较超前，对原型系统的测试显示了很好的性能提升。但最终没有被工业界采用，其中一个重要的原因是针对特定应用定制一个LibOS的工作量大，难以重复使用。人力成本因素导致了它不太被工业界认可。应用程序执行环境计算机科学中遇到的所有问题都可通过增加一层抽象来解决。All problems in computer science can be solved by another level of indirection。– 计算机科学家 David Wheeler现代编译器工具集（以 C 或 Rust 编译器为例）的主要工作流程如下:源代码（source code） –> 预处理器（preprocessor） –> 宏展开的源代码宏展开的源代码 –> 编译器（compiler） –> 汇编程序汇编程序 –> 汇编器（assembler）–> 目标代码（object code）目标代码 –> 链接器（linker） –> 可执行文件（executables）目标平台与目标三元组为什么需要知道目标平台？编译器在将源代码通过编译、链接得到可执行文件的时候需要知道程序要在哪个 平台 (Platform) 上运行才能最后生成可执行文件。这里平台主要是指 CPU 类型、操作系统类型和标准运行时库的组合。如果用户态基于的内核不同，会导致系统调用接口不同或者语义不一致；如果底层硬件不同，对于硬件资源的访问方式会有差异。特别是如果 ISA 不同，则向软件提供的指令集和寄存器都不同。编程语言怎么处理目标平台！Rust编译器通过 目标三元组 (Target Triplet) 来描述一个软件运行的目标平台。我们选择 riscv64gc-unknown-none-elf 目标平台。这其中的 CPU 架构是 riscv64gc CPU 厂商是 unknown 操作系统是 none elf 表示没有标准的运行时库（表明没有任何系统调用的封装支持），但可以生成 ELF 格式的执行程序。Rust 标准库与核心库编程语言的标准库比如Rust 语言标准库–std 或三方库的某些功能会直接或间接的用到操作系统提供的系统调用。但目前我们所选的目标平台不存在任何操作系统支持。幸运的是，Rust 有一个对 Rust 语言标准库–std 裁剪过后的 Rust 语言核心库 core。core库是不需要任何操作系统支持的移除标准库依赖不是说修改编译器编译选项就可以完成针对不同执行环境的编译的，我们需要修改我们的代码改造哪些需要用到之前依赖的执行环境，当然在你没有改造完成之前你也不会编译成功的😂。提供 panic_handler 功能应对致命错误在使用 Rust 编写应用程序的时候，我们常常在遇到了一些无法恢复的致命错误（panic），导致程序无法继续向下运行。这时手动或自动调用 pa…

对于虚拟内存 Virtual Memory的理解
type: item
date: 2023-10-18T22:00:00+08:00
description: 我先来说一下我自己对于虚拟内存或者页表的认知吧。当我还是个学生并第一次听到学到这个词时，我认为它还是很直观简单的。这能有多难呢？无非就是个表单，将虚拟地址和物理地址映射起来，实际可能稍微复杂一点，但是应该不会太难。可是当我开始通过代码管理虚拟内存，我才知道虚拟内存比较棘手，比较有趣，功能也很强大。存在某种形式的映射关系；并且映射关系对于实现隔离性来说有帮助。地址空间 Address Spaces当然一切操作的目的都是为了实现操作系统的隔离性。如果我们不做任何工作，默认情况下我们是没有内存隔离性的，内存泄露问题很容易发生。所以，我们想要某种机制，能够将不同程序之间的内存隔离开来，这样类似的事情就不会发生。一种实现方式是地址空间（Address Spaces）。给包括内核在内的所有程序专属的地址空间。所以，当我们运行cat时，它的地址空间从0到某个地址结束。当我们运行Shell时，它的地址也从0开始到某个地址结束。内核的地址空间也从0开始到某个地址结束。如果 cat 程序想要向地址1000写入数据，那么 cat 只会向它自己的地址1000，而不是 Shell 的地址1000写入数据。所以，基本上来说，每个程序都运行在自己的地址空间，并且这些地址空间彼此之间相互独立。在这种不同地址空间的概念中，cat 程序甚至都不具备引用属于 Shell 的内存地址的能力。 这是我们想要达成的终极目标，因为这种方式为我们提供了强隔离性，cat 现在不能引用任何不属于自己的内存。所以现在我们的问题是如何在一个物理内存上，创建不同的地址空间，因为归根到底，我们使用的还是一堆存放了内存信息的DRAM芯片。页表 Page Table 创建不同的地址空间创建不同的地址空间的最常见的方法，同时也是非常灵活的一种方法就是使用页表（Page Tables）。页表是在硬件中通过处理器和内存管理单元（Memory Management Unit）实现。CPU 正在执行指令，对于任何一条带有地址的指令，其中的地址应该认为是虚拟内存地址而不是物理地址。虚拟内存地址会被转到内存管理单元（MMU，Memory Management Unit）。内存管理单元会将虚拟地址翻译成物理地址。之后这个物理地址会被用来索引物理内存，并从物理内存加载，或者向物理内存存储数据。为了能够完成虚拟内存地址到物理内存地址的翻译，MMU 会有一个表单，表单中，一边是虚拟内存地址，另一边是物理内存地址。通常来说，内存地址对应关系的表单也保存在内存中。所以 CPU 中需要有一些寄存器用来存放表单在物理内存中的地址。 现在，在内存的某个位置保存了地址关系表单，我们假设这个位置的物理内存地址是0x10。那么在 RISC-V 上一个叫做 SATP 的寄存器会保存地址0x10。这样，CPU 就可以告诉 MMU，可以从哪找到将虚拟内存地址翻译成物理内存地址的表单。MMU 并不会保存 page table，它只会从内存中读取 page table，然后完成翻译!每个应用程序都有自己独立的表单，并且这个表单定义了应用程序的地址空间。所以当操作系统将CPU从一个应用程序切换到另一个应用程序时，同时也需要切换SATP寄存器中的内容，从而指向新的进程保存在物理内存中的地址对应表单。表单是如何工作的以 page 为粒度对于每个虚拟地址，在表单中都有一个条目，如果我们真的这么做，表单会有多大？原则上说，在 RISC-V 上会有多少地址，或者一个寄存器可以保存多少个地址？寄存器是 64 bit 的，所以有多少个地址呢？是的，2^64 个地址，所以如果我们以地址为粒度来管理，表单会变得非常巨大。实际情况不可能是一个虚拟内存地址对应 page table 中的一个条目。为每个page创建一条表单条目，所以每一次地址翻译都是针对一个page。而RISC-V中，一个page是4KB，也就是4096Bytes。这个大小非常常见，几乎所有的处理器都使用4KB大小的page或者支持4KB大小的page。虚拟内存地址都是64bit，这也说的通，因为 RISC-V 的寄存器是64bit 的。但是实际上，在我们使用的 RSIC-V 处理器上，并不是所有的64bit 都被使用了，也就是说高25bit 并没有被使用。这样的结果是限制了虚拟内存地址的数量，虚拟内存地址的数量现在只有2^39个，大概是512GB。当然，如果必要的话，最新的处理器或许可以支持更大的地址空间，只需要将未使用的25bit 拿出来做为虚拟内存地址的一部分即可。在剩下的39bit中，有27bit被用来当做index，12bit被用来当做offset。offset必须是12bit，因为对应了一个page的4096个字节。page中的每个字节都可以被offset索引到。物理内存地址是56…

Linux 下的输入法⌨️解决方案— fcitx5 + rime-ice
type: item
date: 2023-10-17T20:25:00+08:00
description: 为了修复输入法选中框中 emoji 符号不能正常显示的问题，我再次调整了我的输入法配置，也对 Linux 下的输入法配置有了比较清晰的理解，方便我日后再次配置，便在这里记录下来。名词定义明白三个概念，什么是 fcitx5，rime 和 rime-ice。fcitx5 是一个输入法框架，通常用于 Linux 操作系统上，用于支持多种不同语言和输入法的输入。配置文件在 /home/melody/.config/fcitx5/conf/RIME | 中州韵输入法引擎 是一个跨平台的输入法算法引擎，可以运行在输入法框架之上，需要进行各种配置后才可以使用的比较舒服。rime-ice 则是一个 RIME 的配置文件仓库。配置文件在 /home/${USER}/.local/share/fcitx5/rime/安装GitHub - Mark24Code/rime-auto-deploy: Make using Rime easy. 一个自用的脚本，帮助无痛快速安装、部署 Rime 输入法（中州韵、小狼毫，鼠须管）以及部署配置。推荐使用这个脚本一键无痛安装。具体使用方法看脚本文档即可，我在安装过程中没有遇到大问题，即使你不是第一次使用 rime，脚本可以自动备份旧的配置文件并且安装新的配置好的文件。配置文件说实话，rime 的配置文件真的非常的混乱，即使是在已经配置好的文件中，还好每个文件中的注释都非常清晰易懂。里面的文件主要有三个类型。XXX. custom. yaml 一些功能配置的模板文件XXX. Schema. Yaml 各个输入法方案的配置文件，不需要使用的模式可以直接删除。配置文件格式 按键对应也包含在里面XXX. Yaml 自动生成的一些文件，修改这里的文件是没有用的。我主要进行了以下配置：一、设置<>键进行翻页在 default. Custom. Yaml 中添加如下代码，重新部署输入法即可实现<>翻页。key_binder: bindings: - { when: has_menu, accept: less, send: Page_Up } - { when: has_menu, accept: greater, send: Page_Down }二、设置模糊音自动配置脚本配置的完整 rime，模糊音居然是自动开启了 nl 模糊，🤔🤔🤔使用体验非常不好，里觉得合理吗同样在模糊音配置文件 rime_ice.custom.yaml 中注释掉对应部分即可。三、配置输入法主题风格，字体rime-ice 的配置自带了很多皮肤字体设定，但是在 fcitx5 中都没有生效。fcitx 5 需要去框架配置中修改皮肤设定，使用 yay 下载皮肤之后按照配置文件中说明的使用即可。字体我推荐霞鹜文楷~非常美丽：D推荐使用这个皮肤GitHub - hosxy/Fcitx5-Material-Color: 一款使用Material Design 配色的 fcitx5 皮肤，喜欢的话给个 star 吧 ヾ(≧へ≦)〃 😉四、修复输入框中 emoji 无法正常显示的问题这个问题才是我这次配置输入法的主要目的！首先发现有同样问题的 issue arch + fcitx5 不显示emoji？ · Issue #50 · fkxxyz/rime-cloverpinyin · GitHub，惊喜！有救了，大家果然给出了解决方案。Fontconfig and Noto Color Emoji | dotfiles 104542 – Color emojis are not displayed104542 – Color emojis are not displayed按照解决方案中的描述，需要修改 /etc/fonts/conf.d/68-color-emoji.conf 这个配置文件，我欣喜的去找，发现我的目录下没有这个配置文件。但是很显然这个配置文件不是自己直接创建就可以使用的类型。我的解决问题之路陷入了迷茫期。这期间反复尝试过了重新安装 emoji 字体，都没有生效，直到我在无意间尝试安装 emoji 的时候发现了这个仓库，救命稻草啊！安装完成之后，果然我也有了需要修改的配置文件，直接修改，再重启输入法，问题解决了！ 总结这样看下来其实并不难，自己主动去配置经历一遍就好了，Linux 下配置是最方便的了！关键是要对自己配置的东西知其然，希望我在下次配置 Linux 输入法的时候不会连配置文件都找不到，不会缕不清配置文件中的关系。

信号量机制
type: item
date: 2023-10-11T22:13:00+08:00
description: 操作系统课程刚学完了信号量机制，通过写这篇文章梳理巩固我自己对于信号量机制的理解，同时记录对一道例题的思考过程，希望能对信号量机制的运用有更加深刻的印象。信号量机制我们从信号量机制的设立意义开始思考，操作系统分时复用，多个进程并发运行，这些进程之间主要存在两种关系。一种是计算机存在一些临界资源公共资源，不同进程对其的访问应当是互斥的，这些进程之间是互斥竞争关系。另一种是当多个进程协作完成作业时，这些进程之间由于一些特定因果关系存在一定的先后关系，此时进程间为同步协作关系。我们希望并发运行的多个进程能够有条不紊的运行，如果获取不到需要的资源或者因为因果关系得不到满足需要阻塞自己，在需求可以被满足的时候被唤醒，那么必须引入进程同步机制。信号量机制就是一个可行的卓有成效的进程同步技术。进程之间使用一个公共信号来表示资源的数量。可以理解为将资源抽象为信号量，举个例子当一个进程想要使用某个临界资源时，先在信号量大于 0获取到资源对应的信号量即可以使用资源，获取到信号量后将信号量减一，如果资源数量≤ 0 了其他进程就不可使用了需要被堵塞，这样就确保了互斥访问。进程不直接接触临界资源，而是先获取信号量，再去获取资源，实现了进程同步！ 不过这也需要确保不同进程对信号量的修改是互斥的才有意义，否则我们的信号量也不可靠，对于信号量的操作采取原语指令原子操作，即执行时不可被打断，这样能够保证某个进程在修改信号量的时候其他进程不可以对其进行修改。对信号量的操作P (wait) 操作，获取某个信号量，如果没有需要的资源会自行堵塞直到被 V 操作唤醒。获取的是互斥关系的信号量指锁定了对应的临界资源！获取同步关系的信号量对应着临界资源的数量减少。V (signal) 操作，释放了某种资源即释放了信号量。这也是信号量机制实现的关键部分！在题目解答里面有更具体的说明。题目有A、B两人通过信箱进行辩论，每个人都从自己的信箱中取得对方的问题。将答案和向对方提出的新问题组成一个邮件放入对方的邮箱中。假设A的信箱最多放M个邮件，B的信箱最多放N个邮件。初始时A的信箱中有x个邮件（0<x<M），B的信箱中有y个邮件（0<y<N）。辩论者每取出一个邮件，邮件数减1。A和B两人的操作过程描述如下：cobegin { A { while(True) { 从A的信箱中取出一个邮件; 回答问题并提出一个新问题; 将新邮件放入B的信箱; } } B { while(True) { 从B的信箱中取出一个邮件; 回答问题并提出一个新问题; 将新邮件放入A的信箱; } } } coend当信箱不为空时，辩论者才能从信箱中取邮件，否则等待。当信箱不满时，辩论者才能将新邮件放入信箱，否则等待。请添加必要的信号量和P、V［或wait()、signal()］操作，以实现上述过程的同步。要求写出完整的过程，并说明信号量的含义和初值。解答解决信号量机制涉及到的问题最重要的是两个过程。关系分析，就是找出多个进程间的关系。信号量的设置以及在合适的位置加入对应的操作。关系分析第一步，将 A 和 B 两位同学的动作过程理解为 2 个不同的进程！我们去思考进程之间的关系。第二步，思考：有哪些临界资源？即不能同时使用的资源？A 信箱，B 信箱。第三步，进程之间有无因果同步关系？试着从 A 和 B 的动作中去分析关系。想想 A 和 B 有什么行为（原子行为）。A：从 A 信箱中拿信，阅读，写回信，投信到 B 信箱。这要看就很清楚了，A 拿信和投信显然和 B 存在同步关系，显然阅读和写回信和 B 进程没什么关系，自己可以独立完成，这里就不涉及到同步关系。A 只有在 A 信箱中有信时才可以从 A 信箱中拿信。B 同理。A 写完信，当 B 信箱中有空位时 A 才可以把信投入 B 信箱。B 同理。信号量设置信号量初始化我们给两个互斥竞争资源设置互斥信号量 mutexA = 1 mutexB = 1！只有一个资源，只能一个进程使用！给同步协作关系设置同步信号量，拿信需要的资源是信封量，我们设置为 fullA fullB，根据题意分别设置初值为 x 和 y。投信需要的资源是空位量，根据题意我们设置为 emptyA emptyB，根据题意分别设置初值为 M - x， N - y。适当的位置加入对信号量的操作接下来需要考虑的是在什么时候加入信号量的操作。这里我觉得是最简单的一步。只需要在进程处理过程中遇到对于有同步关系的操作时加入信号量操作即可。比如题目中，从 A 的信箱中取出一个邮件操作：A 的信箱有邮件，wait (fullA) 即可，去邮箱里那涉及到临界资源 A 邮箱， wait (mutexA) ，关键是记得 signal 操作也就是给信号，表示自己释放了某种资源，拿完信后应该有空位出来需要 signal (emptyA…

记安装 Pytorch 环境 (GPU 版本)
type: item
date: 2023-04-16T23:39:00+08:00
description: 来来回回在三个系统上都安装了一遍pytorch，其实很简单，先安装显卡驱动，再安装CUDA，最后安装pytorch就好。pytorch 的安装需要去官网根据配置需要复制对应的下载指令。（推荐 conda 新建一个pytorch环境安装）教程已经写得很好了，就记录一些自己踩的坑吧显卡驱动非 OPEN安装系统recommend 的 open 版本驱动比如我的是 525-open 后，如果检测不到的话，可以换成对应版本的非 open 其驱动。它是 Linux 的当然推荐开源了环境变量配置在自己的终端文件中太蠢了，当时，只知道一步一步跟着教程走。比如我使用的是 zsh 配置文件就应该放在 vim ~/. zshrc 而不是死死的按照教程的 vim ~/.bashrcbash - shopt command not found in .bashrc after shell updation - Stack Overflow验证是否安装成功验证是否启用了 cuda 加速。如果启用了的话运行结果显然应该是 true。import torch torch.cuda.is_available()参考链接Ubuntu搭建Pytorch，就这一篇就够了\_ubuntu pytorch\_Chengyunlai的博客-CSDN博客新装UBUNTU 22.04安装Nvidia GPU版Pytorch完整教程 | GTrush的个人博客

算法题解：给表达式树添加合适的括号
type: item
date: 2023-03-20T19:37:00+08:00
description: Problems请编写程序将表达式树按中缀表达式输出，并填加必要的括号，要求括号不能冗余，即保证正确运算次序所需的最少括号。如 a∗(b+c) 和 a+(b−c) 中的括号是必要的，而 a+(b∗c) 的括号则是冗余的。假定表达式树中的运算均为二元运算，只涉及加、减、乘、除运算。输入格式:输入为一行字符串，表示带空指针信息的表达式树先根序列，空指针信息用 # 表示，操作数为 a - z 的小写字母，运算符为+、-、$*$、/。输出格式:输出为一行字符串，表示填加必要括号后的中缀表达式。输入样例1:*a##+b##c##输出样例1:a*(b+c)输入样例2:+a##*b##c##输出样例2:a+b*cSolutions先序表达式建树这个 so easy 咯。输出表达式并添加合适的括号法一？思路一只是输出表达式的话中序遍历即可，要加上括号，那么我在中序遍历的时候判断一下，如果当前是运算符并且运算符优先级大于孩子节点优先级的话，就加上括号，很好，想法是可行的，就差代码实现了。这是我当时的代码无修，写法有错误鸭！而且很混乱，该骂！居然还过了一个测试点！void inOrder(node root) { if (root) { inOrder(root->lc); cout << root->data; bool fg = false; if (root->data >= 'a' && root->data <= 'z') { } else { if (root->lc && root->rc) { char lc = root->lc->data; char rc = root->rc->data; char sub; if (!(lc >= 'a' && lc <= 'z')) { sub = lc; } else if (!(rc >= 'a' && rc <= 'z')) { sub = rc; } else { sub = 'a'; } if (sub != 'a') { if (mp[sub] <= mp[root->data]) { fg = true; } } } } if (fg) { cout << "("; } inOrder(root->rc); if (fg) { cout << ")"; } } }思路一代码实现纠正我自己又改了一遍，我觉得这样写是没有问题的。void inOrder(node root) { if (root) { bool fgl = false, fgr = false; if (root->data == '*' || root->data == '/') { if (root->lc && (root->lc->data == '+' || root->lc->data == '-')) { fgl = true; } if (root->rc && (root->rc->data == '+' || root->rc->data == '-')) { fgr = true; } } if (fgl) cout << "("; inOrder(root->lc); if (fgl) cout << ")"; cout << root->data; if (fgr) cout << "("; inOrder(root->rc); if (fgr) cout << ")"; } }换一种有返回值的递归方式这个想法来源于 ChatGPT，对于递归到底应该怎么写我还是不够清楚，或者说在紧急时刻我写不好。==函数的作用即实现题目的要求，实现表达式树的带括号中缀表达式，参数是一棵树，返回值是一个字符串==，我们当然可以假设已经实现了左右子树的带括号序列，再来处理当前节点，在已经有左右字串的基础上加括号就很好理解了。string to_infix(node root) { if (!root) return ""; string str_left = to_infix(root->lc); string str_right = to_infix(root->rc); string value_str(1, root->data); if (root->data == '*' || root->data == '/') { if (root->lc && (root->lc->data == '+' || root->lc->data == '-')) { str_left = "(" + str_left + ")"; } if (root->rc && (root->rc->data == '+' || root->rc->data == '-')) { str_right = "(" + st…