Hi there wave

在读CS/热爱底层系统/正在学习AI Infra。喜欢把东西弄懂,然后写下来。

Linux From Scratch: Chapter 3

chroot,即 change root,用于改变当前进程及其子进程的根目录到指定的目录,创建一个隔离的环境。这是我们脱离宿主系统的第一步,后续所有的操作都在 chroot 环境下进行。从现在开始,所有的命令都要以 root 身份执行,所以必须要小心,否则可能对 LFS 系统造成损害。 chroot 前准备 第一步是改变所有者。目前,$LFS 目录树的所有者都是 lfs,这个用户只存在于宿主系统,并不存在于 LFS 系统。为此,我们要将 $LFS/* 目录的所有者都改为 root: chown --from lfs -R root:root $LFS/{usr,var,etc,tools} case $(uname -m) in x86_64) chown --from lfs -R root:root $LFS/lib64 ;; esac Linux 延续了 UNIX 一切皆文件的设计哲学,根目录下有一些目录是用来和内核进行通信的,比如 /dev、/proc 等,它们被称作虚拟内核文件系统(Virtual Kernel File Systems)。在 chroot 环境中构建系统时,应用程序需要这些接口来获取硬件信息或向内核发送指令,所以必须先把它们挂载到 $LFS 目录树中。 先创建挂载点: mkdir -pv $LFS/{dev,proc,sys,run} 对于/dev 的挂载,在正常的引导过程中,内核会自动挂载 devtmpfs 到 /dev,LFS 官方书考虑到一些内核可能不支持 devtmpfs,选择绑定挂载宿主系统的 /dev 目录: mount -v --bind /dev $LFS/dev 接下来挂载其余的虚拟内核文件系统: mount -vt devpts devpts -o gid=5,mode=0620 $LFS/dev/pts 其中 gid=5 使所有通过 devpts 文件系统创建的设备节点属于编号为 5 的组,mode=0620 使得所有通过 devpts 创建的设备节点的权限模式为 0620,即所属用户可读写,所属组可写。这两个选项共同保证 devpts 创建的设备节点符合 grantpt() 函数的要求,这样就不需要 Glibc 的 pt_chown 辅助程序。 ...

March 14, 2026

Linux From Scratch: Chapter 2

编译一个程序需要编译器,但编译器本身又是一个程序,需要被另一个程序编译出来;编译器需要依赖 C 标准库才能正常运作,但 C 标准库自己又需要被编译器编译出来。在从零开始构建的系统中,这就是一个先有鸡还是先有蛋的问题。 为了解决这个问题,我们需要进行交叉编译。 交叉编译主要有三个概念: Build 指构建程序用的机器 Host 指构建出的程序运行的机器 Target 指编译器产生代码的目标机器 为了构建 LFS,我们要进行以下三个阶段: 用宿主机的编译器编译出一个运行在宿主机上没有 C 标准库支持的残缺交叉编译器。 用交叉编译器,在宿主机上为新系统编译出标准库和完整版的编译器。我们不能用宿主机的编译器来编译标准库,因为这样会使标准库内部链接一些宿主机的代码,造成污染。标准库必须由新系统的编译器来编译。 在新系统上,用完整版编译器重新编译自己,得到不依赖宿主系统的独立编译器。 阶段 Build Host Target 操作描述 1 pc pc lfs 在 pc 上使用 cc-pc 构建交叉编译器 cc1 2 pc lfs lfs 在 pc 上使用 cc1 构建 cc-lfs 3 lfs lfs lfs 在 lfs 上使用 cc-lfs 重新构建 (同时可以测试) 它本身 LFS 基于 Autoconf 构建系统,它使用形如 x86_64-lfs-linux-gnu 的三元组来表示目标系统类型。当我们执行完第一步之后,宿主机就会出现两个编译器:gcc 和 x86_64-lfs-linux-gnu-gcc。后期我们执行 ./configure --host=x86_64-lfs-linux-gnu 时,autoconf 就会去寻找 x86_64-lfs-linux-gnu 开头的编译器、链接器和汇编器,确保构建出的程序是为新系统准备的。 LFS 的大多数软件是基于 make 编译和安装的,完整的安装过程有以下六步: ...

March 9, 2026

Linux From Scratch: Chapter 1

Linux From Scratch 13.0 刚刚于 3 月 5 日发布,我一直想尝试这个从零开始编译 Linux 的过程,这次正好碰到大版本更新,不如现在就行动起来。宿主机是 Arch,我们将按照 LFS 的官方书来构建,主要分为三个部分: 准备工作 构建 LFS 交叉工具链和临时工具 构建 LFS 系统 这一章主要完成第一部分。 宿主系统要求 首先,宿主机必须安装构建 LFS 需要的软件。LFS 第二章很贴心的给了一个 Shell 脚本来检查是否满足要求。在我的系统里,运行后的输出是这样的: bash version-check.sh OK: Coreutils 9.10 >= 8.1 OK: Bash 5.3.9 >= 3.2 OK: Binutils 2.46 >= 2.13.1 OK: Bison 3.8.2 >= 2.7 OK: Diffutils 3.12 >= 2.8.1 OK: Findutils 4.10.0 >= 4.2.31 OK: Gawk 5.4.0 >= 4.0.1 OK: GCC 15.2.1 >= 5.4 OK: GCC (C++) 15.2.1 >= 5.4 OK: Grep 3.12 >= 2.5.1a OK: Gzip 1.14 >= 1.3.12 OK: M4 1.4.21 >= 1.4.10 OK: Make 4.4.1 >= 4.0 OK: Patch 2.8 >= 2.5.4 OK: Perl 5.42.0 >= 5.8.8 OK: Python 3.14.3 >= 3.4 OK: Sed 4.9 >= 4.1.5 OK: Tar 1.35 >= 1.22 OK: Texinfo 7.2 >= 5.0 OK: Xz 5.8.2 >= 5.0.0 OK: Linux Kernel 6.19.6 >= 5.4 OK: Linux Kernel supports UNIX 98 PTY Aliases: OK: awk is GNU OK: yacc is Bison OK: sh is Bash Compiler check: OK: g++ works OK: nproc reports 20 logical cores are available 所有检查都通过了,现在系统已经做好了安装 LFS 的准备。 ...

March 7, 2026

深入理解 SIMD:从 x86 汇编到 AVX2 Intrinsic

SIMD (Single Instruction, Multiple Data) ,本质上就是一条指令同时处理多个数据,旨在实现数据级并行 (Data Level Parallelism)。 一个简单例子是向量加法。传统方式是用循环来对向量中每一对分量求和: for (int i = 0; i < NUM_ELEMS; i++) { sum[i] = a[i] + b[i]; } 从算法层面讲,这已经是最优解了。但当考虑到更底层的原理时,还有很大的优化空间。 仔细分析循环,可以发现对于每个分量,都需要做以下工作: 迭代开销(计数器增加,条件跳转等) 把a中的分量从内存(缓存)中运到寄存器 把b中的分量从内存(缓存)中运到寄存器 用运算单元执行加法 将结果从寄存器中运回内存 优化程序性能,无非就是从这几个点下手。 对于第一点,向量有多少分量,for循环中的比较和自增就要做多少次。但如果把数据分成几组,每组作为一个整体运算,迭代开销就会成倍的减少,这就是循环展开(Loop Unrolling): int i = 0; for (; i <= NUM_ELEMS - 4; i += 4) { sum[i] = a[i] + b[i]; sum[i + 1] = a[i + 1] + b[i + 1]; sum[i + 2] = a[i + 2] + b[i + 2]; sum[i + 3] = a[i + 3] + b[i + 3]; } for (; i < NUM_ELEMS; i++) { sum[i] = a[i] + b[i]; } 循环展开前,程序需要进行NUM_ELEMS次迭代;循环展开后,迭代次数减少到了大约NUM_ELEMS / 4的水平。 循环展开减少了迭代开销和分支预测压力,但每次迭代还是做了四次运算,ALU 在每一时刻只能处理一对数字。观察循环体,每次运算都是取两个数据做加法,而且被分成一组的数在内存中都是连续的。如果有一个大寄存器,一次性可以存 $N$ 个数,从内存取数据操作的次数理论上就会减少到原来的 $1/N$。更进一步,如果能在这个大寄存器里做加法,运算单元的执行次数理论上也会减少到原来的 $1/N$。 这就是 SIMD 的基本思路。其中的大寄存器,叫做向量寄存器。 过去几十年,Intel 的处理器在市场占主导地位,它在 SIMD 指令集领域做的工作如今依然是最被广泛应用的。 Intel 在 1999 年随 Pentium III 处理器发布了 SSE (Streaming SIMD Extensions) 指令集,引入了 8 个 128 位 XMM 寄存器;在后来的 x86-64 环境下,XMM 寄存器扩展为%xmm0-%xmm15 共 16 个。一个 128 位的寄存器可以存 4 个 32 位数据。 ...

February 6, 2026

关于生活的动力

现实很残酷,并非所有人都能做一辈子自己热爱的事业,更多的人行事的动力来自他人的压力。参加过高考的人很容易就能理解,当一天在学校待十几个小时,面对一周只有半天假的时候,没有哪个人会说他是打心底里爱上这种感觉的,更多是一种被别人期待的责任感、对落后同龄人的恐惧和不得不接受社会价值观的无奈。我在上大学之前对其感触良多,即使已临近毕业,也不敢说自己彻底跳出了应试十二年带给我的活在他人评价中的价值观。遗憾的是,这就是大部分人生活的动力。上学时要成绩优异考取名校,上班后要进大厂拿大包,接着就是开迈巴赫娶白富美住汤臣一品,却鲜有问过自己是否真的感觉活在这世上。而这些目标,能做到的也人屈指可数,九成的人在这样竞争中都是失败者,不得不在根深蒂固的观念下,被迫接受自己失败的事实。这样的观念也催生出了很多怪现象:有人喜欢做出一点小成绩就生怕别人不知道似的宣发,以此来向人证明自己并不是失败者;有的“大佬”特别喜欢膜拜他人自称萌新,看似谦虚谨慎实际上是站在高位对下位者进行嘲讽;有的人无法承受如此巨大的压力,陷入抑郁甚至自我了断。 我并不认为这是一个健康的价值观,即使他有诸多问题,某些情况下还是能在客观上给予我们前进的力量。生活的动力不应该全部压在他人的评价上,但完全不在意他人的看法也过于极端了,我们需要自己在其中寻找平衡点。

February 3, 2026

SICP第一章总结

“It is possible that software is not like anything else, that it is meant to be discarded: that the whole point is to always see it as a soap bubble?” 我在大一的时候自学过 CS61A,名字也是 SICP ,只是换成用 Python 授课和对内容做了一些修改,当时的题解仍然躺在我的 GitHub 仓库里。最近陷入了莫名其妙的迷茫,看不清自己未来的路,于是重新捡起了这本号称能阐述计算本质的书,意外读的非常享受,决定每天睡前读几页,尽量完成所有练习。这几天刚读完第一章,在复习 61A 的知识之外又学到了许多,想着写个总结吧。我本来不喜欢写技术博客,但这本书确实是美到我了,值得为她单独写几篇。 如果只从能这一章,甚至这本书里学到一件事,那么就是用抽象的角度思考程序,明白自己该关心什么,不该关心什么。我们把一系列动作写成函数,实际上就是将这些过程抽象出来,用一个名字来代表他。我们关心这个函数能做什么,但不关心怎么做,作为一个思考能力有限的人类来说,这就把问题简化了许多,允许我们用简单的思考来解决大的问题,甚至是以前解决不了的问题。计算机是一个非常复杂的系统,从晶体管的角度来思考如何运行一个 3A 游戏就是天方夜谭,如果没有抽象的思想,我们什么也做不出来。当我们设计 CPU 时,会直接将逻辑门拿来用,而不管他们具体是怎么实现的,这就是逻辑电路给我们的抽象。当我们编写操作系统时,会直接参考 CPU 架构手册,而不管 CPU 是如何实现他们的,这就是 CPU 给我们的抽象。当我们用操作系统写代码时,我们知道编译出来的程序会遵循我们的指令,而不管内存如何分配、进程如何调度、中断如何处理等一系列由操作系统实现的细节。每一层都会做好他们该做的事,将他能做什么以接口的形式向上暴露出来,上一层会只依赖这一层提供的接口而非继续往下,否则这大概率会是一个失败的实现。 Scheme的特性让我们很容易看到程序的结构。除了cond、let这些特殊的关键字之外,一个括号只做一件事情。比如 (* (+ 1 2) (+ 3 4)) 从开头的*就能看出,我们要做乘法,而乘数自己也是一个括起来的表达式,每个都是一个加运算。当遇到括号时,我们就知道程序的构造要向下挖一层了。 对于它来说,要对两个数进行乘运算,但具体是哪两个数,他不关心,交给+来决定。当+计算出结果后,*就会用+提供的值,来进行真正的运算。 括号凸显出来的程序结构对于编译器来说是完美的,但当括号越来越多时,对于我们人来说也会越来也复杂。真正提供有效抽象的,只有一个关键字:define。 (define pi 3.14159) (define radius 10) (* pi (* radius radius)) 314.159 define在这里把数字抽象成了一个用于代表它的名字,我们只需要知道它能做什么,而不需要知道它是什么。pi是圆周率,用它我们可以求出圆的面积,但pi具体是多少,我们不关心,它已经替我们抽象掉了。 这一章最核心的内容是,我们不仅可以抽象数据成一个名字,也可以抽象过程成一个名字。 (define (sqare x) (* x x)) 这里我们知道square可以返回一个值的平方,但它是如何得出平方的,并不用关心。从此以后,square和+、*这样的原始运算没有什么实质性的区别,在我们看来,他们都是一个可以进行特定运算的过程。 ...

January 3, 2026