Одна опечатка в команде rm способна превратить спокойный вечер в ночную смену спасения данных. Хорошая новость: удалённый файл на ext4 почти всегда ещё лежит на диске, команда стёрла лишь ссылку, а блоки никто не перезаписал. Плохая: окно ограничено минутами, каждая новая запись на раздел может занять освободившееся место. Ниже разобрана реальная процедура: механика удаления, фиксация раздела, запуск extundelete, проверка результата и честные границы метода. Всё применимо к ext3 и ext4, для остальных файловых систем названы отдельные инструменты.

Механика удаления файла на ext4 и судьба индексного дескриптора после команды rm

Файл в ext4 держится на двух структурах. Каталог хранит короткую запись, связывающую имя файла с номером индексного дескриптора (inode), а сам дескриптор лежит в таблице дескрипторов своей группы и содержит права, владельца, размер, счётчик жёстких ссылок и карту размещения. В ext4 карту описывают экстенты: непрерывные диапазоны блоков, собранные в дерево прямо внутри дескриптора. Файл в гигабайт на свободном месте укладывается в один экстент, на тесном разделе - в десятки мелких, и это напрямую влияет на исход восстановления.

Команда rm внутри вызывает unlink. Запись имени из каталога изымается, счётчик ссылок уменьшается, при нулевом значении дескриптор получает метку времени удаления dtime, а его блоки помечаются свободными в битовой карте раздела. Байты при этом не стираются: на их место никто не пишет нули, содержимое остаётся на диске, пока распределитель не выделит эти блоки новой записи. Именно это окно и использует extundelete.

Различие поколений здесь принципиальное. В ext3 список блочных указателей оставался в дескрипторе даже после удаления, и утилита читала карту размещения прямо из таблицы. В ext4 при удалении заголовок дерева экстентов в дескрипторе обнуляется, карта теряется, и остаётся один источник: журнал, где хранятся более ранние копии дескриптора из прошлых транзакций. Отсюда рабочее правило: чем интенсивнее пишут на раздел после инцидента, тем быстрее нужная копия уходит из журнала.

Быстрый осмотр на ext3 делает debugfs командой lsdel; на ext4 она малоинформативна: карты в дескрипторах обнулены, картину даёт только журнал.

Немедленное размонтирование раздела или перевод файловой системы в режим только чтения

Освободившиеся блоки сразу возвращаются в общий пул. Пишут на раздел не только люди: растут журналы служб в /var/log, обновляется кэш пакетного менеджера, ядро обновляет метаданные при обычной активности. Даже запуск утилиты восстановления с выводом результата на тот же раздел способен затереть то, что она ищет. Правило простое: сначала прекращают запись, потом думают о восстановлении.

Кто держит раздел открытым для записи, показывают lsof +D /srv/data и fuser -vm /dev/sdb1. Службы останавливают командой systemctl stop имя-службы, затем раздел размонтируют: umount /dev/sdb1. Ответ target is busy означает, что что-то осталось открытым, и список процессов проходят заново.

С корневой системой сложнее: размонтировать её нельзя. Команда mount -o remount,ro / переводит корень в чтение, но сработает лишь при закрытых файлах на запись. Аварийный рычаг - механизм Magic SysRq: echo u > /proc/sysrq-trigger переводит все смонтированные системы в чтение разом, если разрешает параметр kernel.sysrq. Самый надёжный путь - live-носитель: раздел там не смонтирован, утилиту доставляют в работающую сессию.

Есть приятное исключение: если процесс держал файл открытым в момент удаления, дескриптор живёт, пока процесс его не закрыл. Такие находки видны в выводе lsof по слову deleted, а содержимое копируется прямо из proc: cp /proc/2481/fd/4 /mnt/rescue/database.sql. extundelete тут не нужен; закрытие процесса или перезагрузка отнимут источник.

Когда размонтировать раздел нельзя, а live-носитель недоступен, выручает посекторная копия: dd if=/dev/sdb1 of=/mnt/rescue/disk.img bs=4M status=progress читает раздел и пишет образ на другой диск, чтение разделу не вредит. Дальше работают по образу. А вот ленивый вариант umount -l лучше не применять: раздел исчезнет из дерева каталогов, но открытые дескрипторы продолжат писать на устройство.

Отдельная ловушка - e2fsck: проверка проигрывает и очищает журнал вместе с копиями дескрипторов. Сначала восстановление, потом любые проверки и исправления.

Установка extundelete и подготовка отдельного носителя для восстановленных файлов

Пакет доступен в репозиториях крупных дистрибутивов: Debian и Ubuntu ставят его командой apt install extundelete, системы семейства Red Hat - командой dnf install extundelete, при необходимости через EPEL. Утилита опирается на библиотеку libext2fs из набора e2fsprogs и разбирает журнал jbd2. Перед запуском убеждаются, что раздел действительно ext4: тип показывают lsblk -f, findmnt -T /srv/data -o FSTYPE, blkid /dev/sdb1, иначе инструмент откажется работать.

Тонкость с установкой: пакет ставится на системный раздел. Если потеря случилась в корневой системе, каждая запись там уменьшает шансы, поэтому правильный порядок - live-носитель: утилиту ставят в сессию, которая не трогает повреждённый раздел. Для отдельного раздела данных порядок безразличен.

Каталог для результата обязан жить на другом носителе. extundelete создаёт подкаталог RECOVERED_FILES в текущем каталоге и раскладывает туда находки, сохраняя прежние пути. Типовая подготовка: mount /dev/sdc1 /mnt/rescue, mkdir /mnt/rescue/work, cd /mnt/rescue/work. Свободное место проверяют заранее командой df -h /mnt/rescue: если удалили целое дерево, находки займут больше места, чем кажется.

К запуску раздел должен быть размонтирован или переведён в чтение: утилита рассчитана на статичную картину метаданных. Полный список ключей выводит man extundelete.

Пошаговая процедура восстановления файлов через extundelete с примерами команд

Типовой сценарий с разделом /dev/sdb1, точкой монтирования /srv/data и запасным носителем /dev/sdc1 укладывается в шесть шагов:

  1. Уточнить тип и номер раздела командой lsblk -f, чтобы не подставить утилите соседний диск;
  2. Найти и остановить пишущие процессы командами lsof +D /srv/data, fuser -vm /dev/sdb1 и systemctl stop;
  3. Размонтировать раздел командой umount /dev/sdb1 либо перевести его в чтение командой mount -o remount,ro /srv/data;
  4. Подготовить приёмник командами mount /dev/sdc1 /mnt/rescue, mkdir /mnt/rescue/work и cd /mnt/rescue/work;
  5. Запустить полное восстановление командой extundelete --restore-all /dev/sdb1 либо точечный вариант extundelete /dev/sdb1 --restore-file srv/data/report.sql;
  6. Проверить находки в каталоге RECOVERED_FILES, скопировать их на постоянное хранилище и вернуть раздел в работу командой mount /dev/sdb1 /srv/data.

Перед запуском определяются с приоритетом: восстановить всё удаётся редко, первым делом называют самое ценное - базу, конфигурацию, отчёт за утро. Переустанавливаемые пакеты и пересобираемые кэши в список целей не попадают.

Полный прогон с --restore-all перебирает все дескрипторы с меткой удаления и восстанавливает всё, что удалось прочитать из журнала, поэтому на больших разделах он небыстрый и выдаёт много мусора. Точечный режим экономит время, но требует знать путь; указывают его от корня раздела без ведущего слеша: файлу /srv/data/report.sql соответствует аргумент srv/data/report.sql. Для каталога есть ключ --restore-directory, для набора файлов - --restore-files. Когда удалений много и нужен свежий слой, помогает фильтр --after: он принимает отметку времени в секундах Unix-эпохи, а переводит человекочитаемую запись команда date -d "3 hours ago" +%s.

Результат раскладывается по подкаталогам RECOVERED_FILES с прежней структурой. Расширенные атрибуты и списки контроля доступа не восстанавливаются, утилита предупреждает об этом в выводе. Владельца и права возвращают командами chown и chmod, ориентируясь на копию или соседние файлы.

Ограничения журналирования ext4 и отложенное освобождение блоков

Журнал ext4 - кольцевой буфер транзакций метаданных. В режиме ordered, принятом по умолчанию, через него проходят только метаданные, содержимое файлов пишется напрямую в блоки. Для восстановления это критично: журнал хранит старые копии дескрипторов, по которым утилита собирает карту экстентов, но сами байты лежат на прежнем месте лишь до первой перезаписи. Копия дескриптора живёт в журнале, пока транзакции не оборачивают кольцо: на активном разделе это быстро, на тихом - неделями.

Свою лепту вносит отложенное выделение блоков. ext4 не назначает блоки в момент записи, а откладывает решение до выгрузки из кэша страниц ради крупных экстентов. Для производительности это выигрыш, для восстановления - подвох: если файл удалили до того, как ядро довело финальную карту до раздела, часть данных могла вообще не коснуться диска. Такие файлы возвращаются с обрезанным хвостом или пустыми.

Давно удалённые файлы почти не возвращаются: их копии вытеснены из журнала новыми транзакциями. Свежие удаления дают лучший результат, но только там, где после инцидента не писали. Фрагментация мешает косвенно: чем больше экстентов у файла, тем выше шанс, что хотя бы один диапазон уже занят новой записью.

Редкий режим монтирования data=journal пропускает через журнал и содержимое файлов, шансы заметно выше, но включают его нечасто из-за цены в скорости записи. Главное ограничение остаётся физическим: перезаписанный блок не возвращает никакая утилита. Если в освободившиеся блоки успели лечь чужие данные, восстановленный файл получит нулевые дыры или чужой хвост, заметить это можно только по содержимому.

Проверка восстановленных файлов и типичные сценарии частичного успеха

Находкам из RECOVERED_FILES доверяют после проверки, а не до. Тип уточняют командой file: по подписи она отличит текст, архив gzip, образ диска или исполняемый файл. Текст просматривают head -n 40, архивы проверяют командами gzip -t и tar -tf, целостность SQLite-базы - командой sqlite3 база.db "PRAGMA integrity_check". Если у файла была резервная копия, сравнение md5sum или sha256sum сразу покажет, вернулся ли он целиком.

Без копии ориентируются на косвенные признаки: подозрительно мелкий размер, ровные поля нулей в середине, обрыв данных в конце. Типичная картина после --restore-all: треть файлов вернулась целыми, треть с нулевым размером, остальные содержат смесь нулей и уцелевших кусков. Нулевой размер означает, что журнал не сохранил копию дескриптора, утилита нашла лишь запись о существовании файла. Архивы и базы с дырами обычно не собираются обратно, текст правят вручную.

Собранные находки сразу уводят с приёмника на постоянное хранилище: rsync -av /mnt/rescue/work/RECOVERED_FILES/ /storage/restored/. Работать с единственной копией результата так же опрометчиво, как с единственной копией исходного раздела: одна неудачная команда - и второго захода не будет.

Инструменты для других файловых систем и честный вывод про регулярные резервные копии

extundelete рассчитан на ext3 и ext4, и это его граница. Для XFS существует xfs_undelete, для Btrfs - команда btrfs restore, выгружающая уцелевшие файлы в каталог. Для ZFS и Btrfs работают снапшоты, из которых файл копируют обратно; повреждённую таблицу разделов чинит родственный testdisk. Универсальный photorec ищет содержимое по сигнатурам без опоры на метаданные: ему безразлична файловая система, но имена и структура каталогов теряются. Тяжёлый ext4magic вытягивает из журнала версии файла по временным диапазонам, но требует той же дисциплины: раздел без записи, результат на другой носитель.

Профилактика стоит дешевле любой процедуры. Перед массовой правкой снимают LVM-снапшот командой lvcreate -s -L 5G -n before-work /dev/vg/root: мгновенная копия состояния, к которой можно вернуться при любом исходе. Регулярные копии rsync по расписанию, снапшоты на уровне хранилища и проверка возвратом пары файлов дают больше, чем любая экстренная утилита.

Честный вывод из практики: extundelete выручает в узком окне между удалением и первой перезаписью, вне окна результат непредсказуем и зависит от активности журнала, фрагментации и везения. Ни одна утилита не вернёт перезаписанные блоки и не заменит резервную копию. Если файл жалко терять, у него должна быть копия на другом носителе, свежая и проверенная возвратом. Тогда опечатка в rm останется ошибкой на пару минут, а не событием, ломающим неделю.