摘要
本文记录如何在 Apple Silicon Mac 上通过修改 Linsk 实现离线编译与运行,并在物理磁盘上创建 LVM 逻辑卷及 ext4 分区,最终通过 FTP 网络共享在 macOS 上读写访问。核心目标是将 Apple Silicon Mac mini 作为可以随意扩展磁盘的性能强大的 NAS,解决 macOS 原生不支持 LVM 和 ext4 的问题。
声明
本文人类为第一作者, 龙虾为通讯作者. 本文有AI生成内容.
工程仓库
- 上游仓库: [https://github.com/AlexSSD7/linsk]
- 修改后仓库: [https://github.com/qsbye/linsk-plus], Release 中有编译完成的可执行文件.
简介
LVM 逻辑卷管理简介
LVM(Logical Volume Manager,逻辑卷管理器)是 Linux 内核提供的磁盘管理抽象层,它允许在物理磁盘之上创建灵活的存储池(Volume Group),并从中划分出可动态调整大小的逻辑卷(Logical Volume)。相比传统分区表,LVM 的核心优势在于:
- 动态扩容/缩容:无需重新分区即可调整逻辑卷大小
- 跨物理磁盘聚合:可将多块物理磁盘合并为一个卷组
- 快照支持:可创建逻辑卷的时间点快照
- 任意添加物理磁盘和扩容分区:新磁盘加入卷组后,现有逻辑卷可直接扩展
Linsk 简介
[https://github.com/AlexSSD7/linsk]
Linsk 是一个非常务实的开源方案,它证明了"VM 路线"是在 macOS 上支持 LVM 的最务实方案——它完全规避了重写 Linux 内核子系统的巨大工程成本。Linsk 不重新实现任何文件系统或 LVM 逻辑,而是走了一条"借力打力"的路线:在 macOS 上启动一个轻量级 Alpine Linux VM(约 130MB),通过硬件虚拟化(HVF)让 VM 直接访问物理磁盘,然后在 VM 内用原生 Linux 工具(lvm2、cryptsetup、mount)完成所有复杂工作,最后通过 SMB/AFP/FTP 网络共享将文件暴露给 macOS Finder。
核心定位
- 零文件系统实现成本:复用 Linux 内核成熟的 LVM2、ext4、LUKS 等子系统
- 硬件虚拟化直通:通过 QEMU HVF 加速,VM 直接访问物理块设备
- 网络共享前端:支持 SMB(macOS 原生)、AFP(旧版 macOS)、FTP(跨平台)三种协议
- 单二进制分发:Go 编写的 CLI 工具,编译后仅一个可执行文件
源码结构
Linsk 采用 Go 模块结构,源码总计约 5000 行 Go 代码,分布于 30 余个源文件中:
linsk/
├── cmd/ # Cobra CLI 命令定义 (build, run, shell, ls, clean)
├── constants/ # 常量定义 (镜像版本、BIOS 路径等)
├── imgbuilder/ # VM 镜像构建逻辑
├── nettap/ # macOS TAP 网络接口分配
├── osspecifics/ # 跨平台 OS 检查 (darwin/linux/windows)
├── qemucli/ # QEMU 命令行参数构造器
├── share/ # 网络共享后端 (SMB/AFP/FTP)
├── sshutil/ # SSH 连接工具
├── storage/ # 文件下载/缓存/嵌入资源管理
├── utils/ # 通用工具函数
├── vm/ # VM 生命周期管理 (启动、SSH、挂载、共享)
└── main.go # 入口
关键设计模式
- VM 即服务:Alpine VM 作为临时 Linux 环境,用完即销毁,无状态设计
- 设备直通:通过 QEMU
-device virtio-blk-pci将物理磁盘直通给 VM - SSH 桥接:VM 启动后通过本地端口转发建立 SSH 通道,所有操作通过 SSH 执行
- 网络共享抽象:
share.Backend接口统一封装 SMB/AFP/FTP 三种协议
技术亮点
- 轻量级 VM:Alpine Linux 最小化镜像,启动时间 < 30 秒
- 离线运行:修改后支持将 ISO、BIOS、APK 缓存嵌入二进制,无需网络
- 安全设计:默认禁用 VM 网络访问,磁盘直通需显式指定
- 密码固定:共享密码固定为
linsk,便于自动化脚本和开机自启
工程
1. 嵌入资源文件以支持离线运行
Linsk 原版在首次运行时需要从网络下载 Alpine ISO 镜像和 EDK2 UEFI BIOS 文件。为适配离线机器,我们将这些资源嵌入到 Go 二进制中,运行时提取到数据目录。
assets/embed.go
package assetsimport ("embed""fmt""io""os""path/filepath""github.com/AlexSSD7/linsk/utils"
)//go:embed files/*.img files/*.fd files/apks/*.apk
var embeddedFiles embed.FS// ExtractFile extracts an embedded file to the specified destination.
func ExtractFile(name, dst string, size int64) error {src, err := embeddedFiles.Open("files/" + name)if err != nil {return fmt.Errorf("open embedded file %v: %w", name, err)}defer src.Close()if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {return fmt.Errorf("create dir for %v: %w", dst, err)}dstFile, err := os.Create(dst)if err != nil {return fmt.Errorf("create dst file %v: %w", dst, err)}defer dstFile.Close()_, err = io.Copy(dstFile, src)if err != nil {return fmt.Errorf("copy embedded file %v to %v: %w", name, dst, err)}return nil
}
storage/download.go(修改后,从嵌入资源提取而非网络下载)
package storageimport ("fmt""os""path/filepath""github.com/AlexSSD7/linsk/assets""github.com/AlexSSD7/linsk/constants"
)func (s *Storage) downloadFile(name string, size int64) (string, error) {dstPath := filepath.Join(s.dataDir, name)// Check if already extracted.if _, err := os.Stat(dstPath); err == nil {return dstPath, nil}s.log.Info("Extracting embedded file", "to", dstPath, "size", utils.FormatBytes(size))if err := assets.ExtractFile(name, dstPath, size); err != nil {return "", fmt.Errorf("extract embedded file %v: %w", name, err)}info, err := os.Stat(dstPath)if err != nil {return "", fmt.Errorf("stat extracted file: %w", err)}s.log.Info("Successfully extracted embedded file", "to", dstPath, "out-size", utils.FormatBytes(info.Size()))return dstPath, nil
}
资源文件放置目录:
assets/
├── embed.go
└── files/├── alpine-3.20.3-aarch64.img # Alpine ISO 镜像 (~73MB)├── edk2-aarch64-code.fd # EDK2 UEFI BIOS (~67MB)└── apks/├── lvm2-*.apk # LVM2 工具包├── e2fsprogs-*.apk # ext4 工具包└── ... # 其他依赖 APK
2. 嵌入 APK 包缓存
VM 镜像构建过程中需要安装 lvm2、e2fsprogs 等 Alpine 包。原版通过 VM 内的 apk add 从网络下载,离线环境下会失败。我们将 APK 包预下载并嵌入二进制,构建时通过本地仓库安装。
imgbuilder/imgbuilder.go(APK 本地仓库配置片段)
// 在 VM 内配置本地 APK 仓库
cmds := []string{"mkdir -p /etc/apk/cache","cp /media/apks/*.apk /etc/apk/cache/","echo '/etc/apk/cache' >> /etc/apk/repositories","apk add --no-network lvm2 e2fsprogs",
}
3. 修复 FTP 写入权限
原版 Linsk 通过 FTP 共享时,/mnt 目录的权限为 root:root 755,导致 FTP 用户 linsk 无法写入。修改 vm/filemanager.go,在启动共享服务前修复目录权限。
vm/filemanager.go
func (fm *FileManager) fixMountPermissions() error {// Ensure the linsk user can read/write the mount point._, err := fm.RunCmd("chown", []string{"linsk:linsk", "/mnt"}, nil, true)if err != nil {return fmt.Errorf("chown /mnt: %w", err)}_, err = fm.RunCmd("chmod", []string{"775", "/mnt"}, nil, true)if err != nil {return fmt.Errorf("chmod /mnt: %w", err)}return nil
}
在 StartShare 方法中调用:
func (fm *FileManager) StartShare(backend share.Backend, ports share.Ports) (string, error) {if err := fm.fixMountPermissions(); err != nil {return "", fmt.Errorf("fix mount permissions: %w", err)}// ... 启动共享服务
}
4. 固定共享密码为 "linsk"
原版 Linsk 每次运行生成随机密码,不利于自动化脚本和开机自启。修改 cmd/run.go,将密码固定为 linsk。
cmd/run.go
// 原代码:pass, err := sshutil.GenerateRandomPassword(16)
// 修改为固定密码
pass := "linsk"
5. 构建与测试
编译
# 确保资源文件已放置
ls assets/files/
# alpine-3.20.3-aarch64.img edk2-aarch64-code.fd apks/# 编译(Go 1.21+)
go build -o linsk .# 或使用 Makefile
make build
构建 VM 镜像
# 首次运行需要构建 VM 镜像
sudo ./linsk build# 如果镜像已存在,使用 --overwrite 覆盖
sudo ./linsk build --overwrite
构建成功输出:
time=2026-08-16T13:58:56.089+08:00 level=INFO msg="VM image built successfully" path=/Users/workspace/.linsk/3.20.3-aarch64-linsk1.qcow2
测试 LVM + ext4
使用 /Volumes/QSANDISK(/dev/disk4)作为测试磁盘,数据可抹掉。
# 1. 卸载磁盘(macOS 侧)
sudo diskutil unmountDisk /dev/disk4# 2. 进入 Linsk shell,在 VM 内操作磁盘
sudo ./linsk shell "dev:/dev/disk4"
在 VM 内创建 LVM 逻辑卷和 ext4 分区:
# 查看磁盘
localhost:~# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
vda 253:0 0 1G 0 disk
├─vda1 253:1 0 160M 0 part /boot/efi
├─vda2 253:2 0 256M 0 part [SWAP]
└─vda3 253:3 0 606M 0 part /
vdb 253:16 0 28.7G 0 disk
└─vdb1 253:17 0 28.7G 0 part # 清除原有分区表
localhost:~# wipefs -a /dev/vdb# 创建物理卷
localhost:~# pvcreate /dev/vdbPhysical volume "/dev/vdb" successfully created.# 创建卷组
localhost:~# vgcreate testvg /dev/vdbVolume group "testvg" successfully created# 创建逻辑卷
localhost:~# lvcreate -L 28G -n testlv testvgLogical volume "testlv" created.# 创建 ext4 文件系统
localhost:~# mkfs.ext4 /dev/testvg/testlv
mke2fs 1.47.0 (5-Feb-2023)
Creating filesystem with 7340032 4k blocks and 1835008 inodes
Filesystem UUID: 9157f7f1-d43e-4a30-915b-4313f1345bcf
...# 查看结果
localhost:~# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
vda 253:0 0 1G 0 disk
├─vda1 253:1 0 160M 0 part /boot/efi
├─vda2 253:2 0 256M 0 part [SWAP]
└─vda3 253:3 0 606M 0 part /
vdb 253:16 0 28.7G 0 disk
└─testvg-testlv 252:0 0 28G 0 lvm localhost:~# exit
通过 FTP 共享访问
# 使用 mapper 路径指定逻辑卷
sudo ./linsk run --share-backend ftp "dev:/dev/disk4" mapper/testvg-testlv
启动成功输出:
time=... level=INFO msg="Started the network share successfully" backend=ftp
===========================
[Network File Share Config]
The network file share was started. Please use the credentials below to connect to the file server.Type: FTP
URL: ftp://127.0.0.1:9000
Username: linsk
Password: linsk
===========================
使用 Cyberduck 或 Finder 连接 ftp://127.0.0.1:9000,用户 linsk,密码 linsk,即可读写访问 ext4 分区中的文件。
| CyberDuck 通过 FTP 访问 LVM 逻辑卷的 ext4 分区 |
|---|
![]() |
6. 保留预构建 VM 镜像
为避免每次重新构建 VM 镜像,可将构建好的 qcow2 镜像保留在项目目录:
# 复制预构建镜像到项目目录
sudo cp /Users/workspace/.linsk/3.20.3-aarch64-linsk1.qcow2 .# 后续可直接使用,无需重新 build
sudo ./linsk shell "dev:/dev/disk4"
常见问题
Q: linsk build 报错 "image already exists"
A: 使用 --overwrite 参数强制重建:
sudo ./linsk build --overwrite
Q: QEMU 启动报错 dylib 缺失
A: 确保已安装 QEMU 及其依赖:
brew install qemu
若仍报 libglib-2.0.0.dylib 缺失,尝试:
brew reinstall glib
brew link --overwrite glib
Q: linsk run 挂载报错 "unknown filesystem type 'LVM2_member'"
A: 直接指定块设备(如 /dev/disk4)时,Linsk 尝试自动识别文件系统,但 LVM 物理卷的文件系统类型为 LVM2_member,无法直接挂载。需指定逻辑卷路径:
# 错误:sudo ./linsk run --share-backend ftp "dev:/dev/disk4"
# 正确:指定 mapper 路径
sudo ./linsk run --share-backend ftp "dev:/dev/disk4" mapper/testvg-testlv
Q: FTP 连接后只能读取,无法写入
不要使用Finder, 改为使用Cyberduck才能正确读写
A: 确保使用了修改后的版本(已修复 /mnt 权限)。如仍有问题,手动检查 VM 内权限:
# 在 linsk shell 中
localhost:~# ls -ld /mnt
drwxrwxr-x 2 linsk linsk 4096 ... /mnt
若权限不对,手动修复:
localhost:~# chown linsk:linsk /mnt && chmod 775 /mnt
Q: 如何开机自动启动 FTP 共享
A: 由于密码已固定为 linsk,可创建 macOS LaunchAgent:
<!-- ~/Library/LaunchAgents/com.qsbye.linsk.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict><key>Label</key><string>com.qsbye.linsk</string><key>ProgramArguments</key><array><string>/usr/local/bin/linsk</string><string>run</string><string>--share-backend</string><string>ftp</string><string>dev:/dev/disk4</string><string>mapper/testvg-testlv</string></array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
</dict>
</plist>
加载配置:
launchctl load ~/Library/LaunchAgents/com.qsbye.linsk.plist
本文记录如何在 Apple Silicon Mac 上通过修改 [Linsk](https://github.com/AlexSSD7/linsk) 实现离线编译与运行,并在物理磁盘上创建 LVM 逻辑卷及 ext4 分区,最终通过 FTP 网络共享在 macOS 上读写访问。核心目标是将 Apple Silicon Mac mini 作为可以随意扩展磁盘的性能强大的 NAS,解决 macOS 原生不支持 LVM 和 ext4 的问题。