使用 fixhdd.py 修复 HDD 上的坏块

问题:

你的硬盘或 SMART 工具在读取特定块时报告类似此消息的错误:

syslog_example.txt
[3142.686141] end_request: I/O error, dev sda, sector 31415926

无论你读取该块多少次,硬盘仍然返回错误并且不重新分配该块。

背景:

硬盘被编程为直到有人写入该块才重新分配块。这意味着对于普通用户,读取坏块的程序可能不会自行修复错误,因为大多数程序表现出先读后写的使用模式,通常导致在任何块写入之前崩溃。通过使用本文介绍的脚本 fixhdd.py,你可以强制基于 Linux 的操作系统重写块,如果 HDD 剩余重新分配空间则有效修复块错误。该脚本的使用仅推荐给专业 IT 人员。

解决方案

你可以使用此脚本 fixhdd.py 自动写入产生错误的块。虽然存储在这些块中的数据将永远丢失,但写入后你不会遇到任何读取错误。

Syslog 监控模式

fixhdd.py 以多种模式之一运行,包括自动顺序扫描。然而,最直接的模式是持续扫描系统日志以查找如上所述的错误消息。该工具自动从系统日志中提取 LBA(逻辑块地址)并使用 hdparm 写入(如果尚未安装,使用 sudo apt-get install hdparm 或等效命令)。

In order to use this mode, run

fixhdd_loop.sh
sudo fixhdd.py --loop /dev/sda

在后台。在另一个 shell 中,重复运行产生错误消息的程序,直到文件可以无错误读取。每五秒,fixhdd.py 将重新扫描 syslog 并尝试重写所有损坏的块。完成后,使用 Ctrl+C 停止 fixhdd.py

顺序块扫描模式

解决这些错误后,我建议使用 smartctl -t [short|long] 在硬盘上运行 SMART 测试(即使是短两分钟测试也通常会为第一个坏块产生 LBA)。自检完成后,使用 smartctl -a 查找第一个 LBA of first error

对于此示例,我们假设 LBA of first error 为 1234567。要获取 fixhdd.py 的偏移量(即要扫描的第一个 LBA),从 LBA of first error 中减去约 100-1000 的安全余量,以便脚本识别给定 LBA 之前发生的错误。脚本现在将尝试从偏移量开始读取所有 LBA,在此过程中重写任何坏块。你也可以从偏移量 0 开始,等待几小时到几天扫描整个 HDD。

fixhdd_active_scan.sh
sudo fixhdd.py -a -o 1234000 /dev/sda

警告:fixhdd.py 极其危险,可能在几秒钟内销毁所有数据。我建议仅在你理解源代码并知道脚本和 hdparm 如何工作的情况下使用它。即使如此,如果你的任何数据丢失,也是你自己的责任(理论上,hdparm 损坏硬件的可能性很小,但我认为这几乎不可能)。虽然我过去多次使用 fixhdd.py 修复损坏的计算机,但它在其他系统上可能有严重错误。考虑到 hdparm 的强大功能,甚至可能损坏硬件。fixhdd.py 目前不包含模拟模式,并静默绕过 hdparm 的 --yes-i-know-what-i-am-doing 标志。

fixhdd.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
此脚本自动重写发生
ATA 读取错误的扇区。通过重写扇区
(使用 hdparm),HDD/SSD 将用于重新分配
扇区。

**极其危险**
此脚本在覆盖数据前不会询问
并可能销毁所有数据。在你自己的
责任下使用它,仅当你确切知道
你在做什么(或你不在乎)。
预期 fixhdd.py 包含严重错误。

仅在 linux 上运行。必须安装 hdparm。

fixhdd.py 必须以 root 身份运行。它只会写入扇区
如果使用 hdparm 读取它们产生错误。

使用 fixhdd.py --loop 监视 syslog 的读取错误
并重写所有发生错误的扇区。脚本将
每五秒检查日志并且不会退出。

使用 fixhdd.py -a -o  从 LBA  开始扫描坏块。
如果 SMART 自检指示在特定 LBA 处有错误,
请使用此模式并选择小于给定 LBA 的偏移量。
扫描大量 LBA 需要大量时间,
特别是如果许多 LBA 产生错误。

使用 fixhdd -s  重写特定 LBA,但仅
当读取它 。如果你不认为需要主动扫描大量
块,请使用此方法纠正 SMART 指示的错误。

使用 Ctrl+C 停止 fixhdd.py。

更新日志:
    版本 1.1:修复 --loop 导致一元函数被无参数调用
    版本 1.2:修复硬编码 /dev/sda,各种小改进和修复;修复主动扫描
    版本 1.3:Python3 就绪
    版本 1.4:Python3 修复,修复错误/缺失的 sense 数据和不可用的日志
"""
import subprocess
import time
import os
import stat
import sys

__author__ = "Uli Köhler"
__copyright__ = "Copyright 2015-2016 Uli Koehler"
__license__ = "Apache License v2.0"
__version__ = "1.4"
__maintainer__ = "Uli Köhler"
__email__ = "ukoehler@techoverflow.net"
__status__ = "Development"

#通过 dmesg 获取最近坏扇区列表
def getBadSectors(device):
    "从 syslog 解析最近读取的坏扇区列表"
    #TODO 这会获取所有设备的所有坏扇区,而不仅仅是所选设备
    try:
        out = subprocess.check_output('grep "end_request: I/O error" /var/log/syslog', shell=True)
        for line in out.split("\n"):
            line = line.strip()
            if not line: continue
            sector = int(line.rpartition(" ")[2])
            yield sector
    except subprocess.CalledProcessError:
        #通常这表示 grep 未找到任何内容
        return


def isSectorBad(device, sector):
    try:
        output = subprocess.check_output('hdparm --read-sector %d %s' % (sector, device), shell=True, stderr=subprocess.STDOUT)
        output = output.decode("utf-8")
        # 特殊情况:进程成功但有错误消息:
        # SG_IO:错误/缺失的 sense 数据
        if "bad/missing sense data" in output:
            return True
        # 否则:成功 => 扇区不坏
        return False
    except:
        return True


def resetSectorHDParm(device, sector):
    """仅当读取扇区产生 HDD 错误时使用 hdparm 写入扇区"""
    #非零退出代码时将抛出异常
    if isSectorBad(device, sector):
        print(("Sector %d is damaged, rewriting..." % sector))
        #天哪,这非常危险!
        #真的,不开玩笑。甚至可能使情况更糟。
        #它可能有效,但可能永远不会。
        #如果你的数据值一分钱就不要使用。
        out = subprocess.check_output('hdparm --write-sector  %d --yes-i-know-what-i-am-doing %s' % (sector, device), shell=True)
        out = out.decode("utf-8")
        if "succeeded" not in out:
            print (red(out.decode("utf-8").replace("\n")))
    else:
        print(("Sector %d is OK, ignoring" % sector))

def fixBadSectors(device, badSectors):
    "一次性修复坏扇区"
    print(("Checking/Fixing %d sectors" % len(badSectors)))
    [resetSectorHDParm(device, sector) for sector in badSectors]

def checkDmesgBadSectors(device, knownGoodSectors):
    #从 dmesg 获取扇区列表
    dmesgBadSectors = set(getBadSectors(device))
    dmesgBadSectors.difference_update(knownGoodSectors)
    if len(dmesgBadSectors) == 0:
        print ("在 syslog 中未找到新的扇区错误 :-)")
        #更新已知良好的扇区集合
    else:
        fixBadSectors(device, dmesgBadSectors)
        knownGoodSectors.update(dmesgBadSectors)

def loopCheckForBadSectors(device):
    knownGoodSectors = set()
    while True:
        print("等待 5 秒(按 Ctrl+C 中断)...")
        time.sleep(5)
        #超时后重试
        checkDmesgBadSectors(device, knownGoodSectors)

def isBlockDevice(filename):
    "返回给定文件名是否表示有效块设备"
    return stat.S_ISBLK(os.stat(filename).st_mode)

def getNumberOfSectors(device):
    "获取给定设备的物理 LBA 数量"
    #行如:255 heads, 63 sectors/track, 60801 cylinders, total 976773168 sectors
    sectorsLine = subprocess.check_output("LANG=C fdisk -l {0} 2>/dev/null | grep ^Disk | grep sectors".format(device), shell=True)
    print(sectorsLine)
    return int(sectorsLine.strip().split(b" ")[-2])

def performActiveSectorScan(device, offset=0, n=1000):
    "检查硬盘上所有扇区的错误并修复它们。"
    print(("Performing active sector scan of {0} starting at {1}").format(device, offset))
    print((getNumberOfSectors(device)))
    for i in range(offset, min(getNumberOfSectors(device), offset + n)):
        #Reset sector (only if it is damaged)
        resetSectorHDParm(device, i)

if __name__ == "__main__":
    # 解析参数
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("-s", "--sector", nargs="*", default=[], type=int, help="要扫描的扇区列表(除了列在 ")
    parser.add_argument("--loop", action="store_true", help="循环并每隔几秒扫描坏扇区")
    parser.add_argument("-a", "--active-scan", action="store_true", help="主动扫描所有块的错误。使用 --offset 从特定块开始。")
    parser.add_argument("-o", "--offset", default=0, type=int, help="对于主动扫描,开始的块")
    parser.add_argument("-n", default=1000, type=int, help="对于主动扫描,要扫描的块数")
    parser.add_argument("device", default="/dev/sda", help="要使用的设备")
    args = parser.parse_args()
    #检查给定设备是否毕竟是块设备
    if not isBlockDevice(args.device):
        print("错误:device 参数必须是块设备")
        sys.exit(1)
    print(("Trying to fix bad sectors on %s" % args.device))
    # 总是执行一次性测试
    checkDmesgBadSectors(args.device, set())
    # 修复手动添加的坏扇区列表
    fixBadSectors(args.device, args.sector)
    # 主动扇区扫描
    if args.active_scan:
        performActiveSectorScan(args.device, args.offset, args.n)
    # 如果启用,循环检查
    if args.loop: loopCheckForBadSectors(args.device)

更新 2015-07-06:修复 loopCheckForBadSectors()(非常感谢 Andreas Beier 报告此错误!)


Check out similar posts by category: Linux, Python