Речь пойдет о способе извлечения данных с неисправного SSD для случаев когда после попытки чтения любого сбойного сектора — SSD совсем перестает отдавать данные и помогает только отключение включение питания.
Представляю доработанную версию скрипта ddrescue-loop с поддержкой управления USB реле и uhubctl
Для прерывания питания SSD задействовал простое и дешевое решение USB Relay Module LCUS-1 CH340 которые доступны на Aliexpress. И подключение через док станцию AgeStar 31CBNV1C на основе USB-NVMe моста JMicron JMS583
Рассмотрим процесс восстановления на примере случая с неисправными M.2 NVMe SSD производства Kimtigo на контроллере Maxio MAP1202
ddrescue-loop v0.2.1
ddrescue-loop v0.2.1
#!/bin/sh
#ddrescue-loop script writen by gumanzoy
# Compatible only with Linux, not with other *nix!
# Depends on udev /dev and sysfs /sys kernel interfaces
# For SATA requires AHCI compatible motherboard
# For all Intel and modern AMD platforms (AM4 and newer), check the UEFI Setup
# SATA settings to ensure Port Hot Plug is enabled
# For USB requires lsusb from usbutils package
# And optional uhubctl for power off/on cycle
# Or hardware USB Relay Module LCUS-1 CH340
# [RU] forum thread. Обсуждение
# https://forum.ixbt.com/topic.cgi?id=11:47589-31
# /* This program is free software. It comes without any warranty, to
# * the extent permitted by applicable law. You can redistribute it
# * and/or modify it under the terms of the Do What The Fuck You Want
# * To Public License, Version 2, as published by Sam Hocevar. See
# * http://www.wtfpl.net/ for more details. */
VERSION=0.2.1
showhelp () {
echo "ddrescue-loop v""$VERSION"" перезапускает процесс ddrescue в случае его завершения"
echo "Внимание следует соблюдать очередность аргументов"
echo "Указывать ключи в произвольном порядке нельзя!"
echo "Числовые значения аргументов обязательно через пробел"
echo -n "\n"
echo "# ----- SATA ----- SATA ----- SATA ----- SATA ----- SATA -----"
echo "# Остановить/запустить диск на SATA порту:"
echo "-ata -stop"" ""остановить диск на SATA порту "
echo "-ata -scan"" ""сканировать SATA порт "
echo -n "\n"
echo "# Запустить восстановление c SATA:"
echo "ddrescue-loop -ata [-loop ] [-pwc] [-wait ] [-act ] outfile mapfile [ddrescue options]"
echo -n "\n"
echo "# Укажите номер SATA порта к которому подключен диск источник:"
echo -n "-ata "" ""Номер SATA порта цифра (смотрите вывод dmesg)"
echo -n "\n"" ""#: "; ls /sys/class/ata_port
echo -n "\n"
echo "# Функция циклической остановки/перезапуска диска на SATA порту:"
echo "-loop "" "" предельное число попыток"
echo -n "\n"
echo "# Таймер ожидания остановки/перезапуска диска:"
echo "-wait "" ""Время в секундах [10]"
echo -n "\n"
echo "# Переопределить таймаут ожидания исполнения ATA команд:"
echo "-act "" ""Время в секундах [30]"
echo -n "\n"
echo "# ------ USB ------ USB ------ USB ------ USB ------ USB -----"
echo "# Отключить/включить питание USB устройства , методом :"
echo "-usb -pwc hub"" ""Использовать uhubctl --search "
echo "-usb -pwc rle"" ""Использовать USB реле LCUS-1 CH340 RLETTY=""$RLETTY"
echo -n "\n"
echo "# Запустить восстановление c USB:"
echo "ddrescue-loop -usb [-loop ] [-pwc ] [-wait ] outfile mapfile [ddrescue options]"
echo -n "\n"
echo "# Укажите Hex идентификаторы VID:PID USB устройства источника:"
echo "-usb "" "" через двоеточие (смотрите вывод lsusb)"
echo -n "\n"
echo "# Функция циклического перезапуска ddrescue:"
echo "-loop "" "" предельное число попыток"
echo -n "\n"
echo "# Основные:"
echo "outfile"" ""Устройство приемник данных / файл образа"
echo "mapfile"" ""ddrescue map/log файл (обязательно)"
echo -n "\n"
echo "# В конце после mapfile можно указать опции запуска ddrescue через пробел"
echo "# Поддержка зависит от версии. Полный список опций в мануале. Важные:"
echo "-P []"" ""Предпросмотр данных [число строк] по умолчанию 3"
echo "-b 4096"" "" размер сектора (физического блока) [default 512]"
echo "-c "" ""Размер кластера секторов за раз [default 128]"
echo "-O"" #Рекомендую! ""После каждой ошибки заново открывать файл устройства"
echo "-J"" #Опционален ""При ошибке перечитать последний не сбойный сектор"
echo "-r #ИЛИ -r -1"" "" число повторных проходов до перехода к trim"
echo "-m "" ""Ограничить область чтения доменом ddru_ntfsbitmap"
}
get_ata_host () {
until SCSIHOST=`readlink -f /sys/class/ata_port/ata"$1"/device/host?/scsi_host/host?/` \
&& test -d "$SCSIHOST"; do sleep 1; done
}
get_ata_target () {
until SYSFSTGT=`readlink -f /sys/class/ata_port/ata"$1"/device/host?/target?:?:?/?:?:?:?/` \
&& test -d "$SYSFSTGT"; do sleep 1; done
}
get_ata_dev () {
until INDEV=`readlink -f /dev/disk/by-path/pci-*-ata-"$1"` \
&& test -b "$INDEV"; do sleep 1; done
}
device_delete () {
while test -f "$SYSFSTGT"/delete; do echo 1 > "$SYSFSTGT"/delete; sleep 1; done
}
get_usb_dev_by_path () {
INDEV="/dev/"`basename "$1"`
SYSFSTGT="$1""/device/"
}
get_usb_dev_by_id () {
IDVID=`echo -n "$1" | cut -d ":" -f1`
IDPID=`echo -n "$1" | cut -d ":" -f2`
until get_usb_dev_by_path `udevadm trigger -v -n -s block \
-p ID_VENDOR_ID="$IDVID" -p ID_MODEL_ID="$IDPID"` \
&& test -b "$INDEV"; do sleep 1; done
}
power_cycle () {
if [ -n "$USBID" ] && [ "$PWRCTL" = hub ]; then
uhubctl --search "$USBID" --action cycle --delay "$LOOPWAIT"
elif [ "$PWRCTL" = rle ]; then /bin/echo -en "\xA0\x01\x01\xA2" > "$RLETTY" && \
sleep "$LOOPWAIT" && /bin/echo -en "\xA0\x01\x00\xA1" > "$RLETTY"
fi
}
if [ "$1" = "-h" -o "$1" = "--help" ]; then showhelp
exit; fi
if [ "`whoami`" != "root" ]; then
echo Exit. This script should be run as root !
exit 1; fi
if [ -z "$RLETTY" ] && test -c /dev/ttyUSB0; then RLETTY="/dev/ttyUSB0"
elif [ -n "$RLETTY" ] && ! test -c "$RLETTY"; then
echo "RLETTY=""$RLETTY"" control device not found"; exit 1; fi
if [ -n "$1" ] && [ "$1" = "-ata" ]; then
if [ -n "$2" ] && test -d /sys/class/ata_port/ata"$2"; then
SATAP="$2"; get_ata_host "$SATAP"; shift; shift
else echo -n "Please enter correct port number: "; ls /sys/class/ata_port; exit 1; fi
fi
if [ -n "$1" ] && [ "$1" = "-stop" ] && [ -n "$SATAP" ]; then
get_ata_target "$SATAP"; device_delete; exit; fi
if [ -n "$1" ] && [ "$1" = "-scan" ] && [ -n "$SATAP" ]; then
echo '0 0 0' > "$SCSIHOST"/scan; exit; fi
if [ -n "$1" ] && [ "$1" = "-usb" ] && [ -z "$SATAP" ]; then
if [ -n "$2" ] && lsusb -d "$2"; then
USBID="$2"; get_usb_dev_by_id "$USBID"; shift; shift
else echo "Please enter correct USB Device ID:"
lsusb | cut -d ":" -f2,3 | grep -vi hub
exit 1; fi
fi
if [ -n "$1" ] && [ "$1" = "-loop" ]; then
if [ -n "$2" ] && [ "$2" -gt 0 ]; then
DDLOOP="$2"; shift; shift; fi
else DDLOOP=0
fi
if [ -n "$1" ] && [ "$1" = "-pwc" ]; then
if [ -n "$USBID" ] && [ -n "$2" ] && [ "$2" = "hub" -o "$2" = "rle" ]; then
PWRCTL="$2"; echo "PWRCTL=""$2"; shift; shift
elif [ -n "$RLETTY" ]; then
PWRCTL="rle"; echo "PWRCTL=rle"; shift; fi
fi
if [ -n "$1" ] && [ "$1" = "-wait" ]; then
if [ -n "$2" ] && [ "$2" -gt 0 ]; then
LOOPWAIT="$2"; shift; shift; fi
else LOOPWAIT=10
fi
if [ -n "$1" ] && [ "$1" = "-act" ]; then
if [ -n "$2" ] && [ "$2" -gt 0 ]; then
ATACMDT="$2"; shift; shift; fi
fi
if [ -n "$RLETTY" ] && [ "$PWRCTL" = rle ]; then
stty -F "$RLETTY" 9600 -echo && echo "RLETTY=""$RLETTY"; fi
if [ "$DDLOOP" = 0 ]; then
if [ -n "$USBID" ] && [ "$PWRCTL" = hub ]; then power_cycle; exit
elif [ -n "$RLETTY" ] && [ "$PWRCTL" = rle ]; then power_cycle; exit; fi
fi
if [ -z "$SATAP" ] && [ -z "$USBID" ]; then showhelp
exit; fi
OUTFILE="$1"; shift
MAPFILE="$1"; shift
DDOPTS="$@"
DONE=X
LOOPCOUNT=0
until [ "$DONE" = 0 ]; do
if [ -n "$SATAP" ]; then get_ata_target "$SATAP"; get_ata_dev "$SATAP"
elif [ "$LOOPCOUNT" -gt 0 ] && [ -n "$USBID" ]; then get_usb_dev_by_id "$USBID"
fi
if [ -n "$ATACMDT" ]; then echo "$ATACMDT" > "$SYSFSTGT"/timeout
fi
echo ddrescue "-fd" "$INDEV" "$OUTFILE" "$MAPFILE" "$DDOPTS"
ddrescue "-fd" "$INDEV" "$OUTFILE" "$MAPFILE" $DDOPTS
DONE="$?"
if [ "$DONE" != 0 ] && [ "$DDLOOP" -gt 0 ]; then
device_delete &
sleep "$LOOPWAIT"
if [ -n "$PWRCTL" ]; then power_cycle
elif [ -n "$SATAP" ]; then while test -d "$SYSFSTGT"; do
sleep "$LOOPWAIT"; done; fi
if [ -n "$SATAP" ]; then sleep "$LOOPWAIT"
echo '0 0 0' > "$SCSIHOST"/scan; fi
DDLOOP=$(($DDLOOP-1))
LOOPCOUNT=$(($LOOPCOUNT+1))
echo "\n\033[1mDDLOOP #""$LOOPCOUNT"; tput sgr0
date; echo -n "\n"
sleep "$LOOPWAIT"
else DONE=0
fi
done
Как пользоваться. Параметры запуска
Использование с SATA устройствами разобрано в моей первой статье. Если не читали, то рекомендую сначала ознакомится с ней.
Так как я не слишком искушен в sh скриптинге, и вообще не программист — с разбором параметров особо не мудрствовал. Поэтому есть некоторые важные ограничения!
Следует соблюдать очередность аргументов. Указывать ключи в произвольном порядке нельзя!
Числовые значения аргументов обязательно через пробел.
ddrescue-loop -usb
Укажите Hex идентификаторы VID:PID USB устройства источника:-usb
VID:PID через двоеточие (смотрите вывод lsusb)
Функция циклической остановки/перезапуска ddrescue:-loop N
Предельное число попыток N
целое число. Указывать обязательно.
Функция прерывания питания устройства:-pwc hub
Использовать uhubctl --search
-pwc rle
Использовать USB реле LCUS-1 CH340
Таймер ожидания остановки/перезапуска диска:-wait N
Время в секундах. 10 по умолчанию.
В конце после mapfile
можно указать опции запуска ddrescue. Их обрабатывает уже сама ddrescue, можно указывать все как обычно.
Демонстрация работы. Записи вывода терминала
При ошибке чтения сектора и после сообщений в dmesg
uas_eh_device_reset_handler start
uas_eh_device_reset_handler success
Device offlined — not ready after error recovery
I/O error, dev sdd, sector 8985600
Устройство перестает отдавать данные, процесс ddrescue завершается
Can’t reopen input file: No such device or address
Скрипт подает команду на включение реле, затем перезапускает ddrescue и чтение продолжается.
Как это работает
Так как имена устройств sda sdb sdc
не постоянны и буква меняется в зависимости от очередности подключения к системе. Применил вот такое решение: скрипт принимает на вход ID VID:PID USB устройства источника. Их отображает lsusb. В скрипте код получает адрес блочного устройства /dev/sdX
И делает это каждый раз после отключения питания диска перед перезапуском процесса ddrescue
USB Relay Module LCUS-1 CH340
Вариант с USB Type-A
aliexpress.com/item/4001216792789.html
aliexpress.com/item/1005001993993906.html
Вариант с USB Type-C
aliexpress.com/item/1005004323626598.html
aliexpress.com/item/1005004347242232.html
Использование USB реле. Подразумевается подключение питания на USB/SATA диск через контакты реле COM и NC Чтобы в выключенном состоянии питание проходило. А при подаче команды на включение реле — питание отключалось. Скрипт управляет реле с помощью команд:
echo -en "\xA0\x01\x01\xA2" > "$RLETTY"
sleep "$LOOPWAIT"
echo -en "\xA0\x01\x00\xA1" > "$RLETTY"
По умолчанию RLETTY=/dev/ttyUSB0
можно переопределить передав переменную окружения перед запуском скрипта:RLETTY=/dev/ttyUSB1 ddrescue-loop -pwc rle
Также добавил поддержку uhubctl. Теоретически можно использовать вместо реле, но у меня нет подходящего USB хаба. Возможно будет полезно для восстановления с флешек.ddrescue-loop -usb
uhubctl --search "$USBID" --action cycle --delay "$LOOPWAIT"
Подключение реле к док станции
В док станции AgeStar 31CBNV1C есть переключатель для отключения питания. Я подключил реле вместо него.
У переключателя пять контактов. Крайние для крепления к плате. Припаиваться нужно ко второму и третьему слева. На фото отметил красным.
Выпаял переключатель совсем, впаял два провода, залудил обратные концы и убрал в термоусадку. Собрал док станцию обратно в корпус. Вот что получилось.
Непосредственно процесс восстановления
Данный раздел пишу в том числе в расчете на тех кто далек от Linux. Не уверен что у меня получится понятно объяснить, но попробую. По большей части все примечания такого рода убрал под спойлеры.
Использую ПК с GNU/Linux Debian 11 (еще не обновился до Debian 12 по причине лени).
Док станцию подключаю к USB3 порту. А к SATA подключено несколько 3.5″ жестких дисков.
Сохраняю образ в файл, на диск с файловой системой Ext4. Файл создается разреженный (sparse file) таким образом место расходуется только под фактический объем скопированного, а не под весь объем неисправного SSD. При этом файл монтирую в /dev/loopN
это позволяет работать с ним так же как с физическим диском.
Для создания/подключения/отключения образов удобно использовать графический интерфейс gnome-disk-utility
gnome-disk-utility используется не только в среде Gnome. Зависимостей относительно не много.
Создавать образ нужно такого же или большего объема. Для того чтобы не ошибиться можно скопировать и указать размер диска в байтах (отображается в gnome-disks в правой панели вверху при выборе соответствующего диска).
В меню «гамбургер» пункт New Disk Image…, в открывшемся окне выбрать размер в байтах, вставить скопированное число в поле и удалить запятые. Указать имя и путь для сохранения. Нажать Attach new image…
Созданный образ подключится в свободный /dev/loopN
в режиме чтение/запись. При выборе и подключении существующего файла образа он по умолчанию подключается в режиме только чтение. Не забывайте снимать галочку Set up read-only device
Если таблица разделов на SSD читается. То прежде чем запускать копирование можно построить файл домена с помощью утилиты ddru_ntfsbitmap (из состава ddrutility). Это позволяет для разделов с файловой системой NTFS ограничить объем копирования только занятым пространством.
К сожалению в случае с SSD это не позволяет ускорить процесс, а только экономит место под образ. Так как нули из не занятых блоков копируются на полной скорости и без ошибок. Сбойные же сектора располагаются там где были какие то файлы и именно на обработку диском ошибок тратится основная часть времени.
Создание файла домена с помощью ddru_ntfsbitmap
В приведенных ниже командах в /dev/sdX
вместо X
подставить соответствующую устройству букву (можно посмотреть в том же gnome-disk-utility).
Нужно создавать файл домена в привязке ко всему диску, а не к отдельному разделу. Поэтому нужно указывать в командах именно устройство /dev/sdX
а не раздел /dev/sdXN
Для ddru_ntfsbitmap нужно вычислить и указать значение input offset (partition offset) ключ --inputoffset
или коротко -i
Сначала нужно запустить sudo fdisk -l /dev/sdX
Найти в таблице нужный раздел NTFS, и скопировать значение из столбца Start
Затем запустить команду, куда вместо START подставить значение
sudo ddru_ntfsbitmap -i $((START*512)) -m mftdomain.map /dev/sdX domain.map
Будут созданы искомые файлы domain.map
и mftdomain.map
А также еще несколько файлов, имена которых начинаются на __
(двойное нижнее подчеркивание). Они не нужны, их можно удалить.
Запуск процесса копирования
Скрипт ddrescue-loop для удобства запуска можно скопировать в /usr/local/bin/
Скопировать и выдать права на исполнение
sudo zcat ddrescue-loop-v0.2.1.gz > /usr/local/bin/ddrescue-loop
sudo chmod +x /usr/local/bin/ddrescue-loop
Сначала в отдельном терминале запустить dmesg -Wt
чтобы видеть что происходит с диском.
Запускать ddrescue-loop нужно с правами root. Также и dmesg, но только в дистрибутивах где по умолчанию включен kernel.dmesg_restrict=1 (Debian входит в их число). Для краткости команду sudo добавлять не буду, но она подразумевается.
ddrescue-loop -usb 152d:0583 -loop 9999 -pwc rle /dev/loopN mapfile.log -b 4096 -c 32 -O -J
Разберем приведенные параметры:
-
-usb 152d:0583
это ID VID:PID док станции (а точнее контроллера JMicron JMS583 USB-NVMe)
Посмотреть список подключенных устройств можно запустивddrescue-loop -usb
-
-loop 9999 -pwc rle
предельное число попыток перезапуска в цикле и использовать USB реле. -
/dev/loopN
ГдеN
заменить на соответствующую цифру. Предполагается что файл приемник создан и смонтирован (посмотреть можно в gnome-disk-utility)
Вместо этого можете просто указать куда сохранить файл образ, тогда ddrescue создаст его сама. -
mapfile.log
имя ddrescue map/log файла указываем обязательно. -
-b 4096
обязательно указываем реальный размер сектора (физического блока)
По умолчанию 512 и это не соответствует современным накопителям как SSD так и HDD -
-c 32
ограничиваем размер кластера (сколько секторов ddrescue будет пытаться читать за раз в обычном режиме до перехода к trimming). По умолчанию 128, а так как мы увеличили размер сектора то это уже чересчур. -
-O
обязательно указываем. Чтобы ddrescue после каждой ошибки пыталась заново открыть файл устройства. Это необходимо для того чтобы в случае невозможности дальнейшего чтения — процесс ddrescue завершался с ошибкой и скрипт задействовал метод остановки/перезапуска диска. -
-J
тоже указываем, это дополнительная проверка — при ошибке перечитывать последний не сбойный сектор.
Если создавали файл домена то первый проход запускаем с добавлением в конец строки запуска -m mftdomain.map
Затем когда вычитали весь MFT то читаем все остальные задействованные файловой системой сектора -m domain.map
Тонкая подстройка в процессе
Если останавливали ddrescue по Ctrl+C для изменения параметров то перед перезапуском можно задействовать реле или uhubctl (если используем хаб вместо реле)ddrescue-loop -pwc rle
или ddrescue-loop -usb
На начальном этапе можно (но не обязательно) стараться вычитывать в первую очередь крупные беспроблемные участки. Для этого можно перепрыгивать скопления бэдов, добавляя в конце опцию -i
Например -i 30G
если чтение в прямом направлении. И можно читать задом наперед, для этого указывать -R
и -s
Например -R -s 40G
ddrescue-loop -usb 152d:0583 -loop 9999 -pwc rle -wait 4 -act 23 /dev/loopN mapfile.log -b 4096 -c 32 -O -J -m domain.map
Здесь добавлены -wait 4
то есть уменьшен таймер ожидания с 10 до 4-act 23
таймаут ожидания исполнения ATA команд уменьшен с 30 до 23
Эти параметры подбирал экспериментально для описываемого случая для того чтобы постараться уменьшить время ожидания пере-подключения при ошибках SSD.
Значение -act
подбирал с оглядкой на кол-во сообщений uas_eh_device_reset_handler start uas_eh_device_reset_handler success в dmesg после каждого сбойного сектора. При 30 ядро успевало делать две попытки reset’a. При 22 уже три reset’a — а это выходит дольше. Оптимальным оказалось значение 23.
Когда основные стадии процесса вычитки завершены, то в режиме scraping для ускорения можно увеличить размер блока, при этом качество пострадает.
Например указать -b 16Ki -c 1
или -b 32Ki -c 1
вместо -b 4096 -c 32
Когда процесс близится к завершению
В данном случае вычитка одного SSD включая trimming заняла около 14 суток. Но scraping еще не завершен.
Файловые системы с разделов получившегося образа можно монтировать средствами ядра Linux сразу из /dev/loopNpP
, где P
— номер раздела. Это можно делать с помощью gnome-disk-utility. При этом обязательно только в режиме чтения. Надежнее отключить образ и пере-подключить в режиме только чтение.
Но для улучшения результатов, поисков фрагментов MFT и файлов по сигнатурам лучше использовать специализированный софт. Из свободных TestDisk/PhotoRec.
Я уже давно пользуюсь Linux версией DMDE. К сожалению исходники закрыты, это платное ПО. Однако в бесплатной версии (только для личного не коммерческого использования) ограничено только кол-во одновременно восстанавливаемых файлов, но не их размер. Это отлично подходит для оценки возможности восстановления нужных файлов.
Использование такого ПО и разбор разных нюансов с этим связанных точно не поместится в данную статью. Да и мои знания об устройстве файловых систем совсем поверхностные.
Заключение
Восстановление в данном случае считаю успешным. Пользовательские файлы извлечены и читаются. Справедливости ради нужно заметить что не все конечно, но дальше пытаться выцарапывать вряд ли имеет смысл. Так как остались только проблемные области общим объемом 1.54GB и вполне возможно 90% из них не читаемые.
Надеюсь мой опыт и скрипт кому нибудь еще пригодятся. Думаю что потребности в восстановлении данных с SSD меньше не становится. Не забывайте напоминать пользователям о бэкапах.
Благодарю за внимание! И удачных экспериментов!