История еще одного патча: зависшая батарея
Ноутбук засыпает, ноутбук просыпается, батарея «зависает» — более не отдает ни уровень заряда ни другие показатели, вне зависимости от подключения к сети.
Патч ядра Linux и три года изысканий, рассказываю как это было.
Вводная
Автор очень давно использует самые разнообразные версии и вариации Linux и UNIX‑систем для работы и диких развлечений, в том числе на ноутбуках, поэтому старается решать все найденные проблемы, по мере сил.
Временами проблема возвращается заново в новых версиях ядра, будучи решенной в прошлом, временами происходит наоборот и проблема проявляется только в самых свежих версиях Linux.
Бывает приходится приостановить поиски решения и вернуться к изысканиям спустя много лет, когда случайно обнаруживается новый вариант решения.
Описываемая история — как раз из последних.
Проблема
Вкратце проблема заключалась в абзаце, вынесенном в заголовок:
ноутбук засыпает, ноутбук просыпается, после чего батарея «зависает» — более не отдает свой актуальный уровень заряда и все показатели, вне зависимости от подключения к электросети.
Естественно в Windows все работало правильно и стабильно, в любых режимах засыпания.
Ноутбук редкий, ноутбук старый, от вендора, который в гробу видал никогда не любил альтернативные ОС и тем более в страшном сне не мог представить, что одна из его топовых моделей (на свое время) вместо подсчета прибылей успешному бизнесмену, стала бы использоваться для компиляции ядра из исходников и прочих гиковских непотребств.
Никакая отладка ядра и никакие отладочные сообщения не помогли, что неудивительно:
Работа с ACPI — традиционно самая замороченная область ядра Linux, а процессы засыпания и возвращения к работе — сложны и нестабильны по своей сути.
Так что оно глючило, глючит и будет глючить, в любой ОС и на любом оборудовании при любой погоде.
Процесс отлова ошибок связанных с ACPI усложнен тем, что такие ошибки чаще всего «плавающие» — могут появиться не через один цикл «засыпания‑пробуждения» а например через десять, т. е. вам надо десять раз подряд погрузить ноутбук в сон и затем пробудить чтобы отловить ошибку.
Правда ведь отладка это весело?
Решение
Далеко не сразу (ушло примерно три года), путем хитрых запросов к поисковикам и изучения исходников ядра, автор все же смог отыскать концы этой проблемы.
И помог в этом случайный комментарий неизвестного китайского разработчика:
I have few reputation so I can't tell others about solution in their questions, so I will post it here.
Solution : build kernel with this PATCH
Description : I tracked the issue ,I discovered that all was working good until the 4.19.86 kernel.
So I checked DIFF and After many tries maybe 50 or more.
I found the cause : It was (if statement was added in 4.19.86 COMMIT) ,particularly checking if spaceid == 0 [ACPI_ADR_SPACE_SYSTEM_MEMORY]. Commit Link
Разумеется патч китайского автора оказался нерабочий, сообщение было написано про другую модель ноутбука, с другим поведением при сбое и с неверной фиксацией на типе батареи в качестве источника проблемы.
Еще речь шла про совсем уж древнюю версию ядра (4.19, релиз в 2018м году) а само сообщение датировалось 2022 годом.
Но вы ведь и не думали, что все будет настолько просто, верно?
Решение
Посмотрим на код «виновника торжества», файл drivers/acpi/acpica/evregion.c, метод acpi_ev_execute_reg_methods, а еще точнее — вот этот замечательный комментарий:
/*
* These address spaces do not need a call to _REG, since the ACPI
* specification defines them as: "must always be accessible". Since
* they never change state (never become unavailable), no need to ever
* call _REG on them. Also, a data_table is not a "real" address space,
* so do not call _REG. September 2018.
*/
Обратите внимание на дату — 2018й год, год выпуска версии ядра 4.19, в которой и проявилась данная проблема.
По сути самого комментария и исходя из логики, один из коммитеров ядра Linux в далеком 2018 году хотел «сделать как лучше»:
if ((space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) ||
(space_id == ACPI_ADR_SPACE_SYSTEM_IO) ||
(space_id == ACPI_ADR_SPACE_DATA_TABLE)) {
return_VOID;
}
Полагая что строгое соотвествие спецификации ACPI в данном случае бывает всегда — коммитер вставил заглушку, убирающую вызов _REG метода для вроде как системных частей ACPI‑прошивки.
Чем и поломал восстановление статуса батареи при пробуждении ноутбука.
Благими намерениями выстелена дорога в Ад. (ц)
Исправление
Все что нужно сделать для исправления ситуации, это убрать из условия проверки константу ACPI_ADR_SPACE_SYSTEM_MEMORY:
if (
// (space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) ||
(space_id == ACPI_ADR_SPACE_SYSTEM_IO) ||
(space_id == ACPI_ADR_SPACE_DATA_TABLE)) {
return_VOID;
}
Ну и опционально добавить отладку, чтобы убедиться в правильности работы:
if (space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) {
printk(KERN_DEBUG "PRO HEHE : Bypassing for battery is done");
}
Отладка была включена в текущем ядре, само сообщение можно наблюдать на стартовой картинке к статье.
Для того чтобы убедиться в правильности работы, достаточно усыпить ноутбук, пробудить и подергать состояние батареи несколько раз:
Эпилог
Такой патч никогда не примут в аппстрим ядра, потому что он неправилен с точки зрения спецификации ACPI.
Собственно сама проблема с зависшей батареей появилась из-за того что некоторые вендоры класть хотели на стандарты и спецификации.
К сожалению такого рода проблемы в новых версиях ядра Linux появляются все чаще, поэтому и ситуация и ее решение — уже можно сказать типовые и подобным образом решаются ныне и многие другие проблемы с оборудованием.
Так что если на вашем ноутбуке также проявляется описанная проблема с «зависанием» батареи — теперь будете знать в какую сторону копать.
P.S.
Если вы внимательно смотрели на заглавную картинку к статье, могли заметить что автор использовал нестандартное ядро:
XanMod is a general-purpose Linux kernel distribution with custom settings and new features. Built to provide a stable, smooth and solid system experience.
The real-time version is recommended for critical runtime applications such as Linux gaming server / client for eSports, streaming, live productions and
ultra-low latency enthusiasts.
Этот набор патчей ядра Linux действительно сильно ускоряет работу, что особенно заметно на некрожелезе времен молодости Брежнева, которое автор регулярно использует для укрепления стойкости духа.
P.P.S.
На данный момент проблема все также актуальна и вряд ли будет решена в апстриме ядра в обозримом будущем. Поэтому автор продолжает накладывать описанный патч (и еще несколько) при каждой пересборке ядра.
Статья была опубликована на Хабре, оригинал которой находится в нашем блоге.
L9ec: волшебный патч ядра Linux
Если вам неудержимо хочется использовать оборудование из музея для современной разработки — статья специально для вас.
Машины должны служить а не требовать ресурсы. И автор патча l9 об этом знает.
Эпический баг
Сейчас наверное некоторые читатели сильно удивятся:
с 2007 года в ядре Linux живет серьезный баг, приводящий к полному зависанию системы при работе под большой нагрузкой на память.
На дворе на момент написания статьи май 2025 года, так что баг успел отпраздновать совершеннолетие и открыть первую бутылку пива.
Оригинальный репорт выглядит так:
Разумеется разработчики ядра в курсе проблемы, но по ряду причин.. не считают этот баг важным.
Да, вы правильно прочитали:
«полное зависание системы под нагрузкой» и «разработчики не считают важным исправлять» — как вам такие реалии Linux?
Более того, недавно тикет с описанием этого бага вообще закрыли с эпической формулировкой «just become obsolete»:
С легким намеком, что некоторым стоит перестать собирать себе компьютеры по помойкам:
but now I don't bother with less than 32Gb of RAM for a desktop.
Теперь прокрутите обсуждение бага в трекере вниз и посмотрите на последнее сообщение о проблеме:
Оно конечно все замечательно и у самого автора этой статьи давно 64Гб на одной из рабочих машин, а некоторые коллеги успели впихнуть даже 128Гб, причем в ноутбук — чтобы мы наконец увидели SUSE Linux, которая не тормозит.
Но к сожалению одними любителями компьютерного антиквариата данная проблема не ограничивается — на нынешние облачные времена типичное рабочее окружение Linux это виртуальная машина, с ограниченным обьемом памяти. Скорее всего даже ваш корпоративный сайт крутится на виртуальной машине с 4Гб памяти.
Так что на самом деле проблема касается практически всех пользователей Linux, а не только идейных нищебродов энтузиастов, собирающих себе оборудование по музеям.
Как так получилось
Если вы хоть немного понимаете в компьютерах, прочитав абзац выше и сопоставив масштаб проблемы и отношение к ней разработчиков Linux, думаю уже сделали определенные выводы:
либо команда разработки ядра Linux — поголовно некомпетентны, либо
у автора контракт с рептилоидамив описании выше был упущен ряд важных нюансов.
Правда как обычно где‑то между — «особенных» среди современных разработчиков Linux действительно хватает, но ряд нюансов я все же намеренно упустил.
Опишу в какой момент проявляется этот баг:
надо долго и упорно увеличивать нагрузку на использование памяти, причем маленькими порциями и обязательно из нескольких разных процессов — чтобы OOM Killer не успел отработать.
На практике надо либо заниматься тренировкой нейросетей, либо непрерывно гонять тяжелые приложения на Java/Node (в первую очередь IDE) и постоянно запускать сборку больших проектов.
И все это на неподготовленном офисном оборудовании с 4-6 Гб памяти, представляющем историческую ценность, либо в виртуальной машине.
Патч l9ec
Уже давно существует неофициальный патч, решающий описанную проблему с зависанием квадратно-гнездовым радикальным способом:
The kernel does not provide a way to protect the working set under memory pressure. A certain amount of anonymous and clean file pages is required by the userspace for normal operation. First of all, the userspace needs a cache of shared libraries and executable binaries. If the amount of the clean file pages falls below a certain level, then thrashing and even livelock can take place.
По сути этим патчем формируется небольшой объем памяти (тот самый working set), которую запрещается перегружать даже самым хитрым приложениям, откусывающим память по килобайтам.
Разумеется патч заметили и тут находится архив эпической переписки в рассылке Linux Kernel длиною в год, где автор патча пытается объяснить окружающим что он не верблюд и проблема действительно есть.
Однако патч в мейнстрим так и не попал, что наводит на определенные нехорошие мысли.
История с Xanmod
Помимо основной версии ядра т. н. «vanilla», исходники которого выкладываются на широко известном kernel.org, существуют «васянские сборки» — наборы патчей ядра, собранные энтузиастами под конкретную задачу.
Одна из таких сборок называется Xanmod и посвящена работе современного ядра на desktop-системе с минимальными визуальными задержками:
XanMod is a general-purpose Linux kernel distribution with custom settings and new features. Built to provide a stable, smooth and solid system experience.
Так вот на момент появления l9ec патча, он был включен в сборку Xanmod:
С официальной страницы с отзывами, между прочим.
Но в последних 6.х версиях Xanmod его уже нет, на что есть формальная причина — появление вот этого патча, вроде как окончательно решающего проблему c зависанием:
MGLRU is a kernel innovation we've been eager to see merged in 2022 and it looks like that could happen for the next cycle, v5.19, for improving Linux system performance especially in cases of approaching memory pressure.
На данный момент MGLRU в mainline и скорее всего работает прямо сейчас и у вас в системе, если конечно у вас современный линукс и MGLRU не отключен вручную.
К сожалению принцип работы MGLRU другой (см. комментарий выше про 32Гб памяти на десктопе) и тестировался его функционал тоже в другом месте:
On Android, our most advanced simulation that generates memory pressure from realistic user behavior shows 18% fewer low-memory kills, which in turn reduces cold starts by 16%.
Как нетрудно догадаться, «realistic user behavior» на мобильном Android несколько отличается от тотальной перегрузки тяжелыми средствами разработки на дохлом десктопе или еще более слабой виртуальной машине.
Поэтому «продвинутым пользователям Linux» в очередной раз придется заботиться о себе и своих проблемах самостоятельно.
Портирование на 6.х ядро
К сожалению автор патча l9 видимо устав бодаться с идиотами, не стал переносить свой замечательный патч в 6.х ветку ядра, решив что раз более умные ребята из Google выкатили MGLRU — от его решения толку больше не будет.
Как ни странно, но это не так и l9 патч куда более предсказуем и надежен как удар ломом, в отличие от цирка с аж 14 патчами MGLRU:
These initial multi-generational LRU patches amount to 14 patches at the moment and in a patched kernel can be enabled via the LRU_GEN Kconfig switch
Собственно эта статья появилась на свет после того как автор опять словил зависание под нагрузкой во время работы над большим проектом, из-за чего и решил откопать дедовский пулемет портировать известный патч в 6.х ядро.
За основу был взят последний патч для 5.х ветки без учета MGLRU: le9ec-5.15.patch а его логика добавлялась в Xanmod-версию ядра 6.14.5.
Ниже по шагам объясняю как выполнить перенос логики патча, чтобы процедуру можно было повторить и на более новых ядрах и на «vanilla» версиях.
Скачиваем архив с Xanmod ядром и l9-патч по ссылкам выше и распаковываем.
Стоит сразу предупредить, что размер текущей версии ядра Linux в распакованном виде ~1.8 Гигабайт, а для сборки понадобится еще ~28 Гигабайт.
Вот такие нынче ядра.
Разумеется применить готовый diff автоматически для ветки 6.х не получится, так что будем переносить логику патча по шагам.
Всего в рамках патча изменения происходят в пяти файлах:
Поскольку исправлять документацию нам не очень актуально, первый файл можно пропустить. Таким образом первое актуальное исправление находится в файле include/linux/mm.h, куда добавляются глобальные переменные, отвечающие за настраиваемые лимиты:
Все что нужно сделать — вставить строки в файл include/linux/mm.h:
extern unsigned long sysctl_anon_min_kbytes;
extern unsigned long sysctl_clean_low_kbytes;
extern unsigned long sysctl_clean_min_kbytes;
Следующий шаг чуть объемнее:
Необходимо найти массив static struct ctl_table vm_table[] в файле kernel/sysctl.c и добавить внутрь три блока, отвечающих за настройку.
{
.procname = "anon_min_kbytes",
.data = &sysctl_anon_min_kbytes,
.maxlen = sizeof(unsigned long),
.mode = 0644,
.proc_handler = proc_doulongvec_minmax,
},
{
.procname = "clean_low_kbytes",
.data = &sysctl_clean_low_kbytes,
.maxlen = sizeof(unsigned long),
.mode = 0644,
.proc_handler = proc_doulongvec_minmax,
},
{
.procname = "clean_min_kbytes",
.data = &sysctl_clean_min_kbytes,
.maxlen = sizeof(unsigned long),
.mode = 0644,
.proc_handler = proc_doulongvec_minmax,
},
Следующая правка в файле mm/Kconfig, которой добавляется управление новыми настраиваемыми параметрами ядра:
По-сути правки, вам надо добавить в файл mm/Kconfig три блока: ANON_MIN_KBYTES, CLEAN_LOW_KBYTES и CLEAN_MIN_KBYTES вместе со всем содержимым.
Все что выше отвечало лишь за настройку, основная логика патча l9 приходится на файл mm/vmscan.c, в котором будут происходить оставшиеся правки.
Первым делом добавляем локальные переменные:
Затем добавляем логику присваивания значений из параметров ядра:
Ориентируетесь на макрос #define prefetchw_prev_lru_folio, строки добавляются после него:
unsigned long sysctl_anon_min_kbytes __read_mostly = CONFIG_ANON_MIN_KBYTES;
unsigned long sysctl_clean_low_kbytes __read_mostly = CONFIG_CLEAN_LOW_KBYTES;
unsigned long sysctl_clean_min_kbytes __read_mostly = CONFIG_CLEAN_MIN_KBYTES;
Следующая правка добавляется в метод static void get_scan_countкоторый успел поменять сигнатуру:
Я добавил сразу после блока с переменными:
struct pglist_data *pgdat = lruvec_pgdat(lruvec);
struct mem_cgroup *memcg = lruvec_memcg(lruvec);
unsigned long anon_cost, file_cost, total_cost;
int swappiness = sc_swappiness(sc, memcg);
u64 fraction[ANON_AND_FILE];
u64 denominator = 0; /* gcc */
enum scan_balance scan_balance;
unsigned long ap, fp;
enum lru_list lru;
/*
* Force-scan anon if clean file pages is under vm.clean_low_kbytes
* or vm.clean_min_kbytes.
*/
if (sc->clean_below_low || sc->clean_below_min) {
scan_balance = SCAN_ANON;
goto out;
}
Следующая правка в этом же файле должна быть вставлена в этот же метод get_scan_count, но ниже по коду — ориентируйтесь на строку nr[lru] = scan;благо она такая одна:
Я вставил логику проверки сразу над ней:
/*
* Hard protection of the working set.
*/
if (file) {
/*
* Don't reclaim file pages when the amount of
* clean file pages is below vm.clean_min_kbytes.
*/
if (sc->clean_below_min)
scan = 0;
} else {
/*
* Don't reclaim anonymous pages when their
* amount is below vm.anon_min_kbytes.
*/
if (sc->anon_below_min)
scan = 0;
}
nr[lru] = scan;
Следующей правкой добавляется новая функция prepare_workingset_protection, которая должна вызываться из существующего метода shrink_node_memcgs:
Так что вам надо найти функцию shrink_node_memcgs (она такая одна) и вставить новую функцию prepare_workingset_protection над ней:
static void prepare_workingset_protection(pg_data_t *pgdat,
struct scan_control *sc)
{
/*
* Check the number of anonymous pages to protect them from
* reclaiming if their amount is below the specified.
*/
if (sysctl_anon_min_kbytes) {
unsigned long reclaimable_anon;
reclaimable_anon =
node_page_state(pgdat, NR_ACTIVE_ANON) +
node_page_state(pgdat, NR_INACTIVE_ANON) +
node_page_state(pgdat, NR_ISOLATED_ANON);
reclaimable_anon <<= (PAGE_SHIFT - 10);
sc->anon_below_min = reclaimable_anon < sysctl_anon_min_kbytes;
} else
sc->anon_below_min = 0;
/*
* Check the number of clean file pages to protect them from
* reclaiming if their amount is below the specified.
*/
if (sysctl_clean_low_kbytes || sysctl_clean_min_kbytes) {
unsigned long reclaimable_file, dirty, clean;
reclaimable_file =
node_page_state(pgdat, NR_ACTIVE_FILE) +
node_page_state(pgdat, NR_INACTIVE_FILE) +
node_page_state(pgdat, NR_ISOLATED_FILE);
dirty = node_page_state(pgdat, NR_FILE_DIRTY);
/*
* node_page_state() sum can go out of sync since
* all the values are not read at once.
*/
if (likely(reclaimable_file > dirty))
clean = (reclaimable_file - dirty) << (PAGE_SHIFT - 10);
else
clean = 0;
sc->clean_below_low = clean < sysctl_clean_low_kbytes;
sc->clean_below_min = clean < sysctl_clean_min_kbytes;
} else {
sc->clean_below_low = 0;
sc->clean_below_min = 0;
}
}
Собственно последняя правка это вызов новой функции из существующей shrink_node_memcgs:
После внесения всех этих исправлений, запускаем один из вариантов настройки ядра:
make xconfig
И наблюдаем новые поля настройки:
Цепочка сборки и установки ядра совершенно стандартная:
make && make modules && make modules_install && make install
К сожалению это еще не все и прежде чем патч заработает надо будет отключить MGLRU, который как я уже описывал — успели внести в основную ветку ядра:
cat /sys/kernel/mm/lru_gen/enabled
Должен показать 0x0007 если MGLRU включен, отключить можно командой:
echo 0 | sudo tee /sys/kernel/mm/lru_gen/enabled
Вот тут у автора патча лежат готовые скрипты для автоматизации всего этого цирка. Я же просто добавил строчку с отключением в /etc/rc.local.
Пруфы
Для тестов портированного патча, был взят один из моих боевых ноутбуков Lenovo Z580 2012го года выпуска, с 8Гб памяти:
На нем постоянно творится всевозможная дичь — тут пять разных операционных систем и куча проектов и инструментов для разработки на каждой.
Поэтому без особого труда были одновременно запущены:
PostgreSQL с реальной базой
MySQL тоже с реальной базой
Intellij Idea
VSCode
Сборка проекта на Node.js с Webpack и hot reload
Сборка достаточно крупного Java-проекта (~3000 исходных файлов)
Chromium с 20 вкладками
Напоминаю что все это на 8Гб реальной памяти и на ноутбукe. Причем в качестве ОС в этот раз была обычная Ubuntu:
Как-то так это выглядит в действии:
Через неделю после публикации я решил пойти еще дальше и поставил пропатченное с l9 ядро на ноутбук 2007 года с 3Гб памяти. И повторил тесты с нагрузкой. Видео тут.
Все более чем работает и пропатченное ядро замечательно отрабатывает свою пайку.
Эпилог
Можно сколько угодно стебаться с пожеланиями «купи себе наконец нормальный компьютер», скажу что намеренно и давно использую старое железо — в первую очередь для оценки производительности создаваемого ПО.
И это одна из причин, по которой у нас получаются технические чудеса вроде Телепорты.
Если вы пока не дошли до столь глубокой стадии просвещения в разработке — все равно стоит знать, что мы ловили подобные зависания и в виртуальных машинах с Linux, например на CI‑сервере при сборке нескольких проектов одновременно.
Так что актуальность описанного все же высокая и как получилось, что столь простой и очевидный патч, который гарантированно решает проблему до сих пор не используют активно — ума не приложу. Ну и разумеется автору патча лучи респекта, благо это лучший представитель отечественной инженерной школы.
Статья была опубликована на Хабре, оригинал, в котором автор статьи себя не сдерживал и в красках рассказал все что думает о разработчиках ядра Linux как обычно можно найти в нашем блоге.
Эффект чистого холста: почему конца света никогда не будет, а параллельные миры - это наше прошлое
Всем Привет. Я давно об этом размышляю и решила записать свои мысли в эту гипотезу. Многие люди любят рассуждать о конце света или верить в параллельные вселенные, которые существуют прямо сейчас в других измерениях. Но если соединить физику ядра Земли, силу гравитации и законы эволюции, становится понятно, что всё устроено совершенно иначе. Наша Вселенная бессмертна, а параллельные миры просто не могут находиться с нами в одном времени из-за уникального устройства нашей планеты.
Всё начинается с физического фундамента Земли, а именно с силы, которая держит этот гигантский котел в равновесии. Наше ядро состоит из двух частей, где внешняя часть полностью жидкая и представляет собой расплавленный металл, а внутренняя является твердой из-за колоссального давления мантии и всей массы планеты. Гравитация выступает главной силой, которая крепко держит всю планету единым целым и удерживает нас на её поверхности. При этом мантия и земная кора - это лишь остывшая внешняя корка, сдерживающая кипящее металлическое сердце Земли. Но эта сложная система находится в хрупком балансе, и если в теории этот баланс нарушится, например от мощного внешнего космического удара, то ядро сработает как гигантский физический гейзер, из которого жидкий металл и магма под безумным давлением вырвутся струей наружу.
В этот момент начнется не окончательная смерть планеты, а масштабное обновление через лавовую перезагрузку. Раскаленное вещество ядра и мантии полностью растопит земную кору, и вся планета до единого сантиметра покроется бурлящей огненной магмой. Земля не умрет, она просто физически вернется в свое первоначальное состояние, в котором она была четыре с половиной миллиарда лет назад, когда только-только родилась в космосе и была точно таким же лавовым шаром. Именно здесь и запускается Эффект чистого холста, при котором вся старая ДНК полностью стирается под ноль. Любые следы нашей цивилизации, городов, интернета и генетического кода живых существ сгорают в глобальном океане магмы. Из-за этого становится очевидно, что параллельные вселенные, о которых говорят фантасты, на самом деле существовали последовательно во времени, а не одновременно с нами. До нашего цикла на Земле уже могли жить другие развитые существа с совершенно иной ДНК, но их мир закончился лавовой перезагрузкой, которая полностью вернула планету в состояние раскаленного эмбриона, навсегда освободив место для нас.
Однако Земля не может оставаться лавовым шаром вечно, поэтому со временем она снова начинает остывать, формируя твердую кору и воду, и на этой остывшей планете всё снова происходит заново. Из простых химических элементов непременно зародится новая первая клетка-предок в виде бактерии, и эволюция начнет свой ход с чистого листа по тем же самым рельсам развития от простого к сложному. Через миллионы лет на Земле снова появится разум, и это могут быть не люди, а совершенно другие существа, но они обязательно повторят наши ошибки из-за закона неизбежности опыта. Поскольку они будут жить на этой планете впервые, у них не будет нашей исторической памяти и наших учебников. Им придется самостоятельно и с нуля открывать законы физики, изобретать технологии, строить города и делить ресурсы. Из-за полного отсутствия опыта они будут наступать на те же самые грабли, через которые человечество проходит прямо сейчас, ведь сценарий развития разума один, и разумные существа in каждом новом цикле обречены проживать его заново. Конца света не существует, потому что жизнь невероятно упряма, а материя бессмертна, и финал нашего мира - это просто очищение холста и возвращение Земли к моменту своего рождения для создания новой истории.
Поделитесь своим мнением, интересно, что вы думаете по этому поводу.
Snapd, 100% загрузка cpu и баг ядра
Еще одна поучительная история из жизни с Linux, специально чтобы вы потеряли сон и покой, узнав что такое вообще возможно.
Вводная
Эмм с чего бы такого начать, чтобы не испугать раньше времени и не заставить устанавливать *BSD.
Есть на свете одна компания, которой мы помогаем с ИТ и есть у нее несколько виртуальных серверов на Ubuntu Linux, используемых для
половых утехразработки и тестирования.
Ubuntu там использовалась нормальной (для сервера) LTS‑версии, но в какой‑то момент — в погоне за патчами безопасности ее обновили до текущей.
Не совсем «текущей-текущей», которую используют разработчики Ubuntu для обкатки новых версий дистрибутива, а просто без долгой поддержки — примерно то, что ставят себе обычные пользователи Ubuntu Linux на домашние компьютеры.
Все происходило летом 2025 года, поэтому речь про версию 25.04 Ubuntu Linux, которая использует ядро 6.14 (запомните этот важный момент):
Баг
Однажды сисадмин компании-заказчика заметил слишком частую и сильную нагрузку на CPU, создаваемую процессом snapd, который является частью пакетного менеджера Snap.
Эта проблема с перегрузкой CPU для snapd мягко говоря не нова — «проклятый snapd» гадил линуксоидам с момента своего появления на свет и вообще видимо был не придуман а ниспослан свыше, в качестве кары за грехи.
Но в этот раз 100% загрузка CPU происходила.. строго по расписанию:
Разумеется первым делом были опробованы стандартные методы решения, вроде снижения частоты проверок обновлений или полного отключения проклятого сервиса:
Отдельно порадовал ответ ИИ:
Дословно «снести и использовать что-то другое» — первый разумный совет от машины за всю историю развития искусственного интеллекта.
Дальнейшие изыскания привели в багтрекер snapd к упомянутому багу, где уже третий комментарий от разработчика snapd, с приложенной трассировкой вызовов показал что проблема именно в ядре:
Тут стоит добавить, что почти сразу встал вопрос проверки бага на локальной машине, поскольку на момент изучения ситуации локально все успело неоднократно обновиться, а отлаживать ядро Linux на сервере заказчика все же не очень хорошая затея.
Чуть ниже по переписке видно, что баг особо ярко проявляется на ноутбуке, работающем от батареи:
Так что решено было пробовать отловить именно в таких условиях.
На счастье, на машине осталась сборка 6.14 версии ядра с патчами от Xanmod, которая использовалась для статьи про l9ec.
В последние годы в проекте ядра Linux выпускается сильно много промежуточных релизов, поэтому на какой именно версии внутри 6.14 ветки что-то пошло не так еще пришлось выяснять:
Наконец источник проблем был найден:
Чуть ниже по переписке обнаружился и тестовый код на C, демонстрирующий проблему, вот такой:
#include <sys/epoll.h>
#include <sys/time.h>
#include <sys/wait.h>
int main() {
int e = epoll_create1(0);
struct epoll_event event = {.events = EPOLLIN};
epoll_ctl(e, EPOLL_CTL_ADD, 0, &event);
const struct timespec timeout = {.tv_nsec = 1};
epoll_pwait2(e, &event, 1, &timeout, 0);
}
А так это выглядит в действии:
Обратите внимание на загрузку CPU и блокировку выхода из приложения
Как видите, в очередной раз проблема прикладного сервиса уперлась в ядро операционной системы.
Патч целиком находится тут, место исправления выглядит как-то так:
Да, как видите ситуацию радикально исправляет буквально пара символов логической конструкции, главное знать где исправлять.
Текущее состояние
Формально проблема была решена еще летом этого года, патч попал в mainline и пакет с обновлением ядра от команды Ubuntu:
На осень 2025 года даже стабильная версия ядра Linux уже имеет версию 6.16 — т. е. паровоз разработки уехал очень далеко вперед от описываемых проблем:
Так что на момент написания данной статьи вы (по идее) не должны столкнуться с данной проблемой, а если и столкнетесь — все легко решается банальным обновлением версии ядра из пакетов дистрибутива.
При подозрении на описанный баг — попробуйте собрать тестовое приложение (см. выше) и запустить в своем окружении.
Если начнется 100% загрузка CPU запущенным процессом — проблема точно есть, поскольку в ядрах с патчем поведение тестового приложения отличается:
В исправленном ядре тестовое приложение немедленно завершится.
Что касается заказчика, поскольку решение а затем и патч были опубликованы довольно оперативно — раньше чем нам сообщили о проблеме, на время разборок с согласованиями и попаданием в mainline ядра, мы банальным образом перенесли патч вручную в ту версию ядра, которая использовалась на сервере.
Позже обновили уже штатными средствами дистрибутива до текущей актуальной версии.
Эпилог
Если вы не являетесь разработчиком ядра и не пишете патчи каждый день, засыпая в обнимку с отладчиком — т. е. далеки от реалий системного программирования, то из этой статьи сможете вынести несколько интересных выводов и внезапных открытий:
1. Linux — могила, *BSD - сила
Шучу, разумеется неподготовленным пользователям в BSD-системы лучше не лезть совсем, но задуматься (или хотя-бы просто знать) о реалиях функционирования Linux все же стоит. Чтобы факт выноса мозга ядру из прикладного ПО не стал для вас неприятным сюрпризом.
2. Граница между прикладкой и системной разработкой весьма абстрактна
Проще говоря — ее нет совсем и в любой произвольный момент времени у вас есть неиллюзорный шанс наткнуться на баг ядра, даже программируя на JavaScript в браузере.
3. Любой уважающий себя сисадмин и DevOps должны знать С
Пусть на самом примитивном уровне, но хотя-бы собрать и запустить тестовое приложение, демонстрирующее проблему надо уметь. К сожалению все глубокие изыскания по теме «где оно тормозит» или «почему оно упало» рано или поздно приводят к коду на С и отладчику ядра.
И поверьте моему печальному опыту:
изучать эти штуки лучше днем и в спокойной обстановке, а не в режиме аврала и поздно ночью
на работе в выходной день.
4. Считайте деньги, хотя-бы иногда
Описанная в статье проблема случилась в локализованном окружении (на собственных физических серверах компании), но точно такая же Ubuntu используется и облачными провайдерами вроде Amazon, где есть тарификация за использование ресурсов, в первую очередь CPU.
Как нетрудно догадаться, 100% загрузка процессора с интервалом в пять минут в облаке, если ее вовремя не заметить и не исправить — больно отразится на счете, который вам потом выставят.
Так что проверяйте загруженность, пиковых 100% в современных системах быть не должно, если только вы целенаправленно не занимаетесь вычислительными задачами.
P.S.
Статья была опубликована на Хабре, более вольный оригинал как обычно в нашем блоге, копия статьи — в Яндекс Дзене.
Заметка о том, как на самом деле работает лимит памяти в Kubernetes: cgroups v2, overcommit и суровый OOM Killer
В мире Kubernetes принято считать, что requests и limits - это надежные границы, которые полностью изолируют приложения. По факту же, когда память на ноде заканчивается, абстракции кубера отходят на второй план, и в игру вступают механизмы ядра Linux.
Решил разобраться в деталях и провел серию тестов в песочнице (ALT Linux 11, Minikube на Proxmox). Ниже - что из этого получилось.
Важно сразу разделить три разных сценария:
memcg OOM - контейнер упёрся в собственный memory limit.
kubelet eviction - kubelet заметил давление по ресурсам на ноде и начал выселять pod’ы.
global OOM - памяти на ноде не хватило быстрее, чем kubelet успел что-либо сделать, и сработал kernel OOM Killer.
Если смешать эти три механизма, легко случайно сделать неправильные выводы.
1. Лимит контейнера и cgroup v2: что происходит при memcg OOM
Самый частый сценарий: приложение внутри контейнера выходит за свой limits.memory.
В Kubernetes memory limit контейнера в итоге превращается в ограничение на уровне cgroup. В cgroup v2 жёсткий лимит задаётся через memory.max. Если потребление памяти в этой cgroup доходит до лимита и ядро не может освободить достаточно памяти, возникает memcg OOM.
На ALT Linux 11 используется cgroup v2 - как и в большинстве современных дистрибутивов Linux по умолчанию. Для Kubernetes это важный нюанс: в типовой конфигурации kubelet на cgroup v2 для container cgroup выставляется memory.oom.group=1.
Проверить это можно прям на ноде:
cat /sys/fs/cgroup/.../memory.oom.group
Если там 1, то при OOM внутри конкретного контейнера ядро рассматривает процессы этого контейнера как единую группу и убивает их вместе. Это отличается от привычного поведения cgroup v1, где мог умереть один worker-процесс, а основной процесс контейнера продолжал жить, оставляя приложение в полуживом состоянии.
Но тут есть важная оговорка: для multi-container pod это не обязательно означает мгновенную смерть всех контейнеров pod’а.
Если OOM произошёл на уровне cgroup конкретного контейнера, будет убит именно этот контейнер. Если же давление по памяти возникло выше по иерархии cgroup или дошло до node/global OOM, поведение уже зависит от лимитов, QoS, oom_score_adj и того, кого ядро выберет жертвой.
Для диагностики полезно смотреть не только memory.max, но и memory.events:
cat /sys/fs/cgroup/.../memory.max cat /sys/fs/cgroup/.../memory.events
В memory.events можно увидеть счётчики вроде:
high max oom oom_kill oom_group_kill
Они помогают понять, что именно произошло: контейнер приблизился к лимиту, упёрся в memory.max, словил OOM или был убит группой.
2. А что насчёт memory.high?
В cgroup v2 есть не только memory.max, но и memory.high.
memory.max - это жёсткая граница. Если контейнер дошёл до неё и память нельзя освободить, будет OOM.
memory.high - это мягкий порог. При его превышении ядро начинает троттлить процессы в cgroup и заставляет их проходить через reclaim, то есть пытаться освобождать память до того, как ситуация дойдёт до убийства.
Звучит конечно красиво, но в Kubernetes есть нюанс: сам факт использования cgroup v2 ещё не означает, что memory.high реально настроен для ваших контейнеров.
Обычно memory limit контейнера мапится в memory.max. А вот активное использование memory.high связано с MemoryQoS и конкретной конфигурацией kubelet/runtime. Если MemoryQoS не включён или runtime не выставляет этот параметр, memory.high может оставаться равным max, то есть фактически не работать как предварительный тормоз перед OOM.
Проверять надо на живой ноде:
cat /sys/fs/cgroup/.../memory.high
Если там max, никакого троттлинга на этом уровне нет.
3. QoS-классы: кто на самом деле защищён?
Когда память заканчивается на всей ноде, важную роль играет oom_score_adj. Это поправка, которую Kubernetes выставляет процессам контейнеров, чтобы повлиять на выбор жертвы kernel OOM Killer’ом.
QoS-классы в Kubernetes такие:
Guaranteed
Pod получает Guaranteed, только если для каждого контейнера заданы и CPU, и memory request/limit, и при этом:
cpu request == cpu limit memory request == memory limit
Если забыли задать CPU request/limit - это уже не Guaranteed.
Для обычных пользовательских pod’ов это стандартный способ получить сильную защиту от OOM Killer’а:
cat /proc/$PID/oom_score_adj -997
BestEffort
Если у pod’а нет ни requests, ни limits, он получает BestEffort.
Такие процессы получают:
cat /proc/$PID/oom_score_adj 1000
Это первый кандидат на вылет при node/global OOM.
Burstable
Всё остальное - Burstable.
Для Burstable pod’ов oom_score_adj считается по формуле:
oom_score_adj = 1000 - (1000 × memoryRequestBytes) / nodeMemoryCapacityBytes
Результат зажимается в диапазоне:
[2, 999]
То есть чем больше memory request относительно памяти ноды, тем ниже oom_score_adj и тем меньше вероятность быть выбранным OOM Killer’ом.
В моей лабе это хорошо видно:
# Guaranteed pod
cat /proc/$(pgrep stress-ng)/oom_score_adj
-997
# BestEffort pod
cat /proc/$(pgrep alpine)/oom_score_adj
1000
Отдельный нюанс: системные процессы могут быть защищены ещё сильнее. В моей песочнице, например, kubelet имел oom_score_adj=-999, а sshd - -1000.
То есть Guaranteed - это не имба для пода. Это сильная защита по сравнению с обычными workload-процессами, но не абсолютная гарантия жизни.
4. QoS и eviction - не одно и то же
Тут легко ошибиться.
oom_score_adj важен для kernel OOM Killer’а, когда ядро уже само выбирает, кого убить.
А kubelet eviction работает иначе. Если kubelet успевает заметить memory pressure до global OOM, он выселяет pod’ы по своей логике. Там важны:
превышает ли pod свои requests;
PriorityClass;
насколько сильно usage превышает request.
QoS-класс коррелирует с этим поведением, но не является единственным алгоритмом eviction.
Например, pod с низким priority, но потреблением в пределах request, не обязательно будет выселен раньше pod’а с более высоким priority, который сильно вышел за request. Поэтому для анализа инцидента надо понимать, что именно произошло:
контейнер умер из-за своего memory limit;
pod был выселен kubelet’ом;
процесс был убит kernel OOM Killer’ом при global OOM.
Это разные события, и следы у них разные.
5. Global OOM: когда kubelet не успел
Если память на ноде закончилась резко, kubelet может не успеть сделать eviction. Тогда срабатывает обычный kernel OOM Killer.
Для проверки я запускал простой Python-скрипт, который агрессивно захватывал память:
import time
data = []
while True:
data.append(bytearray(100 * 1024 * 1024))
time.sleep(0.1)
В dmesg после этого можно увидеть что-то вроде:
Out of memory: Killed process 1841 (python3) total-vm:10GB, anon-rss:3.7GB, oom_score_adj:0
Здесь важно правильно читать поля.
total-vm - это виртуальное адресное пространство процесса.
anon-rss - реально резидентные анонимные страницы в RAM.
Разница между total-vm и anon-rss хорошо показывает, почему нельзя смотреть только на VIRT в top/ps и делать вывод, что процесс реально занял столько RAM. Но это ещё не вся история overcommit. Для анализа overcommit лучше смотреть глобальные счётчики:
grep -E 'CommitLimit|Committed_AS' /proc/meminfo
Committed_AS показывает объём памяти, который ядро уже пообещало процессам.
CommitLimit показывает предел, после которого новые аллокации в strict mode должны начать отклоняться.
Ещё один важный момент при разборе OOM-логов: не путайте строки invoked oom-killer и Killed process.
Строка вида:
python3 invoked oom-killer
описывает процесс, который наткнулся на нехватку памяти.
А строка:
Out of memory: Killed process ...
описывает уже выбранную жертву.
Иногда это один и тот же процесс, иногда нет.
6. Опасные игры с vm.overcommit_memory
В Linux есть три режима overcommit:
0 — эвристика ядра
1 — always overcommit
2 — strict overcommit
В моей лабе на ALT Linux 11 после старта Minikube/kubelet значение vm.overcommit_memory переключалось в 1.
Проверяется так:
sysctl vm.overcommit_memory
Важно: это node-level sysctl, а не настройка конкретного pod’а или cgroup. Он влияет на поведение всей ноды.
Режим 1 разрешает агрессивный overcommit: процессы могут успешно получать виртуальную память «про запас», а реальные проблемы проявятся позже - когда память начнут фактически трогать и страницы станут резидентными.
Самая опасная ситуация - вручную переключить ноду в strict mode:
sysctl vm.overcommit_memory=2
В режиме 2 ядро начинает проверять, не превышают ли обещанные аллокации общий commit limit.
Упрощённая формула такая:
CommitLimit = SwapTotal + RAM × overcommit_ratio / 100
Более точная формула учитывает huge pages:
CommitLimit = SwapTotal + (RAM - HugeTLB) × overcommit_ratio / 100
В моей лабе было 4 ГБ RAM, swap выключен, overcommit_ratio=50. Поэтому CommitLimit оказался около 2 ГБ:
sysctl vm.overcommit_memory=2
cat /proc/meminfo | grep CommitLimit
CommitLimit: 2005936 kB
Если нода уже нагружена и Committed_AS выше нового CommitLimit, такое переключение может быстро превратить систему в кирпич: новые процессы, fork, SSH-сессии и служебные демоны могут начать получать отказ на выделение памяти.
Перед включением strict mode надо хотя бы проверить:
grep -E 'CommitLimit|Committed_AS' /proc/meminfo
Если Committed_AS уже выше будущего CommitLimit, включать strict mode нельзя без подготовки.
Более безопасный порядок такой:
sysctl vm.overcommit_ratio=80
sysctl vm.overcommit_memory=2
Но и это не рекомендация «делать в проде». Это настройка, которую надо тестировать под конкретный workload. Kubernetes-кластер с контейнерами, JVM, Python, Go-сервисами, базами данных и sidecar’ами может очень неприятно отреагировать на строгий overcommit.
7. Что реально помогают настроить kube-reserved, system-reserved и evictionHard
Чтобы нода не доходила до global OOM, Kubernetes даёт несколько механизмов резервирования.
kube-reserved - ресурсы для kubelet, container runtime и компонентов Kubernetes.
system-reserved - ресурсы для системных демонов ОС.
evictionHard - аварийный порог, при котором kubelet начинает выселять pod’ы.
Например:
kubeReserved:
memory: "512Mi"
systemReserved:
memory: "512Mi"
evictionHard:
memory.available: "500Mi"
Эти параметры не делают pod’ы магически безопасными. Они уменьшают Node Allocatable и создают буфер, чтобы kubelet успел начать eviction до того, как ядро сорвётся в global OOM.
Но если memory spike слишком резкий, kubelet всё равно может не успеть. Тогда решение будет принимать уже kernel OOM Killer.
8. Что делать в целях диагностики
Проверить версию cgroup
stat -fc %T /sys/fs/cgroup
Для cgroup v2 будет:
cgroup2fs
Найти cgroup процесса
cat /proc/$PID/cgroup
Проверить лимиты контейнера
cat /sys/fs/cgroup/.../memory.max
cat /sys/fs/cgroup/.../memory.high
cat /sys/fs/cgroup/.../memory.oom.group
cat /sys/fs/cgroup/.../memory.events
Проверить приоритет для OOM Killer’а
cat /proc/$PID/oom_score
cat /proc/$PID/oom_score_adj
Проверить overcommit
sysctl vm.overcommit_memory
sysctl vm.overcommit_ratio
grep -E 'CommitLimit|Committed_AS' /proc/meminfo
Проверить события Kubernetes
kubectl describe pod <pod>
kubectl get events --sort-by=.lastTimestamp
Если контейнер умер из-за собственного лимита, обычно будет видно OOMKilled.
Если pod выселил kubelet, будет Evicted.
Если был global OOM на ноде, следы надо искать уже в dmesg/journal:
dmesg -T | grep -i -E 'out of memory|oom|killed process'
journalctl -k | grep -i -E 'out of memory|oom|killed process'
Итоги
requests и limits - это важные механизмы, но они не отменяют реальность Linux memory management.
Ключевые выводы всего вышеописанного:
Memory limit контейнера - это cgroup-лимит, а не предварительно зарезервированная RAM.
На cgroup v2 при memory.oom.group=1 процессы внутри контейнера обычно убиваются как группа. Но для multi-container pod это не всегда означает смерть всех контейнеров pod’а.
memory.high - полезный механизм cgroup v2, но не надо считать, что Kubernetes всегда его использует. Проверяйте реальное значение в cgroup.
QoS влияет на oom_score_adj, но kubelet eviction и kernel OOM Killer - разные механизмы.
Guaranteed - это сильная защита, но не гарантия бессмертия для пода. Системные процессы могут быть защищены сильнее, а при тяжёлом global OOM ядро всё равно будет кого-то убивать.
Strict overcommit mode опасен без расчёта Committed_AS и CommitLimit. Особенно на Kubernetes-нодах, где много процессов активно резервируют виртуальную память.
kube-reserved, system-reserved и evictionHard нужны не для красоты. Они дают kubelet шанс выселить pod’ы раньше, чем нода попадёт в global OOM.
Процессорный кэш: что это и как он помогает ускорить вычисления
Количество ядер, их архитектура и частота — три ключевые характеристики центрального процессора. Но не менее важную роль играет еще одна: кэш. Это быстрая память небольшого объема, расположенная на кристалле рядом с вычислительными ядрами. Для чего нужен кэш? Как он работает, на что влияет и как устроен внутри?
Немного истории: память в вычислительных системах
Компьютеры, ноутбуки, планшеты, смартфоны и даже миниатюрные смарт-часы — все эти устройства представляют собой разновидности вычислительных систем. В основе работы любой из них два ключевых компонента: подсистема памяти, в которой находятся данные для расчетов, и процессор, который эти расчеты производит.
На заре появления вычислительные системы состояли из простых пар «процессор — память». Сначала код полностью загружался в память с помощью тумблеров, штекерной панели или перфокарт, и лишь затем процессор начинал считывать его оттуда и выполнять команды. Результаты вычислений выводились либо на панель с индикаторами, либо с помощью «дедушек» современных принтеров — телетайпов, перфораторов или алфавитно-цифровых печатающих устройств.
В конце 70-х годов прошлого века место ранее используемых ртутных, механических и магнитных видов памяти окончательно заняла полупроводниковая. Перфокарты и перфоленты уступили место кассетам с магнитной лентой, а еще через несколько лет — флоппи-дискам. Тогда вычислительные системы наконец обрели близкий к современным формат работы: устройство постоянной памяти (накопитель) — оперативная память (ОЗУ) — процессор.
Типы оперативной и постоянной памяти с тех пор не раз менялись, но общий принцип работы вычислений оставался прежним. Упрощенно его можно представить так:
Программа загружается из постоянной памяти в оперативную.
Процессор считывает данные из оперативной памяти и выполняет над ними вычисления.
Результаты вычислений возвращаются в оперативную память.
Программа использует результаты для передачи на устройство вывода (экран/динамики/принтер), либо для записи обратно в постоянную память.
Частоты ЦП в то время росли не по дням, а по часам. Уже к середине 80-х процессоры стали работать заметно быстрее, чем микросхемы оперативной памяти. Из-за этого все чаще возникали ситуации, которые сегодня называют боттлнеком: во многих задачах ЦП приходилось ждать загрузки из ОЗУ и пропускать рабочие такты, теряя заметную часть производительности.
Решением этой проблемы стало внедрение кэша: очень быстрой памяти малого объема, служащей буфером данных между процессором и оперативкой. В x86-процессорах кэш впервые появился в 1987 году у модели Intel 80386, с тех пор став стандартным элементом практически любой вычислительной системы.
Как работает кэш
Про кэш часто говорят, что «это та же оперативная память, только в разы быстрее и намного меньше по объему». Отчасти это и правда так, ведь кэш выполняет работу, которая до его появления была исключительно делом ОЗУ — максимально быстро передавать процессору данные, необходимые для расчетов.
Однако на деле принципы работы кэша и оперативки заметно отличаются. ОЗУ заполняется только теми данными, которые запрашивают выполняемые программы. Контролирует этот процесс операционная система — то есть, в данном случае управление памятью полностью программное.
Если бы кэш-память можно было бы сделать размером с оперативную, то последняя оказалась не нужна бы вовсе. Но ОЗУ у современных систем исчисляется гигабайтами, а кэш — мегабайтами. Разница в объеме между ними составляет порядка тысячи раз, и подобное положение сохраняется уже четвертое десятилетие.
Но как же кэш при столь малом объеме помогает ЦП получать быстрый доступ к данным выполняемых программ? Дело в том, что такая память намного «умнее». Вместо того, чтобы загружать только используемую в данный момент информацию, кэш заполняется данными на основе:
Принципа временной локальности. Когда процессор делает запрос к определенному адресу в ОЗУ, то он с большой вероятностью будет обращаться к нему снова. Для таких случаев кэш продолжает сохранять ранее запрошенные данные.
Принципа пространственной локальности. Если процессор делает запрос к определенному байту памяти, то он наверняка будет обращаться и к информации в соседних байтах. Поэтому в кэш загружается не один запрашиваемый байт, а целая кэш-линия, обычно 64 байта.
Работы Prefetchers. Это аппаратные блоки предвыборки данных, занимающиеся поиском зависимостей в коде (Instruction Prefetchers) и данных (Data Prefetchers). Когда они «видят», что программа запрашивает информацию в определенной последовательности, то начинают загружать эту последовательность в кэш заранее — еще до того, как эти данные понадобятся процессору.
После того, как кэш заполнился, в дело вступают алгоритмы вытеснения данных. Они решают, какую информацию еще нужно держать в кэше, а какую можно удалить, освободив место для новой. За их работу отвечают:
Контроллер кэша. Хранит и сверяет теги, указывающие на соответствие данных в кэше адресам из ОЗУ.
Блок замещения. Следит за «возрастом» данных в строках кэша, чтобы при поступлении новой информации заменить ей самую старую.
Буфер обратной записи. Вступает в дело после блока замещения: сохраняет старую информацию до тех пор, пока она не будет записана в кэш уровнем ниже или оперативную память.
Агент когерентности. Когда одно ядро ЦП изменяет определенную информацию в ОЗУ, то кэши других ядер продолжают хранить ее старые версии. Этот блок отслеживает изменения в кэшах, помечая старые данные недействительными и организуя загрузку их актуальных версий.
Чтобы отслеживать устаревание данных одновременно во всем массиве кэша и при этом обеспечивать запись новой информации в любую его точку, требуется очень сложная логика. Для решения этой проблемы с конца 80-х годов и по сегодняшний день в процессорах используется множественно-ассоциативный кэш (Set-Associative Cache).
При таком подходе набор адресов оперативной памяти привязывается к нескольким ячейкам кэша (в современных ЦП — от 8 до 20). Когда поступает команда на запись из привязанного адреса, алгоритмы вытесняют информацию из одной ячейки набора, не затрагивая остальные. Это позволяет эффективно избавляться от старых данных, сохраняя актуальную информацию в кэше без необходимости постоянно «гонять» ее из ОЗУ.
За счет слаженной работы вышеописанных алгоритмов и блоков кэш способен предварительно загрузить в себя до 99 % данных, необходимых для расчетов. Это избавляет вычислительный конвейер процессора от простоя, вызванного ожиданием информации из оперативной памяти.
Вдобавок к аппаратным методам управления содержимым кэша существуют также программные. Если разработчик ПО видит, что его код при «самодеятельности» процессора не очень эффективно использует кэш или забивает его ненужными данными, он может использовать:
Команды предвыборки. «Советуют» процессору подтянуть выбранные данные из ОЗУ в кэш заранее.
Команды потоковой записи. Заставляют ЦП записывать данные напрямую в оперативную память, минуя кэш.
Команды обслуживания. Позволяют сбрасывать содержимое кэша частично или полностью.
Устройство иерархии кэшей
Единственный кэш пробыл у процессоров недолго. В 1989 году в Intel 80486 дебютировала система кэширования с двумя уровнями (L1 и L2). А четыре года спустя в Intel Pentium кэш первого уровня для более эффективной работы был поделен на две независимые части — для инструкций (L1 Instruction) и для данных (L1 Data).
С развитием многоядерности в 2007-2008 годах в ЦП появился кэш третьего уровня (L3). В отличие от прочих уровней, он стал общим хранилищем для всех процессорных ядер и помог им обмениваться данными друг с другом без помощи оперативной памяти. А последним появился кэш микроопераций (L0): им оснащены все Intel Сore (со второго поколения) и AMD Ryzen.
К сегодняшнему дню иерархия кэшей у современных процессоров приняла следующий вид:
Кэш L0. Хранит очередь микроопераций, декодированных ядром из инструкций.
Кэш L1I. Используется для инструкций, которым предстоит пройти декодирование.
Кэш L1D. Хранит данные: числа, указатели, логические значения.
Кэш L2. Общий уровень для данных и инструкций каждого ядра.
Кэш L3. Общий уровень для данных и инструкций всех ядер.
Чем выше уровень кэша, тем быстрее он работает. Но чем быстрее ячейки кэш-памяти, тем сложнее их разводка, выше энергопотребление и нагрев. Если сделать L1 размером с L3, то он будет потреблять огромное количество энергии и перегревать ядра даже в простых задачах. Поэтому система кэширования сочетает несколько уровней разного объема, задержка доступа к которым заметно различается. У современных ЦП это:
L0: без задержки или 1 такт, 4000-6000 микроопераций (на ядро),
L1 (I+D): 4-5 тактов, 64-112 кБ (на ядро),
L2: 8-16 тактов, 0.5-3 МБ (на ядро),
L3: 40-55 тактов, 12-192 МБ (общий для всех ядер).
В зависимости от архитектуры процессора, разные уровни могут иметь отличающуюся организацию кэш-памяти:
Инклюзивную. Кэш хранит в себе полную копию информации, которая есть в верхних уровнях. При таком подходе тратится меньше времени на поиск данных между ядрами, но из-за дубликатов не весь объем кэша используется эффективно.
Эксклюзивную. Данные могут находиться только на одном уровне кэша, что позволяет максимально эффективно использовать его объем. Но если одному ядру нужны данные, лежащие в кэше другого, то приходится обновлять информацию на всех уровнях иерархии — это вызывает дополнительную задержку.
Нестрого инклюзивную. Сочетает преимущества двух вышеописанных организаций. Здесь данные могут дублироваться на нижних уровнях кэша, но только в том случае, если так «решили» его алгоритмы работы.
Принцип работы кэша схож при любой организации: когда запускаются вычисления, процессор начинает искать нужную информацию в L0/L1. При неудаче («промах») отправляется запрос в L2, а если ее и там не нашлось — то в L3. В случае, когда нужных данных нет ни на одном уровне кэша, процессору приходится запрашивать их из оперативной памяти. С современной DDR4 и DDR5 такой запрос обходится потерей от 250 до 450 тактов: это в 5-10 раз больше, чем доступ к самому «медленному» L3.
Технологии расширения кэш-памяти
На заре появления микросхемы кэша располагались либо на материнской плате, либо в картридже процессора. Но к 2000 году и Intel, и AMD интегрировали оба уровня кэш-памяти внутрь кристаллов своих ЦП, наконец избавив кэш от роли внешнего элемента.
С тех пор объемы кэшей понемногу росли. Однако главным препятствием к их резкому увеличению все также оставалась сложность SRAM-памяти, требующей много транзисторного бюджета. Создавать процессор, где более половины кристалла занял бы кэш, в 2000-е годы было трудно. Но даже когда такая возможность появилась, проектировать нишевый чип никто не хотел, ведь вместо огромного кэша на той же площади можно было разместить больше вычислительных ядер.
Для решения данной задачи проще всего было вернуться к корням кэша: многочиповой компоновке. Впервые на это в 2013 году решилась компания Intel. Тогда она оснастила свои мобильные Core четвертого поколения дополнительным кристаллом eDRAM объемом 128 МБ. Он работал заметно медленнее L3 и поэтому играл роль общего кэша следующего, четвертого уровня (L4).
Благодаря огромному объему чип eDRAM позволял процессорам тех лет практически не обращаться в ОЗУ напрямую, за счет чего их конвейер не простаивал даже в самых сложных ситуациях. Но память eDRAM была дорога, поэтому так и не стала массовой: в топовых мобильных Intel она изредка встречалась вплоть до восьмого поколения Core, а в десктопе и вовсе появилась только в двух моделях пятого поколения.
В 2022 году схожую идею впервые реализовала компания AMD. Но она выбрала другой путь: вместо добавления относительно медленного чипа L4 «приклеивать» поверх процессорного чиплета быстрый кристалл c дополнительным объемом L3. Эта технология получила название 3D V-Cache.
Объем кэша: когда и почему становится решающим
В отличие от ОЗУ, кэш является высоко интегрированной памятью. Скорость и ассоциативность кэша подбираются на этапе проектирования процессорной архитектуры так, чтобы при ограниченном транзисторном бюджете максимально выгодно устранить ее «узкие» места. Поэтому сравнивать эти параметры напрямую у ЦП разных поколений не имеет смысла.
Гораздо более универсальная характеристика, на которую стоит обращать внимание при выборе ЦП — это общий объем кэша. Хотя из-за отличающейся компоновки процессоров Intel и AMD используют разные подходы к формированию его уровней («синие» — упор на объем L2, «красные» — упор на объем L3), правило «больше — значит лучше» работает здесь почти всегда.
Почему? Все просто: чем объемнее кэши, тем больше в них «влезает» различных данных, которые могут понадобиться ЦП в следующий момент времени. Поэтому процент ситуаций, когда нужная информация оказалась в кэше («попадание») с ростом его объема становится более высоким. За счет этого процессор реже обращается за данными к медленной ОЗУ и, как следствие, меньше теряет свою скорость.
Впрочем, производительность ЦП зависит от объема кэша далеко не всегда. Прирост от его увеличения заметен лишь тогда, когда выполняемый код хаотичен — в играх, 3D-моделировании или инженерном ПО. И чем больше кэш, тем выше он будет. Например, трехкратное увеличение объема кэша у процессоров AMD Ryzen X3D способно дать им в подобных задачах от 10 до 50 % дополнительной скорости.
Но с относительно линейным и предсказуемым кодом современные ЦП не получают от большого кэша заметного буста. К таким ситуациям относится работа с 2D-графикой, рендеринг, монтаж и кодирование видео — в них прирост от увеличения кэша колеблется от 0 до 5 %. Поэтому для подобных сценариев на объем кэша можно не обращать внимания: в них куда важнее архитектура процессора, его тактовая частота и количество ядер.
P/S
Размышлизмы:
Сверхоперативная память процессора. Снижает потери времени на ожидание данных. Конвейер чаще работает и чем он быстрее, тем требовательнее к оперативной памяти. У актуального SRAM удельное быстродействие больше, чем у LPDDR5X, GDDR7 и даже HBM4. Обычный DRAM почти во всём значительно проигрывает.
Но его в сотни или тысячи раз больше. Пришлось пользоваться как посредником между кэшем и SSD или иной постоянной памятью. ПЗУ ещё медленнее, без системного ОЗУ простои будут слишком длительными и это ускорит их поломку. Больше ядер и частот - чаще обращается к кэшу. Больше кэша - меньше обращений к DDR.
Что до практики, много кэша позволяют частично забить на скорость обычной оперативной памяти и системных шин. Но SRAM дорогая, медленным CPU и GPU её добавляют по минимуму или нехватку компенсируют быстрым DRAM.
Обычно старшие APU самые требовательные до ОЗУ. С дискретной видеокартой проблема меньше выражена. И нельзя сравнивать разные архитектуры только по одному параметру. Смотрим комплексно, также долго тестируем большим набором задач.
Своими словами и подробнее, наглядно. Либо со смежными темами. Много кэша уместнее при непредсказуемом потоке вычислений. SRAM дорог и CPU с eDRAM, 3D V - cache или bLLC нужны немногим.
Не массовый продукт, для специфичных задач. Усреднённый оптимальный объём итак реализован в большинстве привычных моделей. Но если ажиотаж с DRAM продлится долго, то может появиться кэш L4. Особенно если DDR6 будет не так хороша, как ожидают.
L3 существовал в 2003 году. Pentium 4 EE сокета 478 на ядре Gallatin. Серверный кристалл Xeon с 2 мегабайтами кэша установили на настольный процессор. Но массовым кэш 3 уровня стал с 2007 года, начиная с линейки Phenom сокета АМ2+. Поначалу было мало и медленно, лучше сделали к 2010.
Упрощающее очеловечивание. SRAM это карлик - гигачад. DRAM это толпа депрессивных астеников. Суммарно последние перенесут намного больше вещей за раз. Но быстро устают, больше едят и каждое действие долгое. Первого уместнее поставить у станка, а не грузчиком назначить.
ЧТО ЕСЛИ ВЫ УПАДЁТЕ НА ЮПИТЕР?
Мы точно не знаем, из чего состоит ядро Юпитера. Но мы знаем, что Юпитер — самая большая и старая планета в нашей Солнечной системе. Его масса более чем в два раза превышает массу всех остальных планет вместе взятых. А с радиусом 70 000 км (44 000 миль) он в 11 раз шире Земли. Эта планета также является газовым гигантом, состоящим из остатков от образования Солнца четыре с половиной миллиарда лет назад.














































