- PVSM.RU - https://www.pvsm.ru -

Как Linux готовится ко сну

Перевели для вас статью Джейкоба Адамса о том, что происходит перед тем, как Linux уходит в сон. Дальше идёт текст оригинала.

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

Мое исследование разделено на несколько частей. В первой речь пойдёт о периоде от вызова режима гибернации до синхронизации всех файловых систем на диск.

Как Linux готовится ко сну - 1

Эта статья написана для Linux версии 6.9.9. Её исходники широко доступны, но проще всего их посмотреть на Bootlin Elixir Cross-Referencer [1]. Каждый фрагмент кода будет начинаться со ссылки на вышеуказанный ресурс с указанием пути к файлу и номера строки, с которой начинается фрагмент.

Содержание:

Отправная точка: /sys/power/state и /sys/power/disk

Эти два системных файла помогают отладить режим гибернации [12] и, таким образом, непосредственно контролируют состояние. Записывая определённые значения в файл /sys/power/state, можно задавать конкретный тип энергопотребления (например, freeze, mem или disk), при этом disk предписывает системе перейти в режим гибернации. Файл /sys/power/disk контролирует, в какой именно режим гибернации перейдёт система  (например, в platform — см. ссылку выше).

Разверни, чтобы узнать подробности о режимах энергопотребления в Linux.

Режимы энергопотребления в Linux

Следует несколько слов сказать о режимах энергопотребления в Linux. Концептуально ядро поддерживает 4 основных состояния (см. System Sleep State [13]):

  • Suspend-to-idle. Процессоры переводятся в состояния простоя. Используется подсистема cpuidle. Замораживается пространство пользователя, приостанавливается отсчёт времени, все устройства ввода-вывода переводятся в состояние с низким энергопотреблением.

  • Suspend-to-standby. В настоящее время этот режим почти не встречается.  См. доклад Лена Брауна «Is Linux Suspend ready for the next decade» [14].

  • Suspend-to-RAM. Отключаются все процессоры, кроме загрузочного (на машинах с несколькими процессорами). В зависимости от возможностей платформы могут выполняться дополнительные действия. В частности, в системах на базе ACPI ядро передаёт управление микропрограмме платформы (BIOS) в качестве последнего шага при переходе в S2RAM, что обычно приводит к отключению питания ещё нескольких низкоуровневых компонентов, которые не контролируются ядром напрямую. Состояние устройств и процессоров сохраняется в память и хранится в ней. Все устройства приостанавливаются и переводятся в состояние низкого энергопотребления. Во многих случаях при входе в S2RAM периферийные шины теряют питание, поэтому устройства должны уметь возвращаться во «включённое» состояние.

  • Suspend-to-disk, также известный как гибернация. Создаётся образ текущего состояния системы. Образ сохраняется на диск, а система выключается. При следующем включении системы этот образ используется для восстановления состояния.

Suspend-to-disk отличается от других состояний. Пробуждение после него больше похоже на перезагрузку, чем на возобновление работы в трёх других методах. Его не включают pm labels[] и mem_sleep_labels[] в kernel/power/suspend.c [15], а основные функции реализованы в kernel/power/hibernate.c [16] вместо kernel/power/suspend.c [17].

Неразбериха с названиями

Linux прокидывает вышеперечисленные состояния питания в различные интерфейсы. В итоге все перечисленные выше состояния питания (кроме suspend-to-disk) везде называются по-разному.

  1. suspend-to-{idle, standby, ram} — общие термины, относящиеся к реальным базовым механизмам.

  2. freeze, standby, mem относятся к интерфейсу sysfs. Важно: mem — это настраиваемое состояние, и оно может указывать на любое состояние из пункта 1.

  3. 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 [18]

https://www.kernel.org/doc/html/latest/admin-guide/pm/sleep-states.html [13]

Это удобно для понимания работы систем — можно отследить изменения после записи новых значений.

Функции show и store

Эти две функции определяются с помощью макроса power_attr:

kernel/power/power.h:80 [19]

#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 не подходит для наших целей, поскольку просто выводит все доступные состояния сна.

kernel/power/main.c:657 [20]

/*
 * 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(). Это наша точка входа.

kernel/power/main.c:715 [21]

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;
}

kernel/power/main.c:688 [22]

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 позаимствован [23] у Android. Он отправляет всю систему в режим ожидания (suspend) или гибернации (hibernate), когда та неактивна. В большинстве десктопных конфигураций эта функция отключена, поскольку она предназначена в первую очередь для мобильных систем и переопределяет то, как режимы ожидания и гибернации работают в обычных условиях.

Все реализовано в виде очереди workqueue, которая проверяет текущее количество событий пробуждения (wakeup), процессов и драйверов, которые должны быть запущены. И если их нет, система переводится в состояние autosleep (обычно в режим ожидания — suspend). Тем не менее это может быть и режим гибернации — достаточно настроить его через /sys/power/autosleep подобно тому, как /sys/power/state включает режим гибернации по команде пользователя.

kernel/power/main.c:841 [24]

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 */

kernel/power/autosleep.c:24 [25]

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 не установлена.

include/linux/suspend.h:407 [26]

static inline int hibernate(void) { return -ENOSYS; }

Проверка доступности режима гибернации

Проверим с помощью функции hibernation_available, доступен ли режим.

kernel/power/hibernate.c:742 [27]

if (!hibernation_available()) {
	pm_pr_dbg("Hibernation not available.n");
	return -EPERM;
}

kernel/power/hibernate.c:92 [28]

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) [29]). Любопытно, что любой уровень блокировки, будь то integrity или confidentiality (см. Linux kernel lockdown, integrity, and confidentiality [30]. — Прим. пер.), запрещает переход в режим гибернации. Потому что в ином случае из сохранённого на диск образа ядра можно извлечь практически всё, что угодно, внести в него изменения и даже перезагрузиться с этим образом.

secretmem_active проверяет, используется ли вызвала много споров [31], хотя и не таких, как опасения по поводу фрагментации при разворачивании памяти ядра (которая в итоге так и не стала реальной проблемой [31]).

cxl_mem_active просто проверяет, активна ли память CXL. Полное объяснение приведено в коммите [32], реализующем эту проверку. Но есть и сокращённое объяснение в cxl_mem_probe, которая устанавливает соответствующий флаг при инициализации устройства памяти CXL.

drivers/cxl/mem.c:186 [33]

Ядро может работать с CXL-памятью на этом устройстве. 
* Не существует определённого в спецификации способа определить,
* сохраняет ли это устройство содержимое при переходе 
* в режим ожидания (suspend), и нет простого способа 
* организовать suspend-образ таким образом, чтобы обойти CXL-память, 
* но это создало бы циклическую зависимость: 
* для восстановления состояния CXL-памяти нужно сначала 
* восстановить работу PCI-шины, но для полного восстановления 
* состояния системы нужно восстановить содержимое всей памяти, 
* включая CXL.

Проверка сжатия

Далее необходимо проверить, включена ли поддержка сжатия, и если да, то включён ли требуемый алгоритм.

kernel/power/hibernate.c:747 [34]

/*
 * Узнаём, какой алгоритм сжатия поддерживается, если сжатие включено.
 */
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 в данном ядре).

kernel/power/hibernate.c:50 [35]

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.

kernel/power/Kconfig:95 [36]

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.

kernel/power/hibernate.c:1425 [37]

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:

kernel/power/hibernate.c:758 [38]

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 определяют, как ядру разрешено обрабатывать запрос на память.

include/linux/gfp_types.h:12 [39]

* Флаги GFP широко используются в Linux при распределении памяти. Аббревиатура GFP 
* означает get_free_pages(), базовую функцию распределения памяти. Не все флаги GFP 
* поддерживаются функциями, способными распределять память.

В случае с гибернацией нас интересуют флаги IO и FS, которые являются операторами возврата (reclaim) памяти. Они указывают системе, что можно использовать операции ввода-вывода или файловую систему для освобождения памяти, если это необходимо для выполнения запроса на выделение памяти.

include/linux/gfp_types.h:176 [40]

* Модификаторы возврата 
* Обратите внимание, что все последующие флаги применимы только 
* к распределениям, поддерживающим спящий режим (например, 
* %GFP_NOWAIT и 
* %GFP_ATOMIC будут их игнорировать).
* 
* %__GFP_IO запускает физические операции ввода-вывода. 
* 
* %__GFP_FS работает с низкоуровневой ФС. Снятие флага позволяет 
* аллокатору не обращаться к файловой системе, на которую уже 
* навешены блокировки.

gfp_allowed_mask определяет, какие флаги разрешено устанавливать в текущий момент.

gfp_allowed_mask — это маска, которая контролирует, какие флаги могут использоваться при выделении памяти. Во время приостановки или гибернации некоторые способы выделения памяти (например, те, которые могут вызвать операции ввода-вывода) становятся опасными. Поэтому система временно запрещает их использование. — Прим. пер.

Как говорится в комментарии ниже, предотвращение установки этих флагов позволяет избежать ситуаций, когда ядру необходимо выполнить операции ввода-вывода для распределения памяти, например чтение из / запись в swap. Но устройства, с которых нужно читать / в которые нужно писать, в данный момент недоступны.

kernel/power/main.c:24 [41]

/*
 * Приведённые ниже функции используются кодом 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 был установлен — он и останется. — Прим. пер.).

kernel/power/main.c:52 [42]

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);

include/linux/sched.h:1633 [43]

#define PF_NOFREEZE		0x00008000	/* Этот поток не замораживать */

Затем устанавливается семафор для гибернации, который не позволяет другим процессам открыть снапшот или возобновить работу из него, пока система переходит в режим гибернации. Эта блокировка также не дает вызвать функцию hibernate_quiet_exec, которая используется драйвером nvdimm для активации прошивки с замораживанием всех процессов и устройств. Это нужно, чтобы гарантировать, что в это время работает только сам драйвер.

kernel/power/hibernate.c:82 [44]

bool hibernate_acquire(void)
{
	return atomic_add_unless(&hibernate_atomic, -1, 0);
}

Подготовка консоли

Далее ядро вызывает pm_prepare_console. Эта функция работает только в том случае, если установлено значение CONFIG_VT_CONSOLE_SLEEP.

Она подготавливает виртуальный терминал (VT) к suspend, при необходимости переключаясь на консоль, используемую только для suspend.

kernel/power/console.c:130 [45]

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.

kernel/power/console.c:94 [46]

/*
 * Есть три случая, когда требуется переключение 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.

kernel/power/console.c:31 [47]

/**
 * 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 — последняя запись в списке возможных консолей, которая просто выполняет роль этакой черной дыры, в которую сбрасываются все сообщения.

kernel/power/console.c:16 [48]

#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 [49]

/* Выполняем инициированное ядром переключение 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 процессора, находящегося в состоянии паники.

kernel/printk/printk.c:2649 [50]

/**
 * 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);

kernel/printk/printk.c:362 [51]

/*
 * Возвращает true, если паникует другой процессор. 
 *
 * Если это так, текущий процессор должен немедленно освободить все ресурсы, связанные 
 * с выводом сообщений, чтобы они могли быть использованы паникующим процессором.
 */
bool other_cpu_in_panic(void)
{
	return (panic_in_progress() && !this_cpu_in_panic());
}

kernel/printk/printk.c:345 [52]

static bool panic_in_progress(void)
{
	return unlikely(atomic_read(&panic_cpu) != PANIC_CPU_INVALID);
}

kernel/printk/printk.c:350 [53]

/* Возвращает 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 — отладочное значение. Используется для указания на необходимость удержания блокировки. Его наличие — первый признак того, что система виртуальных терминалов устроена сложнее, чем кажется на первый взгляд.

kernel/printk/printk.c:373 [54]

/*
 * Используется для отладки бардака, которым является код 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 [55]

/*
 * Обычно во время перехода в спящий режим мы выделяем новую консоль и 
 * переключаемся на неё. При возобновлении работы возвращаемся к исходной 
 * консоли. Такое переключение может проходить неспешно, 
 * поэтому в системах, где фреймбуфер и так справляется 
 * с восстановлением видеорегистров, в переключении нет смысла. Эта 
 * функция отключает переключение, передавая '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. — Прим. пер.).

drivers/tty/vt/vt.c:3153 [56]

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.

drivers/tty/vt/vt.c:315 [57]

void schedule_console_callback(void)
{
	schedule_work(&console_work);
}

console_work — это workqueue, которая будет выполнять заданную задачу асинхронно.

Коллбэк консоли

drivers/tty/vt/vt.c:3109 [58]

/*
 * Это коллбэк переключения консоли. 
 * Переключение консоли в контексте процесса позволяет 
 * выполнять переключения асинхронно (нужно, когда переключаемся 
 * по прерыванию клавиатуры). За синхронизацию с консольным кодом 
 * и предотвращение повторного входа в код переключения консоли 
 * отвечает 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 удаляется состояние курсора.

drivers/tty/vt/vt.c:841 [59]

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 выходит за рамки этой статьи. Код выше даёт общее представление о том, как эта система взаимодействует с гибернацией.

Уведомление цепочки вызовов управления питанием

kernel/power/hibernate.c:767 [60]

pm_notifier_call_chain_robust(PM_HIBERNATION_PREPARE, PM_POST_HIBERNATION)

Вызов цепочки коллбэков управления питанием. Сначала передаётся PM_HIBERNATION_PREPARE, а затем PM_POST_HIBERNATION при запуске или ошибке с другим коллбэком.

kernel/power/main.c:98 [61]

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);
}

Нотификатор управления питанием представляет собой блокирующую цепочку уведомлений, что означает, что он обладает следующими свойствами:

include/linux/notifier.h:23 [62]

 *	Блокирующие цепочки уведомлений: коллбэки цепочек выполняются в контексте процесса.
 *	Функции обратного вызова (callouts) могут использовать блокирующие операции.

Цепочка коллбэков представляет собой связанный список, каждая запись которого содержит приоритет и функцию для вызова. Технически функция принимает значение, но для цепи управления питанием оно всегда NULL.

include/linux/notifier.h:49 [63]

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 связанного списка защищена семафором чтения-записи.

include/linux/notifier.h:65 [64]

struct blocking_notifier_head {
	struct rw_semaphore rwsem;
	struct notifier_block __rcu *head;
};

Поскольку список идёт с приоритетами, при добавлении его приходится перебирать до тех пор, пока не будет найден элемент с более низким приоритетом, перед которым можно вставить текущий элемент.

kernel/notifier.c:252 [65]

/*
 *	Блокирующие цепочки уведомлений. Весь доступ 
 *   к цепочке синхронизируется с помощью 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;
}

kernel/notifier.c:20 [66]

/*
 *	Основные подпрограммы (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;
}

Каждый коллбэк может возвращать одну из нескольких опций. 

include/linux/notifier.h:18 [67]

#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 и возвращается ошибка.

kernel/notifier.c:107 [68]

/**
 * 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.

kernel/power/main.c:69 [69]

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 [70], затем всю файловую систему и, наконец, все блочные устройства, чтобы гарантировать, что все страницы будут записаны на диск.

fs/sync.c:87 [71]

/*
 * Синхронизируем всё. Начинаем с пробуждения потоков для 
 * сброса на диск, чтобы запись шла на всех устройствах параллельно.
 * Затем синхронизируем все иноды, дожидаясь завершения записи 
 * всеми потоками. В этот момент все данные находятся на диске, 
 * поэтому метаданные не меняются. Файловым системам даётся задача
 * синхронизировать метаданные с помощью вызовов ->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 включён, система запускает дополнительные операции синхронизации файловой системы после указанной задержки без каких-либо операций записи.

mm/page-writeback.c:111 [72]

/*
 * Флаг, переводящий машину в «режим ноутбука». 
 * Удваивается как таймаут в jiffies:
 * если в течение этого времени диск оставался неактивен,
 * запускается полная синхронизация.
 */
int laptop_mode;

EXPORT_SYMBOL(laptop_mode);

Jiffies — Интервал между двумя прерываниями системного таймера в ядре Linux (прим. пер.).

Однако при выполнении операции синхронизации файловой системы система добавит дополнительный таймер, чтобы запланировать больше операций записи после задержки laptop_mode. Нам не нужно, чтобы состояние системы менялось во время перехода в спящий режим, поэтому эти таймеры отменяются.

mm/page-writeback.c:2198 [73]

/*
 * Мы в режиме ноутбука и только что провели синхронизацию. 
 * 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.

fs/sync.c:111 [74]

SYSCALL_DEFINE0(sync)
{
	ksys_sync();
	return 0;
}

Конец подготовительного этапа

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

P. S. 

Читайте также в нашем блоге:

Автор: kubelet

Источник [78]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/linux/413395

Ссылки в тексте:

[1] Bootlin Elixir Cross-Referencer: https://elixir.bootlin.com/linux/v6.9.9/source

[2] Отправная точка: /sys/power/state и /sys/power/disk: #otprav_tochka

[3] Этапы перехода в гибернацию:: #Etapy_perehoda

[4] Настройка спящего режима в ядре: #Nastroyka_spyaschego_rezhima

[5] Проверка доступности спящего режима: #Proverka_dostupnosti

[6] Проверка сжатия: #Proverka_czhatiya

[7] Захват блокировок: #Zahvat_blokirovok

[8] Подготовка консоли: #Podgotovka_konsoli

[9] Уведомление цепочки вызовов управления питанием: #Uvedomlenie_tsepochki_vyzovov

[10] Синхронизация файловых систем: #Sinhronizatsia_failovyh_sistem

[11] Конец подготовки: #Konets_podgotovki

[12] отладить режим гибернации: https://www.kernel.org/doc/html/latest/power/basic-pm-debugging.html

[13] System Sleep State: https://www.kernel.org/doc/html/latest/admin-guide/pm/sleep-states.html

[14] «Is Linux Suspend ready for the next decade»: https://youtu.be/Pv5KvN0on0M?si=rPTXXQXiZtQ4JFsz&t=152

[15] kernel/power/suspend.c: https://elixir.bootlin.com/linux/v6.13.1/source/kernel/power/suspend.c#L36

[16] kernel/power/hibernate.c: https://elixir.bootlin.com/linux/latest/source/kernel/power/hibernate.c

[17] kernel/power/suspend.c: https://elixir.bootlin.com/linux/latest/source/kernel/power/suspend.c

[18] https://hackmd.io/@0xff07/linux-pm/%2F%400xff07%2FrkmMQqbu6: https://hackmd.io/@0xff07/linux-pm/%2F%400xff07%2FrkmMQqbu6

[19] kernel/power/power.h:80: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/power.h#L80

[20] kernel/power/main.c:657: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/main.c#L657

[21] kernel/power/main.c:715: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/main.c#L715

[22] kernel/power/main.c:688: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/main.c#L688

[23] позаимствован: https://lwn.net/Articles/479841/

[24] kernel/power/main.c:841: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/main.c#L841

[25] kernel/power/autosleep.c:24: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/autosleep.c#L24

[26] include/linux/suspend.h:407: https://elixir.bootlin.com/linux/v6.9.9/source/include/linux/suspend.h#L407

[27] kernel/power/hibernate.c:742: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/hibernate.c#L742

[28] kernel/power/hibernate.c:92: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/hibernate.c#L92

[29] kernel_lockdown(7): https://man7.org/linux/man-pages/man7/kernel_lockdown.7.html

[30] Linux kernel lockdown, integrity, and confidentiality: https://mjg59.dreamwidth.org/55105.html

[31] вызвала много споров: https://lwn.net/Articles/865256/

[32] коммите: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=9ea4dcf49878bb9546b8fa9319dcbdc9b7ee20f8

[33] drivers/cxl/mem.c:186: https://elixir.bootlin.com/linux/v6.9.9/source/drivers/cxl/mem.c#L186

[34] kernel/power/hibernate.c:747: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/hibernate.c#L747

[35] kernel/power/hibernate.c:50: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/hibernate.c#L50

[36] kernel/power/Kconfig:95: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/Kconfig#L95

[37] kernel/power/hibernate.c:1425: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/hibernate.c#L1425

[38] kernel/power/hibernate.c:758: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/hibernate.c#L758

[39] include/linux/gfp_types.h:12: https://elixir.bootlin.com/linux/v6.9.9/source/include/linux/gfp_types.h#L12

[40] include/linux/gfp_types.h:176: https://elixir.bootlin.com/linux/v6.9.9/source/include/linux/gfp_types.h#L176

[41] kernel/power/main.c:24: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/main.c#L24

[42] kernel/power/main.c:52: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/main.c#L52

[43] include/linux/sched.h:1633: https://elixir.bootlin.com/linux/v6.9.9/source/include/linux/sched.h#L1633

[44] kernel/power/hibernate.c:82: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/hibernate.c#L82

[45] kernel/power/console.c:130: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/console.c#L130

[46] kernel/power/console.c:94: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/console.c#L94

[47] kernel/power/console.c:31: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/console.c#L31

[48] kernel/power/console.c:16: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/console.c#L16

[49] drivers/tty/vt/vt_ioctl.c:1268: https://elixir.bootlin.com/linux/v6.9.9/source/drivers/tty/vt/vt_ioctl.c#L1268

[50] kernel/printk/printk.c:2649: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/printk/printk.c#L2649

[51] kernel/printk/printk.c:362: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/printk/printk.c#L362

[52] kernel/printk/printk.c:345: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/printk/printk.c#L345

[53] kernel/printk/printk.c:350: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/printk/printk.c#L350

[54] kernel/printk/printk.c:373: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/printk/printk.c#L373

[55] drivers/tty/vt/vt_ioctl.c:1308: https://elixir.bootlin.com/linux/v6.9.9/source/drivers/tty/vt/vt_ioctl.c#L1308

[56] drivers/tty/vt/vt.c:3153: https://elixir.bootlin.com/linux/v6.9.9/source/drivers/tty/vt/vt.c#L3153

[57] drivers/tty/vt/vt.c:315: https://elixir.bootlin.com/linux/v6.9.9/source/drivers/tty/vt/vt.c#L315

[58] drivers/tty/vt/vt.c:3109: https://elixir.bootlin.com/linux/v6.9.9/source/drivers/tty/vt/vt.c#L3109

[59] drivers/tty/vt/vt.c:841: https://elixir.bootlin.com/linux/v6.9.9/source/drivers/tty/vt/vt.c#L841

[60] kernel/power/hibernate.c:767: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/hibernate.c#L767

[61] kernel/power/main.c:98: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/main.c#L98

[62] include/linux/notifier.h:23: https://elixir.bootlin.com/linux/v6.9.9/source/include/linux/notifier.h#L23

[63] include/linux/notifier.h:49: https://elixir.bootlin.com/linux/v6.9.9/source/include/linux/notifier.h#L49

[64] include/linux/notifier.h:65: https://elixir.bootlin.com/linux/v6.9.9/source/include/linux/notifier.h#L65

[65] kernel/notifier.c:252: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/notifier.c#L252

[66] kernel/notifier.c:20: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/notifier.c#L20

[67] include/linux/notifier.h:18: https://elixir.bootlin.com/linux/v6.9.9/source/include/linux/notifier.h#L18

[68] kernel/notifier.c:107: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/notifier.c#L107

[69] kernel/power/main.c:69: https://elixir.bootlin.com/linux/v6.9.9/source/kernel/power/main.c#L69

[70] inodes: https://ru.wikipedia.org/wiki/Inode

[71] fs/sync.c:87: https://elixir.bootlin.com/linux/v6.9.9/source/fs/sync.c#L87

[72] mm/page-writeback.c:111: https://elixir.bootlin.com/linux/v6.9.9/source/mm/page-writeback.c#L111

[73] mm/page-writeback.c:2198: https://elixir.bootlin.com/linux/v6.9.9/source/mm/page-writeback.c#L2198

[74] fs/sync.c:111: https://elixir.bootlin.com/linux/v6.9.9/source/fs/sync.c#L111

[75] Как собрать Linux-контейнер с нуля и без Docker: https://goo.su/880354li

[76] Что не так с chroot: почему для контейнеров используется именно pivot_root: https://goo.su/880342li

[77] Неиспользуемые остатки образов в Docker: как удалить зомби-слои и защитить секреты: https://goo.su/877608li

[78] Источник: https://habr.com/ru/companies/flant/articles/884622/?utm_source=habrahabr&utm_medium=rss&utm_campaign=884622