Hibernate в Linux: 0.99 проблемы меньше

в 14:15, , рубрики: hibernate, linux

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js