USB硬盘柜休眠方案(铁威马d8 hybrid混合硬盘柜实测有效,其他自测。适用于硬盘柜内部是 USB转SCSI转ATA 的桥接链路)
环境
- 依赖:Python3(系统自带)、cron(系统自带)、systemd(系统自带)、hdparm(系统自带)
- 无需额外安装任何软件包
停转策略
| 模式 |
方式 |
效果 |
原因 |
| normal(首次停转) |
Python SCSI STOP |
停硬盘+风扇+灯 |
无盘在工作,安全停整柜 |
| post_stop(静默唤醒后) |
hdparm -y |
只停硬盘 |
避免 SCSI STOP 再次触发唤醒 |
| 关机 |
Python SCSI STOP |
停硬盘+风扇+灯 |
所有盘都要停 |
文件清单
| 文件 |
用途 |
/usr/local/bin/scsi-stop.py |
发送 SCSI STOP 命令 |
/usr/local/bin/hdd-auto-spindown.sh |
cron 定时检测,自动停转 |
/usr/local/bin/hdd-powersave.sh |
关机停转钩子 |
/etc/systemd/system/hdd-powersave.service |
systemd 关机服务 |
/var/lib/hdd-spindown/ |
IO 状态记录目录 |
安装步骤
第一步:确认硬盘布局(sda、sdb、sdc这些,飞牛内硬盘信息能看到)x
下文出现的sda、sdb或者 /dev/sda、/dev/sdb按实际填写
第二步:停掉飞牛硬盘电源管理服务(可选,避免意外唤醒,如无可不选择)
systemctl stop trim_diskpowerd
systemctl disable trim_diskpowerd
第三步:创建 Python SCSI STOP 脚本
cat > /usr/local/bin/scsi-stop.py << 'PYEOF'
#!/usr/bin/env python3
import sys, os, ctypes, fcntl
SG_IO = 0x2285
class SgIoHdr(ctypes.Structure):
_fields_ = [
("interface_id", ctypes.c_int),
("dxfer_direction", ctypes.c_int),
("cmd_len", ctypes.c_ubyte),
("mx_sb_len", ctypes.c_ubyte),
("iovec_count", ctypes.c_ushort),
("dxfer_len", ctypes.c_uint),
("dxferp", ctypes.c_void_p),
("cmdp", ctypes.c_void_p),
("sbp", ctypes.c_void_p),
("timeout", ctypes.c_uint),
("flags", ctypes.c_uint),
("pack_id", ctypes.c_int),
("usr_ptr", ctypes.c_void_p),
("status", ctypes.c_ubyte),
("masked_status", ctypes.c_ubyte),
("msg_status", ctypes.c_ubyte),
("sb_len_wr", ctypes.c_ubyte),
("host_status", ctypes.c_ushort),
("driver_status", ctypes.c_ushort),
("resid", ctypes.c_int),
("duration", ctypes.c_uint),
("info", ctypes.c_uint),
]
def scsi_stop(dev):
cdb = (ctypes.c_ubyte * 6)(0x1b, 0x00, 0x00, 0x00, 0x00, 0x00)
sense = (ctypes.c_ubyte * 32)()
hdr = SgIoHdr()
hdr.interface_id = ord('S')
hdr.dxfer_direction = -1
hdr.cmd_len = 6
hdr.mx_sb_len = 32
hdr.dxfer_len = 0
hdr.dxferp = 0
hdr.cmdp = ctypes.addressof(cdb)
hdr.sbp = ctypes.addressof(sense)
hdr.timeout = 20000
fd = os.open(dev, os.O_RDONLY)
try:
fcntl.ioctl(fd, SG_IO, hdr)
print(f"OK: SCSI STOP sent to {dev}")
except OSError as e:
print(f"FAIL: {dev}: {e}", file=sys.stderr)
finally:
os.close(fd)
if __name__ == '__main__':
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} /dev/sdX [/dev/sdY ...]")
sys.exit(1)
for dev in sys.argv[1:]:
scsi_stop(dev)
PYEOF
chmod +x /usr/local/bin/scsi-stop.py
第四步:创建 cron 定时停转脚本
cat > /usr/local/bin/hdd-auto-spindown.sh << 'EOF'
#!/bin/bash
STATE_DIR="/var/lib/hdd-spindown"
mkdir -p "$STATE_DIR"
PATH=/usr/sbin:/usr/bin:/sbin:/bin
# 正常模式:5分钟无IO后停转(SCSI STOP,停硬盘+风扇+灯)
NORMAL_IDLE=5
# 停转后模式:1分钟无IO后再次停转(hdparm -y,只停硬盘)
POST_STOP_IDLE=1
for dev in /dev/sd[a-z]; do
[ -b "$dev" ] || continue
base=$(basename "$dev")
read_ops=$(awk '{print $1}' /sys/block/$base/stat 2>/dev/null)
write_ops=$(awk '{print $5}' /sys/block/$base/stat 2>/dev/null)
current_io="${read_ops:-0}_${write_ops:-0}"
state_file="$STATE_DIR/$base"
count_file="$STATE_DIR/${base}_count"
mode_file="$STATE_DIR/${base}_mode"
mode=$(cat "$mode_file" 2>/dev/null || echo "normal")
if [ "$mode" = "post_stop" ]; then
max_idle=$POST_STOP_IDLE
else
max_idle=$NORMAL_IDLE
fi
if [ -f "$state_file" ]; then
last_io=$(cat "$state_file")
if [ "$current_io" = "$last_io" ]; then
mnt=$(lsblk -n -o MOUNTPOINT "/dev/${base}" 2>/dev/null | head -1)
if [ -n "$mnt" ]; then
open_count=$(lsof +D "$mnt" 2>/dev/null | wc -l)
if [ "$open_count" -gt 0 ]; then
echo "0" > "$count_file"
if [ "$mode" = "post_stop" ]; then
echo "normal" > "$mode_file"
logger -t hdd-spindown "$dev has active users, switch to normal mode"
fi
echo "$current_io" > "$state_file"
continue
fi
fi
count=$(cat "$count_file" 2>/dev/null || echo 0)
count=$((count + 1))
echo "$count" > "$count_file"
if [ "$count" -ge "$max_idle" ]; then
if [ "$mode" = "post_stop" ]; then
hdparm -y "$dev" > /dev/null 2>&1
logger -t hdd-spindown "$dev stopped by hdparm -y (post_stop)"
else
python3 /usr/local/bin/scsi-stop.py "$dev"
logger -t hdd-spindown "$dev stopped by SCSI STOP (normal)"
echo "post_stop" > "$mode_file"
fi
echo "0" > "$count_file"
fi
else
echo "0" > "$count_file"
if [ "$mode" = "post_stop" ]; then
echo "normal" > "$mode_file"
logger -t hdd-spindown "$dev active, switch to normal mode"
fi
fi
fi
echo "$current_io" > "$state_file"
done
EOF
chmod +x /usr/local/bin/hdd-auto-spindown.sh
第五步:设置 cron 定时任务
(crontab -l 2>/dev/null | grep -v "hdd-auto-spindown.sh"; echo "* * * * * /usr/local/bin/hdd-auto-spindown.sh") | crontab -
确认:
crontab -l | grep -v "^#"
第六步:创建关机自动停转钩子
cat > /usr/local/bin/hdd-powersave.sh << 'EOF'
#!/bin/bash
for dev in /dev/sd[a-z]; do
[ -b "$dev" ] || continue
python3 /usr/local/bin/scsi-stop.py "$dev"
done
EOF
chmod +x /usr/local/bin/hdd-powersave.sh
cat > /etc/systemd/system/hdd-powersave.service << 'EOF'
[Unit]
Description=Spin down USB HDDs before shutdown
DefaultDependencies=no
Before=shutdown.target reboot.target halt.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/hdd-powersave.sh
TimeoutStartSec=10
[Install]
WantedBy=halt.target reboot.target shutdown.target
EOF
systemctl daemon-reload
systemctl enable hdd-powersave.service
第七步:验证
手动测试停转:
python3 /usr/local/bin/scsi-stop.py /dev/sdb
观察:硬盘停转 + 风扇停 + 指示灯灭 + 不会立即唤醒。如果硬盘重新启动,再执行hdparm -y 硬盘。
手动执行 cron 脚本:
/usr/local/bin/hdd-auto-spindown.sh
查看日志:
journalctl -t hdd-spindown --no-pager -n 20
确认 cron:
crontab -l | grep -v "^#"
确认关机钩子:
systemctl is-enabled hdd-powersave.service
第八步:BIOS 开启 ErP/EuP(可选但推荐)
关机后切断 USB 5V 待机供电,防止硬盘柜被重新唤醒。
工作逻辑
两种模式
| 模式 |
触发条件 |
等待时间 |
停转方式 |
效果 |
| normal |
正常运行/有IO后 |
5分钟 |
Python SCSI STOP |
停硬盘+风扇+灯 |
| post_stop |
首次停转成功后 |
1分钟 |
hdparm -y |
只停硬盘 |
为什么两种方式
SCSI STOP 会停风扇和指示灯,效果最好
但有概率在静默唤醒场景下再次触发唤醒
hdparm -y 只停硬盘,不停风扇和灯
不会触发再次唤醒,适合频繁的静默唤醒兜底
首次停转用 SCSI STOP → 彻底停止
静默唤醒后用 hdparm -y → 安全兜底,不触发再次唤醒
三重保护
检测到IO变化 → 重置计数,不停转
检测到有进程使用 → 重置计数,不停转
无IO + 无进程使用 → 计数+1 → 达到阈值 → 停转
完整流程
开机
→ 状态:normal
→ cron 每分钟检查一次
运行中(normal 模式)
→ IO有变化 → 重置计数
→ IO无变化 + 有进程使用挂载点 → 重置计数
→ IO无变化 + 无进程使用 → 计数+1
→ 连续5次无IO(5分钟)→ Python SCSI STOP
→ 硬盘停转 + 风扇停 + 灯灭
→ 切换到 post_stop 模式
被静默唤醒(post_stop 模式)
→ 风扇不转,灯不亮,只有硬盘空转
→ IO无变化 + 无进程使用 → 计数+1
→ 连续1次无IO(1分钟内)→ hdparm -y
→ 只停硬盘,不触发再次唤醒
有IO访问
→ 硬盘自动唤醒
→ IO计数变化 → 重置计数 → 切换回 normal 模式
→ 等待5分钟无IO后再用 SCSI STOP 停转
关机
→ systemd 钩子 → Python SCSI STOP → 所有盘停转
→ (若开ErP)USB口断 电 → 硬盘柜彻底断 电
开机
→ USB恢复供电 → 硬盘柜通电 → 硬盘自动上电
→ cron 重新开始监控
手动操作
手动停转单块盘(SCSI STOP,停硬盘+风扇+灯)
python3 /usr/local/bin/scsi-stop.py /dev/sdb
手动停转多块盘
python3 /usr/local/bin/scsi-stop.py /dev/sda /dev/sdb /dev/sdc /dev/sdd
手动停转所有 sd 盘
for dev in /dev/sd[a-z]; do [ -b "$dev" ] && python3 /usr/local/bin/scsi-stop.py "$dev"; done
手动停转单块盘(hdparm -y,只停硬盘)
hdparm -y /dev/sdb
手动执行一次自动停转脚本(模拟 cron)
/usr/local/bin/hdd-auto-spindown.sh
手动执行关机停转脚本
/usr/local/bin/hdd-powersave.sh
查看状态
查看硬盘模式、IO状态和计数
for dev in /dev/sd[a-z]; do [ -b "$dev" ] || continue; base=$(basename "$dev"); io=$(cat /var/lib/hdd-spindown/$base 2>/dev/null || echo "未记录"); count=$(cat /var/lib/hdd-spindown/${base}_count 2>/dev/null || echo "0"); mode=$(cat /var/lib/hdd-spindown/${base}_mode 2>/dev/null || echo "normal"); echo "$base: IO=$io count=$count mode=$mode"; done
输出示例:
sda: IO=9778_1162 count=2 mode=normal
sdb: IO=552_0 count=0 mode=post_stop
查看 cron 日志
journalctl -t hdd-spindown --no-pager -n 20
查看实时日志
journalctl -t hdd-spindown -f
按 Ctrl + C 退出。
查看 cron 任务
crontab -l | grep -v "^#"
查看关机钩子状态
systemctl is-enabled hdd-powersave.service
查看 IO 状态记录
ls -la /var/lib/hdd-spindown/
查看有没有进程在访问硬盘
lsof /dev/sdb
修改参数
修改正常模式空闲时间
编辑 /usr/local/bin/hdd-auto-spindown.sh,修改 NORMAL_IDLE:
NORMAL_IDLE=5 → 5分钟无IO后停转
NORMAL_IDLE=10 → 10分钟无IO后停转
NORMAL_IDLE=30 → 30分钟无IO后停转
修改停转后模式等待时间
编辑 /usr/local/bin/hdd-auto-spindown.sh,修改 POST_STOP_IDLE:
POST_STOP_IDLE=1 → 静默唤醒后1分钟内再次停转
POST_STOP_IDLE=2 → 静默唤醒后2分钟内再次停转
常见问题
硬盘不自动停转
(可选)自带的 trim_diskpowerd 服务可能会定期用 smartctl 检查硬盘,导致IO计数被重置。需要停掉:
systemctl stop trim_diskpowerd
systemctl disable trim_diskpowerd
为5分钟休眠后,风扇停了,硬盘又重新启动
等待1分钟后触发hdparm -y,观察是否停掉不会唤醒
为什么每分钟执行,停转后被静默唤醒
硬盘柜桥接芯片可能会在无IO的情况下唤醒硬盘(风扇不转、灯不亮)。post_stop 模式会在1分钟内用 hdparm -y 再次停转。
关机后硬盘柜重新被唤醒
主机USB口有5V待机供电,硬盘柜检测到USB有电会重新初始化。需要在BIOS开启ErP/EuP。
卸载步骤
crontab -l 2>/dev/null | grep -v "hdd-auto-spindown.sh" | crontab -
systemctl stop hdd-powersave.service
systemctl disable hdd-powersave.service
systemctl daemon-reload
rm -f /usr/local/bin/scsi-stop.py
rm -f /usr/local/bin/hdd-auto-spindown.sh
rm -f /usr/local/bin/hdd-powersave.sh
rm -f /etc/systemd/system/hdd-powersave.service
rm -rf /var/lib/hdd-spindown
如果需要恢复飞牛硬盘电源管理:
systemctl enable trim_diskpowerd
systemctl start trim_diskpowerd
技术原理
USB硬盘柜桥接链路
主机 → USB → 桥接芯片(SCSI层) → 硬盘(ATA层)
hdparm -y(ATA STANDBY):只到达硬盘层,桥接芯片不知道设备要停止,风扇继续转
sg_start --stop(SCSI STOP UNIT):到达桥接芯片层,但打开设备文件时触发桥接芯片唤醒
- Python
SG_IO ioctl:直接通过内核 ioctl 发送 SCSI STOP,不触发设备文件打开事件
为什么两种方式混用
SCSI STOP 效果最好(停硬盘+风扇+灯)
但有概率在静默唤醒场景下再次触发唤醒
hdparm -y 只停硬盘,不影响桥接芯片整体状态
不会触发再次唤醒,适合频繁的静默唤醒兜底
两种方式不在同一次执行中混用,避免桥接芯片冲突
双模式监控逻辑
正常模式(NORMAL_IDLE=5)
首次停转需要5分钟无IO,避免频繁启停
使用 SCSI STOP 彻底停止
停转后模式(POST_STOP_IDLE=1)
被静默唤醒后1分钟内再次停转,快速响应
使用 hdparm -y 安全兜底
有IO活动
立即重置为正常模式,避免打扰用户使用