在 FreeBSD 上以 lsblk(8) 风格列出块设备

当我需要在 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/beadmsysutils/automount,我认为编写 lsblk.sh 是一个不错的主意。我实际上在 2016 年就在这个主题 lsblk(8) Command for FreeBSDFreeBSD Forums 上‘开源’或说分享了这个项目/想法,但由于时间有限,这个“副项目”的开发进度非常缓慢。我最终重新回到它,完成了它。

lsblk.sh 是一个总体上小巧且简单的 shell 脚本,代码行数不到四百行。

image

下面是我在单硬盘笔记本上运行 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 时的其他示例。

image

虽然 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 版本现在避免了 MOUNTLABEL 检测的代码重复(已移入单一统一函数)。同时添加了更多注释以提高代码可读性,并进行了一些小修复……而且脚本再次变得更小 :🙂:

% 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 Systemls(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 中更新前请稍作等待。

最后更新于

这有帮助吗?