From ff87f2d9915031e70973f5c47f354e721cf02f62 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Mon, 22 Jun 2026 22:21:05 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=E5=AF=B9=E9=BD=90=E6=97=97?= =?UTF-8?q?=E6=9D=86=E4=BB=93=E5=BA=93=E2=80=94=E2=80=94=E7=AB=99=E7=82=B9?= =?UTF-8?q?=E9=97=A8=E9=9D=A2=20+=20=E5=AE=8C=E6=88=90=E5=BA=A6=E6=A0=87?= =?UTF-8?q?=E8=AE=B0=20+=20CI=20=E5=AE=88=E6=8A=A4=20+=20=E5=8F=8C?= =?UTF-8?q?=E8=BD=A8=20handbook=20=E8=B5=B7=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 站点门面对齐 TAMCPP: - 首页升级 home layout(hero + 卡片网格 + 成熟度标记) - 新增学习路线图(mermaid 依赖图 + 逐层四段式) - 新增更新日志 changelogs/(v0.1.0: 通识基础 8 篇 + 基建) - 补齐 6 个 layer 首页(修顶栏 404 死链) - 页脚注入 git 版本号(build-info.ts) - 顶栏加路线图/更新日志入口 - build.ts 接入 changelogs/roadmap 分卷 完成度标记体系: - 教程 frontmatter maturity 字段(verified/drafting/growing) - foundations 8 篇 + guides 挂 verified CI 守护: - build-check.yml: PR/push 验证构建 + 死链检查 双轨 handbook 线起步: - mm-buddy 伙伴系统骨架(从笔记 ch7/ch8 整理,对外 🔨 整理中) - kernel 首页内存管理 📚→🔨 --- .github/workflows/build-check.yml | 63 ++++++++ document/changelogs/index.md | 16 ++ document/changelogs/v0.1.0.md | 59 +++++++ document/guides/01-kernel-build.md | 106 ++++++++++++ document/guides/02-rootfs.md | 84 ++++++++++ document/guides/03-qemu.md | 131 +++++++++++++++ document/guides/04-module-build.md | 113 +++++++++++++ document/guides/05-site-build.md | 77 +++++++++ document/guides/_category_.json | 5 + document/guides/index.md | 106 ++++++++++++ document/index.md | 87 +++++++--- document/roadmap/index.md | 94 +++++++++++ document/tutorials/debugging/index.md | 17 ++ document/tutorials/drivers/index.md | 17 ++ document/tutorials/embedded/index.md | 17 ++ ...-toolchain.md => 01-wsl2-env-toolchain.md} | 3 +- ...ini-config.md => 02-kernel-mini-config.md} | 5 +- .../{kernel-build.md => 03-kernel-build.md} | 1 + ...busybox-rootfs.md => 04-busybox-rootfs.md} | 5 +- ...mu-first-boot.md => 05-qemu-first-boot.md} | 3 +- ...b-debug-setup.md => 06-gdb-debug-setup.md} | 3 +- ...ule-hello.md => 07-kernel-module-hello.md} | 5 +- .../08-kernel-module-9p-iteration.md | 153 ++++++++++++++++++ document/tutorials/foundations/index.md | 20 +++ document/tutorials/kernel/index.md | 31 ++++ document/tutorials/kernel/mm/mm-buddy.md | 123 ++++++++++++++ document/tutorials/virtualization/index.md | 17 ++ scripts/build.ts | 101 ++++++++++++ site/.vitepress/config/build-info.ts | 29 ++++ site/.vitepress/config/index.ts | 5 +- site/.vitepress/config/nav.ts | 3 + 31 files changed, 1470 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/build-check.yml create mode 100644 document/changelogs/index.md create mode 100644 document/changelogs/v0.1.0.md create mode 100644 document/guides/01-kernel-build.md create mode 100644 document/guides/02-rootfs.md create mode 100644 document/guides/03-qemu.md create mode 100644 document/guides/04-module-build.md create mode 100644 document/guides/05-site-build.md create mode 100644 document/guides/_category_.json create mode 100644 document/guides/index.md create mode 100644 document/roadmap/index.md create mode 100644 document/tutorials/debugging/index.md create mode 100644 document/tutorials/drivers/index.md create mode 100644 document/tutorials/embedded/index.md rename document/tutorials/foundations/{wsl2-env-toolchain.md => 01-wsl2-env-toolchain.md} (97%) rename document/tutorials/foundations/{kernel-mini-config.md => 02-kernel-mini-config.md} (92%) rename document/tutorials/foundations/{kernel-build.md => 03-kernel-build.md} (99%) rename document/tutorials/foundations/{busybox-rootfs.md => 04-busybox-rootfs.md} (87%) rename document/tutorials/foundations/{qemu-first-boot.md => 05-qemu-first-boot.md} (96%) rename document/tutorials/foundations/{gdb-debug-setup.md => 06-gdb-debug-setup.md} (96%) rename document/tutorials/foundations/{kernel-module-hello.md => 07-kernel-module-hello.md} (97%) create mode 100644 document/tutorials/foundations/08-kernel-module-9p-iteration.md create mode 100644 document/tutorials/foundations/index.md create mode 100644 document/tutorials/kernel/index.md create mode 100644 document/tutorials/kernel/mm/mm-buddy.md create mode 100644 document/tutorials/virtualization/index.md create mode 100644 site/.vitepress/config/build-info.ts diff --git a/.github/workflows/build-check.yml b/.github/workflows/build-check.yml new file mode 100644 index 00000000..5010e6a2 --- /dev/null +++ b/.github/workflows/build-check.yml @@ -0,0 +1,63 @@ +name: Build Check + +# PR 和 push 到 main 时验证站点能构建通过。 +# pnpm build 内置 post-build 死链检查(scripts/build.ts 的 checkSiteIssues), +# 所以这一步同时守住「构建」和「死链纪律」两道关——坏构建或死链都会在 PR 阶段被拦下。 + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # build-info.ts 的 git describe 需要完整历史拿版本号 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Get pnpm store directory + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('site/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Setup build cache + uses: actions/cache@v4 + with: + path: site/.vitepress/.build-cache + key: vitepress-build-${{ runner.os }}-${{ hashFiles('site/pnpm-lock.yaml', 'scripts/build.ts', 'site/.vitepress/config/*') }} + restore-keys: | + vitepress-build-${{ runner.os }}- + vitepress-build- + + - name: Install dependencies + working-directory: site + run: pnpm install --frozen-lockfile + + - name: Build website (含死链检查) + working-directory: site + run: pnpm build + env: + NODE_OPTIONS: --max-old-space-size=6144 + BUILD_CONCURRENCY: 8 diff --git a/document/changelogs/index.md b/document/changelogs/index.md new file mode 100644 index 00000000..b27becb5 --- /dev/null +++ b/document/changelogs/index.md @@ -0,0 +1,16 @@ +--- +title: 更新日志 +description: PenguinLab 每个版本锤炼了什么 +--- + +# 更新日志 + +这站每个版本新锤炼了什么、修了什么,都记在这里。我们用三档标记每篇内容的火候: + +- ✅ **已锤炼** — 读过、跑过、亲手验证过 +- 🔨 **整理中** — 笔记已经收集来,还在整理和验证 +- 📚 **持续补充** — 框架立住了,慢慢往里加 + +## 版本 + +- **[v0.1.0 — 开张:通识基础齐活,地基夯实](./v0.1.0)** — 2026-06-22 diff --git a/document/changelogs/v0.1.0.md b/document/changelogs/v0.1.0.md new file mode 100644 index 00000000..088f66c1 --- /dev/null +++ b/document/changelogs/v0.1.0.md @@ -0,0 +1,59 @@ +--- +title: v0.1.0 — 开张:通识基础齐活,地基夯实 +date: 2026-06-22 +maturity: verified +--- + +# v0.1.0 — 开张:通识基础齐活,地基夯实 + +> 第一个版本。把「在 WSL2 上从零交叉编译一颗 ARM64 内核、做个 BusyBox rootfs、丢进 QEMU 点亮、再用 GDB 远程调试、最后跑起自己的内核模块」这条完整链条,一篇篇锤炼成型。连同一套可复现的基建脚本,和这个网站本身。 + +## 通识基础(foundations)8 篇齐活 + +从环境摸底到内核模块跑起来,一条线打通: + +- **01 WSL2 环境摸底与交叉编译工具链** — WSL2 + Arch Linux 上把 aarch64 交叉编译工具链备齐 +- **02 Mini Config:从零设计一份精简的内核配置** — allnoconfig + merge_config.sh + olddefconfig 三步法,442 项 vs defconfig 952 项 +- **03 编译你的第一个 ARM64 内核** — Linux 6.19.y + GCC 15.2.0,附编译输出标签速查 +- **04 用 BusyBox 构建最小根文件系统** — BusyBox rootfs 打包成 ~1MB 的 cpio.gz +- **05 QEMU 首次启动与 boot log 解读** — QEMU virt 点亮,boot log 四阶段拆解 +- **06 GDB + QEMU 远程调试 ARM64 内核** — 三层排查:`-s -S`、aarch64-linux-gnu-gdb、KASLR 地址偏移 +- **07 从零写第一个内核模块** — Hello World + Makefile,打通开发工具链 +- **08 用 9p 共享目录加速内核模块迭代** — QEMU `-virtfs` 免重打包,改一次 ko 直接迭代 + +这一路踩的坑也都记下来了:`ARCH`/`CROSS_COMPILE` 漏设把 ARM64 编成了 x86、KASLR 让 GDB 断点乱飞、递归 make 的变量不传递(给 `Makefile.arch` 补了 `export`)、printk 的 console 延迟让人误以为模块没加载(信 dmesg buffer,别信串口屏幕)。 + +## 基建速查(guides)— 五条主线串成一条流水线 + +把散落的脚本钉成一份统一认知,免得每次重新 grep: + +- **01 编译 Linux 内核** — `linux-action-scripts.sh` +- **02 制作 BusyBox rootfs** — `rootfs-minimal-maker.sh` +- **03 QEMU 启动与调试** — `qemu-run.sh`(含 9p / `-s -S` 调试开关) +- **04 交叉编译内核模块** — `example/common/Makefile.arch`(arm32/arm64/riscv/x86_64 四架构) +- **05 构建本网站** — `scripts/build.ts` + +产物一律落 `out/build_latest_/`(aarch64 → arm64 输出目录),initrd 优先于镜像,aarch64↔arm64 命名映射这种坑也写死了。 + +## 地基设施 + +- **scripts/build.ts**:分卷并行构建(`BUILD_CONCURRENCY`)+ SHA256 增量缓存(`.build-cache/manifest.json`),改哪卷只重编哪卷 +- **example/common/Makefile.arch**:一份 Makefile 通吃 arm32/arm64/riscv/x86_64 四架构交叉编译 +- **example/mini/00-kernel_module_hello**:第一个可运行示例,教程 07 的配套代码 + +## 这站本身 + +VitePress 搭的,关掉了 `ignoreDeadLinks`——每个链接都得指向真实存在的页面或外链,死链直接构建报错,不放过。 + +## 数据 + +| 项 | 数 | +|---|---| +| 通识基础教程 | 8 篇 | +| 基建速查 | 6 篇(含总览) | +| 可运行示例 | 1 个 | +| 支持架构 | arm32 / arm64 / riscv / x86_64 | + +--- + +下一版要干的:把通识基础往内核子系统(调度、内存、文件系统)推进;同时给这站加上「学习路线图」和每篇的完成度标记,让访客一眼看到这站写到哪了。 diff --git a/document/guides/01-kernel-build.md b/document/guides/01-kernel-build.md new file mode 100644 index 00000000..0869c68e --- /dev/null +++ b/document/guides/01-kernel-build.md @@ -0,0 +1,106 @@ +--- +title: 编译 Linux 内核 +description: 用 linux-action-scripts.sh 交叉编译 ARM32/ARM64 内核——config/build/clean 三板斧、O= 外部构建、配置怎么查怎么改 +maturity: verified +--- + +# 编译 Linux 内核 + +## 这个脚本干啥 + +`scripts/linux-action-scripts.sh` 把「配置内核 → 编译 → 清理」这套交叉编译流程包成几个子命令,统一用 `O=` 外部构建,产物落在 `out/build_latest_/`。 + +## 前置 + +- 内核源码子模块已初始化:`git submodule update --init third_party/linux` +- 交叉工具链在 PATH:ARM64 用 `aarch64-linux-gnu-`,ARM32 用 `arm-none-linux-gnueabihf-` + +## 四个子命令 + +脚本接受任意顺序的子命令串: + +```bash +scripts/linux-action-scripts.sh config # 配置(需 LINUX_DEFCONFIG) +scripts/linux-action-scripts.sh build # 编译(需先有 .config) +scripts/linux-action-scripts.sh config_and_build # 配置 + 编译一步到位 +scripts/linux-action-scripts.sh clean # 删 out/build_latest_/ +``` + +## 关键环境变量 + +| 变量 | 默认 | 说明 | +|------|------|------| +| `ARCH` | `arm` | `aarch64` 会被脚本映射成内核命名 `arm64` | +| `CROSS_COMPILE` | 按 `ARCH` 自动选 | 工具链前缀 | +| `LINUX_DEFCONFIG` | (无) | `config` 命令必填;ARM64 用 `defconfig`,ARM32 用 `vexpress_defconfig` | +| `BUILD_OUTPUT_BASE` | `out/build_latest_` | 产物目录;显式指定时不触发自动备份 | +| `BUILD_JOBS` | `nproc` | 并行度 | + +## O= 外部构建与自动备份 + +脚本始终用 `make O="${BUILD_OUTPUT_BASE}"`,产物和源码树分离。一个贴心的设计:**若输出目录里已有 `.config`,就直接复用**(你只是想重新 `build`);若没有,就把旧目录改名备份成 `build_<时间戳>`,再开新的。 + +所以「改了配置想重新 config」时,要么先 `clean`,要么手动指定一个新的 `BUILD_OUTPUT_BASE`。 + +## 产物在哪 + +``` +out/build_latest_arm64/ +├── .config +├── arch/arm64/boot/Image ← 内核镜像(qemu-run 自动探测它) +├── arch/arm64/boot/dts/*.dtb ← 设备树(virt 机器其实用 QEMU 现场生成的) +├── vmlinux ← 带调试符号的未压缩内核(GDB 用) +└── ... ← 内核模块、Module.symvers 等 +``` + +ARM32 对应 `arch/arm/boot/zImage`。 + +## 实操:编一个 ARM64 内核 + +```bash +ARCH=aarch64 LINUX_DEFCONFIG=defconfig \ + scripts/linux-action-scripts.sh config_and_build +``` + +编完,`out/build_latest_arm64/arch/arm64/boot/Image` 就是 QEMU 要的内核。 + +## 怎么查 / 改内核配置 + +`config_and_build` 用的是 defconfig,很多特性默认就开了。想确认或修改,两条路。 + +**查**:直接 grep 输出目录的 `.config`。比如想确认 9p(virtio-9p 共享目录用的)开没开: + +```bash +grep -iE '9P|VIRTIO' out/build_latest_arm64/.config +``` + +当前 ARM64 defconfig 下,`CONFIG_NET_9P=y`、`CONFIG_NET_9P_VIRTIO=y`、`CONFIG_9P_FS=y` 都已经编进内核——意味着 Guest 这边挂 9p 文件系统**不用再重编内核**,只剩 QEMU 侧加 `-virtfs` 这一步。 + +**改**:进输出目录用内核自带工具改完,再 `build`(不用重 `config`): + +```bash +cd out/build_latest_arm64 +make menuconfig # 图形化勾选 +# 或精确开关单项: +./scripts/config --enable 9P_FS_POSIX_ACL # 比如想开 POSIX ACL +make O=$(pwd) olddefconfig +cd ../.. +ARCH=aarch64 scripts/linux-action-scripts.sh build +``` + +> 小坑:`9P_FS_POSIX_ACL` 默认没开。只有在 9p 挂载点上要 `chown`/`chmod` 并保留 ACL 时才需要它;单纯传 `.ko` 文件用不到。 + +## 换架构:ARM32 + +```bash +ARCH=arm LINUX_DEFCONFIG=vexpress_defconfig \ + scripts/linux-action-scripts.sh config_and_build +``` + +产物落到 `out/build_latest_arm/`,内核镜像是 `zImage`,配合 `vexpress-a9` 机器的 dtb。 + +## 常见坑 + +- **`LINUX_DEFCONFIG` 没设就跑 `config`**:脚本直接报错退出。ARM64 填 `defconfig`,ARM32 填 `vexpress_defconfig`。 +- **改了源码不生效**:确认改的是 `third_party/linux/` 下的文件,且 `build` 用的 `O=` 目录和之前一致——别一不小心换了输出目录,等于从头编译。 +- **`ARCH=aarch64` vs `arm64`**:你输入 `aarch64`(工具链习惯),脚本内部转成 `arm64`(内核习惯),输出目录也用 `arm64`。记住这点就不会找错目录。 diff --git a/document/guides/02-rootfs.md b/document/guides/02-rootfs.md new file mode 100644 index 00000000..d78f6513 --- /dev/null +++ b/document/guides/02-rootfs.md @@ -0,0 +1,84 @@ +--- +title: 制作 BusyBox rootfs +description: 用 rootfs-minimal-maker.sh 编静态 BusyBox、生成 init 脚本、打成 initramfs——以及内核模块怎么塞进去 +maturity: verified +--- + +# 制作 BusyBox rootfs + +## 这个脚本干啥 + +`scripts/rootfs-minimal-maker.sh` 把「编一个静态 BusyBox → 装进最小根目录 → 写好 init 脚本 → 打成 cpio.gz initramfs」一气呵成,产物 `out/build_latest_/rootfs.cpio.gz` 直接喂给 QEMU 的 `-initrd`。 + +## 一条命令全流程 + +```bash +ARCH=aarch64 scripts/rootfs-minimal-maker.sh +``` + +默认走完整流程:配置(defconfig + 静态)→ 编译 → 安装 → 生成 init 脚本 → 打包。 + +## 各阶段在干啥 + +1. **配置**:用 BusyBox `defconfig`,强制开 `CONFIG_STATIC`(编成静态二进制,initramfs 里不依赖动态库),并关掉几个 ARM 上会出问题的 x86 专属项(SHA 硬加速、tc 等)。 +2. **编译**:`make -j`。 +3. **安装**:`make install CONFIG_PREFIX=.../rootfs`,生成 `bin/ sbin/ usr/` 和一堆指向 busybox 的符号链接(ls、cat、sh…)。 +4. **建根目录结构**:建 `proc sys dev tmp etc mnt`,写 `/init`、`/etc/inittab`、`/etc/init.d/rcS`、`/etc/fstab`。 +5. **打包**:`find . | cpio -o -H newc | gzip > rootfs.cpio.gz`。 + +## /init 脚本做了啥 + +这是 initramfs 的入口(内核命令行 `rdinit=/init` 指向它)。核心几步: + +```sh +mount -t proc none /proc +mount -t sysfs none /sys +mount -t tmpfs none /dev +mknod ... /dev/console /dev/ttyAMA0 ... # 建必要设备节点 +exec /bin/sh -i /dev/console 2>&1 # 丢你一个交互 shell +``` + +起来就是一个挂在 console 上的 BusyBox shell,proc/sys/dev 都挂好了,够你 `insmod`/`lsmod`/`dmesg` 折腾。 + +## 几种运行模式 + +| 用法 | 作用 | +|------|------| +| (默认) | 配置 + 编译 + 安装 + 打包,全流程 | +| `--build-only` | 只编译(用已有 .config) | +| `--install-only` | 只安装已有 busybox + 打包 | +| `--pack-only` | **只重新打包 cpio,不编译不安装** | +| `--clean` | 清干净重建 | +| `menuconfig` | 进图形化配置(改完退出,再 `--build-only`) | + +## 内核模块怎么塞进去(现状) + +initramfs 是个静态镜像,ko 没在打包时放进去,Guest 里就看不到。现在的迭代流程: + +```bash +# 1. 把编好的 ko 拷进 rootfs 目录 +cp example/mini/00-kernel_module_hello/hello.ko \ + out/build_latest_arm64/rootfs/ + +# 2. 只重打包,不重新编 BusyBox +ARCH=aarch64 scripts/rootfs-minimal-maker.sh --pack-only + +# 3. 重启 QEMU,Guest 里 insmod hello.ko +``` + +`--pack-only` 就是为此设计的——不重编 BusyBox,只把 rootfs 目录重新打成 cpio.gz,几秒钟的事。但每次改 ko 都得 cp + pack + 重启 QEMU,**这正是 9p 共享目录要解决的痛点**:配通 9p 后,宿主机改 ko,Guest 挂载点立刻可见,cp + pack 这套退役。 + +## 产物 + +``` +out/build_latest_arm64/ +├── busybox/ BusyBox 构建目录(含编出的二进制和 .config) +├── rootfs/ 展开的根目录(改它,然后 --pack-only) +└── rootfs.cpio.gz ← initramfs 镜像(qemu-run 自动探测它) +``` + +## 常见坑 + +- **`--pack-only` 前忘了 cp ko**:包还是旧的,Guest 里 `insmod` 加载的还是老版本。养成「改了 rootfs 目录内容,就 pack-only」的肌肉记忆。 +- **ARCH 映射**:和内核一样,`aarch64` 的输出目录用 `arm64`。 +- **想加 BusyBox applet**:`scripts/rootfs-minimal-maker.sh menuconfig` 勾上(比如 telnetd、httpd),再 `--build-only` 重编。 diff --git a/document/guides/03-qemu.md b/document/guides/03-qemu.md new file mode 100644 index 00000000..fc8e8548 --- /dev/null +++ b/document/guides/03-qemu.md @@ -0,0 +1,131 @@ +--- +title: QEMU 启动与调试 +description: 用 qemu-run.sh 启动自编内核 + initramfs——环境变量速查、initrd/drive 两种根文件系统、网络、GDB 调试一条龙 +maturity: verified +--- + +# QEMU 启动与调试 + +## 这个脚本干啥 + +`scripts/qemu-run.sh` 把「拼一条 QEMU 命令行」封装好了:自动探测内核镜像、initramfs、设备树,按环境变量组装机器/CPU/内存/网络/串口,支持普通启动和 GDB 调试启动。 + +## 最短启动 + +前提:内核和 rootfs 都已编好(见 [编译内核](./01-kernel-build)、[制作 rootfs](./02-rootfs))。 + +```bash +scripts/qemu-run.sh run +``` + +它会自动从 `out/build_latest_arm64/` 找到 `Image` 和 `rootfs.cpio.gz`,启动 ARM64 virt 机器,串口直接接到当前终端。看到 `PenguinLab Initramfs` 和 shell 提示符就成了。 + +退出:**先按 `Ctrl+A`,松开,再按 `X`**。 + +## 环境变量速查 + +| 变量 | 默认 | 说明 | +|------|------|------| +| `QEMU_ARCH` | `aarch64` | `arm` / `aarch64` | +| `QEMU_MACHINE` | `virt` | ARM32 可选 `vexpress-a9` / `vexpress-a15` | +| `QEMU_CPU` | `cortex-a72` | ARM32 用 `cortex-a9` 等 | +| `QEMU_MEMORY` | `1G` | 内存 | +| `QEMU_SMP` | `2` | CPU 核数 | +| `QEMU_SERIAL` | `on` | 串口接 stdio | +| `QEMU_NET` | `off` | 打开网络 | +| `QEMU_NET_USER` | `on` | user 模式(端口转发) | +| `QEMU_NET_TAP` | `off` | TAP 模式 | +| `KERNEL_IMAGE` | 自动探测 | 手动指定内核镜像 | +| `INITRD` | 自动探测 | 手动指定 initramfs | +| `ROOTFS` | (无) | 块设备 rootfs(raw/ext4,走 `-drive`) | +| `QEMU_KERNEL_CMDLINE` | 见下 | 内核命令行 | +| `QEMU_EXTRA_OPTS` | (无) | 任意额外 QEMU 参数 | + +默认内核命令行: + +``` +console=ttyAMA0,115200 root=/dev/ram0 rdinit=/init +``` + +## initrd 模式 vs 块设备模式 + +脚本对根文件系统有两种接法,**initrd 优先**: + +- **initrd(默认)**:探测 `out/build_latest_/rootfs.cpio.gz`,用 `-initrd` 加载,对应 `root=/dev/ram0 rdinit=/init`。这是最小系统的常态。 +- **块设备**:设 `ROOTFS=/path/to/disk.img`,用 `-drive file=...,if=virtio,format=raw` 挂成 virtio 磁盘,适合做完整分区 rootfs 实验。 + +两者都没设,脚本报错退出。 + +## 自动探测的路径 + +不手动指定时,内核和 initrd 都优先从构建输出目录找: + +- 内核(ARM64):`out/build_latest_arm64/arch/arm64/boot/Image` +- 内核(ARM32):`out/build_latest_arm/arch/arm/boot/zImage` +- initrd:`out/build_latest_/rootfs.cpio.gz` +- DTB(ARM32 vexpress):`out/build_latest_arm/arch/arm/boot/dts/vexpress-v2p-ca9.dtb` + +ARM64 + virt 机器**不需要 dtb**——QEMU 会现场生成设备树。 + +## 网络 + +`QEMU_NET=on` 打开网络,两种模式可并存: + +- **user 模式**(默认 `QEMU_NET_USER=on`):QEMU 自带 NAT,并把宿主机 2222 转发到 Guest 22: + + ``` + -netdev user,id=net0,hostfwd=tcp::2222-:22 -device virtio-net-pci,netdev=net0 + ``` + + 宿主机 `ssh -p 2222 root@localhost` 就能进 Guest(前提是 rootfs 里跑了 sshd)。 + +- **TAP 模式**(`QEMU_NET_TAP=on`):桥接到宿主机 `tap0`,适合需要 Guest 有独立 IP、和宿主机同网段通信的场景,需自行配好 tap 接口。 + +## 调试模式:GDB 一条龙 + +```bash +scripts/qemu-run.sh debug +``` + +它在普通命令基础上加了两样: + +- 内核命令行插 `nokaslr`:关掉地址随机化,GDB 断点符号才对得上。 +- `-s -S`:`-s` 开 GDB stub 在 **1234 端口**,`-S` 让 CPU 启动即冻结、等 GDB 接入。 + +宿主机另一边连上去: + +```bash +aarch64-linux-gnu-gdb out/build_latest_arm64/vmlinux \ + -ex 'target remote :1234' +``` + +VSCode 用户配好 `launch.json`(`type=cppdbg`、`miDebuggerServerAddress=localhost:1234`)按 F5 即可。`vmlinux` 是带调试符号的未压缩内核镜像,在构建输出目录根下。 + +## 常见用法 + +```bash +# 默认 ARM64 跑 +scripts/qemu-run.sh run + +# ARM32 vexpress +QEMU_ARCH=arm QEMU_MACHINE=vexpress-a9 scripts/qemu-run.sh run + +# 加内存和核数 +QEMU_MEMORY=2G QEMU_SMP=4 scripts/qemu-run.sh run + +# 开网络 +QEMU_NET=on scripts/qemu-run.sh run + +# 调试 +scripts/qemu-run.sh debug + +# 停掉跑飞的实例 +scripts/qemu-run.sh stop +``` + +## 常见坑 + +- **找不到内核/initrd**:八成是没编,或 `ARCH` 和输出目录对不上(`aarch64`→`arm64`)。先确认 `out/build_latest_arm64/arch/arm64/boot/Image` 存在。 +- **串口没输出**:确认 `QEMU_SERIAL=on`(默认就是),且内核命令行 `console=ttyAMA0` 匹配 virt 机器的 PL011 UART。`printk` 偶有「差一次」的延迟——信 `dmesg` buffer,别光盯屏幕。 +- **退不出来**:QEMU `-nographic` 模式下,退出是 `Ctrl+A` 然后 `X`,不是 `Ctrl+C`(那只是中断 Guest)。 +- **想加 9p 共享目录**:当前脚本还没内置 `-virtfs`,可以先用 `QEMU_EXTRA_OPTS` 手动挂;内核侧的 9p 支持(`CONFIG_NET_9P_VIRTIO`、`CONFIG_9P_FS`)已经是 `=y`,QEMU 那侧配好就能用。完整的 9p 配置会在后续专题展开。 diff --git a/document/guides/04-module-build.md b/document/guides/04-module-build.md new file mode 100644 index 00000000..3b7fa395 --- /dev/null +++ b/document/guides/04-module-build.md @@ -0,0 +1,113 @@ +--- +title: 交叉编译内核模块 +description: 用 example/common/Makefile.arch 把内核模块交叉编译到 ARM32/ARM64/RISC-V/x86_64——四架构 + export 那个坑 +maturity: verified +--- + +# 交叉编译内核模块 + +## 一个模块目录的最小结构 + +看现成的范例 `example/mini/00-kernel_module_hello/`,就两个文件: + +``` +example/mini/00-kernel_module_hello/ +├── hello.c 模块源码(init/exit + module_init/exit + MODULE_LICENSE) +├── Makefile obj-m += hello.o + include 公共 Makefile.arch +└── (make 后生成 hello.ko 等) +``` + +`Makefile` 长这样: + +```makefile +obj-m += hello.o +# Import the common stub +include ../../common/Makefile.arch + +all: + $(MAKE) -C $(KDIR) M=$(CURDIR) modules + +clean: + $(MAKE) -C $(KDIR) M=$(CURDIR) clean +``` + +核心就三件事: + +- `obj-m += hello.o`:告诉内核构建系统「把 hello.o 编成模块 hello.ko」。多文件模块写成 `obj-m += mymod.o` + `mymod-y := a.o b.o`。 +- `include ../../common/Makefile.arch`:引入公共的架构/工具链/KDIR 设定(下面详述)。 +- `all`/`clean`:委托给内核构建系统——`-C $(KDIR)` 进入内核树、`M=$(CURDIR)` 构建外部模块。 + +## Makefile.arch 干啥 + +`example/common/Makefile.arch` 是所有示例模块共享的桩,集中定义三件事: + +```makefile +ARCH ?= arm64 +KDIR ?= $(shell realpath .../out/build_latest_$(ARCH)) + +ifeq ($(ARCH),arm) + CROSS_COMPILE ?= arm-none-linux-gnueabihf- +else ifeq ($(ARCH),arm64) + CROSS_COMPILE ?= aarch64-linux-gnu- +else ifeq ($(ARCH),riscv) + CROSS_COMPILE ?= riscv64-linux-gnu- +else ifeq ($(ARCH),x86_64) + CROSS_COMPILE ?= + KDIR ?= /lib/modules/$(shell uname -r)/build +endif + +export ARCH CROSS_COMPILE +``` + +- `ARCH`:目标架构(默认 `arm64`)。 +- `KDIR`:内核树路径,默认指向 `out/build_latest_$(ARCH)`——**所以编模块前,对应架构的内核得先编过**(见 [编译内核](./01-kernel-build))。 +- `CROSS_COMPILE`:按架构自动选工具链前缀。 +- **末尾的 `export ARCH CROSS_COMPILE`:关键,见下。** + +## 那个 export 坑(重点) + +模块的 `all` 目标会递归调用内核构建: + +```makefile +$(MAKE) -C $(KDIR) M=$(CURDIR) modules +``` + +这一步要进入内核树 `$(KDIR)` 跑构建,内核的 Makefile 会读 `ARCH` 和 `CROSS_COMPILE` 决定**目标架构和工具链**。 + +问题在于:GNU make 里,普通变量赋值(`ARCH ?= arm64`)**默认不导出**给子进程。于是 `$(MAKE) -C $(KDIR)` 起的子 make 看不到 `ARCH`,内核 Makefile 就用它自己的默认值(通常是宿主机 x86),结果要么编错架构、要么在错的目录里找东西,报一堆莫名其妙的错。 + +`export ARCH CROSS_COMPILE` 这行把它们塞进环境变量,子 make 才能继承。PenguinLab 早期就被这个坑过(递归 make 变量不传递),现在这行已经固化在 `Makefile.arch` 里,照着 `include` 就不会再踩。 + +> 一句话记牢:**外部模块构建是「借内核的构建系统」,借的时候得把 ARCH/CROSS_COMPILE 一起递过去,靠 `export`。** + +## 怎么编 + +```bash +cd example/mini/00-kernel_module_hello +make ARCH=arm64 # 编出 hello.ko +make ARCH=arm64 clean # 清理 +``` + +不传 `ARCH` 就用默认 `arm64`。换架构: + +```bash +make ARCH=arm # ARM32 +make ARCH=riscv # RISC-V(需先编过 riscv 内核) +make ARCH=x86_64 # x86_64(KDIR 指向宿主内核) +``` + +产物 `hello.ko` 就在当前目录。 + +## 模块怎么上机 + +编出 `hello.ko` 后,两条路上机验证: + +- **现状(塞 rootfs)**:cp 进 `out/build_latest_arm64/rootfs/` + `rootfs-minimal-maker.sh --pack-only` + 重启 QEMU,Guest 里 `insmod hello.ko`。见 [制作 rootfs](./02-rootfs)。 +- **未来(9p 共享)**:配通 9p 后,把模块目录挂进 Guest,宿主机 `make` 出新 ko,Guest 立刻能 `insmod`,免重打包。 + +## 常见坑 + +- **没编内核就编模块**:`KDIR` 指向 `out/build_latest_`,里面没有内核构建产物(尤其 `Module.symvers`、`scripts/`),模块编译报错。先编内核。 +- **ARCH 和 KDIR 对不上**:`make ARCH=arm64` 但 `out/build_latest_arm64` 是空的——检查内核是不是按 `ARCH=aarch64` 编过(`aarch64`→`arm64` 输出目录)。 +- **改了 Makefile.arch 不生效**:多半是 `ARCH` 没传对,或 `export` 那行被误删。 +- **多文件模块**:`obj-m += mymod.o` 后加 `mymod-y := a.o b.o`,别漏。 diff --git a/document/guides/05-site-build.md b/document/guides/05-site-build.md new file mode 100644 index 00000000..05f420ae --- /dev/null +++ b/document/guides/05-site-build.md @@ -0,0 +1,77 @@ +--- +title: 构建本网站 +description: PenguinLab 站点用 VitePress 构建——分卷并行 + 增量缓存 + 搜索索引合并,以及几个踩过的认知坑 +maturity: verified +--- + +# 构建本网站 + +## 技术栈:VitePress(不是 Docusaurus) + +先纠一个流传过的误会:本站用的是 **VitePress**,不是 Docusaurus。证据摆在眼前——配置在 `site/.vitepress/`,`site/package.json` 的脚本调的是 `vitepress`,构建编排 `scripts/build.ts` 跑的也是 `vitepress build`。早期文档里残留的「Docusaurus」字样是历史包袱,别被带偏。 + +## package.json 的四个脚本 + +```json +{ + "scripts": { + "dev": "vitepress dev .", // 本地热更新预览 + "build": "tsx ../scripts/build.ts", // 生产构建(分卷,推荐) + "build:single": "vitepress build .", // 单实例构建(调试用) + "preview": "vitepress preview ." // 预览构建产物 + } +} +``` + +日常开发: + +```bash +cd site +pnpm install # 首次 +pnpm dev # 起 dev server,改 markdown 即时刷新 +``` + +生产构建: + +```bash +cd site +pnpm build # 走 scripts/build.ts,产物在 site/.vitepress/dist +pnpm preview # 本地预览构建结果 +``` + +> 历史坑:仓库里曾有 `scripts/site-dev.sh`、`scripts/site-serve.sh` 两个脚本,注释写着 Docusaurus、调用 `pnpm start` / `pnpm serve`——但 package.json 里**根本没有** `start` 和 `serve` 这两个脚本,所以它们一直是坏的。现已修正为 `pnpm dev` / `pnpm build` + `pnpm preview`。认准上面四个脚本名就好。 + +## 为什么 build 要单独写个 build.ts + +站点内容分成了多个「卷」:`tutorials/foundations`、`tutorials/kernel`、…、`notes`、`blog`、`guides`。单个 VitePress 实例构建整站,内容一多就慢、还容易内存吃紧。`scripts/build.ts` 的做法是: + +- **分卷并行**:每个卷起一个独立的 VitePress 实例并行构建(并发度 `BUILD_CONCURRENCY`,默认 4),卷与卷之间不互相拖。 +- **增量缓存**:对每个卷算 sha256(内容 + 构建脚本 + package + lockfile),没变的卷直接复用上次产物(缓存在 `site/.vitepress/.build-cache/`),只重建变了的。`--force` 强制全量重建。 +- **搜索索引合并**:VitePress 的本地搜索是按实例一份的,分卷后得把各卷的中/英文搜索索引合并成一份,否则站内搜索搜不全。 +- **跨卷数据统一**:把各卷的 hash map、site data 抹平成一致,保证跨卷跳转和资源引用不断。 + +这一套是「内容多了之后不得不做的工程化」。理解它有助于排查「为什么我改了 A 卷,B 卷没更新」之类的缓存问题——清 `site/.vitepress/.build-cache/` 或加 `--force`。 + +## 卷是怎么定义的 + +两个地方配合: + +- `scripts/build.ts` 的 `VOLUMES` 数组:每个卷 `{ name, srcDir, urlPrefix }`,告诉构建器「`document//` 是一个卷,URL 前缀是 ``」。 +- `site/.vitepress/config/sidebar.ts` 的 `buildSidebar()`:为每个卷注册一个侧边栏,`volumeSidebar()` 会**自动扫描**卷目录、按文件名数字前缀排序、从 frontmatter `title:` 取标题。 + +加一个新卷(本卷 `guides` 就是这样加的): + +1. `build.ts` 的 `VOLUMES` 加一行 `{ name: 'guides', srcDir: 'guides', urlPrefix: '/guides' }`。 +2. `sidebar.ts` 的 `buildSidebar()` 加一行 `'/guides/': volumeSidebar('guides', '/guides')`。 +3. `nav.ts` 加一个导航入口(可选)。 +4. 在 `document/guides/` 下放 `_category_.json`(设卷标签)和 markdown 文章。 + +侧边栏会自动从目录内容生成,不用手写每一项。 + +## 文件命名与排序 + +`sidebar.ts` 按文件名开头的数字排序:`00-xxx`、`01-xxx`… 所以这一卷是 `index.md`(卷首页)+ `01-kernel-build.md`、`02-rootfs.md`… 这样编号,侧边栏顺序就稳了。标题取 frontmatter 的 `title:`,没有就 humanize 文件名。 + +## 多语言 + +站点支持中英双语。中文内容在 `document/`(各卷目录),英文在 `document/en/` 下镜像。`build.ts` 会同时构建两种语言,搜索索引也按语言分别合并。 diff --git a/document/guides/_category_.json b/document/guides/_category_.json new file mode 100644 index 00000000..cbb1382a --- /dev/null +++ b/document/guides/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "项目指南", + "position": 1, + "collapsed": false +} diff --git a/document/guides/index.md b/document/guides/index.md new file mode 100644 index 00000000..6530a645 --- /dev/null +++ b/document/guides/index.md @@ -0,0 +1,106 @@ +--- +title: 项目指南 +description: PenguinLab 的工具链全景——怎么交叉编译内核、做最小 rootfs、跑 QEMU、编内核模块、构建网站 +maturity: verified +--- + +# 项目指南 + +> 这一卷讲的是「怎么用 PenguinLab 这套环境真正干活」——交叉编译内核、做一个最小 rootfs、在 QEMU 里跑起来、编译内核模块、以及这个网站本身怎么构建。面向两类人:刚 clone 仓库不知道从哪下手的新人,和想把某条链路彻底吃透的老手。 + +之所以单独抽一卷写这些,是因为 PenguinLab 的工具链横跨**宿主工具链、内核构建、initramfs、QEMU、模块编译、网站构建**好几层,每层都有自己的脚本和约定。它们散落在各处,容易「每次上手都要重新摸索一遍」——这卷文档把它们一次性钉死,既是给学习者的地图,也是项目的长期资产。 + +## 全景:一条主线串起来 + +PenguinLab 的内核学习闭环,本质是这条流水线: + +``` +third_party/linux (源码 v6.19.9) + │ linux-action-scripts.sh build + ▼ +out/build_latest_/ ← 一切产物的家 + ├─ arch//boot/Image 内核镜像 + ├─ .config 内核配置 + └─ ... + │ +third_party/busybox ──rootfs-minimal-maker.sh──► rootfs.cpio.gz (initramfs) + │ + ▼ +qemu-run.sh run → Image + rootfs.cpio.gz → QEMU virt → 串口 shell + ▲ + │ example/common/Makefile.arch +example/mini//hello.ko (cp 进 rootfs 重打包,或走 9p 免重打包) +``` + +记牢一个目录:**`out/build_latest_/`**。内核镜像、内核配置、BusyBox、rootfs、initramfs 全部落在这儿,按架构分目录(`arm64` / `arm`)。找东西先往这儿看。 + +## 三方依赖与版本 + +| 组件 | 位置 | 版本 | +|------|------|------| +| Linux 内核 | `third_party/linux`(git 子模块) | v6.19.9 | +| BusyBox | `third_party/busybox`(git 子模块) | 构建脚本自动解析打印 | +| 交叉工具链 (ARM64) | 系统 PATH | `aarch64-linux-gnu-gcc` | +| 交叉工具链 (ARM32) | 系统 PATH | `arm-none-linux-gnueabihf-gcc` | +| QEMU | 系统 | `qemu-system-aarch64` / `qemu-system-arm` | + +初始化子模块: + +```bash +git submodule update --init third_party/linux third_party/busybox +``` + +安装工具链(Ubuntu/Debian): + +```bash +sudo apt install gcc-aarch64-linux-gnu gcc-arm-none-linux-gnueabihf \ + qemu-system-arm qemu-user-static +``` + +## 五条主线脚本 + +| 脚本 | 作用 | 关键产物 | +|------|------|----------| +| `scripts/linux-action-scripts.sh` | 交叉编译内核(config/build/clean) | `Image` / `zImage`、dtbs、modules | +| `scripts/rootfs-minimal-maker.sh` | 编 BusyBox + 打 initramfs | `rootfs.cpio.gz` | +| `scripts/qemu-run.sh` | 启动 QEMU 跑内核 | 串口 shell / GDB stub | +| `example/common/Makefile.arch` | 交叉编译内核模块 | `*.ko` | +| `scripts/build.ts` | 构建本网站(VitePress 分卷) | `site/.vitepress/dist` | + +## 最小可跑闭环(速记) + +```bash +# 1. 编内核(ARM64) +ARCH=aarch64 LINUX_DEFCONFIG=defconfig \ + scripts/linux-action-scripts.sh config_and_build + +# 2. 打 rootfs(ARM64) +ARCH=aarch64 scripts/rootfs-minimal-maker.sh + +# 3. 跑起来 +scripts/qemu-run.sh run +# 退出:Ctrl+A 然后 X +``` + +跑通这三步,你就有一个能交互的 ARM64 Linux 最小系统。 + +## 设计约定(为什么是这样) + +- **外部构建 `O=`**:内核和 rootfs 都用 `make O=out/build_latest_` 把产物外置,不污染 `third_party/` 源码树,也方便多架构并存(`out/build_latest_arm64` 与 `out/build_latest_arm` 互不干扰)。 +- **`out/` 不进 git**:全是可重建的构建产物、体积大,所以 `.gitignore` 掉。 +- **架构后缀**:`aarch64`(工具链命名)在内核世界映射成 `arm64`(内核命名),脚本内部做了这层映射,输出目录统一用 `arm64`。这是最容易让人找错目录的一个点。 +- **initrd 优先**:QEMU 默认走 initramfs(`-initrd`)而非块设备 rootfs,最小系统够用且免分区。 + +## 阅读路线 + +按编号顺序读,每篇也独立可查: + +- [编译 Linux 内核](./01-kernel-build) — 内核怎么编、配置怎么改、9p 这种特性怎么查开没开 +- [制作 BusyBox rootfs](./02-rootfs) — initramfs 怎么来、ko 怎么塞进去 +- [QEMU 启动与调试](./03-qemu) — 跑起来、串口、网络、GDB 一条龙 +- [交叉编译内核模块](./04-module-build) — `Makefile.arch` 四架构、`export` 那个坑 +- [构建本网站](./05-site-build) — VitePress 分卷构建,以及几个踩过的认知坑 + +--- + +> 写作约定:本卷面向人读,保留和教程一致的人味(讲清「为什么」,附真实命令),但更偏「使用手册」——能查、能照着做。发现哪处和实际脚本对不上,欢迎在 GitHub 上提 issue 或直接改。 diff --git a/document/index.md b/document/index.md index 20363186..b08152b2 100644 --- a/document/index.md +++ b/document/index.md @@ -1,32 +1,81 @@ --- +layout: home title: 欢迎来到 PenguinLab description: Linux 内核学习站 — 从 QEMU 到内核原理、驱动开发与嵌入式全栈 ---- -# 欢迎来到 PenguinLab +hero: + name: PenguinLab + text: Linux 内核学习站 + tagline: 从 QEMU 点亮第一颗 ARM64 内核,到调度器、内存、驱动、嵌入式全栈——一篇篇锤炼,ARM32/ARM64/RISC-V/x86_64 四架构通吃。 + actions: + - theme: brand + text: 开始学习 + link: /tutorials/foundations/ + - theme: alt + text: 学习路线图 + link: /roadmap/ + - theme: alt + text: 基建速查 + link: /guides/ + - theme: alt + text: 更新日志 + link: /changelogs/ + - theme: alt + text: GitHub + link: https://github.com/Awesome-Embedded-Learning-Studio/PenguinLab -> Linux 内核学习站 — 从 QEMU 到内核原理、驱动开发与嵌入式全栈 +features: + - title: 通识基础 + details: ✅ 8 篇已锤炼——从 WSL2、Mini Config、编译内核、BusyBox rootfs、QEMU 启动、GDB 调试到内核模块与 9p 迭代,一条线打通。 + icon: '' + link: /tutorials/foundations/ + linkText: 开始阅读 -PenguinLab 是一个 Linux 内核学习站,覆盖调度器、内存管理、文件系统、网络栈、驱动开发、嵌入式全栈、调试调优等完整知识图谱。所有实践基于 QEMU,支持 ARM32/ARM64/RISC-V/x86_64 四种架构。 + - title: 内核子系统 + details: 🔨 调度器演进(O(1)→SD/RSDL→CFS→EEVDF→sched_ext)整理中;内存管理、文件系统、网络栈持续铺开。 + icon: '' + link: /tutorials/kernel/ + linkText: 开始阅读 ---- + - title: 驱动开发 + details: 📚 规划中——字符设备、平台驱动、设备树、中断,主线内核驱动开发完整链条。 + icon: '' + link: /tutorials/drivers/ + linkText: 敬请期待 -## 学习路径 + - title: 嵌入式全栈 + details: 📚 规划中——交叉编译、Bootloader、Buildroot、BSP,完整嵌入式 Linux 开发流程。 + icon: '' + link: /tutorials/embedded/ + linkText: 敬请期待 -教程按知识图谱层级组织,每篇教程标注前置知识和后续延伸,你可以按推荐路径循序渐进,也可以根据兴趣自由选择。 + - title: 调试与性能 + details: 📚 规划中——printk、ftrace、perf、eBPF,内核调试和性能分析全栈。 + icon: '' + link: /tutorials/debugging/ + linkText: 敬请期待 -| 层级 | 方向 | 内容 | -|------|------|------| -| 通识基础 | 环境搭建 → 内核模块 → 数据结构 → 进程 | 入门必修,建立内核开发基础 | -| 内核子系统 | 调度器 / 内存管理 / 文件系统 / 网络栈 | 深入内核核心原理 | -| 驱动开发 | 字符设备 → 平台驱动 → 设备树 → 中断 | 掌握主线内核驱动开发 | -| 嵌入式全栈 | 交叉编译 → Bootloader → Buildroot → BSP | 完整嵌入式 Linux 开发流程 | -| 调试与性能 | printk → ftrace → perf → eBPF | 内核调试和性能分析全栈 | -| 虚拟化与容器 | KVM / Namespaces / cgroups | 理解虚拟化和容器的内核基础 | + - title: 虚拟化与容器 + details: 📚 规划中——KVM、Namespaces、cgroups,虚拟化和容器背后的内核机制。 + icon: '' + link: /tutorials/virtualization/ + linkText: 敬请期待 ---- + - title: 基建速查 + details: ✅ 五条主线脚本串成一条流水线——内核编译、rootfs、QEMU、模块交叉编译、网站构建,产物约定和常见坑都钉死了。 + icon: '' + link: /guides/ + linkText: 开始阅读 -## 参考文档 + - title: 更新日志 + details: 每个版本新锤炼了什么、修了什么——这站一直在出东西。 + icon: '' + link: /changelogs/ + linkText: 查看版本 -- [推荐书单](/booklist) -- [QEMU ARM 速查手册](/qemu-reference) + - title: 推荐书单 + details: 内核学习路上的推荐书目,配 QEMU ARM 速查手册。 + icon: '' + link: /booklist + linkText: 查看书单 +--- diff --git a/document/roadmap/index.md b/document/roadmap/index.md new file mode 100644 index 00000000..ea37f270 --- /dev/null +++ b/document/roadmap/index.md @@ -0,0 +1,94 @@ +--- +title: 学习路线图 +description: PenguinLab 怎么学——从哪开始、各板块什么关系、现在写到哪了 +maturity: growing +--- + +# 学习路线图 + +> 这页告诉你:PenguinLab 怎么学——从哪开始、各板块什么关系、现在写到哪了。教程按六个层级组织,每层有明确的前置和成熟度标记。 + +## 整体路线 + +六层从下往上垒,**通识基础是所有东西的地基**,绕不开: + +```mermaid +flowchart TD + L0["Layer 0 · 通识基础 ✅
环境 → 内核 → rootfs → QEMU → GDB → 模块"] + L1["Layer 1 · 内核子系统 🔨
调度器 · 内存 · 文件系统 · 网络"] + L2["Layer 2 · 驱动开发 📚
字符设备 · 平台驱动 · 设备树 · 中断"] + L4["Layer 4 · 调试与性能 📚
printk · ftrace · perf · eBPF"] + L3["Layer 3 · 嵌入式全栈 📚
交叉编译 · Bootloader · Buildroot · BSP"] + L5["Layer 5 · 虚拟化与容器 📚
KVM · Namespaces · cgroups"] + L0 --> L1 + L1 --> L2 + L1 --> L4 + L2 --> L3 + L1 --> L5 + L4 --> L5 +``` + +## 按背景选起点 + +- **完全新手**:从 Layer 0 通识基础开始,一条线打通「环境 → 内核 → 模块」,建立工具链。 +- **会点 C、想懂内核**:Layer 0 快速过,重点放 Layer 1 内核子系统。 +- **做嵌入式、想补内核底子**:Layer 0 + Layer 1 选读,然后 Layer 2 驱动 / Layer 3 嵌入式。 +- **搞性能调优 / 内核应用**:Layer 0 + Layer 1,重点 Layer 4 调试与性能。 + +## 逐层详情 + +### Layer 0 · 通识基础 ✅ 已锤炼 + +- **定位**:内核开发的入门工具链。从零搭起一套「编译内核 + rootfs + QEMU + GDB + 模块」的环境。 +- **关键主题**:WSL2 环境、Mini Config、ARM64 内核编译、BusyBox rootfs、QEMU 启动、GDB 远程调试、内核模块、9p 迭代。 +- **难度 · 前置**:入门,会基本 Linux 命令和 C 即可。 +- **建议节奏**:8 篇循序,约 1–2 周打通。 +- → [进入通识基础](/tutorials/foundations/) + +### Layer 1 · 内核子系统 🔨 整理中 + +- **定位**:内核核心原理——调度器怎么挑任务、内存怎么管、文件怎么存、包怎么转。 +- **关键主题**:调度器(CFS / EEVDF / sched_ext)、内存(Buddy / Slab / 回收)、文件系统(VFS / Ext4)、网络栈。 +- **难度 · 前置**:中级,需要 Layer 0。 +- **建议节奏**:四大子系统各成一线,调度器先行。 +- → [进入内核子系统](/tutorials/kernel/) + +### Layer 2 · 驱动开发 📚 规划中 + +- **定位**:主线内核驱动开发全链条。 +- **关键主题**:字符设备、平台驱动、设备树、中断。 +- **难度 · 前置**:中级,需要 Layer 0 + Layer 1。 +- → [进入驱动开发](/tutorials/drivers/) + +### Layer 3 · 嵌入式全栈 📚 规划中 + +- **定位**:完整嵌入式 Linux 流程,从 QEMU 虚拟平台迁移到真板。 +- **关键主题**:交叉编译、Bootloader(U-Boot)、Buildroot / Yocto、BSP。 +- **难度 · 前置**:中高级,需要 Layer 0 + Layer 2。 +- → [进入嵌入式全栈](/tutorials/embedded/) + +### Layer 4 · 调试与性能 📚 规划中 + +- **定位**:内核调试和性能分析全栈。 +- **关键主题**:printk、ftrace、perf、eBPF。 +- **难度 · 前置**:中高级,需要 Layer 0 + Layer 1。 +- → [进入调试与性能](/tutorials/debugging/) + +### Layer 5 · 虚拟化与容器 📚 规划中 + +- **定位**:虚拟化和容器背后的内核机制。 +- **关键主题**:KVM、Namespaces、cgroups。 +- **难度 · 前置**:高级,需要 Layer 1 + Layer 4。 +- → [进入虚拟化与容器](/tutorials/virtualization/) + +--- + +## 成熟度怎么看 + +每个板块、每篇内容都标了三档火候: + +- ✅ **已锤炼** — 读过、跑过、亲手验证过 +- 🔨 **整理中** — 笔记已经收集来,还在整理和验证 +- 📚 **规划中 / 持续补充** — 框架立住了或还在规划,慢慢加 + +具体每个版本锤炼了什么、修了什么,见[更新日志](/changelogs/)。 diff --git a/document/tutorials/debugging/index.md b/document/tutorials/debugging/index.md new file mode 100644 index 00000000..0f6c3a7a --- /dev/null +++ b/document/tutorials/debugging/index.md @@ -0,0 +1,17 @@ +--- +title: 调试与性能 +description: printk、ftrace、perf、eBPF——内核调试与性能分析全栈 +--- + +# 调试与性能 + +> printk → ftrace → perf → eBPF,内核调试和性能分析全栈。 + +📚 **规划中**,还没开写。通识基础里的 [GDB + QEMU 调试](../foundations/06-gdb-debug-setup)已经打底,这里会往运行时调试和性能分析展开。 + +## 计划 + +- **printk 与日志**:日志级别、`log_buf`、动态调试 +- **ftrace**:tracepoint、function tracer、事件触发 +- **perf**:采样、火焰图、cache 分析 +- **eBPF**:bcc/bpftrace、内核可观测性 diff --git a/document/tutorials/drivers/index.md b/document/tutorials/drivers/index.md new file mode 100644 index 00000000..2757b20b --- /dev/null +++ b/document/tutorials/drivers/index.md @@ -0,0 +1,17 @@ +--- +title: 驱动开发 +description: 字符设备、平台驱动、设备树、中断——主线内核驱动开发 +--- + +# 驱动开发 + +> 字符设备 → 平台驱动 → 设备树 → 中断,掌握主线内核驱动开发的完整链条。 + +📚 **规划中**,还没开写。建议先把[通识基础](../foundations/)和[内核子系统](../kernel/)走通,这块会在那之后铺开。 + +## 计划 + +- **字符设备**:`file_operations`、`ioctl`、`cdev` 生命周期 +- **平台驱动**:probe/remove、总线-设备-驱动模型 +- **设备树**:`compatible`、binding 文档、`of_*` 接口 +- **中断**:`request_irq`、threaded IRQ、上下半部 diff --git a/document/tutorials/embedded/index.md b/document/tutorials/embedded/index.md new file mode 100644 index 00000000..2506955b --- /dev/null +++ b/document/tutorials/embedded/index.md @@ -0,0 +1,17 @@ +--- +title: 嵌入式全栈 +description: 交叉编译、Bootloader、Buildroot、BSP——完整嵌入式 Linux 流程 +--- + +# 嵌入式全栈 + +> 交叉编译 → Bootloader → Buildroot → BSP,完整的嵌入式 Linux 开发流程。 + +📚 **规划中**,还没开写。本站的所有实践基于 QEMU(ARM32/ARM64/RISC-V/x86_64),嵌入式全栈会从 QEMU 虚拟平台起步,再迁移到真板。 + +## 计划 + +- **交叉编译**:工具链构建、sysroot、多架构 +- **Bootloader**:U-Boot 移植与启动流程 +- **Buildroot / Yocto**:整套 rootfs 自动构建 +- **BSP**:板级支持包、设备树定制 diff --git a/document/tutorials/foundations/wsl2-env-toolchain.md b/document/tutorials/foundations/01-wsl2-env-toolchain.md similarity index 97% rename from document/tutorials/foundations/wsl2-env-toolchain.md rename to document/tutorials/foundations/01-wsl2-env-toolchain.md index 5a79593c..d92c9f5d 100644 --- a/document/tutorials/foundations/wsl2-env-toolchain.md +++ b/document/tutorials/foundations/01-wsl2-env-toolchain.md @@ -7,6 +7,7 @@ difficulty: beginner tags: [wsl2, cross-compile, toolchain, arm64] architectures: [arm64] kernel_version: "6.19" +maturity: verified --- ## Welcome Kernel! @@ -136,7 +137,7 @@ CROSS_COMPILE - 指定 binutils 文件名的可选固定部分。CROSS_COMPILE 可以是文件名的一部分或完整路径。在某些设置中,CROSS_COMPILE 也用于 ccache。 ``` -这里有一个很容易踩的坑——`ARCH` 的值和工具链前缀的命名并不总是一一对应。对于 ARM64 来说,内核构建系统用的架构名是 `arm64`,但工具链前缀是 `aarch64-linux-gnu-`,两者不一样。这是历史原因造成的,ARM64 架构在内核社区一直叫 `arm64`,而工具链这边用的是 ARM 官方定义的 `aarch64` 名称。项目的 [linux-action-scripts.sh](scripts/linux-action-scripts.sh) 第 69-72 行专门做了这个映射,把用户传入的 `aarch64` 或 `arm64` 统一转成 Kbuild 认的 `arm64`。 +这里有一个很容易踩的坑——`ARCH` 的值和工具链前缀的命名并不总是一一对应。对于 ARM64 来说,内核构建系统用的架构名是 `arm64`,但工具链前缀是 `aarch64-linux-gnu-`,两者不一样。这是历史原因造成的,ARM64 架构在内核社区一直叫 `arm64`,而工具链这边用的是 ARM 官方定义的 `aarch64` 名称。项目的 [linux-action-scripts.sh](https://github.com/Awesome-Embedded-Learning-Studio/PenguinLab/blob/main/scripts/linux-action-scripts.sh) 第 69-72 行专门做了这个映射,把用户传入的 `aarch64` 或 `arm64` 统一转成 Kbuild 认的 `arm64`。 `CROSS_COMPILE` 更有意思——它不是一个完整的命令名,而是一个前缀。内核构建系统会自动在这个前缀后面拼接 `gcc`、`ld`、`objcopy` 等后缀来找到对应的工具。也就是说,当你设置 `CROSS_COMPILE=aarch64-linux-gnu-` 时,make 会去找 `aarch64-linux-gnu-gcc`、`aarch64-linux-gnu-ld`、`aarch64-linux-gnu-objcopy` 等一系列工具。如果 `CROSS_COMPILE` 是空字符串,make 就直接用本机的 `gcc`、`ld` 等——这就是为什么忘记设这个变量的时候,内核会编译成 x86 架构的,因为这个空字符串前缀导致构建系统用了宿主机的编译器。 diff --git a/document/tutorials/foundations/kernel-mini-config.md b/document/tutorials/foundations/02-kernel-mini-config.md similarity index 92% rename from document/tutorials/foundations/kernel-mini-config.md rename to document/tutorials/foundations/02-kernel-mini-config.md index fe53489a..17a51f1f 100644 --- a/document/tutorials/foundations/kernel-mini-config.md +++ b/document/tutorials/foundations/02-kernel-mini-config.md @@ -10,6 +10,7 @@ architectures: [arm64] kernel_version: "6.19" sources: - notes: document/notes/linux_kernel_programming/ch01.md +maturity: verified --- ## 做什么 @@ -57,7 +58,7 @@ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- \ ### Config Fragment 的设计思路 -我们准备好的 fragment 文件是 [configs/arm64-qemu-virt-learn.config](configs/arm64-qemu-virt-learn.config),它按功能分类组织,每一组都有注释说明"为什么需要这些配置"。当然,您可能会问这些都是啥。如果您不是很清楚——一个最好的办法就是递归下降法的查询相关的概念。我们这里快速的说一下这些内容都是啥: +我们准备好的 fragment 文件是 [configs/arm64-qemu-virt-learn.config](https://github.com/Awesome-Embedded-Learning-Studio/PenguinLab/blob/main/configs/arm64-qemu-virt-learn.config),它按功能分类组织,每一组都有注释说明"为什么需要这些配置"。当然,您可能会问这些都是啥。如果您不是很清楚——一个最好的办法就是递归下降法的查询相关的概念。我们这里快速的说一下这些内容都是啥: 先说平台基础。ARM64 架构本身是必须的,SMP(对称多处理器)也要开,因为我们后续的 QEMU 启动会配两个 CPU 核: @@ -128,4 +129,4 @@ rm -f third_party/linux/.config - [Kbuild 内核构建系统文档](https://www.kernel.org/doc/html/latest/kbuild/kbuild.html) — `ARCH`、`CROSS_COMPILE`、`O=` 等变量的官方定义 - [Linux 内核构建说明](https://docs.kernel.org/admin-guide/README.html) — kernel.org 官方的编译入门指南 -- [configs/arm64-qemu-virt-learn.config](configs/arm64-qemu-virt-learn.config) — 我们的 mini config fragment 文件,每个配置项都有注释 +- [configs/arm64-qemu-virt-learn.config](https://github.com/Awesome-Embedded-Learning-Studio/PenguinLab/blob/main/configs/arm64-qemu-virt-learn.config) — 我们的 mini config fragment 文件,每个配置项都有注释 diff --git a/document/tutorials/foundations/kernel-build.md b/document/tutorials/foundations/03-kernel-build.md similarity index 99% rename from document/tutorials/foundations/kernel-build.md rename to document/tutorials/foundations/03-kernel-build.md index df75482f..959515d7 100644 --- a/document/tutorials/foundations/kernel-build.md +++ b/document/tutorials/foundations/03-kernel-build.md @@ -8,6 +8,7 @@ difficulty: beginner tags: [kernel-build, arm64, kbuild, make] architectures: [arm64] kernel_version: "6.19" +maturity: verified --- ## 做什么 diff --git a/document/tutorials/foundations/busybox-rootfs.md b/document/tutorials/foundations/04-busybox-rootfs.md similarity index 87% rename from document/tutorials/foundations/busybox-rootfs.md rename to document/tutorials/foundations/04-busybox-rootfs.md index c1be1987..9057a38e 100644 --- a/document/tutorials/foundations/busybox-rootfs.md +++ b/document/tutorials/foundations/04-busybox-rootfs.md @@ -8,6 +8,7 @@ difficulty: beginner tags: [busybox, rootfs, initramfs, cpio, arm64] architectures: [arm64] kernel_version: "6.19" +maturity: verified --- ## 做什么 @@ -27,7 +28,7 @@ BusyBox 是嵌入式 Linux 世界的瑞士军刀——它把 `ls`、`cat`、`sh` ### 构建 rootfs -项目提供了一个自动化脚本 [rootfs-minimal-maker.sh](scripts/rootfs-minimal-maker.sh) 来处理整个 rootfs 构建流程。它的核心工作包括:编译 BusyBox(静态链接)、创建 rootfs 目录结构(`bin/`、`sbin/`、`usr/`、`proc/`、`sys/`、`dev/` 等)、安装 BusyBox 的符号链接、生成 `/init` 启动脚本。运行方式是: +项目提供了一个自动化脚本 [rootfs-minimal-maker.sh](https://github.com/Awesome-Embedded-Learning-Studio/PenguinLab/blob/main/scripts/rootfs-minimal-maker.sh) 来处理整个 rootfs 构建流程。它的核心工作包括:编译 BusyBox(静态链接)、创建 rootfs 目录结构(`bin/`、`sbin/`、`usr/`、`proc/`、`sys/`、`dev/` 等)、安装 BusyBox 的符号链接、生成 `/init` 启动脚本。运行方式是: ```bash ARCH=aarch64 ./scripts/rootfs-minimal-maker.sh defconfig @@ -80,5 +81,5 @@ find . -print0 | cpio --null -ov --format=newc 2>/dev/null | gzip -9 > ../rootfs ## 延伸阅读 -- [rootfs-minimal-maker.sh](scripts/rootfs-minimal-maker.sh) — rootfs 构建脚本,特别是 `setup_rootfs()` 函数展示了最小 rootfs 需要哪些目录和文件 +- [rootfs-minimal-maker.sh](https://github.com/Awesome-Embedded-Learning-Studio/PenguinLab/blob/main/scripts/rootfs-minimal-maker.sh) — rootfs 构建脚本,特别是 `setup_rootfs()` 函数展示了最小 rootfs 需要哪些目录和文件 - [BusyBox 官方网站](https://busybox.net/) — BusyBox 项目主页和文档 diff --git a/document/tutorials/foundations/qemu-first-boot.md b/document/tutorials/foundations/05-qemu-first-boot.md similarity index 96% rename from document/tutorials/foundations/qemu-first-boot.md rename to document/tutorials/foundations/05-qemu-first-boot.md index 261886d1..dfa42c3e 100644 --- a/document/tutorials/foundations/qemu-first-boot.md +++ b/document/tutorials/foundations/05-qemu-first-boot.md @@ -8,6 +8,7 @@ difficulty: beginner tags: [qemu, boot, arm64, virt, bootlog] architectures: [arm64] kernel_version: "6.19" +maturity: verified --- ## 做什么 @@ -24,7 +25,7 @@ QEMU 支持 ARM/ARM64 平台上的多种机器型号,从模拟真实开发板 ### 启动命令 -项目提供了 [qemu-run.sh](scripts/qemu-run.sh) 脚本来简化 QEMU 的启动参数,它自动检测编译输出目录里的内核镜像和 rootfs 文件。运行方式很简单: +项目提供了 [qemu-run.sh](https://github.com/Awesome-Embedded-Learning-Studio/PenguinLab/blob/main/scripts/qemu-run.sh) 脚本来简化 QEMU 的启动参数,它自动检测编译输出目录里的内核镜像和 rootfs 文件。运行方式很简单: ```bash ./scripts/qemu-run.sh run diff --git a/document/tutorials/foundations/gdb-debug-setup.md b/document/tutorials/foundations/06-gdb-debug-setup.md similarity index 96% rename from document/tutorials/foundations/gdb-debug-setup.md rename to document/tutorials/foundations/06-gdb-debug-setup.md index 0666c32c..f0af04fd 100644 --- a/document/tutorials/foundations/gdb-debug-setup.md +++ b/document/tutorials/foundations/06-gdb-debug-setup.md @@ -8,6 +8,7 @@ difficulty: beginner tags: [gdb, qemu, debug, kaslr, vscode, arm64] architectures: [arm64] kernel_version: "6.19" +maturity: verified --- ## 做什么 @@ -20,7 +21,7 @@ kernel_version: "6.19" QEMU 默认启动内核时是直接跑完的,不会在任何地方暂停等待调试器连接。要让 GDB 有介入的机会,我们需要两个启动参数:`-s` 是 `-gdb tcp::1234` 的简写,让 QEMU 在 1234 端口开放 GDB 远程调试协议的监听;`-S` 让 QEMU 在启动后立刻冻结 CPU,不执行第一条指令,等待 GDB 发送 `continue` 命令后才继续。 -我们的 [qemu-run.sh](scripts/qemu-run.sh) 脚本为此增加了一个 `debug` 命令,在构建 QEMU 命令时自动追加 `-s -S`: +我们的 [qemu-run.sh](https://github.com/Awesome-Embedded-Learning-Studio/PenguinLab/blob/main/scripts/qemu-run.sh) 脚本为此增加了一个 `debug` 命令,在构建 QEMU 命令时自动追加 `-s -S`: ```bash ./scripts/qemu-run.sh debug diff --git a/document/tutorials/foundations/kernel-module-hello.md b/document/tutorials/foundations/07-kernel-module-hello.md similarity index 97% rename from document/tutorials/foundations/kernel-module-hello.md rename to document/tutorials/foundations/07-kernel-module-hello.md index 2ebf16e8..5980f309 100644 --- a/document/tutorials/foundations/kernel-module-hello.md +++ b/document/tutorials/foundations/07-kernel-module-hello.md @@ -10,6 +10,7 @@ architectures: [arm64] kernel_version: "6.19" sources: - guide: helpers/study-guides/layer-0/kernel-module-basics-0 +maturity: verified --- ## 做什么 @@ -95,7 +96,7 @@ make: *** Error 2 根因藏在 make 的变量传递机制里。`include ../../common/Makefile.arch` 确实在当前这一层 make 里设好了 `ARCH=arm64` 和 `CROSS_COMPILE=aarch64-linux-gnu-`,但 `all` 规则里的 `$(MAKE) -C $(KDIR) ... modules` 是**递归调用 make**——它会切到内核目录、启动一个全新的 make 进程。这里的关键是:当前 make 里的变量是"局部的",新的子 make 进程默认看不到它们,就好比你在函数 A 里设了个局部变量,调用函数 B 时不传参,B 自然访问不到。子 make(也就是内核构建系统)拿不到 `CROSS_COMPILE`,就退回默认的本机 `gcc`,于是报错。 -解法是把这两个变量 `export` 成环境变量,子进程就能继承了。最干净的修法是改共享的 [example/common/Makefile.arch](example/common/Makefile.arch),在文件末尾加一行: +解法是把这两个变量 `export` 成环境变量,子进程就能继承了。最干净的修法是改共享的 [example/common/Makefile.arch](https://github.com/Awesome-Embedded-Learning-Studio/PenguinLab/blob/main/example/common/Makefile.arch),在文件末尾加一行: ```makefile # 导出给内核构建系统的递归子 make(外部模块编译必需) @@ -119,7 +120,7 @@ $ make `hello.ko` 编出来了,但它在宿主机上,怎么把它弄进 QEMU 跑的虚拟机里?这里要先理解我们的 rootfs 是怎么组织的。我们用的是 initramfs 方式启动——`out/build_latest_arm64/rootfs.cpio.gz` 是一个 cpio 打包的根文件系统镜像,内核启动时把它解包到内存里作为根目录。关键性质是:这个镜像在**启动的那一刻就固定了**,它是一个只读快照,启动之后我们在虚拟机 shell 里没法往根目录写新文件。所以宿主机上后来编译出来的 `hello.ko`,自然不在虚拟机的文件系统里——这也是笔者一开始对着 `ls` 找不到 ko 一脸懵的原因。 -最直接的解法是把 `.ko` 放进 rootfs 目录、重新打包 cpio。项目提供了 [scripts/rootfs-minimal-maker.sh](scripts/rootfs-minimal-maker.sh),而且它有个 `--pack-only` 选项,专门用来"只重新打包 cpio、不重新编译 BusyBox",非常轻量: +最直接的解法是把 `.ko` 放进 rootfs 目录、重新打包 cpio。项目提供了 [scripts/rootfs-minimal-maker.sh](https://github.com/Awesome-Embedded-Learning-Studio/PenguinLab/blob/main/scripts/rootfs-minimal-maker.sh),而且它有个 `--pack-only` 选项,专门用来"只重新打包 cpio、不重新编译 BusyBox",非常轻量: ```bash $ cp example/mini/00-kernel_module_hello/hello.ko out/build_latest_arm64/rootfs/ diff --git a/document/tutorials/foundations/08-kernel-module-9p-iteration.md b/document/tutorials/foundations/08-kernel-module-9p-iteration.md new file mode 100644 index 00000000..7a790333 --- /dev/null +++ b/document/tutorials/foundations/08-kernel-module-9p-iteration.md @@ -0,0 +1,153 @@ +--- +title: 用 9p 共享目录加速内核模块迭代 +slug: kernel-module-9p-iteration +prerequisites: + - kernel-module-hello + - qemu-first-boot +next: + - kernel-module-params +difficulty: intermediate +tags: [9p, qemu, kernel-module, 工作流, 调试] +architectures: [arm64, arm] +kernel_version: "6.19" +sources: + - guide: helpers/study-guides/layer-0/kernel-module-basics-1/9p-share.md + - log: helpers/study-guides/layer-0/kernel-module-basics-1/log.txt +maturity: verified +--- + +# 用 9p 共享目录加速内核模块迭代 + +## 做什么 + +学完这篇,我们能: + +- 搞懂 9p 是什么、为什么它能让「改 ko 免重打包」 +- 用 `qemu-run.sh` 的 `QEMU_9P` 开关一键挂起共享目录 +- 在 Guest 里通过 9p 直接 `insmod` 宿主机刚 `make` 出来的 ko,告别 `cp + --pack-only + 重启` 的慢循环 + +## 要了解什么 + +### 先承认一个痛点 + +在没 9p 之前,内核模块的迭代循环是这样的:改 `hello.c` → `make` → `cp hello.ko` 进 rootfs 目录 → `rootfs-minimal-maker.sh --pack-only` 重打包 cpio → 重启 QEMU → `insmod`。每改一行代码,都得把整个 rootfs 重新打包一遍、Guest 重启一遍,几秒钟的改动要等半分钟。改得勤的时候,人是要疯的。 + +9p 要解决的就是这件事。 + +### 9p 是什么(够用就行) + +- **9p** 是贝尔实验室 Plan 9 那套「一切皆文件」哲学里的远程文件协议;Linux 实现的是 `9P2000` 变体,比 NFS 轻、消息更简单。 +- **QEMU 的 `-virtfs`**:把宿主机一个真实目录,通过 virtio 通道用 9p 协议暴露给 Guest;Guest 这边就像挂载一个普通文件系统,读写实际落宿主机磁盘。 +- **为什么能免重打包**:因为 Guest 的挂载点直接映射到宿主机目录——你宿主机 `make` 出新 `.ko`,Guest 里 `ls` 立刻看见,根本不用进 rootfs 镜像。重打包那套流程,在这条路下可以退役了。 + +再往下钻(T-message/R-message 报文格式、trans=fd vs virtio transport、`9P2000.L` 和 `.u` 的区别)属于「用到再查」的范畴,这篇不展开。 + +### `qemu-run.sh` 的 `QEMU_9P` 开关 + +`scripts/qemu-run.sh` 已经把 9p 固化进去了,不用每次手敲一长串 `-virtfs`。四个环境变量控制: + +| 变量 | 默认 | 说明 | +|------|------|------| +| `QEMU_9P` | `off` | 开关,`on` 时挂载 | +| `QEMU_9P_PATH` | (空) | 宿主机要共享的目录,**绝对路径**,`on` 时必填 | +| `QEMU_9P_TAG` | `hostshare` | mount tag,Guest 挂载时用这个名字 | +| `QEMU_9P_SEC` | `none` | `security_model`:`none`/`mapped-xattr`/`passthrough` | + +`none` 最省心(非 root 也能用,传文件够);`passthrough` 要 root,别碰。 + +## 动手试试 + +### 1. 确认内核 9p 已内建 + +我们这个项目的 ARM64 内核默认就把 9p 编进去了(`=y`,不是模块),确认一下: + +```bash +grep -E 'CONFIG_NET_9P=|CONFIG_NET_9P_VIRTIO=|CONFIG_9P_FS=' out/build_latest_arm64/.config +``` + +三条都 `=y` 就行——意味着 Guest 启动就有 9p,不用再 `insmod` 任何 9p 模块,也不用重编内核。 + +### 2. 起一个带 9p 的 QEMU + +把模块源码目录共享给 Guest(这样 `make` 出的 ko 直接可见): + +```bash +QEMU_9P=on QEMU_9P_PATH="$(pwd)/example/mini/00-kernel_module_hello" \ + scripts/qemu-run.sh run +``` + +引导时内核日志会打印: + +``` +9p: Installing v9fs 9p2000 file system support +9pnet: Installing 9P2000 support +``` + +### 3. Guest 里挂载 + +进到 BusyBox shell(`~ #`)后: + +```sh +mkdir -p /mnt/share +mount -t 9p -o trans=virtio,version=9p2000.L hostshare /mnt/share +echo $? # 0 = 挂载成功 +ls /mnt/share # 应看到 hello.c、Makefile、hello.ko ... +``` + +### 4. 真正的迭代:改 ko 不重打包 + +这才是 9p 的价值所在。宿主机改 `hello.c`(比如把 `pr_info` 的文案改一下)→ `make` 出新 `hello.ko`,然后 Guest 里**直接重新 `insmod` 同一个路径**: + +```sh +# Guest 里(ko 已经 insmod 过的话先 rmmod) +rmmod hello +insmod /mnt/share/hello.ko # 加载的就是宿主机刚 make 的新版本 +dmesg | tail -5 # 看新的 printk +``` + +宿主机每次 `make` 覆盖 `hello.ko`,Guest 不用动 rootfs、不用重启,`rmmod` + `insmod` 就能加载到最新版。 + +### 实测输出(对照参考) + +我们用两个不同 build 的 ko(`hello-A.ko` / `hello-B.ko`,只是 `pr_info` 文案不同)演示「Guest 通过 9p 加载宿主机的不同 build,全程不碰 rootfs」。共享目录里放这俩 ko,Guest 操作: + +``` +~ # mount -t 9p -o trans=virtio,version=9p2000.L hostshare /mnt/iter +~ # echo MOUNT_RC=$? +MOUNT_RC=0 +~ # ls /mnt/iter +hello-A.ko hello-B.ko +~ # insmod /mnt/iter/hello-A.ko +[ 17.575143] hello: loading out-of-tree module taints kernel. +[ 17.580515] 9p iter: build A +~ # rmmod hello +[ 19.256294] My First Module exit, say goodbye! +~ # insmod /mnt/iter/hello-B.ko +[ 20.278137] 9p iter: build B +``` + +`build A` → `build B`,两次 `insmod` 加载的是宿主机不同的 ko,rootfs 自始至终没重打包。这就是 9p 给模块开发带来的提速。 + +### 验证清单 + +- [ ] 内核 `.config` 里 `CONFIG_NET_9P`/`CONFIG_NET_9P_VIRTIO`/`CONFIG_9P_FS` 都是 `=y` +- [ ] `QEMU_9P=on QEMU_9P_PATH=... scripts/qemu-run.sh run` 能正常启动 +- [ ] Guest 里 `mount -t 9p ...` 返回 0 +- [ ] Guest `ls` 能看到宿主机共享目录的内容 +- [ ] 宿主机改文件后,Guest 立刻可见(不用任何重打包) + +## 踩过的坑 + +- **`-virtfs` 的 `id=` 必须字母开头**:QEMU 的标识符要求以字母起头,写成 `id=9p0`(数字开头)会被拒:`Parameter 'id' expects an identifier ... starting with a letter`。我们固化进 `qemu-run.sh` 时踩过,现在用的是 `id=fs9p`。 +- **非 root 别用 `passthrough`**:`security_model=passthrough` 要 QEMU 以 root 跑;普通用户用 `none`(或 `mapped-xattr`)。 +- **`mount_tag` 两边要逐字一致**:`QEMU_9P_TAG`(默认 `hostshare`)和 Guest `mount ... hostshare` 必须相同。 +- **`version` 用 `9p2000.L`**:Linux 增强版,推荐;老式 `9p2000.u` 特性少。 +- **`path=` 用绝对路径**:相对路径会指错。 + +## 延伸阅读 + +- 项目指南「[QEMU 启动与调试](/guides/03-qemu)」「[制作 BusyBox rootfs](/guides/02-rootfs)」——理解 9p 替代的重打包流程 +- 前置教程「[第一个内核模块](./07-kernel-module-hello)」——先会编 hello.ko +- QEMU 9p 设置: +- Linux 9p 文件系统文档: + diff --git a/document/tutorials/foundations/index.md b/document/tutorials/foundations/index.md new file mode 100644 index 00000000..055c1ce9 --- /dev/null +++ b/document/tutorials/foundations/index.md @@ -0,0 +1,20 @@ +--- +title: 通识基础 +description: 从环境搭建到内核模块,一条线打通内核开发入门 +maturity: verified +--- + +# 通识基础 + +> 从 WSL2 环境、Mini Config、编译内核、BusyBox rootfs、QEMU 启动、GDB 调试到内核模块与 9p 迭代——一条线打通,建立内核开发的基础工具链。✅ 8 篇已锤炼。 + +## 教程 + +- ✅ [01 WSL2 环境摸底与交叉编译工具链](./01-wsl2-env-toolchain) +- ✅ [02 Mini Config:从零设计一份精简的内核配置](./02-kernel-mini-config) +- ✅ [03 编译你的第一个 ARM64 内核](./03-kernel-build) +- ✅ [04 用 BusyBox 构建最小根文件系统](./04-busybox-rootfs) +- ✅ [05 QEMU 首次启动与 boot log 解读](./05-qemu-first-boot) +- ✅ [06 GDB + QEMU 远程调试 ARM64 内核](./06-gdb-debug-setup) +- ✅ [07 从零写第一个内核模块](./07-kernel-module-hello) +- ✅ [08 用 9p 共享目录加速内核模块迭代](./08-kernel-module-9p-iteration) diff --git a/document/tutorials/kernel/index.md b/document/tutorials/kernel/index.md new file mode 100644 index 00000000..30244272 --- /dev/null +++ b/document/tutorials/kernel/index.md @@ -0,0 +1,31 @@ +--- +title: 内核子系统 +description: 调度器、内存管理、文件系统、网络栈——内核核心原理 +maturity: drafting +--- + +# 内核子系统 + +> 深入内核核心原理:调度器、内存管理、文件系统、网络栈。🔨 调度器演进系列整理中,其余持续铺开。 + +## 调度器 🔨 整理中 + +- 🔨 [演进(一):O(1) 调度器——从遍历到位图的跨越](./sched/sched-evolution-01-o1) +- 🔨 [演进(二):SD/RSDL 风波——一位麻醉医生挑战内核权威](./sched/sched-evolution-02-sd-rsdl) +- 🔨 [演进(三):CFS 诞生——62 小时重写调度器](./sched/sched-evolution-03-cfs) +- 🔨 [演进(四):EEVDF——从虚拟公平到虚拟截止时间](./sched/sched-evolution-04-eevdf) +- 🔨 [演进(五):sched_ext——BPF 可编程调度器的争议之路](./sched/sched-evolution-05-sched-ext) + +## 内存管理 🔨 整理中 + +- 🔨 [伙伴系统:内核怎么管物理页](./mm/mm-buddy) + +Slab/Slub、vmalloc、页面回收、OOM 持续铺开。 + +## 文件系统 📚 规划中 + +VFS、Ext4、页缓存、写时复制。 + +## 网络栈 📚 规划中 + +socket 层、TCP/IP 协议栈、Netfilter、XDP。 diff --git a/document/tutorials/kernel/mm/mm-buddy.md b/document/tutorials/kernel/mm/mm-buddy.md new file mode 100644 index 00000000..f70c81bc --- /dev/null +++ b/document/tutorials/kernel/mm/mm-buddy.md @@ -0,0 +1,123 @@ +--- +title: 伙伴系统:内核怎么管物理页 +slug: mm-buddy +difficulty: intermediate +tags: [内存管理, 伙伴系统, 页面分配器] +architectures: [arm64, x86_64, riscv] +kernel_version: "6.19" +maturity: drafting +prerequisites: + - /tutorials/foundations/03-kernel-build +related: + - /tutorials/foundations/07-kernel-module-hello +sources: + - notes: document/notes/linux_kernel_programming/ch08.md + - notes: document/notes/linux_kernel_programming/ch07.md +--- + +# 伙伴系统:内核怎么管物理页 + +> 🔨 **整理中** · 这篇是从读书笔记(ch07/ch08)整理出来的骨架,核心机制讲透了;但动手部分(QEMU 上看 `/proc/buddyinfo`、写模块验证 `alloc_pages`)还没亲手跑过。等我们在 QEMU 里验过,就升级成 ✅ 已锤炼。 + +## 内核为什么要专门管物理页 + +在用户空间,我们要内存就 `malloc`,用完 `free`,背后怎么分配是 glibc 和内核的事。可一旦进了内核——写模块、写驱动——就得直接面对一个更硬的问题:**物理内存怎么分。** + +先刻一条铁律:**内核内存不会被换到磁盘上。** 用户进程内存不够可以扔 swap,内核不行——管理内存的数据结构要是被换出去了,想读回来还得用内存,这就成了"为了找眼镜得先戴上眼镜"的死锁。所以内核内存常驻 RAM,**浪费内核内存比浪费用户内存代价大得多**。 + +那内核在这块常驻 RAM 上怎么做分配?两层楼:底层是**伙伴系统**(页面分配器,只做大宗页块交易),上层是 **Slab 分配器**(专管小对象)。这篇讲底层——伙伴系统。 + +## 物理内存的三层组织:Node → Zone → Page + +内核不把物理内存看作一整块 RAM,而是三层结构: + +1. **Node(节点)**:NUMA 架构的概念——多路服务器上每个 CPU 挂在自己最近的内存控制器上,访问"本地"内存快、"远程"慢。即便你的 PC 是 UMA(统一内存),内核为了代码通用也假装它有 Node 0。 +2. **Zone(区域)**:每个 Node 划成几个 Zone,主要是给老硬件擦屁股——`DMA`(ISA 设备只能寻址低 16MB)、`DMA32`(低 4GB)、`Normal`(普通)、`HighMem`(32 位高端内存痛点,64 位不需要了)。 +3. **Page(页帧)**:物理内存最小单位,每页对应一个 `struct page`,页大小通常 4KB。 + +这套结构待会儿用 `/proc/buddyinfo` 就能看到。 + +## 伙伴系统的核心:`free_area[MAX_ORDER]` + +页面分配器的核心数据结构在每个 `struct zone` 里:一个数组 `free_area[MAX_ORDER]`。这就是伙伴系统的空闲链表。 + +`MAX_ORDER` 在 x86 和 ARM 上通常是 11,意思是 11 条链表(order 0 到 10),每条挂着不同大小的**物理连续**页块: + +| Order | 页数 | 大小(4KB 页) | +|:---:|:---:|:---:| +| 0 | 1 | 4 KB | +| 1 | 2 | 8 KB | +| 2 | 4 | 16 KB | +| ... | ... | ... | +| 10 | 1024 | 4 MB | + +"伙伴"的来历:把一个 order N 的块对半切,得到两个 order N-1 的块——它俩就是"伙伴"。反过来,一对伙伴都空闲就合并回一个 order N 的块。这就是伙伴系统**反碎片**的魔法:靠不断合并,尽量保住大块连续内存。 + +> 核心分配逻辑在 `mm/page_alloc.c`(Linux 6.19)的 `__alloc_pages()` 系列。行号待亲测核对。 + +## 一个分配请求的一生 + +假设驱动要 128KB。128KB / 4KB = 32 页,32 = 2⁵,分配器去 order 5 链表找: + +1. order 5 有货 → 直接拿走,完事。 +2. order 5 没货 → 去 order 6 找,找到一块 256KB 的,切成两个 128KB 伙伴。 +3. 一半给你,另一半挂回 order 5 留下次用。 + +释放反向走:一块释放,看它的伙伴是不是也空闲,是就合并成更高 order,一路合上去,直到伙伴不空闲或到顶。 + +## 内部碎片:132KB 的坑 + +伙伴系统只认 2 的幂。你要 **132KB**——不是 2 的幂,下一个能装下它的盒子是 order 7(256KB)。结果:申请 132KB,内核给你 256KB,**剩下 124KB 就这么浪费了**。 + +这就是"锯木头"的代价,叫**内部碎片**。内核给了 `alloc_pages_exact()` / `free_pages_exact()` 缓解(多分配一点再把多余还回去),但止不住根上的浪费。 + +经验法则:**需要接近 2 的幂的大块时,想想内部碎片;零碎小对象别找伙伴系统,那是 Slab 的活。** + +## `/proc/buddyinfo`:仓库账本 + +`/proc/buddyinfo` 是伙伴系统的库存清单,每列对应一个 order 上的空闲块数量: + +``` +Node 0, zone DMA 3 2 4 3 3 1 0 0 1 1 3 +Node 0, zone DMA32 31306 10918 1373 942 505 196 48 16 4 0 0 +Node 0, zone Normal 49135 7455 1917 535 237 89 19 3 0 0 0 +``` + +从左到右是 order 0 到 10。order 10 那列要是 0,说明系统里已经找不到一块 4MB 连续物理内存了——哪怕总空闲内存还很多,因为它们碎成了小块。这是伙伴系统最头疼的**外部碎片**。 + +> ⚠️ **待亲测**:上面这段输出是整理时的参考样例。我们会拿到 QEMU ARM64 上 `cat /proc/buddyinfo` 跑一遍记下真实输出,再写个模块用 `alloc_pages` 分几页、观察对应 order 的数字变化——把"分配请求的一生"亲眼看到。 + +## 页面分配器 API 速查 + +这些 API 名字里都带 `page` 或 `free_page`: + +| API | 功能 | 返回 | +|:---|:---|:---| +| `__get_free_page(gfp)` | 分配 1 页 | 内核逻辑地址 | +| `__get_free_pages(gfp, order)` | 分配 2^order 页 | 内核逻辑地址 | +| `get_zeroed_page(gfp)` | 分配 1 页并清零 | 内核逻辑地址 | +| `alloc_page(gfp)` | 分配 1 页 | `struct page *` | +| `alloc_pages(gfp, order)` | 分配 2^order 页 | `struct page *` | + +**关键区别**:`__get_free_page` 返回**地址**(直接能用),`alloc_page` 返回**页描述符** `struct page *`(要 `page_address()` 换成地址)。释放一定配对:`alloc_page` 拿的就用 `__free_pages` 还,别把 `page` 指针当地址塞给 `free_pages`——经典翻车点。 + +## GFP 标志:告诉内核你的底线 + +每个分配函数都有 `gfp_mask` 参数(`GFP_KERNEL`、`GFP_ATOMIC` 等),这是你跟内核签的"生死契约": + +- **`GFP_KERNEL`**:你在**进程上下文**(模块 init、系统调用实现),**允许睡眠**。内存不够内核可以去回收、甚至做 I/O,你等着。 +- **`GFP_ATOMIC`**:你在**原子上下文**(中断处理 ISR、持自旋锁),**绝对不能睡眠**。内存不够就直接失败返回,不许调度。 + +违反这条规矩直接死锁或 panic:持着自旋锁时用 `GFP_KERNEL`,内核尝试睡眠 → 调度器混乱 → 系统挂。所以**中断/持锁里只能 `GFP_ATOMIC`**。 + +## 小结 + +伙伴系统是内核物理内存管理的地基:`zone` 里挂 11 条 `free_area` 链表,按 2 的幂管理页块,靠分裂与合并对抗碎片。它只做大宗页块交易,零碎小对象交给上层 Slab(下一篇)。 + +记住两件事:**内部碎片**(非 2 的幂请求会被向上取整浪费)和 **GFP 上下文纪律**(原子上下文只能 `GFP_ATOMIC`)。 + +## 延伸阅读 + +- 源码:`mm/page_alloc.c`(Linux 6.19),伙伴系统核心;`include/linux/mmzone.h` 看 `struct zone` / `free_area`。 +- kernel.org:[Memory Management guide](https://docs.kernel.org/admin-guide/mm/index.html)、[mm 页分配器](https://docs.kernel.org/core-api/mm-api.html)。 +- 进一步(持续铺开):Slab 分配器、vmalloc、页面回收与 OOM。 diff --git a/document/tutorials/virtualization/index.md b/document/tutorials/virtualization/index.md new file mode 100644 index 00000000..45df0545 --- /dev/null +++ b/document/tutorials/virtualization/index.md @@ -0,0 +1,17 @@ +--- +title: 虚拟化与容器 +description: KVM、Namespaces、cgroups——虚拟化和容器的内核基础 +--- + +# 虚拟化与容器 + +> KVM、Namespaces、cgroups——理解虚拟化和容器背后的内核机制。 + +📚 **规划中**,还没开写。本站用 QEMU 跑内核,天然适合讲 KVM;容器部分会从 namespace + cgroup 的内核原语讲起。 + +## 计划 + +- **KVM**:虚拟化扩展、vCPU、影子页表/EPT +- **Namespaces**:pid/net/mnt/uts/ipc/user +- **cgroups**:v1 vs v2、CPU/内存/IO 子系统 +- **容器运行时**:runc、OCI 规范与内核的接口 diff --git a/scripts/build.ts b/scripts/build.ts index 9e362183..111a5f22 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -29,6 +29,9 @@ const VOLUMES: Volume[] = [ { name: 'embedded', srcDir: 'tutorials/embedded', urlPrefix: '/tutorials/embedded' }, { name: 'debugging', srcDir: 'tutorials/debugging', urlPrefix: '/tutorials/debugging' }, { name: 'virtualization', srcDir: 'tutorials/virtualization', urlPrefix: '/tutorials/virtualization' }, + { name: 'guides', srcDir: 'guides', urlPrefix: '/guides' }, + { name: 'changelogs', srcDir: 'changelogs', urlPrefix: '/changelogs' }, + { name: 'roadmap', srcDir: 'roadmap', urlPrefix: '/roadmap' }, { name: 'notes', srcDir: 'notes', urlPrefix: '/notes' }, { name: 'blog', srcDir: 'blog', urlPrefix: '/blog' }, ] @@ -434,6 +437,101 @@ function unifyCrossVolumeData(distDir: string) { log(` Patched ${patched} files with unified data`) } +// ── Post-build Issue Check ────────────────────────────────── + +/** Read `base` from site config without importing shared.ts (which pulls vitepress). */ +function readBase(): string { + try { + const shared = readFileSync(join(SITE_DIR, '.vitepress', 'config', 'shared.ts'), 'utf-8') + const m = shared.match(/base:\s*['"]([^'"]+)['"]/) + return m ? m[1] : '/' + } catch { return '/' } +} + +/** Resolve a markdown link target (relative to its .md file) to a site URL with base prefix. */ +function resolveLinkUrl(mdDir: string, target: string, base: string): string | null { + let t = target.replace(/\s+['"].*$/, '').replace(/[?#].*$/, '') + if (!t || /^(https?:|mailto:|tel:)/.test(t)) return null + if (t.startsWith('/')) return (base + t.replace(/^\/+/, '')).replace(/\.md$/, '') + const parts: string[] = [] + for (const seg of (mdDir + '/' + t).split('/')) { + if (seg === '' || seg === '.') continue + if (seg === '..') { parts.pop(); continue } + parts.push(seg) + } + return (base + parts.join('/')).replace(/\.md$/, '') +} + +/** + * Unified post-build check: scan markdown sources for dead links. + * Per-volume builds set ignoreDeadLinks so cross-volume links don't cause false + * positives; this checks every markdown link against the full published page set + * (all volumes unified in dist) — focusing on content links authors write, not + * structural ones (favicon/nav/assets/missing translations) that are separate debt. + */ +function checkSiteIssues(distDir: string): void { + logStep('Post-build check: dead links in markdown') + const base = readBase() + + // 1. Collect published pages → URL set (only .html pages, not assets) + const pages = new Set() + function collect(d: string) { + let entries: string[] + try { entries = readdirSync(d) } catch { return } + for (const name of entries) { + if (name.startsWith('.')) continue + const full = join(d, name) + if (statSync(full).isDirectory()) { collect(full); continue } + const rel = relative(distDir, full).replace(/\\/g, '/') + let url: string + if (rel === 'index.html') url = base + else if (rel.endsWith('/index.html')) url = base + rel.replace(/\/index\.html$/, '/') + else if (rel.endsWith('.html')) url = base + rel.replace(/\.html$/, '') + else url = base + rel + pages.add(url) + } + } + collect(distDir) + log(` ${pages.size} addressable URLs (pages + assets)`) + + // 2. Scan document/**/*.md for markdown links + const mdFiles: string[] = [] + ;(function walk(d: string) { + for (const e of readdirSync(d, { withFileTypes: true })) { + const full = join(d, e.name) + if (e.isDirectory()) walk(full) + else if (e.name.endsWith('.md')) mdFiles.push(full) + } + })(DOCUMENTS) + + const linkRe = /\[[^\]]*\]\(([^)]+)\)/g + const broken: Array<{ from: string; link: string }> = [] + for (const md of mdFiles) { + const content = readFileSync(md, 'utf-8') + const mdDir = '/' + relative(DOCUMENTS, md).replace(/\\/g, '/').replace(/\/[^/]*$/, '') + for (const m of content.matchAll(linkRe)) { + const url = resolveLinkUrl(mdDir, m[1].trim(), base) + if (!url) continue + // Skip asset links (images/fonts/files): vitepress hashes & rewrites them + // at build time, so the source path never matches the dist path. Vitepress + // itself errors on truly missing images, so we focus on page links here. + if (/\.(webp|png|jpe?g|gif|svg|bmp|ico|css|js|woff2?|ttf|otf|pdf|zip|tar|gz|mp4|webm)$/i.test(url)) continue + const candidates = [url, url.replace(/\/$/, ''), url + '/'] + if (!candidates.some((c) => pages.has(c))) { + broken.push({ from: relative(DOCUMENTS, md), link: m[1] }) + } + } + } + + log(` scanned ${mdFiles.length} markdown files, ${broken.length} dead link(s)`) + if (broken.length) { + for (const b of broken.slice(0, 50)) log(` ${b.from} → ${b.link}`) + if (broken.length > 50) log(` ... and ${broken.length - 50} more`) + throw new Error(`Post-build check failed: ${broken.length} dead link(s)`) + } + log(` ✓ No dead links in markdown`) +} + // ── Search Index Merge ────────────────────────────────────── function findSearchIndexFiles(dir: string): Map<'root' | 'en', string> { @@ -711,6 +809,9 @@ async function main() { // ── Step 3.5: Unify hash maps and site data ───────────── unifyCrossVolumeData(DIST_FINAL) + // ── Step 3.6: Post-build issue check (dead links, etc.) ─ + checkSiteIssues(DIST_FINAL) + // ── Step 4: Finalize ──────────────────────────────────── logStep('Step 4/4: Finalizing') rmSync(BUILD_TMP, { recursive: true }) diff --git a/site/.vitepress/config/build-info.ts b/site/.vitepress/config/build-info.ts new file mode 100644 index 00000000..295ff48f --- /dev/null +++ b/site/.vitepress/config/build-info.ts @@ -0,0 +1,29 @@ +import { execFileSync } from 'node:child_process' + +// VitePress config 在 Node 运行,CI 已 fetch-depth: 0,可零依赖拿到 git 信号。 +// 版本展示的唯一真相源是 git tag(package.json version 是停滞占位值,不可用)。 + +function git(args: string[]): string { + try { + return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() + } catch { + return '' // 非 git 仓库 / 无 tag → 回退 + } +} + +export interface BuildInfo { + /** git describe 结果,如 v0.1.0 或 v0.1.0-3-gabc1234(-dirty 表示有未提交改动) */ + version: string + /** 7 位短 SHA */ + sha: string + /** 构建日期 YYYY-MM-DD */ + date: string +} + +export function getBuildInfo(): BuildInfo { + return { + version: git(['describe', '--tags', '--always', '--dirty']) || 'dev', + sha: git(['rev-parse', '--short=7', 'HEAD']), + date: new Date().toISOString().substring(0, 10), + } +} diff --git a/site/.vitepress/config/index.ts b/site/.vitepress/config/index.ts index 2ed598d0..f58fde4e 100644 --- a/site/.vitepress/config/index.ts +++ b/site/.vitepress/config/index.ts @@ -2,6 +2,9 @@ import { defineConfig } from 'vitepress' import { sharedBase, sharedThemeConfig, sharedEnThemeConfig } from './shared' import { navZh, navEn } from './nav' import { buildSidebar } from './sidebar' +import { getBuildInfo } from './build-info' + +const buildInfo = getBuildInfo() export default defineConfig({ ...sharedBase, @@ -45,7 +48,7 @@ export default defineConfig({ text: '在 GitHub 上编辑此页', }, footer: { - message: '基于 VitePress 构建', + message: `${buildInfo.version} · ${buildInfo.sha} · ${buildInfo.date}`, copyright: `Copyright ${new Date().getFullYear()} Charliechen`, }, socialLinks: [ diff --git a/site/.vitepress/config/nav.ts b/site/.vitepress/config/nav.ts index ff6421fa..824575fb 100644 --- a/site/.vitepress/config/nav.ts +++ b/site/.vitepress/config/nav.ts @@ -2,6 +2,7 @@ import type { DefaultTheme } from 'vitepress' export const navZh: DefaultTheme.NavItem[] = [ { text: '首页', link: '/' }, + { text: '路线图', link: '/roadmap/' }, { text: '教程', items: [ @@ -13,6 +14,8 @@ export const navZh: DefaultTheme.NavItem[] = [ { text: '虚拟化与容器', link: '/tutorials/virtualization/' }, ], }, + { text: '项目指南', link: '/guides/' }, + { text: '更新日志', link: '/changelogs/' }, { text: '参考', items: [ From dbf9e3d15de9eff842553c0c2cd436223e076e30 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Mon, 22 Jun 2026 22:50:04 +0800 Subject: [PATCH 2/6] update: migrate main features from TAMCPP and refacter the lab dev --- .github/workflows/build-examples.yml | 75 ++++++++++ .../tutorials/foundations/busybox-rootfs.md | 4 +- .../tutorials/foundations/gdb-debug-setup.md | 2 +- .../foundations/kernel-mini-config.md | 4 +- .../tutorials/foundations/qemu-first-boot.md | 2 +- .../foundations/wsl2-env-toolchain.md | 2 +- .../build_examples.cpython-314.pyc | Bin 0 -> 7539 bytes scripts/build_examples.py | 128 ++++++++++++++++++ scripts/qemu-run.sh | 21 +++ scripts/site-dev.sh | 4 +- scripts/site-serve.sh | 2 +- site/.vitepress/config/sidebar.ts | 1 + 12 files changed, 235 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/build-examples.yml create mode 100644 scripts/__pycache__/build_examples.cpython-314.pyc create mode 100644 scripts/build_examples.py diff --git a/.github/workflows/build-examples.yml b/.github/workflows/build-examples.yml new file mode 100644 index 00000000..4424a752 --- /dev/null +++ b/.github/workflows/build-examples.yml @@ -0,0 +1,75 @@ +name: Build Examples + +# example/mini 多架构内核模块编译 smoke。 +# +# 核心约束:内核模块编译(make -C $(KDIR) M=... modules)需要一份预编过的内核树 +# (out/build_latest_,含 scripts/ + Module.symvers 骨架)。所以每个架构先跑 +# `modules_prepare`(只准备编外部模块的最小骨架,比全量编内核轻得多——几分钟 vs 15+ 分钟), +# 缓存这份内核树,再编 example/mini 下的模块。 +# +# 第一版矩阵:arm64 + riscv(两个交叉编译架构,机制一致)。 +# - arm32 暂缓:Makefile.arch 写的 arm-none-linux-gnueabihf- 在 Ubuntu apt 里没对应包 +# (apt 是 gcc-arm-linux-gnueabihf),要 CROSS_COMPILE 覆盖,二期再补。 +# - x86_64 暂缓:Makefile.arch 让 KDIR 默认指 /lib/modules/$(uname -r)/build(runner 宿主 +# 内核,与 6.19.9 源码 vermagic 不匹配),要改成自编树,二期再补。 + +on: + push: + branches: [main] + paths: + - 'example/mini/**' + - 'example/common/**' + - 'scripts/build_examples.py' + - '.github/workflows/build-examples.yml' + pull_request: + paths: + - 'example/mini/**' + - 'example/common/**' + - 'scripts/build_examples.py' + - '.github/workflows/build-examples.yml' + workflow_dispatch: # 手动触发(全架构验证,不受 paths 限制) + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - { arch: arm64, cc: aarch64-linux-gnu-, apt: gcc-aarch64-linux-gnu } + - { arch: riscv, cc: riscv64-linux-gnu-, apt: gcc-riscv64-linux-gnu } + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Init linux submodule + run: git submodule update --init third_party/linux + + - name: Resolve linux submodule SHA + run: echo "LINUX_SHA=$(git ls-tree HEAD third_party/linux | awk '{print $3}')" >> $GITHUB_ENV + + - name: Install toolchain (${{ matrix.arch }}) + run: | + sudo apt-get update + sudo apt-get install -y ${{ matrix.apt }} build-essential bc ccache file + + - name: Cache kernel tree (out/build_latest_${{ matrix.arch }}) + id: kcache + uses: actions/cache@v4 + with: + path: out/build_latest_${{ matrix.arch }} + key: kernel-${{ matrix.arch }}-${{ env.LINUX_SHA }}-modules_prepare + + - name: Prepare kernel tree (modules_prepare) + if: steps.kcache.outputs.cache-hit != 'true' + run: | + KOUT="$PWD/out/build_latest_${{ matrix.arch }}" + mkdir -p "$KOUT" + cd third_party/linux + make ARCH=${{ matrix.arch }} CROSS_COMPILE=${{ matrix.cc }} O="$KOUT" defconfig + make ARCH=${{ matrix.arch }} CROSS_COMPILE=${{ matrix.cc }} O="$KOUT" -j$(nproc) modules_prepare + echo "::notice::modules_prepare 产物大小: $(du -sh "$KOUT" | cut -f1)" + + - name: Build example/mini modules (${{ matrix.arch }}) + run: python3 scripts/build_examples.py ${{ matrix.arch }} ${{ matrix.cc }} diff --git a/document/en/tutorials/foundations/busybox-rootfs.md b/document/en/tutorials/foundations/busybox-rootfs.md index 8079cc5f..1cc3158e 100644 --- a/document/en/tutorials/foundations/busybox-rootfs.md +++ b/document/en/tutorials/foundations/busybox-rootfs.md @@ -15,7 +15,7 @@ Our rootfs doesn't need to be a fully functional Linux distribution. It only nee ### Building the rootfs -The project provides an automation script, [rootfs-minimal-maker.sh](scripts/rootfs-minimal-maker.sh), to handle the entire rootfs build process. Its core tasks include: compiling BusyBox (statically linked), creating the rootfs directory structure (`bin/`, `sbin/`, `usr/`, `proc/`, `sys/`, `dev/`, etc.), installing BusyBox's symbolic links, and generating the `/init` boot script. Run it like this: +The project provides an automation script, [rootfs-minimal-maker.sh](https://github.com/Awesome-Embedded-Learning-Studio/PenguinLab/blob/main/scripts/rootfs-minimal-maker.sh), to handle the entire rootfs build process. Its core tasks include: compiling BusyBox (statically linked), creating the rootfs directory structure (`bin/`, `sbin/`, `usr/`, `proc/`, `sys/`, `dev/`, etc.), installing BusyBox's symbolic links, and generating the `/init` boot script. Run it like this: ```bash ARCH=aarch64 ./scripts/rootfs-minimal-maker.sh defconfig @@ -68,5 +68,5 @@ Why static linking? When compiling BusyBox, we chose static linking (`CONFIG_STA ## Further Reading -- [rootfs-minimal-maker.sh](scripts/rootfs-minimal-maker.sh) — The rootfs build script; the `setup_rootfs()` function in particular shows which directories and files a minimal rootfs needs +- [rootfs-minimal-maker.sh](https://github.com/Awesome-Embedded-Learning-Studio/PenguinLab/blob/main/scripts/rootfs-minimal-maker.sh) — The rootfs build script; the `setup_rootfs()` function in particular shows which directories and files a minimal rootfs needs - [BusyBox Official Website](https://busybox.net/) — The BusyBox project homepage and documentation diff --git a/document/en/tutorials/foundations/gdb-debug-setup.md b/document/en/tutorials/foundations/gdb-debug-setup.md index 15e3624d..d7937810 100644 --- a/document/en/tutorials/foundations/gdb-debug-setup.md +++ b/document/en/tutorials/foundations/gdb-debug-setup.md @@ -8,7 +8,7 @@ In this post, we set up the foundational infrastructure for kernel debugging — By default, when QEMU launches a kernel, it runs straight through without pausing anywhere to wait for a debugger connection. To give GDB a chance to intervene, we need two startup parameters: `-s` is shorthand for `-gdb tcp::1234`, which tells QEMU to listen on port 1234 for the GDB remote debugging protocol; `-S` freezes the CPU immediately after launch, preventing it from executing the first instruction until GDB sends the `continue` command to continue. -Our [qemu-run.sh](scripts/qemu-run.sh) script adds a `debug` command for this, automatically appending `-s -S` when building the QEMU command: +Our [qemu-run.sh](https://github.com/Awesome-Embedded-Learning-Studio/PenguinLab/blob/main/scripts/qemu-run.sh) script adds a `debug` command for this, automatically appending `-s -S` when building the QEMU command: ```bash ./scripts/qemu-run.sh debug diff --git a/document/en/tutorials/foundations/kernel-mini-config.md b/document/en/tutorials/foundations/kernel-mini-config.md index 9c0b0361..aab0d32d 100644 --- a/document/en/tutorials/foundations/kernel-mini-config.md +++ b/document/en/tutorials/foundations/kernel-mini-config.md @@ -43,7 +43,7 @@ After these three steps, the final `.config` inflates from a few dozen options t ### Design Philosophy of the Config Fragment -Our prepared fragment file is [configs/arm64-qemu-virt-learn.config](configs/arm64-qemu-virt-learn.config). It's organized by functional category, with each group annotated with comments explaining "why we need these options." Of course, you might wonder what all these things are. If you're not entirely sure — a great approach is to use a recursive descent method to look up the related concepts. Let's quickly run through what these are: +Our prepared fragment file is [configs/arm64-qemu-virt-learn.config](https://github.com/Awesome-Embedded-Learning-Studio/PenguinLab/blob/main/configs/arm64-qemu-virt-learn.config). It's organized by functional category, with each group annotated with comments explaining "why we need these options." Of course, you might wonder what all these things are. If you're not entirely sure — a great approach is to use a recursive descent method to look up the related concepts. Let's quickly run through what these are: First, the platform basics. The ARM64 architecture itself is mandatory, and SMP (Symmetric Multiprocessing) must be enabled since our QEMU launch will configure two CPU cores: @@ -114,4 +114,4 @@ Running `make O=... Image` directly from the project root `PenguinLab/` will thr - [Kbuild Kernel Build System Documentation](https://www.kernel.org/doc/html/latest/kbuild/kbuild.html) — Official definitions for variables like `ARCH`, `CROSS_COMPILE`, and `O=` - [Linux Kernel Build Instructions](https://docs.kernel.org/admin-guide/README.html) — The official kernel.org getting-started guide for building the kernel -- [configs/arm64-qemu-virt-learn.config](configs/arm64-qemu-virt-learn.config) — Our mini config fragment file, with comments on every config option +- [configs/arm64-qemu-virt-learn.config](https://github.com/Awesome-Embedded-Learning-Studio/PenguinLab/blob/main/configs/arm64-qemu-virt-learn.config) — Our mini config fragment file, with comments on every config option diff --git a/document/en/tutorials/foundations/qemu-first-boot.md b/document/en/tutorials/foundations/qemu-first-boot.md index 0223ec6a..fb6a898e 100644 --- a/document/en/tutorials/foundations/qemu-first-boot.md +++ b/document/en/tutorials/foundations/qemu-first-boot.md @@ -12,7 +12,7 @@ You can use `qemu-system-aarch64 -M help` to see all ARM64 machine types support ### Boot Command -The project provides a [qemu-run.sh](scripts/qemu-run.sh) script to simplify QEMU's boot parameters. It automatically detects the kernel image and rootfs file in the build output directory. Running it is straightforward: +The project provides a [qemu-run.sh](https://github.com/Awesome-Embedded-Learning-Studio/PenguinLab/blob/main/scripts/qemu-run.sh) script to simplify QEMU's boot parameters. It automatically detects the kernel image and rootfs file in the build output directory. Running it is straightforward: ```bash ./scripts/qemu-run.sh run diff --git a/document/en/tutorials/foundations/wsl2-env-toolchain.md b/document/en/tutorials/foundations/wsl2-env-toolchain.md index adc6bcc6..682e6208 100644 --- a/document/en/tutorials/foundations/wsl2-env-toolchain.md +++ b/document/en/tutorials/foundations/wsl2-env-toolchain.md @@ -125,7 +125,7 @@ CROSS_COMPILE - 指定 binutils 文件名的可选固定部分。CROSS_COMPILE 可以是文件名的一部分或完整路径。在某些设置中,CROSS_COMPILE 也用于 ccache。 ``` -There's an easy pitfall to fall into here—the value of `ARCH` doesn't always have a one-to-one correspondence with the toolchain prefix naming. For ARM64, the architecture name used by the kernel build system is `arm64`, but the toolchain prefix is `aarch64-linux-gnu-`; they are different. This is due to historical reasons. The ARM64 architecture has always been called `arm64` in the kernel community, while the toolchain side uses the `aarch64` name officially defined by ARM. The project's [linux-action-scripts.sh](scripts/linux-action-scripts.sh) lines 69–72 specifically handle this mapping, converting user-provided `aarch64` or `arm64` into the `arm64` that Kbuild recognizes. +There's an easy pitfall to fall into here—the value of `ARCH` doesn't always have a one-to-one correspondence with the toolchain prefix naming. For ARM64, the architecture name used by the kernel build system is `arm64`, but the toolchain prefix is `aarch64-linux-gnu-`; they are different. This is due to historical reasons. The ARM64 architecture has always been called `arm64` in the kernel community, while the toolchain side uses the `aarch64` name officially defined by ARM. The project's [linux-action-scripts.sh](https://github.com/Awesome-Embedded-Learning-Studio/PenguinLab/blob/main/scripts/linux-action-scripts.sh) lines 69–72 specifically handle this mapping, converting user-provided `aarch64` or `arm64` into the `arm64` that Kbuild recognizes. `CROSS_COMPILE` is even more interesting—it's not a complete command name, but a prefix. The kernel build system automatically appends suffixes like `gcc`, `ld`, and `objcopy` after this prefix to find the corresponding tools. In other words, when you set `CROSS_COMPILE=aarch64-linux-gnu-`, make will look for `aarch64-linux-gnu-gcc`, `aarch64-linux-gnu-ld`, `aarch64-linux-gnu-objcopy`, and a whole series of other tools. If `CROSS_COMPILE` is an empty string, make will directly use the local machine's `gcc`, `ld`, etc.—this is exactly why, when you forget to set this variable, the kernel gets compiled for the x86 architecture. The empty string prefix causes the build system to use the host's compiler. diff --git a/scripts/__pycache__/build_examples.cpython-314.pyc b/scripts/__pycache__/build_examples.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6ec5541dded4f23e69558602ce7eb872e16be89 GIT binary patch literal 7539 zcmbVR4R8}jmhO>8|FR_iU>j`XaWKXLBV%lg5nv)@{1byw21ofbR)j1Zfh>s`32av` zE7)a`mn0SoNn~(>sA4xJ;exrl3XuQo-d2*Tt6DiWu`_WMsa<44)zuY0smkT*s&cPK z8d*3#E_E$W_e}r1ey`u_*Iz%c(qt$RBysbGeHV2I{T+V@NuGSU|2~0G5YfnwIuT8@ z5PpI$MSc-qiv41~B>g0mq83TB)Guw8`DMjuH)#Q?^HT}ht*8N-I9V1Y%JjS@`C-XT zuT8u!ROeUkR@34N)m(3yO#kv{>?SOQbFIRgrC4~ID`|_t*s>enY1ns`LpOnbY>oM6aH*E3%+ycY{(bw&ZTqe zo8sM@@%*7p`5+;9-rG; z+2iuKsQCHQGjF~(^X6~jzdbq=y*e{;F8;>rvmd^8>(U8|?eXq+{ylg^sf-Vvn0;mR zfsvIfD|guUJNLTW&WiY@YqK8=#os+mnP_t}l^9+g51vhpaCm!qyq?qugPn0aPtBhG zjqEpt4K1{u+OfH`-24R1zxQ~%0&XY!oW5cvdhYhyFU?#Uym|F)%CO%{ z-F*M8*?|l3e~iS3N2pcQw(X77%<%j1p^@3kAH?509)IcD?V~RzB;zHRJvB1(&#xMl z6eM4t|9P*cnqnP{%jah+_XJ$-E?aV0Sc9*R+JyP5r=E2%UY4~1^F9#M=3_wI-rYP6 z+`&lMnV#Ajs*++{tm7b*@C|uyU9GLQhVQlz0V5Uv;}Ob^Db&`icDp=*-qqcn!0PhZ zSBGXVydMt@-Fo)|B`j`Yscs#6`*v_Z51JTwdG@1sjY(}qPf*@~KPiCQ?su|&Tax$q zCr59e`yhVhpQz+2V)lJ@##ugl>M~_+p$>VO{d?WsL)6Vr-nez?aD{Sa_+or`6i@%h z!||gbti?}`B_|5KcI(QC`IPht+qm+F*^%Sea6TNIJ^B{aZrWkCSg4;ZsxfIqtEl+3 z_imkg3A7eJfBDwG{NdK&)A4Yq+{rLrhJuEf!Ek)=H9fQ&jopkl;4>QWL~VKzm(~6} z34QGnsfjaANr8#3p(NC*if(FQr>{nO9pmX+oX-xLjRZ;}A9;E+z1Y5hCv& zftMiaz;%4GK<)e_?9r4*fj))EfR>>SOovZ2k(W+ICe+WB0u|lQ_WUxW7X>y!&xLmD zCXcJGOOV7Xcg;u~eimv(hBX|~#exS!WfZsgf8Fw&VpF%%yss2enXFGdRGR6${+|Bd>|B~@cf zswS6I|9~rhM~r9de`K{V_DH@-PMlo=*~d!A^dzU=_LOOdW&1W8y>n+flMQ`1DeGl; zi+1^)3>EPKU7w*I~tD()k(S_(o)q)ZWvcBC6EKK}9d{qK3YbEYlmwhLx1L>;Ly zG-$<=GGrK(OjaZuN@}!LB)k`+0@NWD`d9?8AyhzFf}SL-A{zh1>yvaB;38ul7v$Uw z%V29!Y1=8q`9sFIkPB@T7a_1MNxvfHfH;C`2Q`VEo`ams@9J@a+XZLh?DcaRhu!B7 zFism(`2zlawbAJGbR`{^Ucz9}IEjyOd0@mougk+J*}xtj<8?S$mJ>4pk6z4)9X(x4 z3G`$hfsDnAT2@xZ>zctP$7lHTXP<*Ch;GOfzhhvuANu& zUo6H_?<_VIq0fqznAVFQFbwRIc*BsAm=mkW0u}RJ9-adk^yjJv4JI!W+~sfP5>2W+*Lqlg~@S3h2-i(tyZ-0;0xc z;B2L|%qo2tQpQ`q8<2bPPqE@gdtF%qjx;)`FeKMcaDobqxeuPyY(QIhW-U*xP#C|^Jz&mh`IJ_rK%@*D#e^zRSWGkHKMbv>V+s+zGL@fBY!T# zOz+&a%VyrW!_u;S8(=U1_x+;PW=`U8JMA7l!MnHH6(Cu^lm~o9PO=RHKV#eN9GI%D ztucU&ioX>k;31Er`z48jTnXSmfDfz0CF6GRG=S8bAN_Xb^=R6?fFtp}q+Ks!N}(%* zmm!b^8I`yZ+`Rg7;(}nLVBTJ6XzM3;wo>K&5==zz`uCrH`bnS7kr*^_A}Gs*69bK?ELvmDS2^aW5X+VAB=dl?osERykN*$F*1!oz+HDhi@0RnB1jKz*=bTB#mfIk0lDa-ectSr9Z&W#xvXhg;zonkp^{ z?HrRAelJB5c_=rWbBc{@IQe4qP)uGqntgT8bwL%3v0 zn-{4%yMAc>nGF-#(h=Ec)de+xs|-!ZG?upkjx$9Dz!^B_5CqCtM^>* zUW}^xwDgPiiA*}Q6=XTvH`MpFwtOnTFyb5$ zU6NgpMf+lFwtl+y^9`SE_7>0sxLH7-C zvck;;)SXBVKeq)R)+wrk{gi}&GGQ!Dq>Wy{BhsSvNmNjbIz`~oABqt=iF6O;`~UEG z3V6)NKKuWeNt8a*4uTf@h&j(Kcub$jDiThDZaDLS>YU_hu(K22=;m4^?Ff9Pw4F#h zL1E=_EQywZ-%T5tHcCFHkvUWYbQD$*pabyKJURd_4pc9MP~gl5H;O$ucxds1vlx(% z?7;fx(?aufq|S8<_L}q3T(s%u5_J+aNuBy69jVx!RYEJ*CoOCvVIu;ffY_G0ECMD; z%2bIF4|wbYwF@DG4$x#G!NovaLnhv>;)UejBWeX#i{%H*Bko9{g${gl@X4Br&du=* z=lU}U9RdlM0jpvmTp-xOA!r%B^4JB_oH#R4}u3)QF9X$3wj_(*etHyF2Qp*|p& zROk=2Jd)ljZ9i@hnGF_KS8?3lO?(Vyi1D9iJZNF9Rq%rBV3#q zUJ`7Gy!(L_wi_c_WfvF4g_wJiqN)jX*f5Qw_-#zS>70fKSN_| zMN>@KJVCZhLwnOHbA&wE65bebUuzg&WxCcddf-yy#l~w5W2;QDm0M#<^90%OokkaI zn$~2VS{Cj)sUN1#cAV}w(>YnR1_0B@fpJY0v?>ZlWRr?D*A;6|R&&+< zy5fXlSRZYNUb-s4UbUBNztU~^tR_Upw9V7G#pl%{?c>ziQPbDCwF8YI(^O&c*~f++ zJF|UYN2np}m{MmCwhgpJ8lsNKmI?K$8-+_xtcz|7HQ?}tDQpk#iV(xgB3)d=ys@r>0&d12E?$tCgvIcN{r zhw_4rv5eJtHJVTDisYT_2)9M`7tN!xcea06ef8i68?RS1##S|b+BfkyK(S}XAMc1+ zcE^;R6Qu3i{G|ix5E-(EvvDLw8L5esMMcr-=<=AR{A;=Xt{5#Yp4H`ss$yhO#01%o z_exO3R^rEdN~CKc0JtivVAcNno(^dYY!%qYpO@sctkeAK7D>w%$zRBoEk^lY45oGP z@p+~0u`KcnQVRK(M%?`67S&@~1t+)Ju!Y%d;8H4q0$jypHd~k1VYBi3hOt6r;!Yv+ z+5OMEU3<7Jhu7l>FpSgVH-L#l?26_0RATRt7U+iQ;wzNR#%~g@`ygCz@P7|*5Q~pd z;>$Qwi!&3>Fxq1t#TmbO`Ir^HOz@_Uso|>>P$MBS>u2!Yn-4iAqnVo$|D&j94nhM4 z3hZAYyCWtD;x4Hn$ooqXq5KxXU*@++a|3PpPNq5b!jTuk&EvA7N!ju-+46DOibMz%SS~_0c^hNDh_2WU&9XXPyLr+hEZLFIjRiUS*NOkD>I|_+b6ja^Q z$q4E9IZEP@drL})HTRl{B4X*CM+pRd`X|JTr%2hc<|EAm?Z>TOlR01t [cross_compile_prefix] + arch: arm64 / riscv / arm / x86_64 + cross_compile_prefix: 如 aarch64-linux-gnu-(留空则让 Makefile.arch 自选) + +前置:内核树 out/build_latest_ 必须已 modules_prepare(由 CI workflow 保证, +本地则要先编过内核或跑 modules_prepare)。 + +工程模式照 TAMCPP build_examples.py:发现 + 并行 + 失败聚合(error 行提取) + ::group::。 +""" +import os +import sys +import subprocess +import pathlib +from concurrent.futures import ThreadPoolExecutor, as_completed + +ROOT = pathlib.Path(__file__).resolve().parent.parent +EXAMPLE_ROOT = ROOT / 'example' / 'mini' +CI = os.environ.get('GITHUB_ACTIONS') == 'true' or os.environ.get('CI') == 'true' + +# ARCH → `file` 输出里应出现的 ELF Machine 关键字 +ELF_MACHINE = { + 'arm64': 'AArch64', + 'arm': 'ARM', + 'riscv': 'RISC-V', + 'x86_64': 'x86-64', +} + + +def discover(): + """发现 example/mini//Makefile(单层,排除 common)。""" + if not EXAMPLE_ROOT.exists(): + return [] + return sorted( + d for d in EXAMPLE_ROOT.iterdir() + if d.is_dir() and (d / 'Makefile').exists() + ) + + +def run(cmd, env, timeout=300): + if CI: + print(f"::group::{' '.join(cmd)}") + try: + return subprocess.run( + cmd, env=env, timeout=timeout, + text=True, capture_output=True, + ) + finally: + if CI: + print("::endgroup::") + + +def build_one(arch, cc, directory): + name = directory.name + env = os.environ.copy() + env['ARCH'] = arch + if cc: + env['CROSS_COMPILE'] = cc + + # 先 clean(清掉上次的 .ko/.mod 等) + run(['make', '-C', str(directory), 'clean'], env, timeout=60) + + # 编译:Makefile.arch 的 all 目标会 make -C $(KDIR) M=$(CURDIR) modules + r = run(['make', '-C', str(directory)], env, timeout=300) + if r.returncode != 0: + errs = [ + l for l in (r.stderr + r.stdout).splitlines() + if 'error:' in l.lower() or 'Error' in l + ] + tail = errs[:20] if errs else (r.stderr or r.stdout).splitlines()[-20:] + return (name, False, 'make 失败\n ' + '\n '.join(tail)) + + # 校验 .ko 产物 + kos = list(directory.glob('*.ko')) + if not kos: + return (name, False, '编译通过但无 .ko 产物') + + # 校验 ELF 机器类型匹配架构(需 file 命令) + expected = ELF_MACHINE.get(arch) + if expected: + for ko in kos: + fr = subprocess.run(['file', str(ko)], capture_output=True, text=True) + if expected not in fr.stdout: + return (name, False, f'{ko.name} 架构不匹配: {fr.stdout.strip()[:120]}') + + return (name, True, f'OK ({len(kos)} .ko)') + + +def main(): + if len(sys.argv) < 2: + print('用法: build_examples.py [cross_compile_prefix]', file=sys.stderr) + return 2 + arch = sys.argv[1] + cc = sys.argv[2] if len(sys.argv) > 2 else '' + + dirs = discover() + if not dirs: + print('未发现 example/mini/*/Makefile(当前无示例可 smoke)') + return 0 + + print(f'ARCH={arch} CROSS_COMPILE={cc or "(Makefile.arch 自选)"}') + print(f'发现 {len(dirs)} 个示例: {[d.name for d in dirs]}') + + results = [] + workers = min(len(dirs), os.cpu_count() or 1) + with ThreadPoolExecutor(max_workers=workers) as ex: + futs = {ex.submit(build_one, arch, cc, d): d.name for d in dirs} + for i, fut in enumerate(as_completed(futs), 1): + name, ok, msg = fut.result() + results.append((name, ok, msg)) + mark = '✓' if ok else '✗' + print(f'[{i}/{len(dirs)}] {mark} {name} — {msg}') + + failed = [r for r in results if not r[1]] + print(f'\nTotal {len(results)}, Passed {len(results) - len(failed)}, Failed {len(failed)}') + for name, _, msg in failed: + print(f' FAILED: {name}: {msg}') + return 1 if failed else 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/qemu-run.sh b/scripts/qemu-run.sh index e774bc36..a2f59922 100755 --- a/scripts/qemu-run.sh +++ b/scripts/qemu-run.sh @@ -97,6 +97,12 @@ INITRD="${INITRD:-}" # Extra options QEMU_EXTRA_OPTS="${QEMU_EXTRA_OPTS:-}" +# 9p shared directory (virtio-9p) +: "${QEMU_9P:=off}" +: "${QEMU_9P_PATH:=}" +: "${QEMU_9P_TAG:=hostshare}" +: "${QEMU_9P_SEC:=none}" + # PID file for tracking running instances PID_DIR="${PROJECT_ROOT}/out/qemu" PID_FILE="${PID_DIR}/qemu.pid" @@ -140,6 +146,12 @@ Devices & Networking: QEMU_TAP_IF - ${QEMU_TAP_IF} (TAP interface name) QEMU_MAC - (MAC address for network) +Shared Directory (9p): + QEMU_9P - ${QEMU_9P} (on, off: 9p shared dir) + QEMU_9P_PATH - (host path to share, required if QEMU_9P=on) + QEMU_9P_TAG - ${QEMU_9P_TAG} (mount tag for guest) + QEMU_9P_SEC - ${QEMU_9P_SEC} (security_model: none/mapped-xattr/passthrough) + Build: BUILD_OUTPUT_BASE - ${BUILD_OUTPUT_BASE} @@ -159,6 +171,9 @@ Examples: # Run with networking enabled QEMU_NET=on $(basename "$0") run + # Run with a 9p shared dir (guest: mount -t 9p -o trans=virtio hostshare /mnt) + QEMU_9P=on QEMU_9P_PATH=/path/to/share $(basename "$0") run + # Stop running QEMU instances $(basename "$0") stop @@ -412,6 +427,12 @@ build_qemu_command() { # Enable semihosting (useful for bare-metal testing) # cmd+=" -semihosting" + # 9p shared directory (virtio-9p). Mount in guest with: + # mount -t 9p -o trans=virtio,version=9p2000.L ${QEMU_9P_TAG} /mnt/... + if [[ "${QEMU_9P}" == "on" && -n "${QEMU_9P_PATH}" ]]; then + cmd+=" -virtfs local,path=${QEMU_9P_PATH},mount_tag=${QEMU_9P_TAG},security_model=${QEMU_9P_SEC},id=fs9p" + fi + # Extra options if [[ -n "${QEMU_EXTRA_OPTS}" ]]; then cmd+=" ${QEMU_EXTRA_OPTS}" diff --git a/scripts/site-dev.sh b/scripts/site-dev.sh index 143e1021..ff8f6729 100755 --- a/scripts/site-dev.sh +++ b/scripts/site-dev.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# PenguinLab Docusaurus dev server (hot-reload) +# PenguinLab VitePress dev server (hot-reload) set -euo pipefail cd "$(git rev-parse --show-toplevel)/site" -exec pnpm start "$@" +exec pnpm dev "$@" diff --git a/scripts/site-serve.sh b/scripts/site-serve.sh index 2c2d5e72..d5de5a4c 100755 --- a/scripts/site-serve.sh +++ b/scripts/site-serve.sh @@ -4,4 +4,4 @@ set -euo pipefail cd "$(git rev-parse --show-toplevel)/site" pnpm build -exec pnpm serve "$@" +exec pnpm preview "$@" diff --git a/site/.vitepress/config/sidebar.ts b/site/.vitepress/config/sidebar.ts index d4e1b58d..b236dfff 100644 --- a/site/.vitepress/config/sidebar.ts +++ b/site/.vitepress/config/sidebar.ts @@ -113,6 +113,7 @@ export function buildSidebar(): DefaultTheme.Sidebar { '/tutorials/embedded/': volumeSidebar('tutorials/embedded', '/tutorials/embedded'), '/tutorials/debugging/': volumeSidebar('tutorials/debugging', '/tutorials/debugging'), '/tutorials/virtualization/': volumeSidebar('tutorials/virtualization', '/tutorials/virtualization'), + '/guides/': volumeSidebar('guides', '/guides'), '/notes/': volumeSidebar('notes', '/notes'), '/blog/': [ { text: '内核新闻', link: '/blog/' }, From 881433670eeb7759be0e20d7f3ba08f1e1b683df Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Mon, 22 Jun 2026 23:11:42 +0800 Subject: [PATCH 3/6] =?UTF-8?q?ci:=20example/mini=20=E5=A4=9A=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E5=86=85=E6=A0=B8=E6=A8=A1=E5=9D=97=E7=BC=96=E8=AF=91?= =?UTF-8?q?=20smoke(arm64+riscv)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - build-examples.yml: matrix arm64+riscv,先编 vmlinux 生成完整 Module.symvers (modules_prepare 不够——不编 vmlinux,缺 _printk 等导出符号,hello.ko modpost 报 "_printk undefined"),缓存内核树(key 含 linux 子模块 SHA),再 build_examples.py 编 - build_examples.py: 发现 example/mini/*/Makefile + make ARCH= 编译 + 校验 .ko(ELF 机器 类型)+ 失败聚合(error 行)+ ::group:: 折叠 - arm32(工具链 -none- 命名坑)/x86_64(KDIR 指宿主内核)二期再补 --- .github/workflows/build-examples.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-examples.yml b/.github/workflows/build-examples.yml index 4424a752..06b56626 100644 --- a/.github/workflows/build-examples.yml +++ b/.github/workflows/build-examples.yml @@ -3,9 +3,9 @@ name: Build Examples # example/mini 多架构内核模块编译 smoke。 # # 核心约束:内核模块编译(make -C $(KDIR) M=... modules)需要一份预编过的内核树 -# (out/build_latest_,含 scripts/ + Module.symvers 骨架)。所以每个架构先跑 -# `modules_prepare`(只准备编外部模块的最小骨架,比全量编内核轻得多——几分钟 vs 15+ 分钟), -# 缓存这份内核树,再编 example/mini 下的模块。 +# (out/build_latest_,含 scripts/ + 完整 Module.symvers)。所以每个架构先编 vmlinux +# (生成完整 Module.symvers,含 _printk 等内核导出符号——modules_prepare 不编 vmlinux, +# Module.symvers 缺符号,外部模块 modpost 会报 "_printk undefined"),缓存这份内核树,再编 example/mini。 # # 第一版矩阵:arm64 + riscv(两个交叉编译架构,机制一致)。 # - arm32 暂缓:Makefile.arch 写的 arm-none-linux-gnueabihf- 在 Ubuntu apt 里没对应包 @@ -59,17 +59,20 @@ jobs: uses: actions/cache@v4 with: path: out/build_latest_${{ matrix.arch }} - key: kernel-${{ matrix.arch }}-${{ env.LINUX_SHA }}-modules_prepare + key: kernel-${{ matrix.arch }}-${{ env.LINUX_SHA }}-vmlinux - - name: Prepare kernel tree (modules_prepare) + - name: Prepare kernel tree (vmlinux → Module.symvers) if: steps.kcache.outputs.cache-hit != 'true' run: | KOUT="$PWD/out/build_latest_${{ matrix.arch }}" mkdir -p "$KOUT" cd third_party/linux make ARCH=${{ matrix.arch }} CROSS_COMPILE=${{ matrix.cc }} O="$KOUT" defconfig - make ARCH=${{ matrix.arch }} CROSS_COMPILE=${{ matrix.cc }} O="$KOUT" -j$(nproc) modules_prepare - echo "::notice::modules_prepare 产物大小: $(du -sh "$KOUT" | cut -f1)" + # 编 vmlinux 生成完整 Module.symvers(含 _printk 等内核导出符号)。 + # modules_prepare 不够:它不编 vmlinux,Module.symvers 缺内核导出符号, + # 外部模块 modpost 会报 "_printk [hello.ko] undefined!"。 + make ARCH=${{ matrix.arch }} CROSS_COMPILE=${{ matrix.cc }} O="$KOUT" -j$(nproc) vmlinux + echo "::notice::vmlinux 内核树大小: $(du -sh "$KOUT" | cut -f1)" - name: Build example/mini modules (${{ matrix.arch }}) run: python3 scripts/build_examples.py ${{ matrix.arch }} ${{ matrix.cc }} From cafe1c7a18b8f6dd807e0506a42ee9e701d7aef6 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Tue, 23 Jun 2026 07:35:04 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix(ci):=20example=20smoke=20=E7=BC=96=20mo?= =?UTF-8?q?dules=20=E5=87=BA=E5=AE=8C=E6=95=B4=20Module.symvers=20+=20?= =?UTF-8?q?=E4=BF=AE=E6=9E=B6=E6=9E=84=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vmlinux/modules_prepare 都不 modpost → Module.symvers 缺 _printk → hello.ko modpost 报 "_printk undefined"。改 make modules(含 modpost vmlinux 阶段)。 第一次 CI 慢(~15min/架构),缓存后秒级。 - build_examples 架构校验改 readelf(file 对 arm64 输出 "ARM aarch64", 与大写 AArch64 匹配失败误判;本地 hello.ko 其实编通了)。 --- .github/workflows/build-examples.yml | 22 ++++++++++++---------- scripts/build_examples.py | 19 +++++++++++-------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build-examples.yml b/.github/workflows/build-examples.yml index 06b56626..7aebacb8 100644 --- a/.github/workflows/build-examples.yml +++ b/.github/workflows/build-examples.yml @@ -3,9 +3,10 @@ name: Build Examples # example/mini 多架构内核模块编译 smoke。 # # 核心约束:内核模块编译(make -C $(KDIR) M=... modules)需要一份预编过的内核树 -# (out/build_latest_,含 scripts/ + 完整 Module.symvers)。所以每个架构先编 vmlinux -# (生成完整 Module.symvers,含 _printk 等内核导出符号——modules_prepare 不编 vmlinux, -# Module.symvers 缺符号,外部模块 modpost 会报 "_printk undefined"),缓存这份内核树,再编 example/mini。 +# (out/build_latest_,含 scripts/ + 完整 Module.symvers)。所以每个架构先编 modules +# (modpost vmlinux 出完整 Module.symvers,含 _printk 等内核导出符号——modules_prepare / vmlinux +# 都不 modpost,Module.symvers 缺符号,外部模块 modpost 会报 "_printk undefined"), +# 缓存这份内核树,再编 example/mini。 # # 第一版矩阵:arm64 + riscv(两个交叉编译架构,机制一致)。 # - arm32 暂缓:Makefile.arch 写的 arm-none-linux-gnueabihf- 在 Ubuntu apt 里没对应包 @@ -59,20 +60,21 @@ jobs: uses: actions/cache@v4 with: path: out/build_latest_${{ matrix.arch }} - key: kernel-${{ matrix.arch }}-${{ env.LINUX_SHA }}-vmlinux + key: kernel-${{ matrix.arch }}-${{ env.LINUX_SHA }}-modules - - name: Prepare kernel tree (vmlinux → Module.symvers) + - name: Prepare kernel tree (modules → Module.symvers) if: steps.kcache.outputs.cache-hit != 'true' run: | KOUT="$PWD/out/build_latest_${{ matrix.arch }}" mkdir -p "$KOUT" cd third_party/linux make ARCH=${{ matrix.arch }} CROSS_COMPILE=${{ matrix.cc }} O="$KOUT" defconfig - # 编 vmlinux 生成完整 Module.symvers(含 _printk 等内核导出符号)。 - # modules_prepare 不够:它不编 vmlinux,Module.symvers 缺内核导出符号, - # 外部模块 modpost 会报 "_printk [hello.ko] undefined!"。 - make ARCH=${{ matrix.arch }} CROSS_COMPILE=${{ matrix.cc }} O="$KOUT" -j$(nproc) vmlinux - echo "::notice::vmlinux 内核树大小: $(du -sh "$KOUT" | cut -f1)" + # 编 modules 生成完整 Module.symvers(modpost vmlinux,含 _printk 等内核导出符号)。 + # modules_prepare / vmlinux 都不够:前者不编 vmlinux,后者只链接不 modpost, + # Module.symvers 缺内核导出符号,外部模块 modpost 报 "_printk [hello.ko] undefined!"。 + # 第一次慢(~15min/架构),缓存命中后(key 含 linux 子模块 SHA)秒级。 + make ARCH=${{ matrix.arch }} CROSS_COMPILE=${{ matrix.cc }} O="$KOUT" -j$(nproc) modules + echo "::notice::modules 内核树大小: $(du -sh "$KOUT" | cut -f1)" - name: Build example/mini modules (${{ matrix.arch }}) run: python3 scripts/build_examples.py ${{ matrix.arch }} ${{ matrix.cc }} diff --git a/scripts/build_examples.py b/scripts/build_examples.py index 403687c9..b3e82b99 100644 --- a/scripts/build_examples.py +++ b/scripts/build_examples.py @@ -24,11 +24,12 @@ EXAMPLE_ROOT = ROOT / 'example' / 'mini' CI = os.environ.get('GITHUB_ACTIONS') == 'true' or os.environ.get('CI') == 'true' -# ARCH → `file` 输出里应出现的 ELF Machine 关键字 +# ARCH → readelf -h 的 Machine 字段里应出现的关键字(小写匹配)。 +# 用 readelf 不用 file:file 对 arm64 输出 "ARM aarch64",容易和大写 AArch64 匹配失败。 ELF_MACHINE = { - 'arm64': 'AArch64', - 'arm': 'ARM', - 'riscv': 'RISC-V', + 'arm64': 'aarch64', + 'arm': 'arm', + 'riscv': 'risc-v', 'x86_64': 'x86-64', } @@ -81,13 +82,15 @@ def build_one(arch, cc, directory): if not kos: return (name, False, '编译通过但无 .ko 产物') - # 校验 ELF 机器类型匹配架构(需 file 命令) + # 校验 ELF 机器类型匹配架构(readelf -h 的 Machine 字段) expected = ELF_MACHINE.get(arch) if expected: for ko in kos: - fr = subprocess.run(['file', str(ko)], capture_output=True, text=True) - if expected not in fr.stdout: - return (name, False, f'{ko.name} 架构不匹配: {fr.stdout.strip()[:120]}') + rr = subprocess.run(['readelf', '-h', str(ko)], capture_output=True, text=True) + machine = [l for l in rr.stdout.splitlines() if 'Machine:' in l] + got = machine[0].lower() if machine else '' + if expected not in got: + return (name, False, f'{ko.name} 架构不匹配: {machine[0].strip() if machine else "无 Machine 字段"}') return (name, True, f'OK ({len(kos)} .ko)') From 8b8857287f2fea60ff391cddd4fb3e3a8e00c6f7 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Tue, 23 Jun 2026 08:05:39 +0800 Subject: [PATCH 5/6] =?UTF-8?q?fix(ci):=20example=20smoke=20=E7=94=A8=20al?= =?UTF-8?q?lnoconfig=20=E9=81=BF=E5=BC=80=20riscv=20kvm=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit defconfig make modules 编到 arch/riscv/kvm/kvm.ko modpost 失败(该模块符号依赖 问题,与 hello 无关)。改 allnoconfig + 开 MODULES/PRINTK: - 无 =m 配置模块 → make modules 只 modpost vmlinux,不碰 kvm - allnoconfig vmlinux 极小,编译 ~3min(vs defconfig ~15min) - hello.c 只用 _printk(pr_info)+ module 宏,allnoconfig 内核够用 --- .github/workflows/build-examples.yml | 33 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build-examples.yml b/.github/workflows/build-examples.yml index 7aebacb8..13f7f604 100644 --- a/.github/workflows/build-examples.yml +++ b/.github/workflows/build-examples.yml @@ -3,16 +3,15 @@ name: Build Examples # example/mini 多架构内核模块编译 smoke。 # # 核心约束:内核模块编译(make -C $(KDIR) M=... modules)需要一份预编过的内核树 -# (out/build_latest_,含 scripts/ + 完整 Module.symvers)。所以每个架构先编 modules -# (modpost vmlinux 出完整 Module.symvers,含 _printk 等内核导出符号——modules_prepare / vmlinux -# 都不 modpost,Module.symvers 缺符号,外部模块 modpost 会报 "_printk undefined"), -# 缓存这份内核树,再编 example/mini。 +# (out/build_latest_,含 scripts/ + 完整 Module.symvers)。 # -# 第一版矩阵:arm64 + riscv(两个交叉编译架构,机制一致)。 -# - arm32 暂缓:Makefile.arch 写的 arm-none-linux-gnueabihf- 在 Ubuntu apt 里没对应包 -# (apt 是 gcc-arm-linux-gnueabihf),要 CROSS_COMPILE 覆盖,二期再补。 -# - x86_64 暂缓:Makefile.arch 让 KDIR 默认指 /lib/modules/$(uname -r)/build(runner 宿主 -# 内核,与 6.19.9 源码 vermagic 不匹配),要改成自编树,二期再补。 +# 用 allnoconfig + 开 CONFIG_MODULES/CONFIG_PRINTK:最小内核、无 =m 配置模块, +# 所以 make modules 只 modpost vmlinux 生成 Module.symvers—— +# ① 不碰 defconfig 里 arch/riscv/kvm/kvm.ko 这种有符号依赖问题会炸的配置模块; +# ② allnoconfig vmlinux 极小,编译快(~3min vs defconfig ~15min); +# ③ hello.c 只用 _printk(pr_info)+ module 宏,allnoconfig 内核完全够。 +# +# 第一版矩阵:arm64 + riscv。arm32(工具链 -none- 命名坑)/x86_64(KDIR 指宿主内核)二期再补。 on: push: @@ -60,21 +59,21 @@ jobs: uses: actions/cache@v4 with: path: out/build_latest_${{ matrix.arch }} - key: kernel-${{ matrix.arch }}-${{ env.LINUX_SHA }}-modules + key: kernel-${{ matrix.arch }}-${{ env.LINUX_SHA }}-allnoconfig - - name: Prepare kernel tree (modules → Module.symvers) + - name: Prepare kernel tree (allnoconfig → Module.symvers) if: steps.kcache.outputs.cache-hit != 'true' run: | KOUT="$PWD/out/build_latest_${{ matrix.arch }}" mkdir -p "$KOUT" cd third_party/linux - make ARCH=${{ matrix.arch }} CROSS_COMPILE=${{ matrix.cc }} O="$KOUT" defconfig - # 编 modules 生成完整 Module.symvers(modpost vmlinux,含 _printk 等内核导出符号)。 - # modules_prepare / vmlinux 都不够:前者不编 vmlinux,后者只链接不 modpost, - # Module.symvers 缺内核导出符号,外部模块 modpost 报 "_printk [hello.ko] undefined!"。 - # 第一次慢(~15min/架构),缓存命中后(key 含 linux 子模块 SHA)秒级。 + # allnoconfig 最小内核,再开模块支持 + printk(_printk 在这),olddefconfig 解依赖。 + make ARCH=${{ matrix.arch }} CROSS_COMPILE=${{ matrix.cc }} O="$KOUT" allnoconfig + ./scripts/config --file "$KOUT/.config" --enable CONFIG_MODULES --enable CONFIG_PRINTK + make ARCH=${{ matrix.arch }} CROSS_COMPILE=${{ matrix.cc }} O="$KOUT" olddefconfig + # 无 =m 配置模块,make modules 只 modpost vmlinux → Module.symvers(含 _printk)。 make ARCH=${{ matrix.arch }} CROSS_COMPILE=${{ matrix.cc }} O="$KOUT" -j$(nproc) modules - echo "::notice::modules 内核树大小: $(du -sh "$KOUT" | cut -f1)" + echo "::notice::allnoconfig 内核树大小: $(du -sh "$KOUT" | cut -f1)" - name: Build example/mini modules (${{ matrix.arch }}) run: python3 scripts/build_examples.py ${{ matrix.arch }} ${{ matrix.cc }} From 9603916ee613ce4e540d230925be429ff0547fa3 Mon Sep 17 00:00:00 2001 From: Charliechen114514 <725610365@qq.com> Date: Tue, 23 Jun 2026 08:22:00 +0800 Subject: [PATCH 6/6] =?UTF-8?q?fix(ci):=20example=20smoke=20=E7=94=A8=20ma?= =?UTF-8?q?ke=20=E9=BB=98=E8=AE=A4=E7=9B=AE=E6=A0=87=20modpost=20vmlinux?= =?UTF-8?q?=20=E5=87=BA=20Module.symvers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit make modules 时 KBUILD_BUILTIN 为空 → 不 modpost vmlinux → Module.symvers 缺 _printk → hello.ko modpost 报 undefined(defconfig/allnoconfig 都一样)。 改用 make(默认,KBUILD_BUILTIN=1):modpost vmlinux 出完整 Module.symvers。 allnoconfig + MODULES + PRINTK,本地验证 Module.symvers 含 _printk(EXPORT_SYMBOL)。 无 =m 配置模块,不碰 riscv kvm,又快(allnoconfig vmlinux 小)又干净。 --- .github/workflows/build-examples.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-examples.yml b/.github/workflows/build-examples.yml index 13f7f604..834fec27 100644 --- a/.github/workflows/build-examples.yml +++ b/.github/workflows/build-examples.yml @@ -6,7 +6,7 @@ name: Build Examples # (out/build_latest_,含 scripts/ + 完整 Module.symvers)。 # # 用 allnoconfig + 开 CONFIG_MODULES/CONFIG_PRINTK:最小内核、无 =m 配置模块, -# 所以 make modules 只 modpost vmlinux 生成 Module.symvers—— +# 所以 make(默认,KBUILD_BUILTIN=1)只编 vmlinux + modpost vmlinux 生成 Module.symvers—— # ① 不碰 defconfig 里 arch/riscv/kvm/kvm.ko 这种有符号依赖问题会炸的配置模块; # ② allnoconfig vmlinux 极小,编译快(~3min vs defconfig ~15min); # ③ hello.c 只用 _printk(pr_info)+ module 宏,allnoconfig 内核完全够。 @@ -59,7 +59,7 @@ jobs: uses: actions/cache@v4 with: path: out/build_latest_${{ matrix.arch }} - key: kernel-${{ matrix.arch }}-${{ env.LINUX_SHA }}-allnoconfig + key: kernel-${{ matrix.arch }}-${{ env.LINUX_SHA }}-allnoconfig-make - name: Prepare kernel tree (allnoconfig → Module.symvers) if: steps.kcache.outputs.cache-hit != 'true' @@ -71,8 +71,11 @@ jobs: make ARCH=${{ matrix.arch }} CROSS_COMPILE=${{ matrix.cc }} O="$KOUT" allnoconfig ./scripts/config --file "$KOUT/.config" --enable CONFIG_MODULES --enable CONFIG_PRINTK make ARCH=${{ matrix.arch }} CROSS_COMPILE=${{ matrix.cc }} O="$KOUT" olddefconfig - # 无 =m 配置模块,make modules 只 modpost vmlinux → Module.symvers(含 _printk)。 - make ARCH=${{ matrix.arch }} CROSS_COMPILE=${{ matrix.cc }} O="$KOUT" -j$(nproc) modules + # 关键:用 make(默认,KBUILD_BUILTIN=1),不是 make modules。 + # make modules 时 KBUILD_BUILTIN 为空 → 不 modpost vmlinux → Module.symvers 缺 + # _printk → hello.ko modpost 报 undefined。make(默认)会 modpost vmlinux 出完整 + # Module.symvers(本地验证含 _printk)。allnoconfig 无 =m,make 不编配置模块(不碰 kvm)。 + make ARCH=${{ matrix.arch }} CROSS_COMPILE=${{ matrix.cc }} O="$KOUT" -j$(nproc) echo "::notice::allnoconfig 内核树大小: $(du -sh "$KOUT" | cut -f1)" - name: Build example/mini modules (${{ matrix.arch }})