本篇文章已有2人收藏 |
收藏 ![[技术·笔记] 修复群晖 NAS 盘位显示不全及 USB 外置硬盘丢失的问题 1 1788156886 portcfg featured](https://blog.grayzhao.com/wp-content/uploads/2026/08/1788156886-portcfg-featured-1024x576.png)
各位旅行者,今天来唠一个我折腾黑群晖时踩的坑——群晖 NAS 盘位显示不全,外加一块 USB 外置硬盘凭空「消失」了。
现象挺邪门的:存储空间管理器里的盘位对不上实际插盘数,插上去的 USB 移动硬盘也不在「外接设备」里露脸,反而神不知鬼不觉地钻进了「内置硬盘」的队伍。o(TヘTo) 折腾一圈才发现,根子不在硬盘、不在接口,而在群晖那四个神秘的 portcfg 参数上。
本教程适用于:DSM 7.x 系列,XPEnology(RR / ARPL / ARC 等引导)。本文以 RR 引导 + DS3617xs 为例,但原理对所有群晖机型通用。
遇到问题
- 盘位显示不全:存储空间管理器里的内置盘数量和实际插盘对不上,看着就像「少了几块盘」。
- USB 外置硬盘「消失」:插上的 USB 移动硬盘不出现在「外接设备 → 外接硬盘」里,反而被误判成了内置盘,差点混进存储池。😱
分析原因
一、四个参数决定一块盘的「身份」
DSM 启动时,需要知道每一块磁盘到底是「内置硬盘」「eSATA 外置」还是「USB 外置」。这个分类不是看物理接口自动判断的,而是靠四个配置值——写在 synoinfo.conf 里、由引导在编译时固化:
| 参数 |
含义 |
internalportcfg |
位掩码:哪些磁盘索引是内置盘(进存储池、显示在「存储空间 → HDD」) |
esataportcfg |
位掩码:哪些磁盘索引是 eSATA 外置盘 |
usbportcfg |
位掩码:哪些磁盘索引是 USB 外置盘(显示在「外接设备 → 外接硬盘」) |
maxdisks |
整数:DSM 最多识别多少块内置盘(上限) |
一句话:前三个是「分类标签」,maxdisks 是「数量上限」。一块盘该归哪类,完全由它的索引落在哪个掩码里决定。
二、位掩码,而且是 1-based
每个 *portcfg 都是一个十六进制数,把它展开成二进制后,每一位对应一个磁盘索引。这里有个最容易踩的坑——它是 1-based 的:
关键规则(1-based):第 N 块盘对应第 (N−1) 位,即磁盘索引 N 的位 = 1 << (N-1)。
举例:要让「磁盘索引 22」被标记为某类,操作的是 bit 21(因为 22−1=21)。常用对照如下:
| 磁盘索引 N |
对应的 bit |
hex |
| 1 |
bit 0 |
0x00000001 |
| 4 |
bit 3 |
0x00000008 |
| 21 |
bit 20 |
0x00100000 |
| 22 |
bit 21 |
0x00200000 |
| 29 |
bit 28 |
0x10000000 |
| 30 |
bit 29 |
0x20000000 |
| 32 |
bit 31 |
0x80000000 |
P.S. 至于怎么确认它确实是 1-based 而非 0-based,第七节的脚本会用一个巧妙的方法验证,先别急。
三、根因:USB 盘落进了内置索引区间
群晖给每块盘分配一个从 1 开始的整数编号(Disk 1、Disk 2……),叫「磁盘索引」:
- 它由开机枚举顺序决定,不是你能直接指定的;
- SATA 盘的索引通常 ≈ 它的 ata 端口号(ata4 → 索引4);
- USB 盘没有固定的 ata 号,会被塞进当前空闲的索引槽(往往紧跟最后一块 SATA 盘)。
正因如此,USB 盘可能落到一个很小的索引——落在内置区间里,从而被误判成内置盘。这就是本次问题的根因。
解决办法
四、通用计算流程
照着下面三步,就能把每块盘归好类、算出掩码。
- 列出所有盘的「索引 + 通道」。SSH 进 DSM 执行:
# 每块盘的 DSM 磁盘索引
for d in /run/synostorage/disks/*; do
dev=$(basename "$d")
echo "$dev id=$(cat $d/id 2>/dev/null)"
done
# 每块盘走的是 SATA(ata) 还是 USB(usb) 通道
for d in /sys/block/sd[a-z]; do
[ -e "$d" ] || continue
echo "$(basename $d) -> $(readlink -f $d/device)"
done
读 id 文件拿索引,看 sysfs 路径里含 ata 还是 usb 判断通道。
- 按物理通道给每个索引打标签:走
ata 且要内置 → internal;走 usb → usb;走 eSATA → esata。
- 把标签转成 hex 掩码:把同一类的所有索引 N 按位或起来即可。
# 伪代码:把该类所有索引 N 的位 OR 起来
mask = 0
for N in 该类所有索引:
mask |= (1 << (N-1)) # 最终写成 0x + 8 位十六进制
P.S. 算起来烦的话,直接用第六、七节的两个脚本,别手算。
五、完整实例:把一块 USB 盘从内置挪到外置
我这次的现场盘位长这样:
| 盘 |
索引 |
通道 |
应归类 |
| sdd |
4 |
ata |
internal |
| sdp |
16 |
ata |
internal |
| sds |
19 |
ata |
internal |
| sdu |
21 |
ata |
internal |
| sdv |
22 |
usb |
→ usb(当前被当内置) |
| sdaa |
27 |
ata |
internal |
| sdab |
28 |
ata |
internal |
| sdac |
29 |
ata |
internal |
目标:把索引 22(sdv,USB 盘)从 internal 挪到 usb。算位:索引 22 → bit 21 → 1<<21 = 0x00200000。
# 改之前
internalportcfg = 0x1DFFFFFF
usbportcfg = 0xE0000000 (bit29/30/31 = 索引30/31/32)
# 改之后(bit21 从 internal 清掉、在 usb 置位)
internalportcfg = 0x1DFFFFFF & ~0x00200000 = 0x1DDFFFFF
usbportcfg = 0xE0000000 | 0x00200000 = 0xE0200000
手算复核一下 0x1DDFFFFF:把 0x1DFFFFFF 展开成二进制,把 bit21 那一位从 1 改成 0,就是 0x1DDFFFFF,对得上。✓
改在哪、怎么生效:这几个值是编译时固化的,不是改 /etc/synoinfo.conf 就能完事。以 RR 引导为例,得改引导盘第一分区 /dev/synoboot1 里的 user-config.yml,然后重新编译引导 + 重启。详见「坑与注意事项」。
附赠两个脚本
六、portcfg-autocalc.sh:诊断 + 自动算掩码
手动算位太费劲,我写了个脚本一把梭。它的逻辑是:对每块有索引的盘,比对「物理通道」和「当前 portcfg 分类」,把不一致的盘标出来,并给出修正后的掩码(只挪不一致盘的位,保留其余范围)。先诊断、再给修正建议,一目了然。
展开查看 portcfg-autocalc.sh 完整代码
展开 / 收起
#!/bin/bash
# portcfg-autocalc.sh
# Diagnose + suggest corrected internalportcfg / usbportcfg / esataportcfg.
#
# Principle (IMPORTANT): internalportcfg must RESERVE the full SATA port range
# (including empty slots, so future disks are auto-internal). So we do NOT
# rebuild it from scratch by OR-ing present disks. Instead we KEEP the current
# masks and only MOVE the bit of any disk whose physical channel disagrees with
# its current portcfg classification (e.g. a USB disk sitting at an internal index).
#
# Ground truth = physical channel from sysfs:
# path contains "usb" -> should be usb
# path contains "ata" -> should be internal (or esata if you list it below)
set -u
# ---- config: list eSATA disk indices here (space separated), if any ----
ESATA_INDICES=""
get_cfg() {
grep -iE "^$1=" /etc/synoinfo.conf 2>/dev/null | head -1 \
| sed -E 's/.*="?0x([0-9A-Fa-f]+)"?.*/\1/I'
}
INT=$(get_cfg internalportcfg); USB=$(get_cfg usbportcfg); ESA=$(get_cfg esataportcfg)
INTV=$((16#$INT)); USBV=$((16#$USB)); ESAV=$((16#$ESA))
nINT=$INTV; nUSB=$USBV; nESA=$ESAV # working copies we will adjust
is_esata_idx() { [ -n "$ESATA_INDICES" ] && echo " $ESATA_INDICES " | grep -q " $1 "; }
class_of_bit() {
local b=$1
if (( USBV & (1<<b) )); then echo usb
elif (( ESAV & (1<<b) )); then echo esata
elif (( INTV & (1<<b) )); then echo internal
else echo none; fi
}
printf "%-5s %-4s %-5s | %-10s %-10s | %s\n" DEV IDX CHAN CUR_CLASS EXPECT STATUS
echo "-----+----+------+----------+----------+------------------"
moves=""
for d in /run/synostorage/disks/*; do
[ -e "$d" ] || continue
dev=$(basename "$d"); id=$(cat "$d/id" 2>/dev/null); [ -z "$id" ] && continue
p=$(readlink -f "/sys/block/$dev/device" 2>/dev/null)
case "$p" in
*usb*) ch="usb"; exp="usb" ;;
*ata*) ch="ata"; if is_esata_idx "$id"; then exp="esata"; else exp="internal"; fi ;;
*) ch="?"; exp="?" ;;
esac
b=$((id-1)) # 1-based
cur=$(class_of_bit $b)
if [ "$cur" = "$exp" ]; then st="ok"; else
st="MISMATCH -> move bit$b to $exp"
# remove this bit from all three working masks, then set it in the expected one
nINT=$(( nINT & ~(1<<b) )); nUSB=$(( nUSB & ~(1<<b) )); nESA=$(( nESA & ~(1<<b) ))
case "$exp" in
internal) nINT=$(( nINT | (1<<b) )) ;;
usb) nUSB=$(( nUSB | (1<<b) )) ;;
esata) nESA=$(( nESA | (1<<b) )) ;;
esac
moves="$moves\n $dev (idx=$id, chan=$ch): bit$b $cur -> $exp"
fi
printf "%-5s %-4s %-5s | %-10s %-10s | %s\n" "$dev" "$id" "$ch" "$cur" "$exp" "$st"
done
echo
echo "=== masks ==="
printf "current internalportcfg = 0x%08X\n" "$INTV"
printf "current usbportcfg = 0x%08X\n" "$USBV"
printf "current esataportcfg = 0x%08X\n" "$ESAV"
echo
printf "suggested internalportcfg = 0x%08X\n" "$nINT"
printf "suggested usbportcfg = 0x%08X\n" "$nUSB"
printf "suggested esataportcfg = 0x%08X\n" "$nESA"
echo
if [ -n "$moves" ]; then
echo "=== changes (bit moves) ==="
echo -e "$moves"
echo
echo "Apply: edit user-config.yml on /dev/synoboot1 with the suggested masks,"
echo " recompile the RR loader, reboot. (See portcfg-guide.md.)"
else
echo "No mismatch found. Current masks agree with physical channels -> nothing to change."
fi
下载 portcfg-autocalc.sh
用法:传到 NAS 直接 bash portcfg-autocalc.sh。如果你有 eSATA 盘,把脚本顶部的 ESATA_INDICES="" 填上它的索引(空格分隔),那些 ata 通道的盘会被归到 esataportcfg 而非 internal。
七、portcfg-base-check.sh:判定 1-based 还是 0-based
还有个隐藏坑:1-based 和 0-based 对大多数盘的分类是一样的(因为低位常全置位或全空),所以光看一块盘往往判不出来。只有「区分盘」——索引 N 使得 bit(N-1) 和 bitN 落在不同掩码的盘——才是判定关键。这个脚本就是自动找出区分盘、并给最终结论的。
展开查看 portcfg-base-check.sh 完整代码
展开 / 收起
#!/bin/bash
# Verify whether portcfg is 1-based (bit N-1 = disk N) or 0-based (bit N = disk N)
# Ground truth = physical channel from sysfs (usb->expect usb; ata->expect internal/esata).
# Only "distinguishing" disks (where the two interpretations give different classes) are decisive.
set -u
get_cfg() {
grep -iE "^$1=" /etc/synoinfo.conf 2>/dev/null | head -1 \
| sed -E 's/.*="?0x([0-9A-Fa-f]+)"?.*/\1/I'
}
INT=$(get_cfg internalportcfg); USB=$(get_cfg usbportcfg); ESA=$(get_cfg esataportcfg)
INTV=$((16#$INT)); USBV=$((16#$USB)); ESAV=$((16#$ESA))
echo "internalportcfg = 0x$INT"
echo "usbportcfg = 0x$USB"
echo "esataportcfg = 0x$ESA"
echo
class_of_bit() {
local b=$1
if (( USBV & (1<<b) )); then echo usb
elif (( ESAV & (1<<b) )); then echo esata
elif (( INTV & (1<<b) )); then echo internal
else echo none; fi
}
printf "%-5s %-4s %-5s | %-16s %-16s | %s\n" DEV IDX CHAN "1-based(bitN-1)" "0-based(bitN)" DISTINGUISH
echo "-----+----+------+------------------+------------------+-----------"
m1=0; m0=0; nd=0
for d in /run/synostorage/disks/*; do
[ -e "$d" ] || continue
dev=$(basename "$d"); id=$(cat "$d/id" 2>/dev/null); [ -z "$id" ] && continue
p=$(readlink -f "/sys/block/$dev/device" 2>/dev/null)
case "$p" in
*usb*) ch="usb"; exp=usb ;;
*ata*) ch="ata"; exp="int/esata" ;;
*) ch="?"; exp="?" ;;
esac
b1=$((id-1)); b0=$id
c1=$(class_of_bit $b1); c0=$(class_of_bit $b0)
if [ "$c1" = "$c0" ]; then dif="no"; else dif="YES <--key"; nd=$((nd+1)); fi
printf "%-5s %-4s %-5s | %-16s %-16s | %s\n" "$dev" "$id" "$ch" "$c1(bit$b1)" "$c0(bit$b0)" "$dif"
done
echo
echo "=== Verdict from distinguishing disks ==="
if [ "$nd" -eq 0 ]; then
echo "No distinguishing disk found (both interpretations agree on every disk)."
echo "Cannot decide base; need a disk whose bit(N-1) and bitN fall in different cfgs."
exit 0
fi
for d in /run/synostorage/disks/*; do
[ -e "$d" ] || continue
dev=$(basename "$d"); id=$(cat "$d/id" 2>/dev/null); [ -z "$id" ] && continue
p=$(readlink -f "/sys/block/$dev/device" 2>/dev/null)
case "$p" in *usb*) ch=usb;; *ata*) ch=ata;; *) ch=?;; esac
b1=$((id-1)); b0=$id
c1=$(class_of_bit $b1); c0=$(class_of_bit $b0)
[ "$c1" = "$c0" ] && continue
case "$ch" in
usb) ok1=$([ "$c1" = usb ] && echo match || echo MISMATCH); ok0=$([ "$c0" = usb ] && echo match || echo MISMATCH) ;;
ata) ok1=$({ [ "$c1" = internal ]||[ "$c1" = esata ];} && echo match || echo MISMATCH); ok0=$({ [ "$c0" = internal ]||[ "$c0" = esata ];} && echo match || echo MISMATCH) ;;
*) ok1=unknown; ok0=unknown ;;
esac
[ "$ok1" = match ] && m1=$((m1+1))
[ "$ok0" = match ] && m0=$((m0+1))
echo "$dev: idx=$id chan=$ch | 1-based->$c1($ok1) 0-based->$c0($ok0)"
done
echo
echo "Distinguishing-disk tally: 1-based matches=$m1, 0-based matches=$m0"
if [ "$m1" -gt "$m0" ]; then echo "CONCLUSION: portcfg is 1-based (bit N-1 = disk index N)"
elif [ "$m0" -gt "$m1" ]; then echo "CONCLUSION: portcfg is 0-based (bit N = disk index N)"
else echo "CONCLUSION: tie or undecided (check the table for misconfigured disks)"; fi
下载 portcfg-base-check.sh
坑与注意事项
- USB 盘索引不稳定:增减 SATA 盘会改变 USB 盘的索引。本次把「索引22=USB」固化进掩码,只要不动 SATA 盘就稳定;动了就要重算。
- 编译时固化,别只改运行时:只改
/etc/synoinfo.conf 会被引导在每次启动时覆盖回去,必须改 user-config.yml + 重新编译。
- 编译时别重跑自动检测:RR 菜单里的「磁盘自动检测」可能用枚举结果重算 portcfg,覆盖你手改的值,只选「编译/Build」。
- 挂载引导盘要加
-o loop:直接挂会报 wrong fs type。
# 挂载引导盘第一分区(必须 -o loop)
mount -t vfat -o loop,rw /dev/synoboot1 /mnt
- maxdisks 可能被机型钳制:配置写了不一定生效(DS3617xs 实测写 32 → 实际 26)。
- 改之前先记索引:盘一旦被正确分类为外置,就会从
/run/synostorage/disks/ 消失,届时再查索引就难了。
- 1-based 别搞反:索引 N 操作 bit (N-1),搞反会把相邻盘误分类。
- 先备份:改
user-config.yml 前先 cp 一份(如 user-config.yml.bak)。
遗留问题
- USB 盘的索引会随 SATA 盘增减而漂移,所以这套「固化索引到掩码」的方案只在盘位不变时稳定;哪天加盘、减盘了,记得重新跑一遍脚本、重算一次。
免责声明:本文涉及引导配置的修改与重新编译,操作有风险。动手前请务必先备份 user-config.yml 及重要数据,并确认自己理解每一步在干什么。黑群晖/引导相关操作请自行斟酌,出现数据丢失或系统异常,作者不承担责任。
🔗 相关阅读:同为群晖 NAS 折腾,修复黑群晖无法生成缩略图及无法转码视频 一篇记录了用第三方 ffmpeg 替换修复;远程访问家中的服务,Frp 穿透群晖 NAS 获取真实 IP 也值得一读。
Comments NOTHING