可移动的 Linux-To-Go 实现方案
Linux-To-Go顾名思义就是类似windows-to-go的一个把系统环境带着走的系统。如果是IT打工人,应该想要一个能自由带着走的笔记本吧,更进一步,笔记本可以换成现在性能满足要求的PSSD(Portable SSD)
我会从系统引导到日常使用说明我是如何解决一些实现上的细节,假设有多台电脑,公司,家里。系统也就是Linux在PSSD上,PSSD跟着人走,你人只有一个,所以当前最新的系统时间线也只有一个。系统有快照,并且会跟另一个位置的镜像系统快照同步,你可以通过快照回退到旧的时间线的系统。快照同步可以解决PSSD丢失问题,当然了需要勤快照或者设置成自动快照备份并同步。
系统引导
现今的主板,从EFI启动到Grub之后,然后grub自动通过不同设备,对不同硬件加载对应的内核参数,然后引导进入Gentoo系统。当然你如果所有硬件都一模一样,那么grub就简单了。
if [ "x$grub_platform" = xefi ]; then
set gfxpayload=keep
insmod bli
fi
insmod gzio
insmod part_gpt
insmod fat
insmod btrfs
### https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.7.0.pdf
###
smbios --type 1 --get-string 4 --set board_vendor
### /sys/devices/virtual/dmi/id/product_name
smbios --type 1 --get-string 5 --set board_name
### /sys/devices/virtual/dmi/id/product_sku
smbios --type 1 --get-string 25 --set board_sku
### /sys/devices/virtual/dmi/id/product_family
smbios --type 1 --get-string 26 --set board_family
insmod regexp
regexp --set=rootdisk '(.*),.*' $root
set root="$rootdisk,gpt2"
probe --set=rootuuid --fs-uuid $rootdisk,gpt2
## get activefs
for activefs_path in ($root)/activefs-*; do
set activefs=${activefs_path}
break
done
for activefs_path in ($root)/activefs-*; do
echo ${activefs_path}
if test "x$activefs_path" \> "x$activefs" ; then
set activefs=${activefs_path}
fi
done
echo $activefs
## get kernelversion
for vmlinuz_path in ${activefs}/boot/vmlinuz-*-x86_64; do
regexp --set=kernelversion '.*/boot/vmlinuz-([0-9\.]+-gentoo.*)-x86_64' ${vmlinuz_path}
break
done
for vmlinuz_path in ${activefs}/boot/vmlinuz-*-x86_64; do
#echo ${vmlinuz_path}
regexp --set=kernelversion_path '.*/boot/vmlinuz-([0-9\.]+-gentoo.*)-x86_64' ${vmlinuz_path}
if test "x$kernelversion_path" \> "x$kernelversion" ; then
set kernelversion=${kernelversion_path}
fi
done
echo $kernelversion
if [ "x$board_family" = "xPrecision" ]; then
echo ${board_name}
set linuxcmd="linux ${activefs}/boot/vmlinuz-${kernelversion}-x86_64 root=UUID=${rootuuid} rootflags=ssd,ssd_spread,noatime,compress=zstd:3,discard=async rw acpi_osi=Linux mitigations=off sysrq_always_enabled=1 systemd.unified_cgroup_hierarchy=1 snd.slots=snd_usb_audio,snd_hda_intel binder.devices=binder,hwbinder,vndbinder randomize_kstack_offset=0 i915.mitigations=off psi=1 net.ifnames=0 systemd.unit=graphical.target zswap.compressor=zstd zswap.zpool=zsmalloc zswap.enabled=0 acpi_mask_gpe=0x42 modules_load=ftdi_sio,orbment_dt orbment_dt.dpi_scale_x=60 orbment_dt.dpi_scale_y=53 orbment_dt.wlr_renderer=2 usb-storage.quirks=152d:0583:f"
eval ${linuxcmd}
initrd ${activefs}/boot/intel-uc.img ${activefs}/boot/initramfs-${kernelversion}-x86_64.img
elif [ "x$board_name" = "xMS-7C35" ]; then
echo ${board_name}
set linuxcmd="linux ${activefs}/boot/vmlinuz-${kernelversion}-x86_64 root=UUID=${rootuuid} rootflags=ssd,ssd_spread,noatime,compress=zstd:3,discard=async rw acpi_osi=Linux mitigations=off sysrq_always_enabled=1 systemd.unified_cgroup_hierarchy=1 binder.devices=binder,hwbinder,vndbinder randomize_kstack_offset=0 net.ifnames=0 initcall_blacklist=acpi_cpufreq_init amd_pstate.shared_mem=1 amd_pstate=active modules_load=nct6775 systemd.unit=graphical.target zswap.compressor=zstd zswap.zpool=zsmalloc zswap.enabled=0 mt7921e.disable_aspm=1 drm.edid_firmware=HDMI-A-2:edid/ehomewei.bin modules_load=nct6775,ftdi_sio,orbment_dt orbment_dt.dpi_scale_x=3 orbment_dt.dpi_scale_y=1 orbment_dt.wlr_renderer=2 usb-storage.quirks=152d:0583:f"
eval ${linuxcmd}
#apparmor=1 security=apparmor lsm=lockdown,yama,apparmor,bpf amd_pstate.shared_mem=1 amd_pstate=passive
#usb-storage.quirks=152d:0583:u
initrd ${activefs}/boot/amd-uc.img ${activefs}/boot/initramfs-${kernelversion}-x86_64.img
这是部分grub.cfg代码,从配置代码里面看出,grub 会自动搜索当前ESP分区所在磁盘的下一个btrfs root分区进行启动,不同硬件会有不同参数,这里我自动搜索了当前ESP所在磁盘的下一个btrfs分区的UUID进行启动,这样可以让这一份grub可以在别的 ESP上,也可以正确启动到对应的系统。
关于 linux的 启动参数。
- 我添加了 ssd_spread,这个用于让btrfs全局分配可用空间,而不是最小化使用让主控分配,这样可以让廉价SSD速度稍微快一点
- btrfs开启了zstd:3 压缩
- discard 异步也开起来,我就不每周周期trim了
- 会通过board_family来匹配正确的硬件,如果名称匹配就启动到对应的内核参数
- btrfs 分区是一个 subvolid=5 的主卷,通过查找最新一个 activefs-xxxxx-rw 子卷进行启动,该子卷里面是完整的linux根目录分区,包含home目录,boot目录,内核vmlinuz文件也在其中boot目录里面,所以内核跟子卷在一起,而不是放在ESP分区里面。也就是内核也可以版本控制。子卷里面的activefs-xxxx-rw/boot/vmlinuz 跟 activefs-xxxx-rw/lib/modules/的内核是匹配的不会出现意外启动失败。
ROOT 分区
该btrfs分区 的subvolid=5下面有多份系统镜像, btrfs的subvolid=5不会直接进行挂载,btrfs 会设置 activefs 为 default subvolue,所以不指定subvol的话,默认mount会mount到设置的 default最新activefs
btrfs subvolume list /
ID 283 gen 331310 top level 5 path activefs-202402131805-rw
ID 293 gen 273316 top level 5 path rootfs-202404250135
ID 294 gen 291083 top level 5 path rootfs-202405031342
ID 295 gen 312138 top level 5 path rootfs-202405130327
ID 296 gen 326424 top level 5 path rootfs-202405191812
其中activefs-xxxxxxx-rw 是 最新时间线的可改写的系统,其他 rootfs-xxxxx 均为 Readonly快照,这些子卷可以非常方便的通过工具 在PSSD插入当前电脑的时候,把当缺少的系统快照同步到家里,或者公司的硬盘上做备份,这点相当重要。毕竟PSSD要是丢了什么都没了。
另外,btrfs的 Data,MetaData,System如果不是单份全部改成单份,SSD没必要双份,特别是Data,
btrfs balance start -sconvert=single -mconvert=single -dconvert=single /
btrfs filesystem usage /
PSSD 选择
该u盘ssd一定要选择一个可靠的并且便携的,我使用Chipfancier UME Nano,以前使用 external SSD enclosure 这种M2 硬盘盒子,后来发现带一条线太麻烦,不能方便放到口袋,于是换成u盘形式的ssd。
还有关于PSSD的选择涉及到主控是否支持UASP,linux对 USAP的支持也是需要看硬件的,如果过于老旧的主板可能需要关闭UASP,比如usb-storage.quirks=152d:0583:u 这个内核参数。当前的USB主控桥接器有JMS583,ASM的,RTL的,还有Silicon Motion SM2320这种一体的。一定要选择新的。关于UASP也跟是否支持DISCARD有关,如果用了JMS583,就需要像我一样手动开启 USAP的TRIM支持
/etc/udev/rules.d/10-ssd.rules
ACTION=="add|change", ATTRS{idVendor}=="152d", ATTRS{idProduct}=="0583", SUBSYSTEM=="scsi_disk", ATTR{provisioning_mode}="unmap"
ACTION=="add|change", ATTRS{idVendor}=="152d", ATTRS{idProduct}=="0583", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", ATTR{queue/rotational}="0"
ACTION=="add|change", ATTRS{idVendor}=="152d", ATTRS{idProduct}=="0583", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", ATTR{queue/scheduler}="kyber"
再后来,我换掉了chipfancier,换成了三星PSSD t7,因为该盘三星自己的主控,并且各种标准都有实现。比那些只是贴盘的产品好很多。
操作系统 systemd
我只试过systemd的系统,因为有systemd-gpt-auto-generator 可以自动发现我的ESP分区,这样,ls /efi 就自动挂载当前系统的ESP了。
以前我整个btrfs所在分区用luks磁盘加密,现在觉得没必要。
关于swap,这个通过zram挂载,就不用PSSD的文件系统当swap了,也不用各个地方的硬盘做swap,如果需要的话,也可以通过给SWAP分区打LABEL,然后通过LABEL挂载,并且挂载参数加nofail。 cat /etc/udev/rules.d/10-zram.rules
KERNEL=="zram0", SUBSYSTEM=="block", DRIVER=="", ACTION=="add", ATTR{disksize}=="0", ATTR{disksize}="4096M", RUN+="/sbin/mkswap $env{DEVNAME}"
如果跟我一样是gentoo,那么编译软件的时候记得要用 x86-64 generic 的编译优化参数,不应该是具体的cpu了,防止别的硬件指令集没有而出现问题。 /etc/portage/make.conf
COMMON_FLAGS="-march=x86-64 -mtune=generic -O2 -pipe"
应用软件
不同系统的硬件内存不一样,而且显示器分辨率DPI也不一样,这需要在各个启动程序的启动脚本进行动态调整,比如有cgroup内存限制,需要计算百分比限制。浏览器启动的时候,自动适配当前显示器的DPI大小防止字体大小不一致。
sway也就是 桌面启动的时候有些许判断,不同硬件自定义桌面的一些环境变量
# Environ
export HARDWARE_MODEL=$(</sys/devices/virtual/dmi/id/board_name)
DPI_SCALE_X=$(</sys/module/orbment_dt/parameters/dpi_scale_x)
DPI_SCALE_Y=$(</sys/module/orbment_dt/parameters/dpi_scale_y)
GDK_DPI_SCALE=$(printf %.16f $(echo ${DPI_SCALE_X} / ${DPI_SCALE_Y} | bc -l))
export GDK_DPI_SCALE
case ${HARDWARE_MODEL} in
"MEG X570 ACE (MS-7C35)")
export WLR_NO_HARDWARE_CURSORS=1
;;
*)
true
esac
case $(</sys/module/orbment_dt/parameters/wlr_renderer) in
"1")
export WLR_RENDERER=gles2
;;
"2")
export WLR_RENDERER=vulkan
#export VK_INSTANCE_LAYERS=VK_LAYER_MESA_overlay
;;
*)
true
esac
exec /usr/bin/sway $@ 2>~/.local/share/sway/sway.log 1>&2
比如这是启动shell的时候,对nushell 限制内存使用率
mem_size = int(os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES') / 1024)
mem_limit_high = int(mem_size * 0.725)
mem_limit_max = int(mem_size * 0.725)
env = os.environ
os.execve("/usr/bin/systemd-run", ["/usr/bin/systemd-run",
"--user",
"--scope",
"-u", "nu-{}.scope".format(uuid.uuid4()),
"-p", "CPUQuota={}%".format((multiprocessing.cpu_count()-1)*100),
"-p", "MemoryHigh={}K".format(mem_limit_high),
"-p", "MemoryMax={}K".format(mem_limit_max),
"-p", "ManagedOOMMemoryPressure=kill",
"nu",
],
env
)
firefox 会计算当前DPI大小并且写入 firefox prefs.ini 启动的时候适配
ls -l /usr/lib64/firefox/browser/defaults/preferences/local-settings.js
lrwxrwxrwx 1 root root 40 Dec 26 17:47 /usr/lib64/firefox/browser/defaults/preferences/local-settings.js -> /run/user/1000/firefox-local-settings.js
def firefox_prefs_dpi(dpi_scale):
with open("{}/firefox-local-settings.js".format(XDG_RUNTIME_DIR), 'w') as fp:
fp.write("pref(\"layout.css.devPixelsPerPx\", \"{}\", locked);\n".format(dpi_scale))
fp.write("pref(\"browser.discovery.enabled\", false, locked);\n")
fp.write("pref(\"browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons\", false, locked);\n")
fp.write("pref(\"browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features\", false, locked);\n")
fp.write("pref(\"browser.sessionstore.interval\", 1800000, locked);\n")
fp.flush()
if "GDK_DPI_SCALE" in env.keys():
firefox_prefs_dpi(env["GDK_DPI_SCALE"])
env.pop("GDK_DPI_SCALE")
如果用了 foot terminal ,也会计算终端的字体大小
fontsize = 12
try:
settings = Gio.Settings.new("org.gnome.desktop.interface")
gdk_dpi_scale = settings.get_double("text-scaling-factor")
fontsize = round(gdk_dpi_scale * fontsize)
except:
pass
try:
gdk_dpi_scale = os.environ["GDK_DPI_SCALE"]
gdk_dpi_scale = float(gdk_dpi_scale)
fontsize = round(gdk_dpi_scale * fontsize)
except:
pass
os.execve("/usr/bin/foot", ["/usr/bin/foot",
"-f", "monospace:size={}".format(fontsize),
"tmux", "new-session", "-A", "-s", "id-{}".format(os.getuid())
],
os.environ
)