Перевели для вас статью Джейкоба Адамса о том, что происходит перед тем, как Linux уходит в сон. Дальше идёт текст оригинала.
Как Linux переходит в сон? Как ему потом удаётся восстановить первоначальное состояние? Пытаясь понять, где проходит граница между аппаратным и программным обеспечением, я с головой зарылся в глубины языка С.
Мое исследование разделено на несколько частей. В первой речь пойдёт о периоде от вызова режима гибернации до синхронизации всех файловых систем на диск.

Эта статья написана для Linux версии 6.9.9. Её исходники широко доступны, но проще всего их посмотреть на Bootlin Elixir Cross-Referencer. Каждый фрагмент кода будет начинаться со ссылки на вышеуказанный ресурс с указанием пути к файлу и номера строки, с которой начинается фрагмент.
Содержание:
Отправная точка: /sys/power/state и /sys/power/disk
Эти два системных файла помогают отладить режим гибернации и, таким образом, непосредственно контролируют состояние. Записывая определённые значения в файл , можно задавать конкретный тип энергопотребления (например, freeze, mem или disk), при этом disk предписывает системе перейти в режим гибернации. Файл power/state
контролирует, в какой именно режим гибернации перейдёт система (например, в power/disk
platform — см. ссылку выше
).
Разверни, чтобы узнать подробности о режимах энергопотребления в Linux.
Режимы энергопотребления в Linux
Следует несколько слов сказать о режимах энергопотребления в Linux. Концептуально ядро поддерживает 4 основных состояния (см. System Sleep State):
-
Suspend-to-idle. Процессоры переводятся в состояния простоя. Используется подсистема cpuidle. Замораживается пространство пользователя, приостанавливается отсчёт времени, все устройства ввода-вывода переводятся в состояние с низким энергопотреблением.
-
Suspend-to-standby. В настоящее время этот режим почти не встречается. См. доклад Лена Брауна «Is Linux Suspend ready for the next decade».
-
Suspend-to-RAM. Отключаются все процессоры, кроме загрузочного (на машинах с несколькими процессорами). В зависимости от возможностей платформы могут выполняться дополнительные действия. В частности, в системах на базе ACPI ядро передаёт управление микропрограмме платформы (BIOS) в качестве последнего шага при переходе в S2RAM, что обычно приводит к отключению питания ещё нескольких низкоуровневых компонентов, которые не контролируются ядром напрямую. Состояние устройств и процессоров сохраняется в память и хранится в ней. Все устройства приостанавливаются и переводятся в состояние низкого энергопотребления. Во многих случаях при входе в S2RAM периферийные шины теряют питание, поэтому устройства должны уметь возвращаться во «включённое» состояние.
-
Suspend-to-disk, также известный как гибернация. Создаётся образ текущего состояния системы. Образ сохраняется на диск, а система выключается. При следующем включении системы этот образ используется для восстановления состояния.
Suspend-to-disk отличается от других состояний. Пробуждение после него больше похоже на перезагрузку, чем на возобновление работы в трёх других методах. Его не включают pm labels[] и mem_sleep_labels[] в kernel/power/suspend.c, а основные функции реализованы в kernel/power/hibernate.c вместо kernel/power/suspend.c.
Неразбериха с названиями
Linux прокидывает вышеперечисленные состояния питания в различные интерфейсы. В итоге все перечисленные выше состояния питания (кроме suspend-to-disk) везде называются по-разному.
-
suspend-to-{idle, standby, ram}
— общие термины, относящиеся к реальным базовым механизмам. -
freeze, standby, mem
относятся к интерфейсу sysfs. Важно:mem
— это настраиваемое состояние, и оно может указывать на любое состояние из пункта 1. -
s2idle, shallow, deep
предназначены для настройкиmem
. Они нужны, чтобы прокинуть состояние из пункта 1 вmem
из пункта 2.
Ещё раз:
-
suspend-to-idle/standby/ram — общие термины для состояний питания;
-
feeze, standby, mem — термины, используемые интерфейсом sysfs.
/sys/power/state в sysfs — интерфейс для управления состоянием энергопотребления системы в Linux. С помощью cat /sys/power/state можно посмотреть поддерживаемые состояния энергопотребления вашей системы (например, freeze, mem, disk).
Если в этот файл записать одно из состояний, система в него перейдёт.
В данном случае freeze означает переход в режим suspend-to-idle, а disk — переход в режим suspend-to-disk. С параметром mem сложнее, поскольку он настраивается пользователем. Он может переводить систему в suspend-to-ram (если этот режим поддерживается) или suspend-to-idle в качестве резервного механизма, особенно если suspend-to-ram не поддерживается. Конкретным поведением управляет /sys/power/mem_sleep, и у него свой персональный набор обозначений для этих состояний:
-
s2idle, shallow, deep — термины для настройки поведения mem.
Здесь s2idle (как вы догадались) означает suspend-to-idle; shallow — suspend-to-standby; а deep — suspend-to-ram. Файл /sys/power/mem_sleep определяет, в какой режим перейдёт система, когда mem запишется /sys/power/state. Посмотреть доступные опции можно с помощью команды cat /sys/power/mem_sleep.
const char * const pm_labels[] = {
[PM_SUSPEND_TO_IDLE] = "freeze",
[PM_SUSPEND_STANDBY] = "standby",
[PM_SUSPEND_MEM] = "mem",
};
const char *pm_states[PM_SUSPEND_MAX];
static const char * const mem_sleep_labels[] = {
[PM_SUSPEND_TO_IDLE] = "s2idle",
[PM_SUSPEND_STANDBY] = "shallow",
[PM_SUSPEND_MEM] = "deep",
};
Источники:
https://hackmd.io/@0xff07/linux-pm/%2F%400xff07%2FrkmMQqbu6
https://www.kernel.org/doc/html/latest/admin-guide/pm/sleep-states.html
Это удобно для понимания работы систем — можно отследить изменения после записи новых значений.
Функции show и store
Эти две функции определяются с помощью макроса power_attr
:
#define power_attr(_name)
static struct kobj_attribute _name##_attr = {
.attr = {
.name = __stringify(_name),
.mode = 0644,
},
.show = _name##_show,
.store = _name##_store,
}
show
вызывается при чтении, а store
— при записи.
state_show
не подходит для наших целей, поскольку просто выводит все доступные состояния сна.
/*
* state контролирует состояния сна системы.
*
* show() возвращает доступные состояния сна: "mem", "standby",
* "freeze" и "disk" (гибернация).
* Описание режимов см. в Documentation/admin-guide/pm/sleep-states.rst
*
* store() принимает одно из этих строковых значений, преобразует его
* в соответствующее численное значение и запускает переход в сон.
*/
static ssize_t state_show(struct kobject *kobj, struct kobj_attribute *attr,
char *buf)
{
char *s = buf;
#ifdef CONFIG_SUSPEND
suspend_state_t i;
for (i = PM_SUSPEND_MIN; i < PM_SUSPEND_MAX; i++)
if (pm_states[i])
s += sprintf(s,"%s ", pm_states[i]);
#endif
if (hibernation_available())
s += sprintf(s, "disk ");
if (s != buf)
/* convert the last space to a newline */
*(s-1) = 'n';
return (s - buf);
}
С другой стороны, state_store
обеспечивает нужную точку входа. Если в файл state
записать значение «disk», вызывается hibernate()
. Это наша точка входа.
static ssize_t state_store(struct kobject *kobj, struct kobj_attribute *attr,
const char *buf, size_t n)
{
suspend_state_t state;
int error;
error = pm_autosleep_lock();
if (error)
return error;
if (pm_autosleep_state() > PM_SUSPEND_ON) {
error = -EBUSY;
goto out;
}
state = decode_state(buf, n);
if (state < PM_SUSPEND_MAX) {
if (state == PM_SUSPEND_MEM)
state = mem_sleep_current;
error = pm_suspend(state);
} else if (state == PM_SUSPEND_MAX) {
error = hibernate();
} else {
error = -EINVAL;
}
out:
pm_autosleep_unlock();
return error ? error : n;
}
static suspend_state_t decode_state(const char *buf, size_t n)
{
#ifdef CONFIG_SUSPEND
suspend_state_t state;
#endif
char *p;
int len;
p = memchr(buf, 'n', n);
len = p ? p - buf : n;
/* Сначала проверяем гибернацию. */
if (len == 4 && str_has_prefix(buf, "disk"))
return PM_SUSPEND_MAX;
#ifdef CONFIG_SUSPEND
for (state = PM_SUSPEND_MIN; state < PM_SUSPEND_MAX; state++) {
const char *label = pm_states[state];
if (label && len == strlen(label) && !strncmp(buf, label, len))
return state;
}
#endif
return PM_SUSPEND_ON;
}
Можно ли было догадаться просто по именам функций? Да, но так мы точно знаем, что до вызова этой функции ничего другого не происходит.
Autosleep
Наша первая остановка — система autosleep. Во фрагменте кода выше видно, что ядро блокирует pm_autosleep_lock
перед проверкой текущего состояния.
Механизм autosleep позаимствован у Android. Он отправляет всю систему в режим ожидания (suspend) или гибернации (hibernate), когда та неактивна. В большинстве десктопных конфигураций эта функция отключена, поскольку она предназначена в первую очередь для мобильных систем и переопределяет то, как режимы ожидания и гибернации работают в обычных условиях.
Все реализовано в виде очереди workqueue, которая проверяет текущее количество событий пробуждения (wakeup), процессов и драйверов, которые должны быть запущены. И если их нет, система переводится в состояние autosleep (обычно в режим ожидания — suspend). Тем не менее это может быть и режим гибернации — достаточно настроить его через подобно тому, как power/autosleep
включает режим гибернации по команде пользователя. power/state
static ssize_t autosleep_store(struct kobject *kobj,
struct kobj_attribute *attr,
const char *buf, size_t n)
{
suspend_state_t state = decode_state(buf, n);
int error;
if (state == PM_SUSPEND_ON
&& strcmp(buf, "off") && strcmp(buf, "offn"))
return -EINVAL;
if (state == PM_SUSPEND_MEM)
state = mem_sleep_current;
error = pm_autosleep_set_state(state);
return error ? error : n;
}
power_attr(autosleep);
#endif /* CONFIG_PM_AUTOSLEEP */
static DEFINE_MUTEX(autosleep_lock);
static struct wakeup_source *autosleep_ws;
static void try_to_suspend(struct work_struct *work)
{
unsigned int initial_count, final_count;
if (!pm_get_wakeup_count(&initial_count, true))
goto out;
mutex_lock(&autosleep_lock);
if (!pm_save_wakeup_count(initial_count) ||
system_state != SYSTEM_RUNNING) {
mutex_unlock(&autosleep_lock);
goto out;
}
if (autosleep_state == PM_SUSPEND_ON) {
mutex_unlock(&autosleep_lock);
return;
}
if (autosleep_state >= PM_SUSPEND_MAX)
hibernate();
else
pm_suspend(autosleep_state);
mutex_unlock(&autosleep_lock);
if (!pm_get_wakeup_count(&final_count, false))
goto out;
/*
* Если пробуждение произошло по неизвестной причине, ждём,
* чтобы избежать бесконечного цикла засыпаний и пробуждений.
*/
if (final_count == initial_count)
schedule_timeout_uninterruptible(HZ / 2);
out:
queue_up_suspend_work();
}
static DECLARE_WORK(suspend_work, try_to_suspend);
void queue_up_suspend_work(void)
{
if (autosleep_state > PM_SUSPEND_ON)
queue_work(autosleep_wq, &suspend_work);
}
Этапы перехода в гибернацию
Настройка режима гибернации в ядре
Важно отметить, что большинство функций, связанных с режимом гибернации, ничего не делают, если в конфигурации Kconfig не задано значение CONFIG_HIBERNATION
. Например, вот как выглядит функция hibernate
, если переменная CONFIG_HIBERNATE
не установлена.
static inline int hibernate(void) { return -ENOSYS; }
Проверка доступности режима гибернации
Проверим с помощью функции hibernation_available
, доступен ли режим.
if (!hibernation_available()) {
pm_pr_dbg("Hibernation not available.n");
return -EPERM;
}
bool hibernation_available(void)
{
return nohibernate == 0 &&
!security_locked_down(LOCKDOWN_HIBERNATION) &&
!secretmem_active() && !cxl_mem_active();
}
Параметр nohibernate
управляется флагами командной строки ядра. Его можно установить, задав либо nohibernate
, либо hibernate=no
.
Хук security_locked_down
для модулей безопасности Linux (Linux Security Modules) предотвращает переход в режим гибернации. Он не даёт перейти в него, если состояние записывается на незашифрованное устройство хранения (см. kernel_lockdown(7)). Любопытно, что любой уровень блокировки, будь то integrity или confidentiality (см. Linux kernel lockdown, integrity, and confidentiality. — Прим. пер.), запрещает переход в режим гибернации. Потому что в ином случае из сохранённого на диск образа ядра можно извлечь практически всё, что угодно, внести в него изменения и даже перезагрузиться с этим образом.
secretmem_active
проверяет, используется ли memfd_secret
, и если да, то предотвращает переход в режим гибернации. memfd_secret
возвращает файловый дескриптор, который можно отобразить в память процесса, но эта память отключена от адресного пространства ядра. Переход в гибернацию с памятью, к которой не имеет доступа даже ядро, открывает доступ к этой памяти тому, у кого есть доступ к образу гибернации. Эта особенность секретной памяти вызвала много споров, хотя и не таких, как опасения по поводу фрагментации при разворачивании памяти ядра (которая в итоге так и не стала реальной проблемой).
cxl_mem_active
просто проверяет, активна ли память CXL. Полное объяснение приведено в коммите, реализующем эту проверку. Но есть и сокращённое объяснение в cxl_mem_probe
, которая устанавливает соответствующий флаг при инициализации устройства памяти CXL.
Ядро может работать с CXL-памятью на этом устройстве.
* Не существует определённого в спецификации способа определить,
* сохраняет ли это устройство содержимое при переходе
* в режим ожидания (suspend), и нет простого способа
* организовать suspend-образ таким образом, чтобы обойти CXL-память,
* но это создало бы циклическую зависимость:
* для восстановления состояния CXL-памяти нужно сначала
* восстановить работу PCI-шины, но для полного восстановления
* состояния системы нужно восстановить содержимое всей памяти,
* включая CXL.
Проверка сжатия
Далее необходимо проверить, включена ли поддержка сжатия, и если да, то включён ли требуемый алгоритм.
/*
* Узнаём, какой алгоритм сжатия поддерживается, если сжатие включено.
*/
if (!nocompress) {
strscpy(hib_comp_algo, hibernate_compressor, sizeof(hib_comp_algo));
if (crypto_has_comp(hib_comp_algo, 0, 0) != 1) {
pr_err("%s compression is not availablen", hib_comp_algo);
return -EOPNOTSUPP;
}
}
Флаг nocompress
устанавливается с помощью параметра командной строки hibernate: hibernate=nocompress
.
Если сжатие включено, то hibernate_compressor
копируется в hib_comp_algo
. Так запрашиваемая настройка сжатия (hibernate_compressor
) синхронизируется с текущей настройкой сжатия (hib_comp_algo
).
Оба значения являются символьными массивами размера CRYPTO_MAX_ALG_NAME
(128 в данном ядре).
static char hibernate_compressor[CRYPTO_MAX_ALG_NAME] = CONFIG_HIBERNATION_DEF_COMP;
/*
* Алгоритм сжатия/распаковки, который будет использоваться
* при сохранении/загрузке образа на диск. Позже он будет использован
* в файле 'kernel/power/swap.c' для распределения потоков
* сжатия.
*/
char hib_comp_algo[CRYPTO_MAX_ALG_NAME];
hibernate_compressor
по умолчанию использует lzo
, если этот алгоритм включён, иначе используется lz4
. С помощью hibernate_compressor
значение по умолчанию можно переопределить на lzo
или lz4
.
choice
prompt "Default compressor"
default HIBERNATION_COMP_LZO
depends on HIBERNATION
config HIBERNATION_COMP_LZO
bool "lzo"
depends on CRYPTO_LZO
config HIBERNATION_COMP_LZ4
bool "lz4"
depends on CRYPTO_LZ4
endchoice
config HIBERNATION_DEF_COMP
string
default "lzo" if HIBERNATION_COMP_LZO
default "lz4" if HIBERNATION_COMP_LZ4
help
Default compressor to be used for hibernation.
static const char * const comp_alg_enabled[] = {
#if IS_ENABLED(CONFIG_CRYPTO_LZO)
COMPRESSION_ALGO_LZO,
#endif
#if IS_ENABLED(CONFIG_CRYPTO_LZ4)
COMPRESSION_ALGO_LZ4,
#endif
};
static int hibernate_compressor_param_set(const char *compressor,
const struct kernel_param *kp)
{
unsigned int sleep_flags;
int index, ret;
sleep_flags = lock_system_sleep();
index = sysfs_match_string(comp_alg_enabled, compressor);
if (index >= 0) {
ret = param_set_copystring(comp_alg_enabled[index], kp);
if (!ret)
strscpy(hib_comp_algo, comp_alg_enabled[index],
sizeof(hib_comp_algo));
} else {
ret = index;
}
unlock_system_sleep(sleep_flags);
if (ret)
pr_debug("Cannot set specified compressor %sn",
compressor);
return ret;
}
static const struct kernel_param_ops hibernate_compressor_param_ops = {
.set = hibernate_compressor_param_set,
.get = param_get_string,
};
static struct kparam_string hibernate_compressor_param_string = {
.maxlen = sizeof(hibernate_compressor),
.string = hibernate_compressor,
};
Затем с помощью crypto_has_comp
проверяется, поддерживается ли запрашиваемый алгоритм. Если он не поддерживается, процесс гибернации прерывается с ошибкой EOPNOTSUPP
.
В рамках crypto_has_comp
алгоритм инициализируется, загружаются нужные модули ядра и выполняется код инициализации.
Захват блокировок
Следующим шагом будет захват блокировок сна и гибернации с помощью lock_system_sleep
и hibernate_acquire
:
sleep_flags = lock_system_sleep();
/* Snapshot-устройство не должно открываться, пока мы работаем */
if (!hibernate_acquire()) {
error = -EBUSY;
goto Unlock;
}
Сначала lock_system_sleep
помечает текущий поток (thread) как незамораживаемый (frozen), что очень пригодится в дальнейшем. Затем он захватывает system_transistion_mutex
, который блокирует создание снапшотов или изменение способа их создания, возобновление работы из образа гибернации, переход в любое suspend-состояние или перезагрузку.
Маска GFP
Ядро выдаёт предупреждение, если маска gfp
изменяется с помощью pm_restore_gfp_mask
или pm_restrict_gfp_mask
без удержания system_transistion_mutex
.
Флаги GFP определяют, как ядру разрешено обрабатывать запрос на память.
* Флаги GFP широко используются в Linux при распределении памяти. Аббревиатура GFP
* означает get_free_pages(), базовую функцию распределения памяти. Не все флаги GFP
* поддерживаются функциями, способными распределять память.
В случае с гибернацией нас интересуют флаги IO
и FS
, которые являются операторами возврата (reclaim) памяти. Они указывают системе, что можно использовать операции ввода-вывода или файловую систему для освобождения памяти, если это необходимо для выполнения запроса на выделение памяти.
* Модификаторы возврата
* Обратите внимание, что все последующие флаги применимы только
* к распределениям, поддерживающим спящий режим (например,
* %GFP_NOWAIT и
* %GFP_ATOMIC будут их игнорировать).
*
* %__GFP_IO запускает физические операции ввода-вывода.
*
* %__GFP_FS работает с низкоуровневой ФС. Снятие флага позволяет
* аллокатору не обращаться к файловой системе, на которую уже
* навешены блокировки.
gfp_allowed_mask
определяет, какие флаги разрешено устанавливать в текущий момент.
gfp_allowed_mask
— это маска, которая контролирует, какие флаги могут использоваться при выделении памяти. Во время приостановки или гибернации некоторые способы выделения памяти (например, те, которые могут вызвать операции ввода-вывода) становятся опасными. Поэтому система временно запрещает их использование. — Прим. пер.
Как говорится в комментарии ниже, предотвращение установки этих флагов позволяет избежать ситуаций, когда ядру необходимо выполнить операции ввода-вывода для распределения памяти, например чтение из / запись в swap. Но устройства, с которых нужно читать / в которые нужно писать, в данный момент недоступны.
/*
* Приведённые ниже функции используются кодом suspend/hibernate
* для временного изменения gfp_allowed_mask, чтобы избежать
* использования ввода-вывода при выделении памяти во время приостановки
* работы устройств. Чтобы избежать гонок с кодом suspend/hibernate, их
* всегда следует вызывать при захваченном system_transition_mutex
* (gfp_allowed_mask также следует изменять только при наличии
* system_transition_mutex, если только код suspend/hibernate
* гарантированно не будет выполняться параллельно с этой модификацией).
*/
static gfp_t saved_gfp_mask;
void pm_restore_gfp_mask(void)
{
WARN_ON(!mutex_is_locked(&system_transition_mutex));
if (saved_gfp_mask) {
gfp_allowed_mask = saved_gfp_mask;
saved_gfp_mask = 0;
}
}
void pm_restrict_gfp_mask(void)
{
WARN_ON(!mutex_is_locked(&system_transition_mutex));
WARN_ON(saved_gfp_mask);
saved_gfp_mask = gfp_allowed_mask;
gfp_allowed_mask &= ~(__GFP_IO | __GFP_FS);
}
Sleep-флаги
Заблокировав system_transition_mutex
, ядро фиксирует предыдущее состояние флагов потоков в sleep_flags
. Потом оно используется при удалении PF_NOFREEZE
, если тот не был ранее установлен для текущего потока (то есть если PF_NOFREEZE
не был установлен, то вернутся изначальные флаги из sleep_flags
, если PF_NOFREEZE
был установлен — он и останется. — Прим. пер.).
unsigned int lock_system_sleep(void)
{
unsigned int flags = current->flags;
current->flags |= PF_NOFREEZE;
mutex_lock(&system_transition_mutex);
return flags;
}
EXPORT_SYMBOL_GPL(lock_system_sleep);
#define PF_NOFREEZE 0x00008000 /* Этот поток не замораживать */
Затем устанавливается семафор для гибернации, который не позволяет другим процессам открыть снапшот или возобновить работу из него, пока система переходит в режим гибернации. Эта блокировка также не дает вызвать функцию hibernate_quiet_exec
, которая используется драйвером nvdimm
для активации прошивки с замораживанием всех процессов и устройств. Это нужно, чтобы гарантировать, что в это время работает только сам драйвер.
bool hibernate_acquire(void)
{
return atomic_add_unless(&hibernate_atomic, -1, 0);
}
Подготовка консоли
Далее ядро вызывает pm_prepare_console
. Эта функция работает только в том случае, если установлено значение CONFIG_VT_CONSOLE_SLEEP
.
Она подготавливает виртуальный терминал (VT) к suspend, при необходимости переключаясь на консоль, используемую только для suspend.
void pm_prepare_console(void)
{
if (!pm_vt_switch())
return;
orig_fgconsole = vt_move_to_console(SUSPEND_CONSOLE, 1);
if (orig_fgconsole < 0)
return;
orig_kmsg = vt_kmsg_redirect(SUSPEND_CONSOLE);
return;
}
Первое, что нужно сделать, — это проверить, действительно ли нужно переключать VT.
/*
* Есть три случая, когда требуется переключение VT при переходе
* в спящий режим / выходе из него:
* 1) ни один драйвер так или иначе не указал требования, поэтому
* сохраняем старое поведение;
* 2) консольный suspend (параметр no_console_suspend. — Прим. пер.)
* отключён, а мы хотим видеть отладочные сообщения во время перехода
* в спящий режим / выхода из него;
* 3) какой-либо зарегистрированный драйвер требует переключения VT.
*
* Если ни одно из этих условий не выполняется, то есть имеется хотя бы
* один драйвер, которому переключение не требуется, и нет ни одного,
* которому оно нужно, можно обойтись без него, чтобы выход из спящего
* режима выглядел чуть красивее (и переход в suspend тоже, но этого
* пользователь обычно не видит из-за того, например, что крышка его
* ноутбука уже закрыта).
*/
static bool pm_vt_switch(void)
{
struct pm_vt_switch *entry;
bool ret = true;
mutex_lock(&vt_switch_mutex);
if (list_empty(&pm_vt_switch_list))
goto out;
if (!console_suspend_enabled)
goto out;
list_for_each_entry(entry, &pm_vt_switch_list, head) {
if (entry->required)
goto out;
}
ret = false;
out:
mutex_unlock(&vt_switch_mutex);
return ret;
}
В комментарии во фрагменте кода выше перечислены условия, при которых выполняется переключение. Давайте поговорим о них подробнее.
Сначала блокируется vt_switch_mutex
, чтобы ничто не могло изменить список, пока он изучается.
Далее анализируется сам список pm_vt_switch_list
. Этот список содержит драйверы, требующие переключения во время перехода в режим suspend. Они сообщают об этом с помощью pm_vt_switch_required
.
/**
* pm_vt_switch_required — переключать VT при переходе в спящий режим
* @dev: устройство
* @required: если true, то вызывающему драйверу требуется переключение
* VT при переходе в спящий режим / выходе из него.
*
* Различные консольные драйверы могут требовать или не требовать
* переключения VT при переходе в спящий режим / выходе из него
* в зависимости от того, как обрабатывается восстановление графического режима
* и того, что запущено.
*
* Драйверы также могут указать, что переключение им не требуется, — это
* сэкономит время и устранит мерцание экрана, — передавая в качестве
* аргумента 'false'. Если какой-либо загруженный драйвер требует
* переключения VT или в командной строке был передан аргумент
* no_console_suspend, переключение VT произойдёт.
*/
void pm_vt_switch_required(struct device *dev, bool required)
Далее проверяется console_suspend_enabled
. Это значение устанавливается в false параметром ядра no_console_suspend
, но по умолчанию оно равно true.
Наконец, если в списке pm_vt_switch_list
есть какие-либо записи, то система проверяет, не требуют ли они переключения VT.
Только если ни одно из этих условий не выполняется, возвращается false.
Если переключение VT все-таки требуется, в SUSPEND_CONSOLE
сначала перемещается активный в данный момент виртуальный терминал (консоль) (vt_move_to_console
), а затем текущее местоположение сообщений ядра (vt_kmsg_redirect
). SUSPEND_CONSOLE
— последняя запись в списке возможных консолей, которая просто выполняет роль этакой черной дыры, в которую сбрасываются все сообщения.
#define SUSPEND_CONSOLE (MAX_NR_CONSOLES-1)
Любопытно, что это отдельные функции. Хотя TIOCL_SETKMSGREDIRECT позволяет перенаправлять сообщения ядра на заданный виртуальный терминал, по умолчанию он совпадает с активной консолью.
(Примечание: ioctl — это специальные операции ввода-вывода для конкретных устройств. Они позволяют выполнять действия, выходящие за рамки стандартных операций с файлами — чтения, записи, поиска и т. д.)
Система сохраняет предыдущую активную консоль и место хранения сообщений ядра в переменных orig_fgconsole и orig_kmsg. После выхода из сна эти значения помогают восстановить состояние консоли и лога ядра.
Важно: orig_fgconsole фиксирует не только номер консоли, но и ошибки. Перед тем как работать с журналом ядра при переходе в сон или пробуждении, необходимо проверить, что orig_fgconsole не меньше нуля. Иначе можно столкнуться с некорректным поведением системы.
drivers/tty/vt/vt_ioctl.c:1268
/* Выполняем инициированное ядром переключение VT для приостановки/возобновления работы */
static int disable_vt_switch;
int vt_move_to_console(unsigned int vt, int alloc)
{
int prev;
console_lock();
/* Гарфический режим — вплоть до Х */
if (disable_vt_switch) {
console_unlock();
return 0;
}
prev = fg_console;
if (alloc && vc_allocate(vt)) {
/* Пока не можем освободить виртуальную консоль, ибо
* это может привести к проблемам с отображением на экран. */
console_unlock();
return -ENOSPC;
}
if (set_console(vt)) {
/*
* Не удалось переключиться на SUSPEND_CONSOLE.
* Сообщаем об этом вызывающей функции,
* пусть она решает, что делать.
*/
console_unlock();
return -EIO;
}
console_unlock();
if (vt_waitactive(vt + 1)) {
pr_debug("Suspend: Can't switch VCs.");
return -EINTR;
}
return prev;
}
В отличие от большинства функций блокировки, console_lock перед захватом семафора консоли проверяет, не паникует ли в этот момент другой процессор. (Если процессор паникует, он должен успеть вывести отладочную информацию в консоль до перезагрузки. Поэтому у него приоритет выше, а доступ других процессоров к терминалу временно блокируется. — Прим. пер.)
Паники
Система отслеживает панику с помощью атомарного целого числа, которое хранит ID процессора, находящегося в состоянии паники.
/**
* console_lock — блокирует печать в консольной субсистеме.
*
* Блокировка гарантирует, что ни одна консоль не выполняет
* или не будет выполнять коллбэк write().
*
* Может заснуть, ничего не возвращает.
*/
void console_lock(void)
{
might_sleep();
/* В случае паники console_lock должен оставаться у паникующего CPU.*/
while (other_cpu_in_panic())
msleep(1000);
down_console_sem();
console_locked = 1;
console_may_schedule = 1;
}
EXPORT_SYMBOL(console_lock);
/*
* Возвращает true, если паникует другой процессор.
*
* Если это так, текущий процессор должен немедленно освободить все ресурсы, связанные
* с выводом сообщений, чтобы они могли быть использованы паникующим процессором.
*/
bool other_cpu_in_panic(void)
{
return (panic_in_progress() && !this_cpu_in_panic());
}
static bool panic_in_progress(void)
{
return unlikely(atomic_read(&panic_cpu) != PANIC_CPU_INVALID);
}
/* Возвращает true, если паникует текущий процессор. */
bool this_cpu_in_panic(void)
{
/*
* Здесь можно использовать raw_smp_processor_id(), потому
* что задача не может быть перенесена ни на panic_cpu, ни с него.
* Если panic_cpu уже установлен, и мы сейчас не выполняемся на нем
* то мы никогда на нем и не будем выполняться.
*/
return unlikely(atomic_read(&panic_cpu) == raw_smp_processor_id());
}
console_locked
— отладочное значение. Используется для указания на необходимость удержания блокировки. Его наличие — первый признак того, что система виртуальных терминалов устроена сложнее, чем кажется на первый взгляд.
/*
* Используется для отладки бардака, которым является код VT,
* отслеживая, захвачен ли семафор консоли. Это определенно не идеальный
* инструмент отладки (неизвестно, удерживаем ли его _МЫ_ и участвуем
* ли в гонках), но он помогает отслеживать странности в консольном
* коде, когда мы оказываемся в местах, которые надо заблокировать
* без удержания консольного семафора.
*/
static int console_locked;
console_may_schedule
показывает, можно ли перейти в спящий режим и запланировать другие задачи, пока удерживается блокировка. Как мы увидим позже, подсистема виртуального терминала не может безопасно обрабатывать повторные вызовы одних и тех же функций, поэтому есть всевозможные хаки, которые помогают гарантировать, что все важные куски кода, которые невозможно выполнить повторно, выполнены.
Отключение переключателя VT
Как говорится в приведённом ниже комментарии, когда графическим отображением занимается другая программа, нет необходимости в переключении виртуальных терминалов. Поэтому в ядре предусмотрен соответствующий выключатель. Интересно, что он используется только тремя драйверами — его аппаратная поддержка не слишком распространена.
drivers/gpu/drm/omapdrm/dss
drivers/video/fbdev/geode
drivers/video/fbdev/omap2
drivers/tty/vt/vt_ioctl.c:1308
/*
* Обычно во время перехода в спящий режим мы выделяем новую консоль и
* переключаемся на неё. При возобновлении работы возвращаемся к исходной
* консоли. Такое переключение может проходить неспешно,
* поэтому в системах, где фреймбуфер и так справляется
* с восстановлением видеорегистров, в переключении нет смысла. Эта
* функция отключает переключение, передавая '0'.
*/
void pm_set_vt_switch(int do_switch)
{
console_lock();
disable_vt_switch = !do_switch;
console_unlock();
}
EXPORT_SYMBOL(pm_set_vt_switch);
Остальная часть функции vt_switch_console
ничем не примечательна. Она просто выделяет место, если это необходимо, для создания требуемого виртуального терминала, а затем устанавливает текущий виртуальный терминал с помощью функции set_console
.
Set Console виртуального терминала
С помощью set_console
мы начинаем входить в ту самую безумную подсистему виртуальных терминалов. Как уже говорилось, изменять её состояние следует крайне осторожно, поскольку параллельные процессы могут привести к полной неразберихе.
При этом сам вызов set_console
фактически не выполняет никакой работы по изменению состояния текущей консоли. Вместо этого он указывает, какие изменения необходимы, а затем планирует их (добавляет в workqueue. — Прим. пер.).
int set_console(int nr)
{
struct vc_data *vc = vc_cons[fg_console].d;
if (!vc_cons_allocated(nr) || vt_dont_switch ||
(vc->vt_mode.mode == VT_AUTO && vc->vc_mode == KD_GRAPHICS)) {
/*
* Поскольку переключение консоли неизбежно приведет
* к ошибке в console_callback() или change_console(),
* отменяем планирование обратного вызова для оптимизации.
*
* Это безопасно, поскольку существующие пользователи
* функции set_console()
* игнорируют значение, возвращаемое ею.
*/
return -EINVAL;
}
want_console = nr;
schedule_console_callback();
return 0;
}
Проверка vc->vc_mode == KD_GRAPHICS
отменяет переключение в suspend-консоль, если система работает в графическом режиме.
Флаг vt_dont_switch
используется в ioctls VT_LOCKSWITCH и VT_UNLOCKSWITCH
и не позволяет системе переключать виртуальный терминал, если пользователь явно заблокировал это.
Флаг VT_AUTO
означает, что автоматическое переключение виртуальных терминалов включено и, следовательно, преднамеренное переключение на suspend-терминал не требуется.
Однако если вы работаете в виртуальном терминале, то механизм меняется. Переменная want_console
говорит системе, что необходимо перейти на требуемый виртуальный терминал, а само переключение планируется с помощью schedule_console_callback
.
void schedule_console_callback(void)
{
schedule_work(&console_work);
}
console_work
— это workqueue, которая будет выполнять заданную задачу асинхронно.
Коллбэк консоли
/*
* Это коллбэк переключения консоли.
* Переключение консоли в контексте процесса позволяет
* выполнять переключения асинхронно (нужно, когда переключаемся
* по прерыванию клавиатуры). За синхронизацию с консольным кодом
* и предотвращение повторного входа в код переключения консоли
* отвечает console_lock.
*/
static void console_callback(struct work_struct *ignored)
{
console_lock();
if (want_console >= 0) {
if (want_console != fg_console &&
vc_cons_allocated(want_console)) {
hide_cursor(vc_cons[fg_console].d);
change_console(vc_cons[want_console].d);
/* Мы изменяем консоль, только если она уже выделена. Новая консоль не создается в обработчике прерывания. */
}
want_console = -1;
}
...
console_callback
сначала проверяет, есть ли консоль, которую хотят изменить через want_console
, а затем переключается на неё, если она не является текущей и уже была выделена (allocated). Сначала с помощью hide_cursor
удаляется состояние курсора.
static void hide_cursor(struct vc_data *vc)
{
if (vc_is_sel(vc))
clear_selection();
vc->vc_sw->con_cursor(vc, false);
hide_softcursor(vc);
}
Полное погружение в драйвер tty
выходит за рамки этой статьи. Код выше даёт общее представление о том, как эта система взаимодействует с гибернацией.
Уведомление цепочки вызовов управления питанием
pm_notifier_call_chain_robust(PM_HIBERNATION_PREPARE, PM_POST_HIBERNATION)
Вызов цепочки коллбэков управления питанием. Сначала передаётся PM_HIBERNATION_PREPARE
, а затем PM_POST_HIBERNATION
при запуске или ошибке с другим коллбэком.
int pm_notifier_call_chain_robust(unsigned long val_up, unsigned long val_down)
{
int ret;
ret = blocking_notifier_call_chain_robust(&pm_chain_head, val_up, val_down, NULL);
return notifier_to_errno(ret);
}
Нотификатор управления питанием представляет собой блокирующую цепочку уведомлений, что означает, что он обладает следующими свойствами:
* Блокирующие цепочки уведомлений: коллбэки цепочек выполняются в контексте процесса.
* Функции обратного вызова (callouts) могут использовать блокирующие операции.
Цепочка коллбэков представляет собой связанный список, каждая запись которого содержит приоритет и функцию для вызова. Технически функция принимает значение, но для цепи управления питанием оно всегда NULL
.
struct notifier_block;
typedef int (*notifier_fn_t)(struct notifier_block *nb,
unsigned long action, void *data);
struct notifier_block {
notifier_fn_t notifier_call;
struct notifier_block __rcu *next;
int priority;
};
Head связанного списка защищена семафором чтения-записи.
struct blocking_notifier_head {
struct rw_semaphore rwsem;
struct notifier_block __rcu *head;
};
Поскольку список идёт с приоритетами, при добавлении его приходится перебирать до тех пор, пока не будет найден элемент с более низким приоритетом, перед которым можно вставить текущий элемент.
/*
* Блокирующие цепочки уведомлений. Весь доступ
* к цепочке синхронизируется с помощью rwsem.
*/
static int __blocking_notifier_chain_register(struct blocking_notifier_head *nh,
struct notifier_block *n,
bool unique_priority)
{
int ret;
/*
* Этот код используется во время загрузки, когда
* переключение задач ещё не работает и прерывания
* должны оставаться отключёнными. В такие моменты
* нельзя вызывать down_write().
*/
if (unlikely(system_state == SYSTEM_BOOTING))
return notifier_chain_register(&nh->head, n, unique_priority);
down_write(&nh->rwsem);
ret = notifier_chain_register(&nh->head, n, unique_priority);
up_write(&nh->rwsem);
return ret;
}
/*
* Основные подпрограммы (routines) цепочки уведомлений.
* Экспортируемые подпрограммы накладываются
* поверх них, с добавлением соответствующей блокировки.
*/
static int notifier_chain_register(struct notifier_block **nl,
struct notifier_block *n,
bool unique_priority)
{
while ((*nl) != NULL) {
if (unlikely((*nl) == n)) {
WARN(1, "notifier callback %ps already registered",
n->notifier_call);
return -EEXIST;
}
if (n->priority > (*nl)->priority)
break;
if (n->priority == (*nl)->priority && unique_priority)
return -EBUSY;
nl = &((*nl)->next);
}
n->next = *nl;
rcu_assign_pointer(*nl, n);
trace_notifier_register((void *)n->notifier_call);
return 0;
}
Каждый коллбэк может возвращать одну из нескольких опций.
#define NOTIFY_DONE 0x0000 /* Без разницы */
#define NOTIFY_OK 0x0001 /* Подходит */
#define NOTIFY_STOP_MASK 0x8000 /* Не вызываем дальше */
#define NOTIFY_BAD (NOTIFY_STOP_MASK|0x0002)
/* Плохое/Вето-действие */
Если при уведомлении цепочки функция возвращает STOP
или BAD
, предыдущие части цепочки вызываются снова с PM_POST_HIBERNATION
и возвращается ошибка.
/**
* notifier_call_chain_robust — информирование зарегистрированных
* уведомителей (notifiers) о событии и откат при ошибке.
* @nl: указатель на голову цепочки блокирующих уведомителей.
* @val_up: значение, передаваемое в неизменном виде
* функции-уведомителю.
* @val_down: значение, передаваемое в неизменном виде
* функции-уведомителю при восстановлении после ошибки на @val_up.
* @v: указатель, передаваемый в неизменном виде
* функции-уведомителю.
*
* ПРИМЕЧАНИЕ: Важно, чтобы цепочка @nl не менялась между двумя
* вызовами notifier_call_chain() так, чтобы перебирались
* одни и те же коллбэки обработчиков; это исключает любое
* использование RCU.
*
* Return: возвращаемое значение вызова @val_up.
*/
static int notifier_call_chain_robust(struct notifier_block **nl,
unsigned long val_up, unsigned long val_down,
void *v)
{
int ret, nr = 0;
ret = notifier_call_chain(nl, val_up, v, -1, &nr);
if (ret & NOTIFY_STOP_MASK)
notifier_call_chain(nl, val_down, v, nr-1, NULL);
return ret;
}
RCU, Read-Copy-Update — механизм синхронизации в Linux, который позволяет нескольким потокам одновременно читать данные (прим. пер.).
Каждый из этих коллбэков зависит от конкретного драйвера, поэтому воздержимся от дальнейшего обсуждения.
Синхронизация файловых систем
Следующий шаг — убедиться, что все файловые системы синхронизированы на диск.
Это реализуется с помощью простой вспомогательной функции, которая определяет, сколько времени занимает полная операция синхронизации ksys_sync
.
void ksys_sync_helper(void)
{
ktime_t start;
long elapsed_msecs;
start = ktime_get();
ksys_sync();
elapsed_msecs = ktime_to_ms(ktime_sub(ktime_get(), start));
pr_info("Filesystems sync: %ld.%03ld secondsn",
elapsed_msecs / MSEC_PER_SEC, elapsed_msecs % MSEC_PER_SEC);
}
EXPORT_SYMBOL_GPL(ksys_sync_helper);
ksys_sync
запускает набор потоков сброса на диск для каждой файловой системы, даёт задачу синхронизировать их inodes, затем всю файловую систему и, наконец, все блочные устройства, чтобы гарантировать, что все страницы будут записаны на диск.
/*
* Синхронизируем всё. Начинаем с пробуждения потоков для
* сброса на диск, чтобы запись шла на всех устройствах параллельно.
* Затем синхронизируем все иноды, дожидаясь завершения записи
* всеми потоками. В этот момент все данные находятся на диске,
* поэтому метаданные не меняются. Файловым системам даётся задача
* синхронизировать метаданные с помощью вызовов ->sync_fs().
* Наконец, сохраняем все блочные устройства, потому что некоторые
* файловые системы (например, ext2) просто записывают метаданные
* (такие, как inodes или bitmaps) в кэш страниц блочных устройств
* и не синхронизируют их самостоятельно в ->sync_fs().
*/
void ksys_sync(void)
{
int nowait = 0, wait = 1;
wakeup_flusher_threads(WB_REASON_SYNC);
iterate_supers(sync_inodes_one_sb, NULL);
iterate_supers(sync_fs_one_sb, &nowait);
iterate_supers(sync_fs_one_sb, &wait);
sync_bdevs(false);
sync_bdevs(true);
if (unlikely(laptop_mode))
laptop_sync_completion();
}
Здесь применяется интересная схема, когда iterate_supers
запускает sync_inodes_one_sb
, а затем sync_fs_one_sb
для каждой известной файловой системы. (Каждая активная файловая система регистрируется в ядре с помощью структуры, известной как суперблок, который содержит ссылки на все иноды, а также указатели функций для выполнения различных необходимых операций, например синхронизации. — Прим. автора.) Кроме того, дважды вызываются sync_fs_one_sb
и sync_bdevs
, сначала без ожидания завершения операций, а затем с ожиданием завершения.
Когда laptop_mode
включён, система запускает дополнительные операции синхронизации файловой системы после указанной задержки без каких-либо операций записи.
/*
* Флаг, переводящий машину в «режим ноутбука».
* Удваивается как таймаут в jiffies:
* если в течение этого времени диск оставался неактивен,
* запускается полная синхронизация.
*/
int laptop_mode;
EXPORT_SYMBOL(laptop_mode);
Jiffies — Интервал между двумя прерываниями системного таймера в ядре Linux (прим. пер.).
Однако при выполнении операции синхронизации файловой системы система добавит дополнительный таймер, чтобы запланировать больше операций записи после задержки laptop_mode
. Нам не нужно, чтобы состояние системы менялось во время перехода в спящий режим, поэтому эти таймеры отменяются.
/*
* Мы в режиме ноутбука и только что провели синхронизацию.
* laptop_io_completion планирует очередную запись, однако
* больше ничего записывать не нужно, поэтому запланированная
* запись отменяется.
*/
void laptop_sync_completion(void)
{
struct backing_dev_info *bdi;
rcu_read_lock();
list_for_each_entry_rcu(bdi, &bdi_list, bdi_list)
del_timer(&bdi->laptop_mode_wb_timer);
rcu_read_unlock();
}
В качестве примечания: функция ksys_sync
просто вызывается, когда используется системный вызов sync
.
SYSCALL_DEFINE0(sync)
{
ksys_sync();
return 0;
}
Конец подготовительного этапа
На этом подготовка к переходу в спящий режим завершена. Я решил пока прерваться на этом моменте. Далее система начнет полное замораживание пользовательского пространства, чтобы затем сбросить память в образ и наконец перейти в режим гибернации. Но об этом мы расскажем в следующих статьях.
P. S.
Читайте также в нашем блоге:
Автор: kubelet