Hibernate
В Linux поддержка гибернации оставляет желать лучшего. Проблем хватает. Я написал этот документ, чтобы рассмотреть одну из них.
Ошибка выделения памяти
Неважно, какой у вас размер swap. Гибернация может не сработать даже если на swap достаточно свободного места. Догадайтесь, почему??? ... Да — фрагментация. Похоже, система пытается выделить непрерывный участок в swap.
Так вот. Очень печально, правда? Уже 2025 год, а поддержка гибернации в Linux всё ещё оставляет желать лучшего. Я пишу этот документ, потому что действительно хочу, чтобы моя Linux-система была такой же надёжной, как современные Windows или MacOSX.
Отказ от ответственности
К сожалению, особенности работы свопа и гибернации в Linux не позволяют на 100% гарантировать успешность предложенного подхода. Независимо от того, что вы делаете и как реализуете процесс гибернации, всегда существует шанс, что он не сработает.
Давайте начнём
Моя стратегия довольно проста. Вместо того чтобы использовать один большой раздел swap, я создаю два раздела: первый для swap — монтируется автоматически, а второй — для процесса гибернации.
И я не использую KDE/GNOME/UPower для гибернации. Я создаю собственный сервис гибернации, который отслеживает уровень заряда батареи, и когда он достигает критического уровня, выполняется swapon для раздела гибернации, после чего система немедленно переводится в режим гибернации (с флагами принудительного выполнения и игнорированием препятствий). Да, есть вероятность, что ядро Linux сразу начнёт использовать этот новый swap, но эта вероятность крайне мала. К тому же, при создании отдельного раздела для гибернации, изначально выделяйте для него немного больше места, чем требуется. Например, если у вас 32 ГБ оперативной памяти, сделайте раздел для гибернации объёмом 33–34 ГБ. Чем больше пространства вы зарезервируете, тем меньше вероятность неудачи гибернации. Именно поэтому я назвал статью "0.99 проблемы меньше", а не "1-ой проблемой меньше" :-)
Команды для инициализации swap и гибернации
Swap:
cryptsetup luksFormat /dev/disk/by-partuuid/<swap-partuuid>
cryptsetup open /dev/disk/by-partuuid/<swap-partuuid> cryptswap
mkswap -L swap /dev/mapper/cryptswap
swapon /dev/mapper/cryptswap
Hibernate:
cryptsetup luksFormat /dev/disk/by-partuuid/<hibernate-partuuid>
cryptsetup open /dev/disk/by-partuuid/<hibernate-partuuid> crypthibernate
mkswap -L swap /dev/mapper/crypthibernate
Отредактируйте /etc/crypttab:
cryptswap PARTUUID=<swap-partuuid> none luks,swap
crypthibernate PARTUUID=<hibernate-partuuid> none luks,swap
Убедитесь, что /etc/fstab содержит правильную запись. Не включайте раздел для гибернации:
...
/dev/mapper/cryptswap none swap sw 0 0
...
Команда mkswap выдаст UUID вашего нового раздела для гибернации (не путайте его с partuuid).
Отредактируйте /etc/initramfs-tools/conf.d/resume:
RESUME=UUID=<hibernate-uuid>
Отредактируйте /etc/default/grub. Ваша строка CMDLINE должна выглядеть примерно так:
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash resume=UUID=<hibernate-uuid> no_console_suspend"
Наконец, обновите initramfs и grub:
update-initramfs -u
update-grub
Игнорируйте предупреждение, которое update-initramfs выдаёт о том, что раздел с каким-то UUID отсутствует. Если этот UUID не ваш, то проблем не будет. Система как-то запоминает последний UUID и жалуется, что не может его найти. Странно, но что поделаешь.
Перезагрузите систему.
Теперь вы можете протестировать ваш новый крутой процесс гибернации:
swapon "/dev/mapper/crypthibernate"
systemctl hibernate --force --ignore-inhibitors
swapoff "/dev/mapper/crypthibernate"
Пример автоматизированного скрипта
Для тех, кто хочет использовать это в своих настройках, я делюсь примером скрипта для мониторинга критического уровня заряда батареи и гибернациии.
Для деталей смотрите мой проект на Гитхабе. Ansible playbook playbooks/configure_power_management.yml.
Теперь скрипт.
/usr/local/bin/force-hibernate.sh:
#!/usr/bin/env bash
set -euo pipefail
# Locate the battery device (assumes only one battery)
BATTERY_DEVICE=$(upower -e | grep -m 1 BAT)
# If no battery device is found, exit.
if [ -z "$BATTERY_DEVICE" ]; then
exit 0
fi
# Check the battery state; only proceed if discharging.
BATTERY_STATE=$(upower -i "$BATTERY_DEVICE" | grep -m 1 'state:' | awk '{print $2}')
if [ "$BATTERY_STATE" != "discharging" ]; then
# System is plugged in or fully charged – do nothing.
exit 0
fi
# Get battery percentage
BATTERY_LEVEL=$(upower -i "$BATTERY_DEVICE" | grep percentage | awk '{print $2}' | sed 's/%//')
CRITICAL_THRESHOLD=8
# Compare battery level with threshold
if [ "${BATTERY_LEVEL:-0}" -le "${CRITICAL_THRESHOLD:-0}" ]; then
if swapon "/dev/mapper/crypthibernate" 2>&1 | logger -t force-hibernate; then
logger -t force-hibernate "Hibernation swap enabled successfully: /dev/mapper/crypthibernate"
else
if swapoff "/dev/mapper/crypthibernate" 2>&1 | logger -t force-hibernate; then
logger -t force-hibernate "Successfully swapped off /dev/mapper/crypthibernate"
swapon "/dev/mapper/crypthibernate" 2>&1 | logger -t force-hibernate;
logger -t force-hibernate "Hibernate space swapped on back again: /dev/mapper/crypthibernate"
else
logger -t force-hibernate "Fallback action failed: swapoff/swapon: /dev/mapper/crypthibernate"
fi
fi
logger -t force-hibernate "Memory usage after swapon:"
free -h 2>&1 | logger -t force-hibernate
lsblk /dev/mapper/crypthibernate 2>&1 | logger -t force-hibernate
swapon --show 2>&1 | logger -t force-hibernate
# The system will hibernate now
logger -t force-hibernate "Invoking systemctl hibernate..."
systemctl hibernate --force --ignore-inhibitors 2>&1 | logger -t force-hibernate
fi
Чтобы скрипт работал, в вашей системе должен быть установлен upower. Но убедитесь, что сервис upower был настроен на меньший уровень заряда батареи:
/etc/UPower/UPower.conf:
[UPower]
UsePercentageForPolicy=true
PercentageLow=10
PercentageCritical=5
PercentageAction=3
CriticalPowerAction=Shutdown
Отключение свопа гибернации, чтобы система не писала в него после возвращения из глубокого сна
Невозможно использовать мой кастомный скрипт гибернации и прямо в нём вызвать команду swapoff сразу после "systemctl hibernate...". Последняя возвращает контроль в скрипт сразу после вызова, а не после выхода из сна, как я думал изначально (точнее как мне подсказала самая совершенная на момент написания статьи модель нейросети - gpt o3). Так что я долго не мог понять, почему процесс гибарнации завершается с ошибкой и ничего не происходит.
Решается эта проблема несложно. В Ubuntu 24.04 нужно добавть hook-скрипт к systemd-sleep. У меня он лежит тут - /usr/lib/systemd/system-sleep/swapoff-hibernate.sh:
#!/usr/bin/env bash
set -euo pipefail
case "$1" in
post)
logger -t force-hibernate "System woke up from hibernation. Disabling hibernation swap on /dev/mapper/crypthibernate"
if swapoff "/dev/mapper/crypthibernate" 2>&1 | logger -t force-hibernate; then
logger -t force-hibernate "Hibernation swap disabled successfully: /dev/mapper/crypthibernate"
else
logger -t force-hibernate "Failed to disable hibernation swap: /dev/mapper/crypthibernate; It may be already swapped off. Check swap state:"
swapon --show 2>&1 | logger -t force-hibernate
fi
;;
esac
Зачем нужен скрипт. Почему не использовать такой же хук в systemd-sleep для swapon свопа гибернации
Ответ: потому что это не сработает.
Как я ни пытался настроить стандартные механизмы гибернации при помощи systemd - не получается. Всегда проверка свободного места в свопе происходит то того как выполняется команда swapon. Если кто знает как этого добаться, пожалуйста подскажите.
Пример systemd сервиса
/etc/systemd/system/force-hibernate.timer
[Unit]
Description=Run Force Hibernate script every 30 seconds
[Timer]
OnBootSec=0s
OnUnitActiveSec=30s
AccuracySec=5s
# WARNING: don't turn on Persistent - it will not work. Timer will never start, if you stop/start it
# Persistent=true
[Install]
WantedBy=timers.target
/etc/systemd/system/force-hibernate.service
[Unit]
Description=Force Hibernate on Critical Battery
[Service]
Type=oneshot
ExecStart=/usr/local/bin/force-hibernate.sh
И сделать его автоматически запускаемым:
systemctl enable force-hibernate.timer
systemctl start force-hibernate.timer
Не перепутайте force-hibernate.timer с force-hibernate.service. Включать нужно таймер, а не сервис
Проверка работы таймера:
systemctl status force-hibernate.timer
systemctl list-timers --all
Логи в журнале:
journalctl -t force-hibernate --no-pager
Для полной безопасности
«Полная безопасность» означает, что необходимо хотя бы синхронизировать диски и корректно завершать работу системы.
Я настраиваю KDE (или другую среду рабочего стола) так, чтобы происходило отключение при критическом уровне заряда батареи. Например, если мой скрипт переводит систему в гибернацию при 8% заряда, то я настраиваю KDE на выключение при достижении 4% заряда. Как я уже писал выше,
KDE использует UPower для настройки события действия при критическом заряде батареи. Не знаю как KDE настраивает UPower сервис. Я просто сделал настройку KDE идентичной той, которая у меня находится в файле конфигурации здесь - /etc/UPower/UPower.conf
Если вы используете другое окружение, то вам нужно разбираться с ним самостоятельно. Но в общем-то думаю принцип вам понятен.
И не забывайте про возможность использования клавиши SysRq, в случае, если система всё же зависнет (что у меня случается с завидной регулярностью).
Не забывайте о настройках BIOS
Конечно, вы настраиваете свой ноутбук так, чтобы он переходил в режим сна при закрытии крышки. Но батарея ноутбука в какой-то момент разрядится. Для этого необходимо настроить BIOS так, чтобы система пробуждалась из сна при критическом уровне заряда. Это обеспечит выполнение вашего кастомного скрипта гибернации.
Поздравляю
Желаю вам счастливой и безопасной ГИБЕРНАЦИИ!!!
P.S.: Всё описанное выше собрано воедино в моём небольшом Ansible проекте для конфигурации Ubuntu 24.04:
https://github.com/artem-korolev/arch-on-zfs/tree/ubuntu-ansible
https://github.com/artem-korolev/arch-on-zfs/tree/ubuntu-ansible/roles/power_management
Автор: prorokxp