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
ссылка на оригинал статьи https://habr.com/ru/articles/881860/
Добавить комментарий