使用 fixhdd.py 修复 HDD 上的坏块
问题:
你的硬盘或 SMART 工具在读取特定块时报告类似此消息的错误:
[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
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。
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 标志。
#!/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 报告此错误!)