banner
约 2,100 字
7 分钟

飞牛os控制风扇转速的脚本实现降噪

-
-
无标签

摘要

针对拾光坞 G2 NAS 风扇噪音大且系统无控制的问题,作者利用拾光坞提供的底层工具 fanctrl,编写了 fan_control.sh 脚本,实现了基于 CPU 和硬盘温度的动态风扇控制。该脚本具备多源温度检测、休眠保护及逐步升速功能,并配置为开机自启服务。此方案有效解决了 BIOS 阉割导致的静音难题,兼顾了硬盘安全与使用体验。

前言

双十一入了拾光坞 G2,性价比其实很高,3 盘位完全满足我的需求,还是品牌 NAS,官方支持刷机且有 3 年质保,到手直接刷飞牛,近乎完美。

唯一缺点就是默认 40% 的风扇有点吵。飞牛系统没有风扇控制的选项,装了 CoolerControl 但没有风扇驱动,BIOS 好像也是阉割过的、没有风扇控制功能。找了一圈发现 fanctrl 可以控制,改写了一个控温脚本,目前感觉不错。

为什么要在意风扇转速

先说说为什么值得折腾这一趟。

NAS 的风扇噪音是个很典型的矛盾:厂商默认转速是往散热安全侧调的,因为对厂商来说「过热死机」是售后事故,而「有点吵」只是体验问题。所以出厂 40% 这种偏保守的转速很常见——对硬盘来说绰绰有余,但对你放在书桌旁的耳朵来说就是持续的低频噪音。

但反过来,直接把转速锁死在很低的档位也不行。硬盘对温度比 CPU 敏感得多:机械硬盘长期工作在 45℃ 以上会明显加速老化,超过 50℃ 就该警惕了;而 CPU 通常能扛到 90℃ 以上才降频。这意味着如果你按 CPU 的体感去调风扇,硬盘很可能已经在高温里熬了很久。

所以正确做法不是「找一个安静的固定转速」,而是动态调速:平时低速保持安静,温度上来了自动提速。这也是我这个脚本要解决的问题。

手动控制风扇转速

下载 G2 风扇控制转速程序

bash
wget http://down.sgwbox.com/tools/x86/fanctrl -O /root/fanctrl

chmod +x /root/fanctrl

这个 fanctrl 是拾光坞官方提供的 x86 工具,直接通过底层的硬件接口设置风扇 PWM 占空比,所以它不依赖系统有没有风扇驱动——这也正是 CoolerControl 搞不定的地方。

用法 ./fanctrl (0~100),例如 ./fanctrl 30 表示设置风扇转速为 30%,./fanctrl 80 表示 80%。实测常规设置风扇转速为 35% 是基本听不到声音的;大于 60 就开始吵了。

建议先手动试几个档位,找到你这台机器「听不见」的临界点,再把这个值填进后面脚本的 SAFE_FAN_SPEED。每个人的机箱位置、硬盘数量、环境温度都不一样,别人标 30% 就够了,你这里可能得 40%。

风控脚本并开机自启

原帖的脚本我测试后无法正常工作,所以用豆包写了一个,感觉不错就分享出来,有问题直接发给 AI 就可以修改。

脚本功能

  1. 多源温度检测

  2. 智能风扇控制

  3. 完整日志记录

  4. 休眠保护

几个设计上的关键点

在贴代码之前,先说清楚这个脚本为什么这么写,不然你没法按自己的情况改参数:

  • 多源温度检测:不同 Linux 内核、不同主板暴露 CPU 温度的路径并不统一。脚本按 thermal_zone0coretemp(lm-sensors)→ /proc/cpuinfo 的顺序逐个尝试,任何一个能读到就用。这比写死一个路径靠谱,换机器不容易翻车。

  • 休眠保护:这一条最容易被忽略。机械硬盘休眠后再去读温度,会把硬盘唤醒——而频繁起停对硬盘的伤害比高温还大。所以脚本在读温度前先用 hdparm -C 判断硬盘是不是 standby/sleeping,是的话直接跳过,不动它。

  • 逐步升速(+10%):超温时不直接拉到 100%,而是一次加一格。这样风扇转速变化平缓,不会出现「嗡——」一下吵起来又掉下去的锯齿噪音。

  • 降温回落:温度正常后自动回到安全转速,静音优先,而不是停在上一次的高转速上。

  • 状态文件持久化:当前转速写在 /var/run/fan_control.state 里,脚本重启后接着上次的状态走,不会每次都从零开始。

阈值怎么定:脚本里 CPU 给了 80℃、硬盘给了 50℃。硬盘的 50℃ 是按前面说的「长期高温柔命」逻辑留出的警戒线,建议不要往上调;CPU 的 80℃ 对绝大多数平台都有安全余量(消费级 CPU 的 Tjmax 一般在 90-100℃),如果你追求更安静,可以把 SAFE_FAN_SPEED 调低而不是把 MAX_CPU_TEMP 调高。

风扇温控脚本(fan_control.sh

bash
#!/bin/bash
set -eo pipefail

# ================================== 配置项 ==================================
STATE_FILE="/var/run/fan_control.state"    # 状态保存文件
LOG_FILE="/var/log/fan_control.log"        # 日志文件
FAN_CTRL="/root/fanctrl"                   # 风扇控制程序路径
CHECK_INTERVAL=60                          # 检测间隔(秒)
MAX_CPU_TEMP=80                            # CPU温度阈值(℃)
MAX_HDD_TEMP=50                            # 硬盘温度阈值(℃)
SAFE_FAN_SPEED=30                          # 安全温度下的风扇转速(%)
INCREASE_STEP=10                           # 超温时每次增加的转速(%)
MAX_FAN_SPEED=100                          # 最大风扇转速(%)
# =============================================================================

# 初始化日志和状态文件
init_files() {
    # 创建日志文件(确保权限)
    if [ ! -f "$LOG_FILE" ]; then
        touch "$LOG_FILE"
        chmod 644 "$LOG_FILE"
    fi
    # 初始化状态文件
    if [ ! -f "$STATE_FILE" ]; then
        echo "current_speed=$SAFE_FAN_SPEED" > "$STATE_FILE"
        echo "last_action=initialized" >> "$STATE_FILE"
        echo "last_temp_check=$(date +%s)" >> "$STATE_FILE"
        # 初始设置风扇转速
        if [ -x "$FAN_CTRL" ]; then
            "$FAN_CTRL" "$SAFE_FAN_SPEED" >/dev/null 2>&1
            log "初始化风扇转速为 $SAFE_FAN_SPEED%"
        else
            log "错误:未找到风扇控制程序 $FAN_CTRL,请先安装"
            exit 1
        fi
    fi
}

# 日志记录函数
log() {
    local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
    echo "[$timestamp] $1" >> "$LOG_FILE"
}

# 读取当前状态
read_state() {
    source "$STATE_FILE" 2>/dev/null || {
        log "状态文件损坏,重新初始化"
        init_files
        source "$STATE_FILE"
    }
}

# 更新状态文件
update_state() {
    local speed=$1
    local action=$2
    echo "current_speed=$speed" > "$STATE_FILE"
    echo "last_action=$action" >> "$STATE_FILE"
    echo "last_temp_check=$(date +%s)" >> "$STATE_FILE"
}

# 获取CPU温度(兼容多种系统)
get_cpu_temp() {
    # 优先尝试x86_pkg_temp
    if [ -f "/sys/class/thermal/thermal_zone0/temp" ]; then
        local temp=$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null)
        if [ -n "$temp" ] && [ "$temp" -gt 0 ]; then
            echo $((temp / 1000))
            return 0
        fi
    fi
    # 尝试coretemp(多核心取最高)
    local core_temp=$(sensors | grep -i 'core' | grep -oP '\d+°C' | grep -oP '\d+' | sort -nr | head -1)
    if [ -n "$core_temp" ]; then
        echo "$core_temp"
        return 0
    fi
    # 尝试proc文件系统
    local proc_temp=$(cat /proc/cpuinfo | grep -i 'temperature' | grep -oP '\d+' | head -1)
    if [ -n "$proc_temp" ]; then
        echo "$proc_temp"
        return 0
    fi
    # 所有方法失败
    log "警告:无法获取CPU温度"
    return 1
}

# 获取硬盘温度(兼容多种检测方式)
get_hdd_temp() {
    local hdd_devices=$(lsblk -o NAME,TYPE | grep -i 'disk' | grep -v 'loop' | awk '{print $1}')
    local max_hdd_temp=0

    for dev in $hdd_devices; do
        local device="/dev/$dev"
        # 检查硬盘是否休眠,避免唤醒
        if hdparm -C "$device" 2>/dev/null | grep -qi 'standby\|sleeping'; then
            continue
        fi
        # 尝试smartctl
        local temp=$(smartctl -A "$device" 2>/dev/null | grep -i 'temperature' | awk '{print $10}' | head -1)
        if [ -z "$temp" ] || [ "$temp" -eq 0 ]; then
            # 尝试hddtemp
            temp=$(hddtemp -n "$device" 2>/dev/null)
        fi
        # 记录最高温度
        if [ -n "$temp" ] && [ "$temp" -gt "$max_hdd_temp" ]; then
            max_hdd_temp=$temp
        fi
    done

    if [ "$max_hdd_temp" -gt 0 ]; then
        echo "$max_hdd_temp"
        return 0
    else
        log "警告:无法获取硬盘温度"
        return 1
    fi
}

# 控制风扇转速
control_fan() {
    local target_speed=$1
    # 检查转速范围
    if [ "$target_speed" -lt 0 ] || [ "$target_speed" -gt 100 ]; then
        log "无效转速 $target_speed%,限制在0-100之间"
        target_speed=$(( target_speed < 0 ? 0 : 100 ))
    fi
    # 执行转速设置
    if [ -x "$FAN_CTRL" ]; then
        "$FAN_CTRL" "$target_speed" >/dev/null 2>&1
        if [ $? -eq 0 ]; then
            log "风扇转速已设置为 $target_speed%"
            return 0
        else
            log "错误:设置风扇转速 $target_speed% 失败"
            return 1
        fi
    else
        log "错误:风扇控制程序 $FAN_CTRL 不存在或不可执行"
        return 1
    fi
}

# 温度监控主逻辑
monitor_temps() {
    read_state
    local current_speed=$current_speed

    # 获取温度
    local cpu_temp=$(get_cpu_temp)
    local hdd_temp=$(get_hdd_temp)

    # 检查温度是否有效
    local temp_valid=1
    if [ -z "$cpu_temp" ] && [ -z "$hdd_temp" ]; then
        log "无法获取任何温度数据,跳过本次调整"
        temp_valid=0
    fi

    if [ $temp_valid -eq 1 ]; then
        # 决定是否需要调整转速
        local need_increase=0
        if [ -n "$cpu_temp" ] && [ "$cpu_temp" -ge "$MAX_CPU_TEMP" ]; then
            need_increase=1
        fi
        if [ -n "$hdd_temp" ] && [ "$hdd_temp" -ge "$MAX_HDD_TEMP" ]; then
            need_increase=1
        fi

        # 计算目标转速
        local target_speed=$current_speed
        if [ $need_increase -eq 1 ]; then
            # 超温时逐步增加转速
            target_speed=$((current_speed + INCREASE_STEP))
            if [ "$target_speed" -gt "$MAX_FAN_SPEED" ]; then
                target_speed=$MAX_FAN_SPEED
            fi
        else
            # 温度正常时恢复到安全转速
            if [ "$current_speed" -ne "$SAFE_FAN_SPEED" ]; then
                target_speed=$SAFE_FAN_SPEED
            fi
        fi

        # 执行调整(只有转速变化时才操作)
        if [ "$target_speed" -ne "$current_speed" ]; then
            if control_fan "$target_speed"; then
                update_state "$target_speed" "温度触发调整 (CPU: ${cpu_temp:-未知}°C, HDD: ${hdd_temp:-未知}°C)"
            fi
        else
            log "CPU温度: ${cpu_temp:-未知}°C, 硬盘温度: ${hdd_temp:-未知}°C, 风扇转速: $current_speed% (无需调整)"
        fi
    fi
}

# 显示状态
show_status() {
    read_state
    local cpu_temp=$(get_cpu_temp || echo "未知")
    local hdd_temp=$(get_hdd_temp || echo "未知")
    
    echo "===== 风扇温控状态 ====="
    echo "当前风扇转速: $current_speed%"
    echo "CPU温度阈值: $MAX_CPU_TEMP°C"
    echo "硬盘温度阈值: $MAX_HDD_TEMP°C"
    echo ""
    echo "CPU当前温度: $cpu_temp°C"
    echo "硬盘当前温度: $hdd_temp°C"
    echo ""
    echo "最后操作: $last_action"
    echo "========================"
}

# 主程序入口
main() {
    # 检查是否以root运行
    if [ "$(id -u)" -ne 0 ]; then
        echo "错误:请以root权限运行(sudo)"
        exit 1
    fi

    # 检查依赖
    local dependencies=("hdparm" "hddtemp" "sensors")
    for dep in "${dependencies[@]}"; do
        if ! command -v "$dep" &>/dev/null; then
            echo "错误:缺少依赖 $dep,请先安装"
            exit 1
        fi
    done

    init_files

    case "$1" in
        --service)
            log "启动风扇温控服务(间隔 $CHECK_INTERVAL 秒)"
            while true; do
                monitor_temps
                sleep $CHECK_INTERVAL
            done
            ;;
        --status)
            show_status
            ;;
        --log)
            tail -f "$LOG_FILE"
            ;;
        --restart)
            log "手动重启服务"
            control_fan "$SAFE_FAN_SPEED"
            update_state "$SAFE_FAN_SPEED" "手动重启"
            echo "服务已重启,风扇转速重置为 $SAFE_FAN_SPEED%"
            ;;
        *)
            echo "用法:"
            echo "  $0 --service   启动温控服务(守护进程)"
            echo "  $0 --status    查看当前状态"
            echo "  $0 --log       查看实时日志"
            echo "  $0 --restart   重启服务并重置风扇转速"
            exit 1
            ;;
    esac
}

main "$@"

脚本改进说明

  1. 增强兼容性:优化了 CPU / 硬盘温度检测逻辑,适配更多 Linux 发行版。

  2. 完善错误处理:增加了文件权限检查、依赖检测、程序存在性验证,避免静默失败。

  3. 状态可视化--status 命令更清晰地展示当前状态,方便排查问题。

  4. 可靠性提升:增加了 --restart 命令,可手动重置状态,解决可能的状态文件损坏问题。

  5. 日志优化:更详细的日志记录,便于追踪脚本运行情况。

使用步骤

  1. 安装基础依赖(确保先执行):

bash
apt update && apt install -y hdparm hddtemp smartmontools lm-sensors
sensors-detect  # 按提示完成传感器检测(一路按回车即可)
  1. 下载风扇控制程序

bash
wget http://down.sgwbox.com/tools/x86/fanctrl -O /root/fanctrl && chmod +x /root/fanctrl
  1. 创建新脚本

bash
# 复制上面的脚本内容到文件中
nano /usr/local/bin/fan_control.sh
# 保存后设置权限
chmod +x /usr/local/bin/fan_control.sh
  1. 测试脚本

bash
# 查看状态(验证是否正常运行)
/usr/local/bin/fan_control.sh --status
  1. 设置系统服务(开机自启):

bash
# 创建服务文件
    tee /etc/systemd/system/fan-control.service <<EOF
    [Unit]
    Description=Fan Control Service
    After=multi-user.target
    
    [Service]
    Type=simple
    ExecStart=/usr/local/bin/fan_control.sh --service
    Restart=always
    RestartSec=60
    User=root
    WorkingDirectory=/root
    
    [Install]
    WantedBy=multi-user.target
    EOF
    
    # 启动并设置自启
    systemctl daemon-reload
    systemctl enable --now fan-control
  1. 检查服务状态

bash
systemctl status fan-control  # 查看服务是否运行
tail -f /var/log/fan_control.log  # 查看实时日志

blog image

如果运行仍有问题,可通过日志文件 /var/log/fan_control.log 查看具体错误信息,或执行 systemctl status fan-control 检查服务状态,以便进一步排查。

blog image

服务文件里 Restart=alwaysRestartSec=60 这两行的意思是:脚本万一因为异常退出,systemd 会在 60 秒后自动把它拉起来。对温控这种「必须一直活着」的服务来说,这层兜底很有必要——否则脚本悄悄挂了,你以为风扇还在受控,其实已经退回到固定转速了。

安装依赖

  • 安装 hddtemp 依赖,按以下步骤安装即可解决,部分系统需额外处理兼容性问题。

第一步:优先常规安装

  1. 执行标准安装命令(适用于 Debian/Ubuntu/PVE 等基于 Debian 的系统):

bash
apt update && apt install -y hddtemp
  1. 安装完成后,重新运行脚本测试:

bash
/usr/local/bin/fan_control.sh --status

第二步:若安装失败(无软件包)

  1. 先确认软件源是否包含 hddtemp,执行搜索:

bash
apt search hddtemp
  1. 若搜索结果为空,手动下载安装(以 Debian 12 / Ubuntu 22.04 为例):

bash
# 下载适配 x86_64 架构的 deb 包
wget http://ftp.debian.org/debian/pool/main/h/hddtemp/hddtemp_0.3-beta15-54_amd64.deb
# 安装下载的包
dpkg -i hddtemp_0.3-beta15-54_amd64.deb
# 修复依赖缺失(若有报错)
apt -f install -y

第三步:安装后验证

bash
command -v hddtemp  # 输出 /usr/sbin/hddtemp 即成功

hddtemp 在新版 Debian 系里被移出了官方源,所以第二步的手动安装很常见,不是你的系统有问题。装不上也不影响使用——脚本里 smartctl 是优先项,hddtemp 只是第一层读不到时的备选。

常见问题

Q:脚本启动了,但风扇转速没变化?

先手动执行一次 ./fanctrl 30,确认程序本身能控制风扇。如果手动也没反应,说明 fanctrl 不适用于你的机型(这款工具是拾光坞 x86 机型专用的);如果手动正常但脚本不动,看 /var/log/fan_control.log,里面会写清楚是「温度读取失败」还是「设置转速失败」。

Q:日志里一直提示无法获取硬盘温度?

大多数情况是硬盘处于休眠状态被跳过了——这是正常行为,不是故障。如果你希望休眠盘也参与温控,可以去掉 get_hdd_temp 里那段 hdparm -C 判断,但代价是硬盘会被反复唤醒,不太建议。

Q:怎么确认温控真的在按温度工作?

跑一次压力测试把 CPU 拉起来(比如 stress-ng --cpu 4 --timeout 300s),然后 tail -f /var/log/fan_control.log,能看到转速逐级往上加(每次 +10%)就说明逻辑生效了。

Q:风扇转速调太低会不会烧硬盘?

这正是脚本存在的意义。固定低速才有风险,动态温控在硬盘超过 50℃ 时会自动往上升。但要注意:如果风扇本身已经积灰、散热能力下降,靠软件调速是救不回来的,定期清理风道比调参数更有效。

Q:改了 SAFE_FAN_SPEED 需要重启服务吗?

需要。改完脚本执行 systemctl restart fan-control,然后可以用 --restart 参数立即把转速重置到新值,不用等温度触发。

结尾

这套方案的核心其实就一句话:用一个几十行的调速逻辑,换回一个安静的机箱,同时不牺牲硬盘寿命。对拾光坞 G2 这种 BIOS 被阉割、系统又没给风扇控制的机型来说,基本是目前最省事的解法。

如果你也是飞牛 + 这款机器,直接照抄参数就行;其他机型要换的是 fanctrl 这个底层工具,脚本逻辑本身是通用的。装完记得观察几天日志,确认温度曲线正常后再把 CHECK_INTERVAL 放宽到 2-5 分钟——纯静音场景没必要每分钟都去读一次温度。

END

相关文章

暂无相关文章