news 2026/9/16 22:52:31

QEMU实战:从initramfs到ext4制作根文件系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
QEMU实战:从initramfs到ext4制作根文件系统

内核编译出来只是前半程,后半程是今天要聊的这件事:根文件系统。如果你手里刚有一个编译好的 bzImage,兴冲冲地用 QEMU 启动,多半会卡在这样一行日志上:Kernel panic - not syncing: VFS: Unable to mount root fs。别急,这不是内核坏了,而是内核找不到它的用户态世界。这篇文章就把在 QEMU 虚拟机里制作一个根文件系统、和内核一起跑起来的过程完整过一遍。适合刚编译完内核、还没见过登录提示符的人,也适合后面准备做内核移植和驱动调试的人。整个过程完全可以跟着敲,不需要服务器,一台普通 Linux 宿主机就够。

我会按自己的实际调试顺序来讲:先解释内核启动时到底在找什么,再对比 initramfs 和 ext4 磁盘镜像两条路线,然后从 BusyBox 搭最小根目录,到打包、启动、排错,最后落到持久化镜像。全程用 x86_64 的 QEMU 串口控制台演示,ARM64 的差异我会单独标注出来。

1. 启动链条里的那一环:内核编译完为什么还要做根文件系统

很多刚编译完内核的人都有个错觉:有了 vmlinuz/bzImage,系统就能自己跑起来。实际上内核只是启动链路的一半,它需要一个“用户态世界”才能完成启动。这个用户态世界,就是根文件系统(root filesystem,简称 rootfs)加上里面存着的 init 程序和应用工具。

1.1 内核态到用户态的那一跳

Linux 的启动过程,大致是这样一个顺序:

  1. QEMU 模拟固件/引导加载器加载内核映像,比如-kernel bzImage
  2. 内核解压、初始化内存管理、调度器、中断、各种驱动子系统。
  3. 内核挂载一个初始根文件系统。
  4. 内核在根文件系统里寻找并执行 init 进程,也就是 PID 1。
  5. init 进程读取配置、挂载其余文件系统,然后拉起 shell 或业务进程。

前两步属于内核态,后面的步骤需要有一个能访问的文件系统。也就是说,内核要“交出控制权”,必须有一个它可以挂着、并且里面放好了用户态程序的目录树

可以把内核理解成发动机,rootfs 是底盘、方向盘和驾驶室。发动机再好,不接上底盘,车也动不了。QEMU 在这里扮演的是车间里的测试台,它只负责模拟硬件,并不负责替内核准备“驾驶室”。

1.2 VFS panic 到底在说什么

如果 QEMU 启动时只给了-kernel bzImage,没有给根文件系统,你大概率看到类似日志:

[ 0.000000] Linux version ... ... [ 1.234567] VFS: Cannot open root device "" or unknown-block(0,0) [ 1.234567] Please append a correct "root=" boot option [ 1.234567] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)

这里的VFS是 Linux 虚拟文件系统层。它负责把各种文件系统(ext4、proc、sysfs、ramfs 等)统一成/下的目录视图。内核启动后期会调用 VFS 去挂载根设备,如果设备号不存在、驱动没编进内核、文件系统类型没启用,它就只能 panic。

所以“制作根文件系统”本质上要做两件事:准备一份目录树和用户态程序,然后用 QEMU 能识别的方式交给内核。目录树本身并不神秘,核心是这几个目录:

/bin /sbin /etc /dev /proc /sys /tmp /root

这里面/bin/sbin放可执行程序,/etc放配置,/dev放设备节点,/proc/sys是内核暴露信息的虚拟文件系统挂载点。最小系统不需要很大,几 MB 就能跑起来。

2. 两条路线:initramfs 和 ext4 磁盘镜像怎么选

给内核准备 rootfs,常见有两种做法:initramfs 和块设备镜像。这两条路线的原理和适用场景差别很大,我在项目里都试过,先说结论:新手调试先走 initramfs,等启动逻辑稳定后再迁到 ext4 镜像验证持久化。

2.1 initramfs:常驻内存的临时根

initramfs 的本质是一个 cpio 格式的归档文件,里面打包了完整的目录树。内核启动时读入这个归档,解压到内存中的 rootfs(基于 ramfs/tmpfs),然后在这个内存文件系统里执行 init。

它的特点:

  • 不需要真实块设备驱动,只要内核支持 initramfs 和 gzip 解压就能跑。
  • 不占磁盘、启动快,非常适合做内核早期启动流程验证。
  • 内容在内存里,掉电/重启后全部丢失。

我在做内核驱动调试时,最喜欢用 initramfs。因为驱动还没写好的时候,少一个存储控制器驱动,磁盘根本认不出来,而 initramfs 绕过了整个存储链路,问题是少一层就少一半。

2.2 ext4 镜像:持久化才是真实需求

块设备镜像是用dd建一个文件,格式化成 ext4,再把目录树放进去。QEMU 把这个文件模拟成一块硬盘,内核通过root=/dev/vda这样的参数去挂载它。

它的特点:

  • 是真实的块设备,能验证存储栈、驱动、文件系统。
  • 数据可以持久化,重启后仍在。
  • 启动依赖更多内核配置,比如 virtio 驱动、ext4 文件系统支持。

做产品、做开发板系统,最后都会落到这种方案。它更接近真实机器,但也意味着任何一个环节没配对都会启动失败。

2.3 先内后外的调试顺序

我自己的习惯是“先内后外”。第一遍先用 initramfs 把 init 流程和 BusyBox 跑通,确认 shell 能起来;然后再把同一份目录树灌进 ext4 镜像,从内存根切换到磁盘根。这样如果 ext4 启动失败,基本可以断定问题出在存储驱动、root 参数或文件系统配置上,而不是 init 脚本写错。两步隔离,排查效率高很多。

下面这张表可以帮你快速判断该走哪条路:

对比项initramfsext4 镜像
存放位置内存 rootfs虚拟块设备
持久化
驱动要求高,需存储/总线驱动
启动速度稍慢
适合场景早期启动、驱动调试系统开发、接近真实环境

3. 用 BusyBox 搭最小用户态和根目录骨架

目录树里不能只有 shell 一个程序,否则连lsmountcat都得自己用 C 语言重新实现。常规做法是引入 BusyBox,它把几百个常用命令集成到一个可执行文件里,通过符号链接暴露命令名。

3.1 静态编译 BusyBox,一次编译到处跑

BusyBox 的编译本身不复杂,真正容易踩坑的是动态链接。默认配置下 BusyBox 会动态链接到 glibc,生成的文件很小,但在一个空 rootfs 里没有 glibc 动态库,启动时就会报can't load library 'libm.so.6',非常难查。

所以我的建议是:最小系统里无条件开启静态编译。

wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2 tar jxf busybox-1.36.1.tar.bz2 cd busybox-1.36.1 make defconfig make menuconfig

在 menuconfig 里进入:

Busybox Settings ---> Build Options ---> [*] Build static binary (no shared libs)

选中之后保存退出,然后编译:

make -j$(nproc) make install

末尾的make install会把结果装到_install目录。如果宿主机器架构和目标架构一致,比如都是 x86_64,上面命令就够了。假如你要给 ARM64 做,需要在make前指定工具链:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc) make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- install

默认make install目标目录是_install,也可以显式改成自己想要的路径:

make CONFIG_PREFIX=$HOME/labs/rootfs install

静态编译的代价是文件体积大一些,但换来的好处是“一个文件就是一个用户态”,移动到哪里都能跑,不用跟着拷贝一堆.so

3.2 目录骨架与设备节点:少了它们起不来

拿到_install之后,开始组装根目录。我通常会建一个独立的工程目录,比如~/labs/rootfs,后续所有改动都基于这个目录,而不是直接改 initramfs 归档。这样可以随时重新打包,也能 diff 看改了什么。

ROOTFS=~/labs/rootfs mkdir -p ${ROOTFS}/{bin,sbin,etc,dev,proc,sys,tmp,root} cp -a busybox-1.36.1/_install/* ${ROOTFS}/ mknod -m 622 ${ROOTFS}/dev/console c 5 1 mknod -m 666 ${ROOTFS}/dev/null c 1 3

这里最容易被忽略的是设备节点。/dev/console是内核控制台对应的字符设备,主设备号 5,次设备号 1;/dev/null是主设备号 1、次设备号 3。如果不提前建这两个节点,早期的输出可能直接消失,shell 也可能报can't access tty

为什么需要手动建?因为这时候 rootfs 里还没有任何 udev/devtmpfs 机制,内核不会自动帮你把设备节点变出来。后面我会在 init 脚本里挂 devtmpfs,到那时/dev下的其他设备节点会自动生成,但 console 和 null 这两尽量在打包时就放在根目录里。

etc目录下还可以补两个最简单的基础文件,有些程序会读:

echo "root:x:0:0:root:/root:/bin/sh" > ${ROOTFS}/etc/passwd echo "root:x:0:" > ${ROOTFS}/etc/group

不写也能进 shell,但写上更稳,后面加 telnetd、login 之类功能时不会忽然踩到缺文件。

3.3 初始化脚本:/init 与 /etc/inittab 的分工

根目录里有程序,还得让内核知道启动后执行谁。initramfs 有一个约定:内核默认去找根目录下的/init可执行文件;如果找不到,才会退而尝试/sbin/init/bin/sh这些路径。

我习惯在 rootfs 顶部放一个/init脚本,用它完成早期挂载,然后交给 BusyBox 的 init:

cat > ${ROOTFS}/init << 'EOF' #!/bin/sh mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs devtmpfs /dev exec /sbin/init EOF chmod +x ${ROOTFS}/init

这里把/proc/sys/dev都挂上。/proc/sys挂载后,你才能看到/proc/cmdline/sys/class这些内核信息;devtmpfs 挂载后,块设备、tty 设备节点会自动出现。

然后在etc/inittab里写 BusyBox init 的配置:

cat > ${ROOTFS}/etc/inittab << 'EOF' ::askfirst:-/bin/sh ::ctrlaltdel:/sbin/reboot ::shutdown:/bin/umount -a -r EOF

askfirst的含义是:当内核有可用控制台时,先确认一次,再弹出一个 shell。对调试来说这个行为很方便,按一下回车就进入交互界面。-前缀表示这是一个登录 shell,会读取/etc/profile之类的配置。

如果你习惯 sysvinit 风格,也可以不写/init,而是把挂载命令放进/etc/init.d/rcS,然后让 inittab 调用:

::sysinit:/etc/init.d/rcS

两种写法都能跑,区别只是挂载发生在 init 之前还是之后。我选择/init做挂载,是因为它更贴合 initramfs 的设计本意,逻辑更清晰:先让系统目录可见,再切换到正式的 init 管理用户态进程。

4. 打包 initramfs,用 QEMU 把系统真正拉起来

目录树就绪后,下一步是打成 initramfs 归档。这里有一个高频错误:打包时把顶层目录也带进去了。内核解包 initramfs 时,它是从归档根开始解析的,如果里面第一层是rootfs/,那内核在根目录找不到/init,启动依然会失败。

4.1 cpio 打包:格式和权限都是细节

打包命令:

cd ${ROOTFS} find . -print0 | cpio --null -ov --format=newc | gzip -9 > ~/labs/initramfs.cpio.gz

注意几个点:

  • --format=newc必须指定,Linux 内核只认 newc 格式的 cpio。
  • 必须在ROOTFS目录内部执行,确保归档里第一层是.也就是根目录。
  • find不加rootfs/前缀,否则打包结果就整体多了一层。
  • 权限必须保留,/init和后续脚本如果没有执行权限,内核会报No working init found

打完包可以确认一下内容:

cd ~/labs zcat initramfs.cpio.gz | cpio -tv | head

这条命令会把归档内容列出来,重点看有没有/init,权限是不是-rwxr-xr-x

4.2 QEMU 启动参数逐个拆解

现在可以启动虚拟机了。x86_64 平台的 QEMU 命令:

qemu-system-x86_64 \ -kernel bzImage \ -initrd initramfs.cpio.gz \ -append "console=ttyS0 rdinit=/init" \ -nographic

参数逐一说明:

  • -kernel bzImage:指定内核映像,一般编译完的内核在arch/x86/boot/bzImage
  • -initrd initramfs.cpio.gz:让 QEMU 把归档作为 initramfs 交给内核。
  • -append:向内核传递启动参数。console=ttyS0指定串口控制台,rdinit=/init显式告诉内核在 initramfs 里执行/init
  • -nographic:禁用图形窗口,把串口重定向到当前终端,这样日志能直接打到屏幕。

如果你在 ARM64 上做,QEMU 命令稍有不同:

qemu-system-aarch64 \ -M virt -cpu cortex-a53 \ -kernel Image.gz \ -initrd initramfs.cpio.gz \ -append "console=ttyAMA0 rdinit=/init" \ -nographic

差别主要在机器模型-M virt、CPU 型号,以及串口名ttyAMA0。ARM 的 defconfig 通常会启用 virt 平台的 pl011 串口,但如果你是从零裁剪的内核,一定要确认CONFIG_SERIAL_AMBA_PL011_CONSOLE开着,否则看不到任何输出。

4.3 第一次进入 shell:成功标志与退出方法

如果一切顺利,你会看到内核日志刷到末尾,出现这样一段:

Please press Enter to activate this console.

按下回车,进入#提示符。这时候可以验证系统状态:

# ls / /bin /dev /etc /init /proc /root /sbin /sys /tmp /usr # cat /proc/cmdline console=ttyS0 rdinit=/init # ps PID USER VSZ STAT COMMAND 1 root 0 S init

能敲命令,能看内核信息,说明根文件系统和内核已经配合起来了。退出 QEMU 有两种方式:在 shell 里执行poweroff -f,或直接按Ctrl+A X强制退出 QEMU。Ctrl+A X是 QEMU 的 nographic 退出快捷键,别按成Ctrl+C,那只会发中断信号。

5. 启动失败排查:从日志反推到底哪里没配对

这个项目里真正耗时间的其实不是“搭成功”,而是“搭失败后怎么定位”。下面按我遇到的频率,把典型故障和排查思路列一版。

5.1 VFS: Unable to mount root fs

日志关键词:

Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)

这个错误最直接的原因就是内核没找到根。对应到 initramfs 场景,通常有三种情况:

  1. 内核没开启 initramfs 支持,CONFIG_BLK_DEV_INITRD没打开。
  2. -initrd参数没传,或路径写错。
  3. initramfs 归档格式不对,比如 cpio 不是 newc,或压缩格式内核不支持。

先检查内核配置:

grep CONFIG_BLK_DEV_INITRD .config grep CONFIG_RD_GZIP .config

如果CONFIG_BLK_DEV_INITRD不是y,回到make menuconfig打开:

General setup ---> [*] Initial RAM filesystem and RAM disk (initramfs/initrd) support

如果确认配置没问题,再检查启动日志里有没有这一行:

[ 0.000000] RAMDISK: [mem ...] [ 0.000000] Unpacking initramfs...

Unpacking initramfs才说明内核读到了归档。

5.2 No working init found

日志关键词:

Kernel panic - not syncing: No working init found. Try passing init= option to kernel.

这说明内核找到了 rootfs,但执行 init 失败。常见原因按发生频率从高到低:

  1. /init文件没有执行权限。
  2. /init是动态链接的 ELF,缺少动态库。
  3. /init脚本的 shebang 写成/bin/bash,但 BusyBox 里没有 bash。
  4. 打包时多包了一层目录,内核在根目录根本找不到/init

排查手段是解包看内容:

zcat initramfs.cpio.gz | cpio -tv | grep init

确认权限后,用file检查二进制类型。静态 BusyBox 会输出statically linked,动态的会写dynamically linked

5.3 can't access tty 与串口控制台问题

shell 能起来,但敲回车后提示:

/bin/sh: can't access tty; job control turned off

这句话看起来吓人,其实不是致命错误。它说明 shell 找不到合适的前台控制终端,通常是因为/dev/console设备节点不存在,或者 inittab 的 askfirst 没指定 tty。解决办法是回到 rootfs,检查/dev/console节点:

ls -l ${ROOTFS}/dev/console

如果完全没有任何输出,优先怀疑两个方向。一个是console=ttyS0对应的串口驱动没编进内核,x86 上要确认CONFIG_SERIAL_8250_CONSOLE=y;另一个是 QEMU 显示模式问题,确认启动参数里有-nographic,并且没有同时开着图形窗口抢走焦点。

5.4 内核编译选项速查表

排错到最后,很多问题会汇总到内核 config。我把这个项目涉及到的关键配置整理成一张表:

功能需要开启的内核配置
initramfs 支持CONFIG_BLK_DEV_INITRD=y
gzip 压缩 initramfsCONFIG_RD_GZIP=y
devtmpfs 自动挂载CONFIG_DEVTMPFS=yCONFIG_DEVTMPFS_MOUNT=y
proc 文件系统CONFIG_PROC_FS=y
sysfs 文件系统CONFIG_SYSFS=y
ext4 文件系统CONFIG_EXT4_FS=y
virtio 块设备CONFIG_VIRTIO=yCONFIG_VIRTIO_PCI=yCONFIG_VIRTIO_BLK=y
x86 串口 consoleCONFIG_SERIAL_8250=yCONFIG_SERIAL_8250_CONSOLE=y
ARM64 pl011 consoleCONFIG_SERIAL_AMBA_PL011=yCONFIG_SERIAL_AMBA_PL011_CONSOLE=y

一个容易忽略的细节是:这些选项如果编成=m,模块不会自动出现在 initramfs 里。在根文件系统场景下,跟启动强相关的配置一律建议=y,否则内核还是找不到。

6. 进阶:把根文件系统迁到 ext4 镜像,验证持久化

initramfs 跑通之后,趁热打铁把它迁到 ext4 磁盘镜像。这一步做完,你就拥有一个能在 QEMU 里反复启动、数据不丢的“小系统”了。

6.1 生成 ext4 镜像并灌入用户态

先在宿主机上创建一块“虚拟硬盘”:

dd if=/dev/zero of=rootfs.ext4 bs=1M count=512 mkfs.ext4 -L mylinux rootfs.ext4

dd生成一个 512MB 的空白文件,mkfs.ext4把它格式化成 ext4 文件系统。镜像大小按需调整,512M 对于最小系统来说很宽裕。

然后通过 loop 设备挂载镜像,把之前做好的 rootfs 目录树复制进去:

sudo mkdir -p /mnt/rootfs sudo mount -o loop rootfs.ext4 /mnt/rootfs sudo cp -a ${ROOTFS}/* /mnt/rootfs/ sudo umount /mnt/rootfs

这里mount -o loop会把rootfs.ext4这个普通文件当作块设备挂载,属于开发机上常用的手法。复制完成后卸载,镜像里已经有了完整的根目录。

6.2 更换 QEMU 存储后端

这次启动不再用-initrd,而是把rootfs.ext4作为虚拟硬盘挂给 QEMU:

qemu-system-x86_64 \ -kernel bzImage \ -drive file=rootfs.ext4,format=raw,if=virtio \ -append "root=/dev/vda rw console=ttyS0" \ -nographic

-drive参数里的if=virtio让 QEMU 模拟一块 virtio 磁盘,Linux 内核会把它识别为/dev/vda。因此root=/dev/vda表示根文件系统在这块虚拟盘上。rw表示以读写方式挂载根,这样系统内才能写文件。

如果你用的内核没有编 virtio 驱动,可以把if=virtio换成if=ide,同时root=/dev/sda。两种方式都能跑,virtio 更接近现代虚拟化环境,ide 兼容性更老派稳妥。

注意,这次启动不需要-initrd,因为根文件系统不再是内存里的 cpio,而是磁盘镜像。内核日志里也不会再出现Unpacking initramfs,取而代之的是 ext4 挂载日志:

EXT4-fs (vda): mounted filesystem with ordered data mode.

vda 这个名字取决于你选的总线。如果看到VFS: Cannot open root device "vda",那就是内核里对应驱动没编进去,回上一章速查表逐项检查。

6.3 验证持久化:write、sync、重启

ext4 根最核心的价值就是持久化。进去之后做个简单验证:

# echo "hello from ext4" > /marker.txt # sync # cat /marker.txt hello from ext4

sync这步非常关键。Linux 文件写入先进入页缓存,不会立刻落盘,如果你不执行sync就直接强杀 QEMU,镜像数据可能只停留在宿主机页缓存里。热词里把 sync 和 vfs 放在一起,其实背后就是这么一回事:VFS 缓存要刷回块设备,靠的就是 sync。

然后退出 QEMU,再次用同一条命令启动,进去后查看:

# cat /marker.txt hello from ext4

文件还在,说明系统已经具备真实持久化能力了。往后在这个 rootfs 里装程序、改配置,都能保留在rootfs.ext4里。

7. 把这份最小系统变成后续开发模板

到此,一套最小根文件系统已经在 QEMU 里稳定运行了。后面它还能怎么用,我觉得比启动本身更有价值。

7.1 从“能进 shell”到“能跑自己的程序”

把你自己写的静态编译程序扔进 rootfs 的/bin,重新打包 initramfs 或复制进 ext4 镜像,启动后就能直接运行。比如写一个/bin/hello,在 shell 里执行,就能验证程序在目标内核上能否正确运行。

如果要做驱动调试,initramfs 是个非常方便的载体。驱动编译成.ko后,你可以在/init脚本里加一行insmod /xxx.ko,或者提前放进/lib/modules/$(uname -r)。由于 initramfs 不依赖块设备,驱动还没适配存储控制器之前也能完成加载测试,把变量控制在最小范围。

后续想加网络功能,可以在etc/inittab里加上启动脚本,用udhcpc自动获取 IP,再配 dropbear 做 SSH。最小系统很快就能转成可联网的开发环境。

7.2 我保留的几个操作习惯

踩过几轮坑之后,我总结出这几个固定习惯,能节省大量时间:

  • rootfs 源目录才是工程,initramfs.cpio.gz 只是构建产物。每次改文件都改源目录,重新打包。这样遇到问题能 diff,而不是解压归档到处翻。
  • 打包前检查权限。脚本类文件统一chmod +x,再打 cpio。很多No working init found问题其实出在init没有x位。
  • 改内核 config 后,先用 grep 验证再编译。比如grep CONFIG_EXT4_FS .config,避免menuconfig里选项没保存、实际没生效的尴尬。
  • 进 QEMU 之前先想清楚退出方式。-nographicCtrl+A X是保住手速的关键。没有图形化环境也能串口调试,习惯之后比开图形窗口高效得多。
  • 改完 rootfs 马上测试,避免积累多个变量。一次只改一处,比如只改 init 脚本、只加一个驱动,跑一把看结果,再继续下一步。

这套流程我在 x86 和 ARM64 上各完整跑过一遍。第一次在 ARM64 上因为串口参数写错,黑屏了很久,后来发现只是ttyS0ttyAMA0的差异。当你把这些细节都理顺之后,再回头看“内核启动找不到根”的 panic,就会发现它其实只是启动链路上一个非常明确的提示:你的用户态世界还没有准备好。现在,它准备好了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 22:50:47

Arduino IDE开发环境搭建全攻略:从安装到烧录的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 22:49:18

SAP物料基本计量单位更改实战:MMAM前置检查与常见报错排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 22:48:17

Dify本地知识库部署与外网访问实战:Docker部署到手机端访问

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 22:46:59

基于51单片机的步进电机控制:角度、速度与方向自动调度设计

简介&#xff1a;面向51单片机学习者和电子设计竞赛者的步进电机运转系统完整资料包&#xff0c;解决多参数自动控制与单次/循环运行模式的设计需求。资源共44个文件&#xff0c;压缩后9.37MB&#xff0c;涵盖原理图、仿真图、流程图、元件清单、Keil工程源码与hex烧录文件及演…

作者头像 李华
网站建设 2026/9/16 22:46:36

LabVIEW 2024安装全攻略:从NIPM到驱动配置避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华