在 FreeBSD 上以 lsblk(8) 风格列出块设备
作者:𝚟𝚎𝚛𝚖𝚊𝚍𝚎𝚗
2019/09/27
当我需要在 Linux 系统上工作时,我通常会怀念很多 FreeBSD 上很棒的工具,比如仅举几例:
sockstat
gstat
top -b -o res
top -m io -o total
usbconfig
rcorder
beadm/bectl
idprio/rtprio
……但有时候——虽然很少——Linux 上也有一些非常有用的工具,而 FreeBSD 上则没有。例子就是 lsblk(8),它专注于一件事,而且做得相当好——列出块设备及其内容。它也有一些问题,比如在完全应用 ZFS 池使用的磁盘上,它会显示两个分区,而不是直接显示 ZFS 的信息——但我们都知道,在某些圈子里,CDDL 许可的 ZFS 在那个 GPL 世界里是多么地“不受待见”。
来自 Linux 系统的 lsblk(8) 示例输出:
$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sr0 11:0 1 1024M 0 rom
sda 8:0 0 931.5G 0 disk
|-sda1 8:1 0 500M 0 part /boot
`-sda2 8:2 0 931G 0 part
|-vg_local-lv_root (dm-0) 253:0 0 50G 0 lvm /
|-vg_local-lv_swap (dm-1) 253:1 0 17.7G 0 lvm [SWAP]
`-vg_local-lv_home (dm-2) 253:2 0 1.8T 0 lvm /home
sdc 8:32 0 232.9G 0 disk
`-sdc1 8:33 0 232.9G 0 part
`-md1 9:1 0 232.9G 0 raid10 /data
sdd 8:48 0 232.9G 0 disk
`-sdd1 8:49 0 232.9G 0 part
`-md1 9:1 0 232.9G 0 raid10 /data而 FreeBSD 在这方面提供了什么?可以使用命令 camcontrol(8) 和 geom(8)。你也可以使用 gpart(8) 命令来列出分区。下面是我在单磁盘笔记本上运行这些命令的输出。
# camcontrol devlist
<Samsung SSD 860 EVO mSATA 1TB RVT41B6Q> at scbus1 target 0 lun 0 (ada0,pass0)
% geom disk list
Geom name: ada0
Providers:
1. Name: ada0
Mediasize: 1000204886016 (932G)
Sectorsize: 512
Mode: r1w1e2
descr: Samsung SSD 860 EVO mSATA 1TB
lunid: 5002538e402b4ddd
ident: S41PNB0K303632D
rotationrate: 0
fwsectors: 63
fwheads: 1
# gpart show
=> 40 1953525088 ada0 GPT (932G)
40 409600 1 efi (200M)
409640 1024 2 freebsd-boot (512K)
410664 984 - free - (492K)
411648 1953112064 3 freebsd-zfs (931G)
1953523712 1416 - free - (708K)它们以可接受的方式提供了所需信息,但仅适用于磁盘数量较少的系统。如果你想显示所有系统驱动器内容的汇总呢?这时 lsblk.sh 就派上用场了。虽然 lsblk(8) 拥有许多有趣的功能,如 --perms/--scsi/--inverse 模式,我这里重点提供的只是最基本的功能——列出系统块设备及其内容。由于我在编写 shell 脚本方面有长期且愉快的经验,例如 sysutils/beadm 或 sysutils/automount,我认为编写 lsblk.sh 是一个不错的主意。我实际上在 2016 年就在这个主题 lsblk(8) Command for FreeBSD 的 FreeBSD Forums 上‘开源’或说分享了这个项目/想法,但由于时间有限,这个“副项目”的开发进度非常缓慢。我最终重新回到它,完成了它。
lsblk.sh 是一个总体上小巧且简单的 shell 脚本,代码行数不到四百行。
下面是我在单硬盘笔记本上运行 lsblk.sh 命令的示例输出。
% lsblk.sh
DEVICE MAJ:MIN SIZE TYPE LABEL MOUNT
ada0 0:5b 932G GPT - -
ada0p1 0:64 200M efi efiboot0 <UNMOUNTED>
ada0p2 0:65 512K freebsd-boot gptboot0 -
<FREE> -:- 492K - - -
ada0p3 0:66 931G freebsd-zfs zfs0 <ZFS>
<FREE> -:- 708K - - -同样的输出在图形窗口中显示。

下面是来自一台拥有两块系统固态硬盘(da0/da1)和两块机械数据盘(da2/da3)的服务器的 lsblk.sh 输出示例。
# lsblk.sh
DEVICE MAJ:MIN SIZE TYPE LABEL MOUNT
da0 0:be 224G GPT - -
da0p1 0:15a 200M efi efiboot0 <UNMOUNTED>
da0p2 0:15b 512K freebsd-boot gptboot0 -
<FREE> -:- 492K - - -
da0p3 0:15c 2.0G freebsd-swap swap0 <UNMOUNTED>
da0p4 0:15d 221G freebsd-zfs zfs0 <ZFS>
<FREE> -:- 580K - - -
da1 0:bf 224G GPT - -
da1p1 0:16a 200M efi efiboot1 <UNMOUNTED>
da1p2 0:16b 512K freebsd-boot gptboot1 -
<FREE> -:- 492K - - -
da1p3 0:16c 2.0G freebsd-swap swap1 <UNMOUNTED>
da1p4 0:16d 221G freebsd-zfs zfs1 <ZFS>
<FREE> -:- 580K - - -
da2 0:c0 11T GPT - -
da2p1 0:16e 11T freebsd-zfs - <ZFS>
<FREE> -:- 1.0G - - -
da3 0:c1 11T GPT - -
da3p1 0:16f 11T freebsd-zfs - <ZFS>
<FREE> -:- 1.0G - - -下面是我在其他系统上测试 lsblk.sh 时的其他示例。
虽然 lsblk.sh 并不是地球上最快的脚本(因为需要进行大量解析),但它能够很好地完成工作。如果你想在系统中安装它,只需输入以下命令:
# fetch -o /usr/local/bin/lsblk https://raw.githubusercontent.com/vermaden/scripts/master/lsblk.sh
# chmod +x /usr/local/bin/lsblk
# hash -r || rehash
# lsblk如果有时间,我可以考虑在 lsblk.sh 脚本中添加哪些其他原创的 Linux lsblk(8) 子命令/选项/参数呢?:🙂:
此致敬礼。
更新 1 – 添加 USAGE/HELP 信息
刚刚添加了一些用法信息,可以通过以下任意参数显示:
h
-h
--h
help
-help
--help
依我看,为这么一个简单的工具写 man 页面是没有必要的。我想等 lsblk.sh 工具在功能和选项上扩展到可与 Linux lsblk(8) 相媲美时,再创建专门的 man 页面。下面是它的显示效果。
# lsblk.sh --help
usage:
BASIC USAGE INFORMATION
=======================
# lsblk.sh [DISK]
example(s):
LIST ALL BLOCK DEVICES IN SYSTEM
--------------------------------
# lsblk.sh
DEVICE MAJ:MIN SIZE TYPE LABEL MOUNT
ada0 0:5b 932G GPT - -
ada0p1 0:64 200M efi efiboot0 <UNMOUNTED>
ada0p2 0:65 512K freebsd-boot gptboot0 -
<FREE> -:- 492K - - -
ada0p3 0:66 931G freebsd-zfs zfs0 <ZFS>
LIST ONLY da1 BLOCK DEVICE
--------------------------
# lsblk.sh da1
DEVICE MAJ:MIN SIZE TYPE LABEL MOUNT
da1 0:80 2.0G MBR - -
da1s1 0:80 2.0G freebsd - -
da1s1a 0:81 1.0G freebsd-ufs root /
da1s1b 0:82 1.0G freebsd-swap swap SWAP
hint(s):
DISPLAY ALL DISKS IN SYSTEM
---------------------------
# sysctl kern.disks
kern.disks: ada0 da0 da1此致敬礼。
更新 2 – 代码重组与重写 75%
……至少这是 git(1) 在 commit 信息中告诉我的内容。
% git commit (...)
[master 12fd4aa] Rework entire flow. Split code into functions. Add many useful comments. In other words its 2.0 version.
1 file changed, 494 insertions(+), 505 deletions(-)
rewrite lsblk.sh (75%)经过几个高效小时的工作,lsblk.sh 的新版本现已发布。
它的源代码行数类似,但现在缩小了四分之一……同时功能更多、准确性更高。这是 “少即是多” 的绝佳例子。
% wc scripts/lsblk.sh.OLD
491 2201 19721 scripts/lsblk.sh.OLD
% wc scripts/lsblk.sh
494 1871 15472 scripts/lsblk.sh一些没有简单解决方案的问题如下所述。
其中之一是 FAT 文件系统的“双重”标签。我们既有 /dev/gpt/efiboot0 标签,也有 FAT 标签 EFISYS。必须在两者中做出选择。由于并非所有 FAT 文件系统都有标签,我选择了 GPT 标签。
% glabel status | grep ada0p1
gpt/efiboot0 N/A ada0p1
msdosfs/EFISYS N/A ada0p1我也无法覆盖 FUSE 挂载。当你挂载——例如——/dev/da0 设备为 NTFS(使用 ntfs-3g)或 exFAT(使用 mount.exfat)时,mount(8) 输出没有明显区别。
% mount -t fusefs
/dev/fuse on /mnt/ntfs (fusefs)
/dev/fuse on /mnt/exfat (fusefs)当我通过守护进程(如 sysutils/automount)挂载此类文件系统时,我会在 /var/run/automount.state 文件中记录设备挂载到的目录。然后,当我收到 /dev/da0 设备的 detach 事件时,我就知道该卸载哪个挂载点……但当只有 /dev/fuse 设备时,这是不可能的。
……或者,也许你知道有什么方法可以从 /dev/fuse(或 FUSE 一般)中提取设备挂载位置的信息吗?
下面展示更新后的效果。
这里是各种非 ZFS 文件系统的挂载情况:
% mount -t nozfs
devfs on /dev (devfs, local, multilabel)
linprocfs on /compat/linux/proc (linprocfs, local)
tmpfs on /compat/linux/dev/shm (tmpfs, local)
/dev/label/ASD on /mnt/tmp (msdosfs, local)
/dev/fuse on /mnt/ntfs (fusefs)
/dev/md0s1f on /mnt/ufs.other (ufs, local)
/dev/gpt/OTHER on /mnt/fat.other (msdosfs, local)
/dev/md0s1a on /mnt/ufs (ufs, local)……现在 lsblk.sh 显示它们的方式如下。
% lsblk.sh
DEVICE MAJ:MIN SIZE TYPE LABEL MOUNT
ada0 0:56 932G GPT - -
ada0p1 0:64 200M efi gpt/efiboot0 -
ada0p2 0:65 512K freebsd-boot gpt/gptboot0 -
<FREE> -:- 492K - - -
ada0p3 0:66 931G freebsd-zfs - <ZFS>
<FREE> -:- 708K - - -
md0 0:28f 1.0G MBR - -
md0s1 0:294 512M freebsd - -
md0s1a 0:29a 100M freebsd-ufs root /mnt/ufs
md0s1b 0:29b 32M freebsd-swap label/swap SWAP
md0s1e 0:29c 64M freebsd-ufs - -
md0s1f 0:29d 316M freebsd-ufs - /mnt/ufs.other
md0s2 0:296 256M ntfs - -
md0s3 0:297 256M fat32 msdosfs/ONE -
md1 0:2a4 1.0G msdosfs LARGE
md2 0:298 2.0G GPT - -
md2p1 0:29f 2.0G ms-basic-data gpt/OTHER /mnt/fat.other我为此使用了一些基于文件的内存设备。现在,默认情况下 lsblk.sh 也会显示内存磁盘的内容。
% mdconfig.sh -l
md0 vnode 1024M /home/vermaden/FILE
md2 vnode 2048M /home/vermaden/FILE.GPT
md1 vnode 1024M /home/vermaden/FILER下面是在 xterm(1) 终端中的显示效果。

此致敬礼。
更新 3 – 添加 geli(8) 支持
我认为添加 geli(8) 支持可能会很有用。最新的 lsblk.sh 版本现在避免了 MOUNT 和 LABEL 检测的代码重复(已移入单一统一函数)。同时添加了更多注释以提高代码可读性,并进行了一些小修复……而且脚本再次变得更小 :🙂:
% wc lsblk.sh.1.0
491 2201 19721 lsblk.sh.1.0
% wc lsblk.sh.2.0
493 1861 15415 lsblk.sh.2.0
% wc lsblk.sh
488 1820 15332 lsblk.sh此次更新大约修改了 40%(根据 git commit 显示:191 行新增,196 行删除)。
# git commit (...)
[master ec9985a] Add geli(8) support. Avoid code duplication and move MOUNT/LABEL detection into function. More comments. Minor fixes.
1 file changed, 191 insertions(+), 196 deletions(-)还忘了提到,现在得益于智能优化(比如避免重复操作,并将 grep(1) | awk(1) 管道聚合为单个 awk(1) 查询),lsblk.sh 的运行速度比最初版本快了三倍 :🙂:
下面是添加 geli(8) 支持后的新输出。

此致敬礼。
更新 4 – 添加 fuse(8) 支持
如我在 更新 2 中所述,跟踪 fuse(8) 下的挂载设备及其挂载位置非常困难,因为挂载完成后,所有挂载的设备都会神奇地变成 /dev/fuse。
经过一些研究,我发现这个信息(在 FreeBSD 下通过 fuse(8) 接口实际挂载的设备位置)可以在挂载 procfs 文件系统于 /proc 后获取。你只需要查看所有 ntfs-3g 进程的 cmdline 条目。虽然不完美,但至少可以获取到这些信息。
# mount -t procfs proc /proc
# ps ax | grep ntfs-3g
45995 - Is 0:00.00 ntfs-3g /dev/md1s2 /mnt/ntfs
59607 - Is 0:00.00 ntfs-3g /dev/md3 /mnt/ntfs.another
83323 - Is 0:00.00 ntfs-3g /dev/md3 /mnt/ntfs.another
# pgrep ntfs-3g
59607
83323
45995
% pgrep ntfs-3g | while read I; do cat /proc/$I/cmdline; echo; done
ntfs-3g/dev/md3/mnt/ntfs.another
ntfs-3g/dev/md3/mnt/ntfs.another
ntfs-3g/dev/md1s2/mnt/ntfs这是用于检测 fuse(8) 挂载点的代码原型。
if [ -e /proc/0/status ]
then
FUSE_MOUNTS=$(
while read PID
do
cat /proc/${PID}/cmdline
echo
done << ________EOF
$( pgrep ntfs-3g )
________EOF
)
FUSE_MOUNTS=$( echo "${FUSE_MOUNTS}" | sort -u )
FUSE_MOUNTS=$( echo "${FUSE_MOUNTS}" | sed 's|ntfs-3g||g' )
FUSE_CHECKS=$( echo "${FUSE_MOUNTS}" | grep /dev/${TARGET}/ )
if [ "${FUSE_CHECKS}" != "" ]
then
MOUNT=$( echo "${FUSE_CHECKS}" | sed "s|/dev/${TARGET}||g" )
fi
fi
fi……我刚刚意识到,我找到了获取该信息的新方法(更好),无需挂载 /proc 文件系统——你只需要显示 ntfs-3g 进程及其命令行参数,例如如下方式:
% ps -p $( pgrep ntfs-3g | tr '\n' ',' | sed '$s/.$//' ) -o command | sed 1d
ntfs-3g /dev/md1s2 /mnt/ntfs
ntfs-3g /dev/md3 /mnt/ntfs.another
ntfs-3g /dev/md3 /mnt/ntfs.another因此,在我考虑到这最初仅针对 NTFS(ntfs-3g(8) 进程)后,我也添加了 exFAT 支持,通过搜索 mount.exfat 的 PID。现在 fuse(8) 挂载点检测可以同时支持 NTFS 和 exFAT 文件系统……而且支持该功能的代码甚至更简洁。
# 尝试从进程中获取 fuse(8) 挂载点
if [ "${MOUNT_FOUND}" != "1" ]
then
FUSE_PIDS=$( pgrep mount.exfat ntfs-3g | tr '\n' ',' | sed '$s/.$//' )
FUSE_MOUNTS=$( ps -p "${FUSE_PIDS}" -o command | sed 1d | sort -u )
MOUNT=$( echo "${FUSE_MOUNTS}" | grep "/dev/${TARGET} " | awk '{print $3}' )
fi我还修改了 MAJOR 和 MINOR 号的显示方式——从 HEX 改为 DEC,就像 Linux 一样。FreeBSD Base System 的 ls(1) 会以 HEX 显示,例如你会得到 0x2af 值:
% ls -l /dev/md4
crw-rw---- 1 root operator 0x2af 2019.09.29 05:18 /dev/md4但使用 FreeBSD Ports 中的 GNU 等价工具 gls(1)(来自 sysutils/coreutils 包),则以 DEC 值显示 MAJOR 和 MINOR。gls(1) 只是 Linux 世界的 ls(1),由于 FreeBSD 的 Base System 已有 ls(1),开发者在名称前加了 ‘g’(代表 GNU)来区分。
% gls -l /dev/md4
crw-rw---- 1 root 2, 175 2019-09-29 05:18 /dev/md4使用 stat(1) 工具也可以更方便/快速获取:
MAJ=$( stat -f "%Hr" /dev/${DEV} )
MIN=$( stat -f "%Lr" /dev/${DEV} )最新的 lsblk.sh 如下所示:

这也是我还没有将 lsblk.sh 添加到 FreeBSD Ports 的原因——几天内就发布了几版带有重要新功能的版本 :🙂:
此致敬礼。
更新 5 – 再次重写 69%
在进一步研究 gpart(8) 后,我发现使用参数 -p 是个重大改变。使用 -p 参数后,它会直接显示带有分区名的输出,不再需要自己寻找 PREFIX 并“创建”分区名。
默认 gpart(8) 输出:
# gpart show md0
=> 63 2097089 md0 MBR (1.0G)
63 1048576 1 freebsd (512M)
1048639 524288 2 ntfs (256M)
1572927 524225 3 fat32 (256M)使用参数 -p 的输出:
# gpart show -p md0
=> 63 2097089 md0 MBR (1.0G)
63 1048576 md0s1 freebsd (512M)
1048639 524288 md0s2 ntfs (256M)
1572927 524225 md0s3 fat32 (256M)这一发现导致 lsblk.sh 进行了相当大幅重写。git commit 估计此次重写达 69%:
# git commit (...)
(...)
1 file changed, 487 insertions(+), 501 deletions(-)
rewrite lsblk.sh (69%)最新 lsblk.sh 的特性如下:
修复了之前的 BUG。
可以检测 exFAT 标签。
运行速度提升 20%。
SLOC 减少 10%。
代码量减少 15%。
正确处理整个设备上的 bsdlabel(8)。
正确处理整个设备上的 exFAT。
代码差异如下:
# wc lsblk.sh
487 1791 13705 lsblk.sh
# wc lsblk.sh.OLD
544 1931 16170 lsblk.sh.OLD最新 lsblk.sh 看起来仍然如以前,但我现在用 ‘-’ 替代了以前的 ‘’ 标记:

更新 6 – 新的更新与修复版本
lsblk.sh 已更新至 3.4 版本——并已在 FreeBSD Ports 树中更新,在 sysutils/lsblk Port 中可用。
该版本的 变更日志 如下:
在磁盘列表中添加 sysctl -n kern.disks。
在 __gpart_present 函数中重置 LABEL。
修复 gpart(8) 的 [bootme] 和 [bootonce] 标志行为。
禁用 GPTID 显示标签。
添加 -d|–disks 选项,仅列出整个磁盘。
请注意,lsblk.sh 使用 diskinfo(8),为了正常工作,你需要属于 operator 组。你可以这样将自己添加到该组:
# pw groupmod operator -m yourself……或者通过编辑 /etc/group 文件。
示例输出如下:

更新 7 – 更多修复
lsblk.sh 已更新至 3.5 版本——并已在 FreeBSD Ports 树中更新,在 sysutils/lsblk Port 中可用。
该版本的 变更日志 如下:
列出磁盘时移除输出中的控制序列和颜色。
diskinfo(8) 仅用于 md(4) 磁盘,因为 geom(4) 不支持它们。
添加新注释并重新整理部分旧注释。
为 SIZE 收集和显示增加额外检查。
当整个设备没有分区时,正确显示 exFAT 文件系统标签。
修复 NTFS-3G 挂载点显示。
检查 automount(8) 的 /var/run/automount.state 以处理 fusefs(5) 文件系统。
仅 md(4) 磁盘的大小需要属于 operator 组。所有其他磁盘大小现在通过 geom(8) 命令收集。
更新 8 – 优化标签
现在 3.9 版本的 lsblk(8) 比以往更好。

该版本的 变更日志 如下:
改进 glabel(8) 标签搜索。
改进 gpart(8) 输出标签处理。
将冗长的 Microsoft 分区名称替换为合理名称。
改进 exFAT 处理。
改进 md(4) 磁盘大小处理。
移除标签中的冗余空格。
为 -d 选项添加 TOTAL SYSTEM STORAGE。
移除主设备检查循环中的子 shell。
已提交 PR 283268 ,因此在 Ports 中更新前请稍作等待。
最后更新于
这有帮助吗?