Почему телефон грелся
Первая часть закончилась тем, что телефон заработал, и какое-то время этим всё и ограничивалось. postmarketOS на mainline-ядре, сверху Docker, Tailscale и один небольшой сервис, которому от меня ничего не требовалось. Я убрал телефон на полку и перестал о нём думать — ровно этого я от него и хотел.
Пока про сервер не думаешь, ты о нём ничего и не знаешь. Всё, что я о нём знал, сводилось к списку запущенных контейнеров. В каком он состоянии, насколько горячий, не изнашивается ли постепенно аккумулятор — этого я сказать не мог. Поэтому я стал записывать показания: несколько значений в файл каждые пять минут, в основном чтобы убедиться, что сервис, удерживающий заряд на половине, всё ещё делает свою работу.
Сервис работал. Заодно выяснилось, что процессор всё это время держался на 70 °C. Не под нагрузкой, а в покое, днём и ночью, на устройстве без вентилятора и без радиатора, где тепло уходит только через собственный алюминиевый корпус.
Сначала я просто с этим смирился. Snapdragon 2016 года без активного охлаждения греется, 70 °C вполне в пределах того, что нормально для кремния, а других вопросов у меня к телефону не было.
Вопрос, который стоило задать сразу: чем он занят, что даёт такую температуру. Дальше — ответ на него.
Телеметрия
Сборщик
Portainer, установленный на телефоне, показывает потребление процессора и памяти по контейнерам. О самом устройстве он не знает ничего: ни заряда, ни тока, ни температуры. Всё это находится уровнем ниже того, что видит Docker, и получить эти показания было нечем.
Первое, что я решил: постоянно работающего процесса не будет. Он выполнялся бы на том же телефоне, потребление которого я собирался измерить, и сам попал бы в результаты измерений. Вместо этого systemd-таймер раз в пять минут запускает shell-скрипт, тот читает несколько файлов, дописывает строку в CSV и завершается. Один запуск стоит около 50 мс процессорного времени.
[Timer]OnBootSec=2minOnUnitActiveSec=5minAccuracySec=30s
Допуск в тридцать секунд разрешает systemd сдвинуть запуск и выполнить его заодно с другими таймерами. Так сборщик почти не будит телефон сам по себе, а пользуется теми пробуждениями, которые всё равно происходят.
Больше всего данных приходит с счётчика заряда. В OnePlus 3T стоит bq27541, включённый последовательно с аккумулятором: он определяет заряд по току, прошедшему в обе стороны, а не по напряжению на клеммах, поэтому его показаниям можно верить и под нагрузкой. Mainline описывает его как обычный источник питания, так что процент заряда, ток, напряжение и температура аккумулятора лежат в четырёх файлах в /sys/class/power_supply/bq27541-0/.
К ним я добавил ещё три значения. Флаг online у зарядного устройства: без него потом не понять, снят замер тока при отключённом кабеле или при подключённом. Температуру самой горячей из зон cpu*-thermal, а не какой-то одной: самое горячее ядро процессора всё время меняется, и показания одной выбранной зоны оказываются заниженными. И загрузку процессора, посчитанную как разница показаний /proc/stat с предыдущим запуском; сам снимок лежит в скрытом файле рядом с CSV.
Просмотр показаний
CSV файл нужно ещё и читать, и здесь действует то же ограничение. Просмотрщик не работает постоянно: systemd поднимает его на время одного запроса.
[Socket]ListenStream=8088Accept=yes
Accept=yes означает, что соединение принимает сам systemd и передаёт его программе как стандартный ввод и вывод. Благодаря этому просмотрщик написан как обычный скрипт, который читает запрос и печатает страницу. Процесс Python существует ровно столько, сколько длится один просмотр.
Что показали данные
Сервис, удерживающий заряд на половине, работал именно так, как задумывалось. Если брать по одному замеру в день, снятому в один и тот же час, ток через аккумулятор каждый раз нулевой, а сам заряд постепенно снижается с 50 % до 46 %: аккумулятор саморазряжается, а сервис его не подзаряжает. Ошибается он в меньшую сторону, то есть в ту, которая меня устраивает.
В соседней колонке картина была другой. Дневная средняя температура процессора не опускалась ниже 65.2 °C и не поднималась выше 68.1.
Объяснения в файле не нашлось. Загрузка процессора в среднем 2.3 %, за всё время ни разу не превысила 10 %, так что никакой фоновый процесс её не создавал. Температура аккумулятора — 24.7 °C, значит греется SoC, а не батарея.
Замер, снятый с отключённым кабелем, дал -610 mA при 3806 mV. Это 2.3 Вт на сервере, четыре ядра процессора которого простаивают 97 % времени.
Неисправностью это не выглядело: сервисы работали, ошибок в логах не было. Оставался только вопрос, куда уходят 2.3 Вт, пока сервер ничего не делает.
cpuidle
Первым делом стоило разобраться, что Linux делает с ядром процессора, которому нечего выполнять. За это отвечает cpuidle: говернор оценивает, надолго ли ядро останется без работы, а драйвер платформы переводит его в одно из состояний сна, которые есть у железа. За выход из каждого состояния приходится платить временем на пробуждение, но это время известно заранее.
На arm64 драйвер платформы не может погасить ядро своими силами. Регистры, которые действительно снимают с него питание, принадлежат прошивке, работающей на более высоком уровне привилегий, и доступа к ним у Linux нет. Есть PSCI, Power State Coordination Interface — стандарт ARM, описывающий несколько номеров функций, которые вызываются инструкцией smc, то есть переходом в эту самую прошивку. Один вызов поднимает ядро, второй его гасит, третий переводит в состояние сна. У третьего всего один аргумент — параметр состояния.
Одним этим числом и ограничивается всё, что Linux сообщает прошивке о нужном ему состоянии.
Со стороны Linux этот вызов делает драйвер cpuidle-psci.
Я посмотрел, с какой конфигурацией собрано ядро:
CONFIG_CPU_IDLE=y# CONFIG_CPU_IDLE_GOV_LADDER is not setCONFIG_CPU_IDLE_GOV_MENU=y# CONFIG_CPU_IDLE_GOV_TEO is not set## ARM CPU Idle Drivers## CONFIG_ARM_PSCI_CPUIDLE is not set# end of ARM CPU Idle Drivers
Первые строки в порядке: сам cpuidle собран, говернор выбран. А в последнем блоке CONFIG_ARM_PSCI_CPUIDLE выключен. Это и есть тот самый драйвер, который обращается к прошивке за состояниями, и в этой сборке его нет.
Без него выбирать cpuidle не из чего, и в простое выполняется то, что предусмотрено архитектурой по умолчанию: инструкция wfi. Она останавливает выборку команд до прихода прерывания, но питание с ядра не снимает.
Пакет с ядром
Эту конфигурацию писал не я. postmarketOS собирает один пакет ядра на всё семейство msm8996, и его получают все устройства на этом чипе: OnePlus 3 и 3T, Xiaomi Mi 5, Mi 5s Plus и Mi Note 2, три варианта Sony Xperia X Performance. В вики нескольких из них упоминаются зависания при включённом CPU idle — видимо, поэтому его и отключили.
Дерево устройств
Состояния сна на arm64 описаны в дереве устройств. Это файл данных, который сообщает Linux, какое железо стоит в системе, потому что определить его опросом почти невозможно. Для этого чипа состояния описывает только msm8996.dtsi, и ни одна из десяти плат на msm8996 в mainline описание не переопределяет. Значит, простой на всех этих телефонах устроен одинаково:
CPU_SLEEP_0: cpu-sleep-0 {compatible = "arm,idle-state";idle-state-name = "standalone-power-collapse";arm,psci-suspend-param = <0x00000004>;entry-latency-us = <130>;exit-latency-us = <80>;min-residency-us = <300>;};
Одно состояние, и только для случая, когда ядро процессора засыпает в одиночку. arm,psci-suspend-param — то самое число, которое уходит в прошивку и называет нужное состояние. Остальные три значения нужны говернору для расчёта: 130 мкс на вход, 80 мкс на возврат и не входить вовсе, если простой короче 300 мкс.
Из этих чисел позже поправили только одно — задержку входа. Изначально там стояло 40 мкс, а правильное значение вывели из сток дерева устройств, где уровень описан не входом и выходом, а задержкой пробуждения и полными накладными расходами:
entry-latency-us = qcom,time-overhead - qcom,latency-us = 210 - 80 = 130
Это единственный случай, когда для msm8996 в mainline взяли число из стокового ядра.
msm8998
msm8998, Snapdragon следующего года, описывает четыре состояния там, где msm8996 описывает одно: удержание и снятие питания для каждого из двух кластеров, 0x00000002 и 0x40000003. У его состояний со снятием питания есть ещё и свойство, которого у состояния msm8996 нет:
local-timer-stop;
Оно сообщает, что собственный таймер ядра процессора в этом состоянии останавливается, а значит пробуждение нужно передать таймеру, который продолжает идти. В других dtsi-файлах SoC Qualcomm на arm64 это свойство объявлено.
Дальше числа msm8998 правили ещё дважды, и оба раза задолго до того, как я занялся этой темой. Интереснее вторая правка. Задержки там были написаны из расчёта на засыпание одного ядра, тогда как эти состояния снимают питание ещё и с общего для кластера кэша L2, а его возвращать дольше. С такими заниженными значениями и при включённом динамическом изменении частоты говернор входил в эти состояния на простоях, которых не хватало, чтобы их завершить, и SoC перезагружался или зависал, ничего не оставляя в логах.
Описание msm8996 с тех пор не менялось. Я включил драйвер и собрал ядро.
Контрольный эксперимент
Телефон не отвечает
Я переключил CONFIG_ARM_PSCI_CPUIDLE в y, больше ничего не менял и загрузил ядро через fastboot boot, чтобы не прошивать ради одной пробы. Не поднялось ничего. На маке не появилось USB-сетевого устройства, обращаться было некуда, и понять, упало ядро или просто не дошло до поднятия интерфейса, я не мог.
Ровно это и описано в вики, так что сам вывод я под сомнение не поставил, а стал искать ошибку в том, как описано железо.
Сначала я попробовал не сократить число состояний, а добавить. Я задействовал genpd — механизм доменов питания, через который последнее засыпающее ядро может запросить состояние не только для себя, но и для всего кластера. Каждому ядру процессора я дал свой домен, домены ядер подчинил домену кластера, оба кластера — системному домену, и на каждом уровне прописал свой параметр состояния. Ядро не поднялось.
Затем я взялся за сам параметр. 0x00000004 подозрителен уже на вид. Бит типа состояния в нём сброшен, а это говорит прошивке, что ядро сохраняет своё содержимое, — то есть ровно обратное тому, что называется снятием питания. Я выставил бит и попробовал 0x40000004, считая тогда, что именно это значение сток ядро собирает для того же состояния одного ядра. Результат тот же. Я добавил сверху local-timer-stop — свойство, которого у msm8996 нет, а у всех сопоставимых SoC есть: если ядро процессора гасится, а его пробуждение висит на таймере, который останавливается вместе с ним, вывести его будет нечем. Снова ничего. Я попробовал 0x00000002, удержание вместо снятия питания, ровно то значение, которое для него использует msm8998: на менее глубокое состояние шансов было больше. Тоже нет. Я попробовал 0x00000001, самое осторожное значение в кодировке, рассудив, что если что-то и заработает, то это. Оно повело себя так же, как всё остальное.
Шесть сборок: стоковое значение и пять вариантов к нему. Все отказали одинаково, и вот в этом я увидел закономерность.
Объяснения
Каждый отказ порождал объяснение, и все три опирались на факты, которые для этого чипа верны.
Самое простое объяснение — сам параметр. У SoC того поколения от Qualcomm различаются младшие четыре бита, и везде в дереве там 2 или 3: msm8916, msm8976, msm8998, sdm630. Только msm8996 использует 4, и это согласуется с симптомом: прошивка отвергает незнакомый идентификатор состояния, ядро не возвращается.
Второе объяснение — контроллер прерываний. msm8996 единственный SoC в mainline, для которого действует отдельное исключение, отключающее часть обычной обработки простоя, и ядро процессора, вернувшееся из настоящего снятия питания, вполне могло бы вернуться с выключенным собственным таймером.
Третье — Linux сообщает CPU: All CPU(s) started at EL1, /dev/kvm отсутствует, поддержки виртуализации в логе нет. Причина в том, что EL2, уровень исключений, который Linux на arm64 обычно занимает сам, здесь занят гипервизором Qualcomm, и каждый вызов PSCI проходит через него на пути в TrustZone. Mainline об этом слое ничего не знает, так что «гипервизор неправильно обрабатывает вызов» объясняет зависание, не требуя, чтобы было сломано что-то ещё.
Все три я проверил позже. К тому, что я видел здесь, ни одно из них отношения не имело.
Смущало, что все отказы одинаковы. Конфигурации, которые просят у прошивки разного — снятия питания, удержания, простого ожидания, не должны отказывать одинаково, вплоть до одного и того же пропавшего USB-устройства.
Поэтому я перестал менять ядро и поменял командную строку. Загрузил тот же самый бинарник, что и в последней неудачной попытке, добавив cpuidle.off=1. С выключенным cpuidle Linux вообще не входит в состояния простоя, PSCI_CPU_SUSPEND не вызывается ни разу, и ни один из трёх механизмов выше произойти не может. Отказало точно так же.
Это сразу отменило все три объяснения, потому что они касались вызова, которого больше не было. И это означало, что все мои результаты ничего не стоят: отказ вызывало не то, что я менял. Я начал подозревать сам процесс сборки, и эту версию было просто проверить. Я пересобрал ровно тот коммит, из которого было собрано рабочее прошитое ядро, с его собственной исходной конфигурацией: cpuidle выключен, ни одной моей правки. Оно тоже не поднялось.
Загрузочные образы
Рабочее ядро лежало на диске загрузочным образом, и каждая тестовая сборка тоже, а Android-boot-образ несёт версию ядра прямо в заголовке:
РАБОЧЕЕ прошитое: "6.12.95-msm8996 ... #2-op3t" kernel=10484779 ramdisk=11639553ЭКСПЕРИМЕНТ: "6.12.95 ... #10-op3t" kernel=10664564 ramdisk=10428024
Тот же исходник, та же конфигурация, а initramfs стал меньше на 1.2 МБ. Смотреть, впрочем, надо на строку версии, и это единственное поле, которое совпадает у всех отказавших сборок: рабочее ядро называет себя 6.12.95-msm8996, а всё, что собирал я, сообщало версию 6.12.95.
Модули ядро ищет в /lib/modules/<версия>. На устройстве этот каталог называется /lib/modules/6.12.95-msm8996, поэтому ядро с версией 6.12.95 смотрит в несуществующий каталог и не загружает ничего — ни драйвер USB-гаджета, ни драйвер сети через USB, ни WiFi, ни sshd. Ядро загружалось каждый раз, и у него не оставалось ни одного интерфейса, чтобы об этом сообщить.
Суффикс берётся из CONFIG_LOCALVERSION, а пустым он оказался потому, что конфигурация в моём репозитории была сгенерирована для ядра 7.2-rc1, тогда как собираю я 6.12.95. make olddefconfig в такой ситуации выбрасывает примерно 450 символов, которых в старом дереве нет, подставляет значения по умолчанию для остальных и успешно завершается. CONFIG_LOCALVERSION="-msm8996" ушёл вместе с ними.
Была и вторая ошибка. fastboot boot загружает ядро в память через штатный загрузчик, минуя lk2nd, а именно lk2nd вносит правки для устройства перед передачей управления. Так что загруженное таким способом ядро полную систему здесь не поднимает никогда, даже если с самим ядром всё в порядке.
И третье: я судил о каждой попытке по одному только порту 22, забыв, что postmarketOS загружается в две стадии, отвечающие на разных портах: initramfs поднимает сеть и предлагает telnet на 23-м порту задолго до того, как появится sshd.
Проверки перед прошивкой
Из этого выросли три скрипта. check-kernel.sh не пропускает образ на устройство, пока тот не совпадёт с отпечатком заведомо рабочего: строка версии совпадает, размер initramfs в пределах 5 %, в конфигурации есть CONFIG_LOCALVERSION="-msm8996" и нет символов из более нового ядра. Пока эта проверка не пройдена, я не делаю никаких выводов из того, что показывает устройство.
probe-device.sh заменил проверку порта:
# So: ping + port 23 = booted, sitting in initramfs (usually rootfs trouble)# ping + port 22 = fully booted# ping, no ports = kernel alive, stalled before any service started# no ping at all = genuinely dead / no USB gadget
Третий скрипт, telnet-device.sh, запрашивает у initramfs-оболочки один и тот же набор данных: uname, командную строку ядра, список блочных устройств, точки монтирования, содержимое каталога модулей, вывод lsmod и хвост dmesg. Именно по этому набору обычно и видно, на каком этапе остановилась загрузка.
Правило, которое я отсюда вынес: пересобрать заведомо рабочую конфигурацию в текущем окружении и убедиться, что она всё ещё работает, прежде чем списывать отказ на проверяемое изменение.
С исправленной конфигурацией и этими проверками я собрал ту же правку в один символ и прочитал счётчики из initramfs-оболочки:
DRIVER=psci_idle GOVERNOR=menustate0 WFI usage=2942 time=2292104 usstate1 cpu-sleep-0 usage=7742 time=22910277 us
Счётчики обновлялись, и ядро продолжало отвечать.
Что дала правка
С включённым драйвером у Linux появилось одно состояние сна, и притом неглубокое: 0x00000004, бит типа состояния сброшен, то есть ядро процессора сохраняет своё содержимое. Напрашивается следующий шаг — то же состояние с выставленным битом, — но прошивка его отвергает. На запрос 0x40000004 у состояния растёт счётчик rejected, 16797 отказов, а usage остаётся ровно нулевым. Что означает этот счётчик, я тогда ещё не знал. Ясно было только, что более глубокое стандартное состояние мне недоступно.
Вендорный SMC
Вендорное ядро тоже не уходит глубже через PSCI. Для своего основного уровня снятия питания с ядра оно выставляет в дереве устройств свойство qcom,hyp-psci и вместо вызова suspend делает фиксированный SMC:
__invoke_psci_fn_smc(0xC4000021, 0, 0, 0);
Этот идентификатор функции — вендорное расширение: поле владельца 4 означает пространство PSCI, а номер функции 0x21 лежит за пределами всего, что выделяет спецификация. Обслуживает вызов гипервизор Qualcomm на EL2 — тот самый слой, который был моим третьим объяснением зависаний. Значит, у него есть собственное состояние простоя, недоступное через стандартный интерфейс.
Главное свойство этого вызова: он завершается и передаёт управление обратно, на следующую инструкцию.
Обычный PSCI-suspend так себя не ведёт. Он гасит ядро и не возвращается вовсе, поэтому Linux заранее отдаёт прошивке адрес, с которого продолжить после пробуждения, и сам сохраняет всё, что ядро потеряет. Здесь ничего этого не нужно: адрес отдавать некуда, сохранять нечего, а всё, что ядро теряет, прошивка восстанавливает сама, прежде чем вернуть управление.
Поэтому вызов ставится в то же место, где стоит wfi. И если прошивка его проигнорирует, ядро просто не уснёт: потеряется один переход, а не ядро.
Так что я добавил в драйвер поддержку этого уровня, а в дереве устройств рядом с уже имеющимся состоянием появилось второе:
CPU_SLEEP_1: cpu-sleep-1 {compatible = "qcom,idle-state-hyp-psci", "arm,idle-state";idle-state-name = "hyp-power-collapse";arm,psci-suspend-param = <0x00000000>;entry-latency-us = <40>;exit-latency-us = <120>;min-residency-us = <400>;};
Параметр состояния игнорируется, потому что аргументов у вызова нет, и указан только для того, чтобы описание было корректным. Задержки взяты у того вендорного уровня, который это состояние повторяет, и именно на них говернор опирается при выборе.
Температура
Обе правки я решил проверить вместе, и на этот раз не через fastboot boot, а прошивкой. Загруженное в память ядро минует lk2nd и полную систему на этом устройстве не поднимает, так что проверять его можно только из initramfs-оболочки. Чтобы снимать показания во время работы сервера, ядро должно быть прошито, а телефон загружаться как обычно.
Устройство загрузилось, и сборщик показаний продолжил работать после смене ядра:
|
температура процессора |
среднее |
пик |
|---|---|---|
|
до |
65.2 – 68.1 °C |
71.7 – 80.0 °C |
|
после |
46.0 – 47.9 °C |
54.1 – 57.0 °C |
Из второй строки выбиваются два дня с пиками в 67 °C. В эти дни я гонял телефон в загрузчик и обратно, проверяя очередное ядро, а в загрузчике он греется сильнее всего. К работе сервера эти пики отношения не имеют. В остальном не изменилось ничего: те же сервисы, та же нагрузка, то же зарядное устройство.
Потребление
Надёжно здесь измерена только температура. С потреблением сложнее.
Счётчик заряда подключен последовательно с аккумулятором, то есть видит только то, сколько потребляет плата, когда питается от аккумулятора. При подключённом кабеле зарядное устройство питает систему напрямую, и этот ток счётчик не видит вовсе. Поэтому цифра потребления имеет смысл только при отключённом кабеле, а до правки сборщик зафиксировал: -610 mA при 3806 mV, то есть 2.32 Вт.
После правки я отключил телефон от питания намеренно и дал ему разрядиться до нуля. Так получается нормальная кривая разряда: в среднем 366 мА за весь прогон, 1.32 Вт.
Кластерные и системные состояния
Сами ядра процессора глубже засыпать уже не могут. Значит, оставшееся потребление шло не от них, а от блоков, которые они делят между собой.
Уровни
Дальше всё происходит на одном из трёх уровней:
cpu0 cpu1 cpu2 cpu3 ядро wfi | SMC гипервизора | снятие через PSCI \ / \ / L2 L2 кластер удержание (gdhs) | снятие питания (fpc) \ / L3 + когерентная шина система удержание | снятие питания
и одним из двух путей вниз:
говернор -> драйвер cpuidle -> PSCI CPU_SUSPEND -> гипервизор EL2 -> прошивка EL3 \-> вендорный SMC 0xC4000021 -> гипервизор EL2
Четыре ядра собраны в две пары, и у каждой пары свой кэш L2. Ниже состояний отдельного ядра есть уровень, который действует на пару: когда оба её ядра спят, общий L2 можно перевести в удержание, и тогда он сохранит содержимое при пониженном напряжении, либо обесточить целиком и содержимое потерять. Стоковое ядро называет эти уровни l2-gdhs и l2-fpc и держит отдельно для каждого кластера — энергоэффективного и производительного.
Над этим есть уровень, действующий сразу на обе пары. Когда спят все четыре ядра, можно погасить и то, что кластеры делят между собой: L3 и когерентную шину, которая согласует кэши, и пути к памяти. У сток ядра это system-ret и system-fpc.
Всё это один и тот же вызов PSCI: в параметре состояния просто указан уровень аффинности выше отдельного ядра. Составить такой параметр mainline умеет — механизм доменов питания на то и существует, чтобы последнее засыпающее ядро могло сделать запрос от имени всего кластера. Не хватает в mainline только самих параметров для этого чипа.
Любая конфигурация, которую я пробовал, перезагружала устройство на первом же отключении кластера: и плоские кластерные состояния на каждом ядре, и иерархия доменов питания, и варианты с битом типа состояния и без него.
Диагностика после сбоя
Чтобы прочитать, что Linux успел записать перед сбросом, нужна последовательная консоль, дамп после сбоя или хотя бы способ отличить панику от сброса. Ни одного из трёх у меня не было.
Отладочный UART — та же линия на 1.8 В на разъёме USB, до которой не удалось добраться ещё в первой части, и переходника для неё у меня по-прежнему нет.
Linux можно настроить так, чтобы он сохранял последний лог в зарезервированной области памяти, но на этом устройстве эта область не переживает перезагрузку: загрузчик обнуляет оперативную память при каждом сбросе. Я проверил это и для области, которую резервирует описание самого SoC, и для той, которую резервирует плата, после намеренного сбоя и после обычного systemctl reboot. /sys/fs/pstore был пуст каждый раз.
Снаружи паника и сброс прошивкой выглядят одинаково. Устройство пропадает, возвращается, и его логи начинаются с загрузки.
Причина сброса в контроллере питания
Контроллер питания хранит регистр с причиной последнего перезапуска, и его можно прочитать через отладочный интерфейс regmap, когда телефон снова поднялся:
# grep -iE '^080a:' /sys/kernel/debug/regmap/0-00/registers080a: 02
0x080a — регистр причины тёплого сброса. По битам можно отличить программный сброс от срабатывания собственного сторожевого таймера контроллера, аппаратного сбоя и удержания кнопки питания:
0x01 SOFT 0x02 PS_HOLD 0x04 PMIC_WD 0x08 GP10x10 GP2 0x20 KPDPWR+RESIN 0x40 RESIN_N 0x80 KPDPWR_N
Каждый раз, когда устройство отказывало на кластерном состоянии, в регистре стояло 0x02, PS_HOLD: процессор сам снял сигнал, который держит плату под питанием. Это исключает зависание и исключает самостоятельный сбой гипервизора или TrustZone. Но отличить панику ядра от сброса по инициативе прошивки регистр не может — в обоих случаях он показывает одно и то же. И читать его надо на следующей же загрузке, пока его не перезаписал очередной перезапуск.
Раз узнать причину после сбоя нечем, остаётся единственный работающий подход: заранее устроить всё так, чтобы отказ либо не убивал устройство, либо наступал после известного числа попыток.
Вендорное ядро
Стоковое ядро, с которым телефон вышел с завода, входит во все эти состояния. Само оно к mainline отношения не имеет: это отдельное дерево 3.18, со своим драйвером простоя lpm-levels.c и своим описанием уровней в msm8996-pm.dtsi. Оба файла всё это время лежали у меня на диске.
Арифметика
Первое, что здесь важно: сток ядро не хранит таблицу параметров. Оно вычисляет их из двух макросов и того уровня, в который решило войти.
#define PSCI_POWER_STATE(reset) ((reset) << 30)#define PSCI_AFFINITY_LEVEL(lvl) (((lvl) & 0x3) << 24)
Каждый уровень в том дереве задаёт режим для ядра процессора, кластерный уровень задаёт ещё и режим для L2, а системный вдобавок режим для системы. Режим ядра уходит в младшие четыре бита, режим L2 в следующие четыре, режим системы в байт над ними, уровень аффинности в биты 25:24. Бит 30 берётся из флага qcom,is-reset.
Вот с флагом я и ошибался. Собираемое значение берёт is-reset у того процессорного уровня, в который происходит вход:
power_state = PSCI_POWER_STATE(cluster->cpu->levels[idx].is_reset);
а в сток дереве устройств это свойство выставлено только на кластерных и системных уровнях. Ни на одном процессорном его нет. Значит, бит 30 не выставляется никогда и ни на какой глубине, и прошивка получает вот такие значения:
0x00000004 одиночное ядро0x01000034 аффинность 1, L2 в удержание0x01000044 аффинность 1, L2 обесточен0x02002344 аффинность 2, системное удержание0x02003444 аффинность 2, системное снятие питания
Единственное состояние, объявленное в mainline, имеет значение 0x00000004 — то же самое число. А кластерные состояния, которые собирал я, были 0x41000034 и 0x41000044: значения сток ядра с битом, который вендорное ядро не выставляет никогда.
Бит 30
Со стороны Linux этот бит значим. Именно по нему mainline решает, потеряет ли ядро процессора своё содержимое:
static inline bool psci_power_state_loses_context(u32 state){const u32 mask = psci_has_ext_power_state() ?PSCI_1_0_EXT_POWER_STATE_TYPE_MASK :PSCI_0_2_POWER_STATE_TYPE_MASK;return state & mask;}
Если бит сброшен, вызов suspend уходит напрямую, без адреса возобновления и без сохранения. Если выставлен, вызов идёт через cpu_suspend() с завершающей функцией, и Linux сам всё сохраняет и восстанавливает.
Я считал, что сломаны оба варианта: с битом прошивка вызов отвергает, а без бита прошивка гасит кластер, Linux пропускает сохранение, и ядро возвращается с испорченным контекстом. Вторая догадка оказалась неверной, и это стало видно, когда я прочитал, как то же самое делает стоковое ядро:
if (state_id & PSCI_POWER_STATE_BIT)return __cpu_suspend(state_id, psci_suspend_finisher);elsereturn psci_ops.cpu_suspend(state_id, 0);
Раз msm8996 никогда не выставляет бит, сток ядро всегда идёт по второй ветке, без адреса входа и без сохранения, причём для отключения кластера тоже, не только для одиночного ядра. Состояние EL1 сохраняет и восстанавливает сама прошивка, а потом возвращает управление вызвавшему. Немодифицированный mainline для параметра без бита делает ровно то же самое, так что исправлять там было нечего, и патч, которым я заставлял Linux сохранять контекст, был удалён.
Ядро или прошивка
Правильные числа сами по себе ничего не дали, а подозревать оставалось две вещи: всё, что Linux делает на пути в кластерное состояние, и вызов прошивки в конце этого пути.
Разделить их оказалось просто, потому что менять нужно только последний шаг. Домены питания продолжали считаться, runtime PM по-прежнему усыплял домен, контроллер пробуждений взводился, уведомления кластера срабатывали. Поменял я одно: в самом конце вместо составленного кластерного значения подставил в psci_cpu_suspend_enter() заведомо безобидное состояние отдельного ядра.
Код доменов собрал 0x41000044 и полностью прошёл весь этот путь 1083 раза за 82 секунды, а устройство осталось живо.
Со стороны Linux это снимает все подозрения. Но снимает меньше, чем мне тогда казалось: подставленное значение 0x40000004 эта прошивка как раз отвергает. Значит, вызов в конце проваливался на всех 1083 итерациях, и ни одно ядро так и не уснуло. Эксперимент доказывает, что сам этот путь устройство не перезагружает, и ничего не говорит о том, способна ли прошивка выполнить отключение кластера.
Ошибки в чтении счётчиков
Два числа, на которые я смотрел, означали не то, что я думал.
Первое — rejected в статистике cpuidle. Растущий rejected при нулевом usage я прочитал как сбой широковещательного таймера: Linux якобы решает, что войти в состояние безопасно нельзя, и отступает. Всё ровно наоборот. В cpuidle.c счётчик изменяется ровно в одном месте:
} else {dev->last_residency_ns = 0;dev->states_usage[index].rejected++;}
и это ветка, в которую попадают, когда обработчик входа в состояние вернул отрицательное значение. То есть в состояние вошли, вызов сделали, и он вернулся с ошибкой. Сбой широковещания сюда не попадает вовсе: в этом случае Linux без сообщений откатывается к менее глубокому состоянию и засчитывает usage. Значит, те 16797 отказов были 16797 вызовами SMC, которые отклонила прошивка, а это куда более полезный факт.
Второе — -95. Его вернула попытка усыпления с переданным адресом возобновления, и я прочитал это как отказ прошивки принять адрес. На самом деле значение приходит из arch/arm64/kernel/suspend.c:
/* * Never gets here, unless the suspend finisher fails. * Successful cpu_suspend() should return from cpu_resume(), * returning through this code path is considered an error * If the return value is set to 0 force ret = -EOPNOTSUPP * to make sure a proper error condition is propagated */if (!ret)ret = -EOPNOTSUPP;
Это значение выставляет сам Linux, а не прошивка: завершающая функция вернула управление, хотя не должна была возвращаться вообще. То есть прошивка вызов приняла, ядро не погасила и штатно вернула управление, что и есть правильное поведение для состояния, у которого бит типа говорит, что ядро продолжает работать.
Что прошивка принимает и что отвергает
Если оба счётчика прочитаны верно, тот же набор запросов складывается в связную картину:
|
запрос |
результат |
|---|---|
|
|
принят, тысячи входов, стабильно |
|
|
возвращает успех, не гася ядро; наружу это |
|
|
устройство падает сразу |
|
|
устройство падает сразу |
Идентификатор уровня отдельного ядра прошивка трактует как ожидание и вправе на него не реагировать. А на идентификатор первого уровня аффинности она реагирует, с адресом возобновления и без него. Значит, дело было не в форме вызова: я отправлял те же значения и тем же способом, а устройство всё равно падало.
Версии, которые не подтвердились
min-child-idx
Пережить отключение кластера удалось благодаря ограничению, которого я не реализовал вовсе.
Дерево устройств mainline даёт каждому ядру процессора два состояния простоя, а код доменов питания позволяет каждому ядру выбирать между ними самостоятельно. Поэтому последнее засыпающее ядро могло собрать отключение кластера в тот момент, когда второе ядро кластера находится внутри вызова SMC к гипервизору. Это состояние, в которое прошивка перевела второе ядро сама, и ядро, которое составляет параметр, о нём ничего не знает.
Сток ядро запрещает ровно это. На каждом его кластерном уровне стоит qcom,min-child-idx = <2>: кластерный уровень разрешён, только если все дочерние ядра находятся не глубже заданного, и никогда, пока одно из них остаётся в состоянии гипервизора.
В mainline такого механизма нет, но само ограничение воспроизводится руками. Достаточно отключить сток состояние на всех ядрах в рантайме, и тогда единственным доступным останется состояние PSCI.
for c in /sys/devices/system/cpu/cpu[0-9]*/cpuidle/state1; do echo 1 > $c/disable; done
Устройство двадцать раз вошло в кластерное состояние, вернулось из каждого, продержалось около ста секунд и перезагрузилось. Все попытки до этого отказывали на первом же отключении кластера.
Бюджет
Двадцать удачных отключений, а потом отказ, говорят больше, чем отказ на первой же попытке: значит, то, что ломается, накапливается. Чтобы это измерить, я добавил на кластерный путь два управляющих параметра.
Первый — рантайм-переключатель. Устройство грузится с выключенными кластерными состояниями, а включаю я их из оболочки, и тогда фатальное состояние прерывает сессию, за которой я наблюдаю, а не загрузку, прочитать которую я не могу. Второй — бюджет: psci_cluster_budget, атомарный счётчик, который уменьшается на каждом входе с кластерным параметром и закрывает переключатель сам, когда дойдёт до нуля.
echo 5 > /sys/module/cpuidle_psci/parameters/psci_cluster_budgetecho 1 > /sys/module/cpuidle_psci/parameters/psci_allow_cluster
Если снять ограничение на частоту печати, вокруг вызова видно вот что:
cpu3 entering composed 0x1000034 (entry 1)cpu3 returned from 0x1000034 ret=2
При бюджете 1 устройство продолжает отвечать. При бюджете 5 — перезагружается.
Редистрибьютор GIC
Версия про контроллер прерываний оставалась лучшей из тех, что у меня были, и теперь появился способ проверить её на фиксированном числе отключений.
У каждого ядра процессора есть своя часть контроллера прерываний, где хранится настройка его собственных прерываний, в том числе архитектурного таймера, которым это ядро и будят. При выходе из простоя mainline восстанавливает регистры интерфейса процессора и ничего больше, а собственную часть настраивает функция, которую вызывают только при горячем подключении ядер. Ядро, вернувшееся из настоящего снятия питания, обнаружило бы свой таймер выключенным.
Так что я стал сохранять эту часть перед входом и восстанавливать после: регистр групп, приоритеты, настройку и набор разрешений. Разрешения — последними, чтобы прерывания не пошли раньше, чем всё остальное встанет на место.
При бюджете 5 устройство продолжило работать. Все пять входов в логе, все пять возвратов, устройство отвечает там, где любая прежняя сборка перезагружалась, но через несколько секунд оно всё-таки перезагрузилось.
Причина этой задержки в том, что уведомления кластера приходят только тому ядру, которое составило параметр, тогда как снятие питания задевает оба ядра кластера, и второе не восстанавливалось вовсе. Перенос сохранения и восстановления в поядерные уведомления, которые приходят обоим, сделал только хуже, потому что восстановление сначала снимает все разрешения и лишь потом выставляет сохранённые:
writel_relaxed(~0, rbase + GICR_ICENABLER0);
На входе в кластерное состояние это безобидно. На каждом обычном выходе из простоя, тысячи раз в секунду, это на мгновение выключало ядру его собственный таймер.
Окончательно версию опровергло стоковое ядро. Оно эту часть контроллера нигде не сохраняет и не восстанавливает, а когда я загрузился в рекавери, где оно и работает, счётчики показали несколько сотен уже выполненных отключений кластера. И это на системе, которая просто лежала и ждала. Если бы железо теряло здесь настройку прерываний, стоковое ядро не работало бы вообще. Мой патч чинил то, что не было сломано, и я его удалил.
Регистр GICR_WAKER
Здесь есть и настоящее задокументированное отличие, специфичное для msm8996.
Есть регистр, который вводит часть контроллера прерываний конкретного ядра в режим пониженного потребления и выводит обратно. Порядок такой: записал запрос, дождался подтверждения. Сток ядро делает это на каждом входе в простой и выходе из него. В mainline msm8996 единственный SoC в дереве с отдельным исключением, из-за которого выполняющая это функция ничего не делает. Коммит, добавивший исключение, сообщает, что плата DB820c перезагружается при обращении к регистру, и связывает ограничение с гипервизором.
Я добавил рантайм-переключатель, обходящий исключение и делающий эту запись. На этой плате запись ничего не перезагружает, подтверждение приходит, а отключение кластера по-прежнему приводит к перезагрузке.
Только один кластер
Перед входом в свой самый глубокий системный уровень стоковое ядро включает отдельный контроллер, который потом и будит чип; в дереве устройств этот уровень помечен специальным свойством. В mainline такого механизма нет. Если бы отказ упирался в него, он проявлялся бы только тогда, когда оба кластера погашены одновременно, а этому в опытах с бюджетом ничто не мешало.
Так что я добавил переключатель, разрешающий не больше одного отключённого кластера одновременно, и повторил прогон. Он дошёл до того же счёта и до того же сброса.
Array Power Mux
У сток ядра есть драйвер, которого в mainline нет ни для одного SoC: Array Power Mux, переключающий массивы памяти процессора и L2 между двумя шинами питания. Он переключает их, когда заданное напряжение процессора пересекает порог, потому что ниже этого напряжения массивы уже не держат своё содержимое на основной шине.
Как раз в удержании L2 и обязан сохранить содержимое при пониженном напряжении. Если массивы остались на основной шине и переключать их некому, то выживет устройство или нет, зависит от напряжения в этот момент, то есть от рабочей точки. Заодно объясняется и то, почему отказ вероятностный: одно отключение обычно проходит, пять обычно нет.
А главное, отсюда следовал тест: зафиксировать максимальную частоту cpu, чтобы напряжение оставалось выше порога, и повторить прогон.
Фиксированные частоты
|
частота |
результат |
|---|---|
|
максимум (2188800 / 2342400) |
1120 отключений, ни одного отказа |
|
652800 |
перезагрузка |
|
307200 (минимум) |
перезагрузка |
Такого разброса за всё расследование не давало больше ничто. До этого любая сборка отказывала на двадцати отключениях, что бы я ни менял, а на максимальной частоте та же сборка выполнила 1120.
И это не Array Power Mux. В дереве устройств нет ни opp-microvolt, ни cpu-supply, а в mainline вообще нет драйвера регуляторов для процессорных шин этого SoC: напряжение процессора он не меняет никогда и ни на какой частоте. Версия, породившая тест, не может объяснить его результат, потому что механизма, через который она действует, в mainline нет.
Тактирование и cpufreq
Ещё две версии я проверил так же быстро, и обе отпали.
Драйвер тактирования процессора переключает выход ниже 600 МГц, беря тактовый сигнал с ветви с делителем, а не напрямую с PLL. Настоящая смена режима, ровно на границе между частотой, на которой всё работает, и той, на которой падает. Вот только 652800 выше этой границы, и на 652800 оно падает.
Вторая версия — сами переходы по частоте: отключение, попавшее на изменение напряжения или тактового сигнала, проявлялось бы именно так, от случая к случаю. Я зафиксировал минимум и максимум на одном значении, 1593600, чтобы cpufreq было нечего делать, и оно всё равно упало.
Запас по времени
Оставалось еще одна объяснение: не пропущено вообще ничего, а какая-то последовательность на пути отключения и восстановления успевает завершиться на высокой частоте и не успевает на низкой.
Это по крайней мере можно было проверить, не отключая ни одного кластера, и от такой проверки устройство не падало. Я снял состояние всех тактовых сигналов на рабочей частоте и на той, где происходил отказ, и свёл их по именам:
|
сигнал |
максимум (работает) |
1593600 (отказ) |
|---|---|---|
|
|
2188.8 МГц |
1593.6 МГц |
|
|
2342.4 |
1593.6 |
|
|
1593.6 |
1382.4 |
|
альтернативные PLL, |
без изменений |
без изменений |
Смены режима нет нигде. Мультиплексоры на обеих настройках стоят на той же ветви, частота шины не пересекает собственный порог, альтернативные PLL и вспомогательная частота не двигаются. Меняются только частоты, и все в одну сторону.
То есть переменной действительно была скорость, а не конфигурация, и вопрос звучал не «что mainline не программирует», а «что не успевает». Чтением исходников на это не ответить. Ответить могло устройство, которое делает всё это правильно.
TWRP
TWRP — небольшая система на Android, которой пользуются для прошивки и восстановления, и работает она на сток ядре. Ядро, которое делает всё правильно, и root по adb в наличии. До сих пор я читал его как исходники. А это ещё и работающая система, и её idle-драйвер сам считает, сколько раз и в какое состояние заходили ядра.
lpm_stats
adb shell mount -t debugfs none /sys/kernel/debugadb shell cat /sys/kernel/debug/lpm_stats/stats
Данные за несколько минут в рекавери:
|
уровень |
производительный кластер |
энергоэффективный |
всего |
|---|---|---|---|
|
|
562 |
5 |
567 |
|
|
84 |
50 |
134 |
|
|
— |
— |
1 / 2 |
|
поядерный |
7522 / 3012 |
1447 / 958 |
12939 |
|
поядерный |
365 / 206 |
650 / 63 |
1284 |
567 кластерных удержаний и 134 полных снятия питания с L2 — на том же самом телефоне и с той же прошивкой, что у меня. Значит, сломано было на моей стороне, а не в TrustZone или гипервизоре.
Две нижние строки заодно опровергают ещё одно моё допущение. Два поядерных состояния сосуществуют без конфликта: в состояние гипервизора входят примерно вдесятеро чаще, чем в состояние PSCI, и min-child-idx этому не мешает, потому что он блокирует только кластерный уровень, пока ядро находится не в том состоянии.
Фиксированные частоты на сток ядре
Меня интересовала зависимость от частоты: это было последнее оставшееся объяснение, и проверять его надо было на ядре, которое работает.
adb shell 'for c in /sys/devices/system/cpu/cpu[0-9]/cpufreq; do cat $c/scaling_max_freq > $c/scaling_min_freq; done'
Прочитать данные, подождать, прочитать снова. В правой колонке — что на тех же рабочих частотах делало моё ядро:
|
частота |
вендорное ядро |
моё |
|---|---|---|
|
307200 (минимум) |
+1193 gdhs, +1111 fpc за 30 с |
перезагрузка |
|
1593600 |
~650 отключений за 25 с |
перезагрузка |
|
максимум |
+251/+535 perf, +403/+665 pwr, |
1120 отключений, работает |
Больше двух тысяч отключений кластера за полминуты на самой низкой частоте, половина из них с полным обесточиванием L2 — и это ровно та частота, на которой моё ядро падало через восемь секунд.
В последней строке есть число, которого я не ожидал. system-ret — это когда оба кластера погашены одновременно. Я считал такое состояние экзотической конечной целью, а сток ядро входит в него больше двадцати раз в секунду, как в обычный режим.
Версия про запас по времени теперь отпала. Если бы какая-то последовательность на пути отключения кластера не успевала завершиться на низких частотах, вендорное ядро отказывало бы хуже всего там, где оно медленнее всего. А оно, наоборот, делает больше всего отключений на самой низкой частоте.
Тактирование и напряжения
Ещё два замера, пока под рукой было работающее ядро.
Я снял те же тактовые сигналы и сравнил с показаниями своего ядра. На 1593600 тактирование кластеров совпадает частота в частоту. На самой низкой рабочей точке вендорное ядро держит частоту когерентной шины на 192 МГц — всемеро медленнее, чем было у меня в момент отказа, — и на этой скорости отключает кластеры непрерывно.
Процессорные шины питания, из sysfs регуляторов, в тех же трёх точках:
307200 1593600 максимумkryo0 798681 → 798681 → 798681 мкВkryo1 745089 → 709361 → 834409 мкВkryo0-retention 490527 → 490527 → 490527 мкВkryo1-retention 490527 → 490527 → 490527 мкВ
Первые две строки закрывают вопрос о напряжении. Одна шина не двигается вообще, вторая меняется в пределах примерно 125 мВ, причём немонотонно, так что «mainline тратит лишнее, потому что не снижает напряжение процессора» тоже неверно.
Последние две — настоящий пробел, не связанный с отказом. Сток ядро задаёт для каждой процессорной шины явное напряжение удержания, около 0.49 В: тот уровень, до которого шина опускается, пока кластер в удержании, ровно то состояние, в которое я и пытался войти. В mainline драйвера процессорных шин для этого SoC нет, поэтому это напряжение не задаёт никто, и железо остаётся с тем, что оставил загрузчик. От этого зависит, сколько эти состояния реально экономят. Но причина отказа была не в этом: устройство перезагружалось на любой глубине, включая те, где питание снимается, а не удерживается.
Железо отключает кластеры на любой рабочей точке, причём с шиной, работающей медленнее, чем когда-либо работала моя. Array Power Mux, ветвь тактирования и запас по времени — всем трём нужно было, чтобы чип этого не умел, а он умеет.
Значит, измеренная мной зависимость от частоты не была свойством чипа. Она была свойством моего ядра, а объяснял я её чипом.
Тайминги
Я пошёл перечитывать собственные правки — и нашёл в дереве устройств странные значения, которые сам туда и добавил.
Ещё раньше, разбираясь с другой проблемой, я занизил тайминги у обоих процессорных состояний: глубокое состояние не проходило по ограничению на задержку, и мне нужно было заставить говернор его использовать. Задержки входа и выхода я опустил до 20 и 25 мкс, минимальное время в состоянии — до 150 и 250. Для той задачи это сработало, я пошёл дальше и обратно их так и не вернул. В коммит эта правка так и не попала — потому и продержалась так долго: она оставалась в рабочем дереве, из которого собиралось каждое тестовое ядро, и ни в одном diff я её не видел.
Арифметика говернора
По этим трём числам Linux и выбирает состояние, других данных у него нет. Задержки входа и выхода — стоимость перехода. Минимальное время — запрет: не входить, если ядро не проведёт в простое хотя бы этот срок. Ниже этой границы переход обходится дороже, чем состояние успевает сэкономить.
Кластерное состояние собирается не там, где его выбирают. Говернор кластерный уровень не выбирает никогда — он выбирает процессорный, а последнее засыпающее ядро добавляет к нему параметр кластера. Поэтому решает всё минимальное время процессорного состояния: именно оно определяет, дойдёт ли дело до полного отключения кластера.
Я выставил там 250 мкс, а должно было быть 800. Через этот порог проходит полное отключение кластера — то самое, в которое вендорное ядро не идёт при простое короче 5000 мкс: столько нужно, чтобы погасить оба ядра, сбросить общий кэш, снять с него питание и поднять всё обратно. Говернор отправлял туда пару ядер при простоях в двадцать раз короче, чем нужно.
Это ровно та поломка, которую описывает патч для msm8998: тайминги заданы неверно, говернор входит в состояние слишком часто, и SoC перезагружается, ничего не оставляя в логах. Патч этот я читал и сам же ссылался на него как на повод быть аккуратнее с таймингами. А потом собрал ровно то же своими руками и объяснял контроллерами прерываний и шинами питания.
Возвращённые значения
CPU_SLEEP_0 (SMC гипервизора) 40 / 80 / 300 CPU_SLEEP_1 (снятие питания, PSCI) 80 / 130 / 800 CLUSTER_SLEEP_0 (удержание L2) 90 / 180 / 1000 CLUSTER_SLEEP_1 (снятие питания) 700 /1000 / 5000 вход / выход / минимальное время, мкс
У состояния PSCI вход и выход перепутаны местами. По тому правилу пересчёта, что я приводил выше, 80 мкс вендорного уровня — это цена пробуждения, и стоять они должны в exit-latency-us, а в entry-latency-us — 210 − 80 = 130. Ровно столько там и стояло в mainline до моих правок. На результат ниже это не повлияло: говернор в своей модели складывает обе задержки, а решало здесь минимальное время.
Повторный эксперимент
Я повторил эксперимент с частотами, только на полной глубине и без ограничения по бюджету: зафиксировать частоту, разрешить отключение кластеров, спуститься на ступень ниже — и так до минимума.
|
частота |
результат |
накопленных входов |
|---|---|---|
|
максимум (2188800 / 2342400) |
работает |
4471 |
|
1593600 |
работает |
11372 |
|
1132800 |
работает |
20575 |
|
844800 |
работает |
27047 |
|
614400 |
работает |
33494 |
|
307200 (минимум) |
работает |
39149 |
Тридцать девять тысяч полных отключений кластера, вплоть до самой низкой частоты. Прежние сборки отказывали на 1593600 все до одной, и большинство — не дойдя до двадцати отключений.
Зависимость от частоты объясняется сама, стоит вернуть на место минимальное время. На верхних частотах телефон справляется с работой быстро, промежутки между пробуждениями получаются длинными, и даже заниженный порог перекрывает настоящий простой. Чем ниже частота, тем дольше та же работа и тем короче промежутки — и порог, заниженный в двадцать раз, начинает пропускать отключения кластера, которым не хватает времени завершиться. Частота ничего не определяла. Она просто показывала, насколько длинными получались простои.
Прежние значения
Те значения, которые стояли до всех моих правок, тоже были нормальными, но устройство всё равно перезагружалось. Просто тогда оно несло на себе сразу несколько изменений: иерархию доменов питания, контроллер пробуждений с родительским доменом над кластерами, параметры состояний с выставленным битом 30 и принудительное сохранение контекста. Всё это убиралось по одному в предыдущих разделах, каждое по своим причинам, и ни одно поодиночке не сделало устройство стабильным. Минимальное время оказалось последней и единственной причиной.
Драйвер
С исправленными таймингами отключение кластера заработало и через домены питания — со всеми накладными расходами, которые они за собой тянут. А вендорный драйвер к тому моменту я изучил довольно хорошо, чтобы понимать, как мало на самом деле для этого нужно.
Почему не genpd
Домены питания — штатный механизм mainline для этой самой задачи: кто ушёл последним. На железе, которое под них и проектировали, он правильный. Каждому ядру свой домен, домены вложены друг в друга, засыпающее ядро укладывает спать свой, а на последнем поверх складывается состояние родителя, и всё это уходит в прошивку.
Домен засыпает через runtime PM, а тот обходит иерархию, берёт блокировки и вызывает обработчики каждого домена — и всё на входе в простой, с выключенными прерываниями, на ядре, которое вот-вот остановится. Слишком много работы ради одного вопроса: последнее ли это работающее ядро в кластере.
Сток ядро отвечает на него спинлоком и двумя масками ядер на кластер. Я написал: drivers/cpuidle/cpuidle-msm8996.c без genpd.
Последний выходящий
У каждого кластера есть маска его ядер и маска тех из них, что сейчас в простое. Ядро отмечает себя при входе, и если после этого маски совпали, значит оно выходит последним и добавляет параметр кластера к собственному запросу. А если простаивают все кластеры, то вместо кластерного добавляет системный:
raw_spin_lock(&msm8996_topo_lock);cpumask_set_cpu(dev->cpu, &cluster->in_sync);if (msm8996_cluster_depth && cpumask_equal(&cluster->in_sync, &cluster->child_cpus) && !msm8996_ipi_pending(&cluster->child_cpus)) {if (msm8996_system_depth && msm8996_all_clusters_idle()) {state |= (msm8996_system_depth >= 2) ? msm8996_system_fpc : msm8996_system_ret;system = true;} else {state |= (msm8996_cluster_depth >= 2) ? msm8996_cluster_pc : msm8996_cluster_gdhs;}composed = true;}raw_spin_unlock(&msm8996_topo_lock);if (composed)cpu_cluster_pm_enter();ret = psci_cpu_suspend_enter(state) ? -1 : idx;if (composed)cpu_cluster_pm_exit();
Блокировка одна на всю топологию, а не по штуке на кластер: чтобы понять, что простаивают все кластеры, читать их надо разом, одним согласованным снимком. Стоковое ядро добивается того же — рекурсивно заходит в блокировку родителя.
msm8996_ipi_pending — защита, дословно перенесённая из сток ядра: флаг на каждое процессорное ядро, который выставляется, когда для него подняли межпроцессорное прерывание, и снимается, когда ядро его приняло. Кластер не должен отключиться с ещё не обработанным прерыванием. Из-за этого флага приходится править arch/arm64/kernel/smp.c — двадцать строк архитектурного кода ради одного SoC — это много. Он здесь только потому, что есть у сток ядра.
Принадлежность к кластеру берётся из поля аффинности в регистре идентификации каждого ядра, а не из дерева устройств:
cpuidle-msm8996: cluster 0: cpus 0-1cpuidle-msm8996: cluster 1: cpus 2-3
Это совпадает с делением сток ядра на энергоэффективный и производительный кластеры.
Подключается всё это тремя строками в общем драйвере PSCI: если при настройке ядра домен питания не нашёлся, самое глубокое состояние уходит моему драйверу.
if (!data->dev)msm8996_cpuidle_attach(drv, state_count, psci_states[state_count - 1]);
Итоговое дерево устройств
Сами состояния я переставил местами. Уровень гипервизора стал первым из двух процессорных, а не последним, потому что код доменов подменяет обработчик входа у самого глубокого состояния. Пока там стоял вендорный SMC, вызов без аргументов заменялся обычным suspend с параметром-заглушкой, который прошивка, естественно, отвергала — сотни тысяч раз. Состояние PSCI ушло в конец и вернуло себе local-timer-stop. Когда состояние становится настоящим снятием питания вместе с кластерным уровнем, таймер ядра останавливается, и Linux нужно об этом знать. А кластерные и системные параметры переехали из поядерного списка в отдельный узел: напрямую их не выбирает ни одно ядро.
domain-idle-states {CLUSTER_SLEEP_0: cluster-sleep-0 {arm,psci-suspend-param = <0x01000034>; /* удержание L2 */};CLUSTER_SLEEP_1: cluster-sleep-1 {arm,psci-suspend-param = <0x01000044>; /* L2 обесточен */};};
Проверка уровней по одному
Каждый уровень я включал из рабочей системы, по одному, оставив ярусы выключенными на загрузке:
-
кластерное снятие питания,
0x01000044, общий L2 теряет питание: 3690 входов, работает -
системное удержание,
0x02002344, оба кластера погашены, L3 в удержании: 712 входов, работает -
системное снятие питания,
0x02003444: 2079 входов за десять секунд, работает
Последнее — самое глубокое состояние, какое есть у стокового ядра, и mainline на msm8996 теперь поддерживает каждую ступень той же лестницы: ядро, удержание кластера, снятие питания с кластера, системное удержание, системное снятие питания.
Самый глубокий из этих уровней работает ещё и без того, что сток ядро для него считает обязательным. Системное снятие питания — единственный уровень, помеченный в стоковом дереве как qcom,notify-rpm. Перед входом сток ядро сообщает менеджеру ресурсов питания, что процессор уходит в сон, и взводит всегда включённый контроллер пробуждений ближайшим срабатыванием таймера: когда всё остальное обесточено, будить чип больше некому. У системного удержания такой пометки нет, и там этого не делает даже сток ядро. А в mainline такого и не сделаешь: драйвер контроллера пробуждений выставляет себя только как домен питания, а вызова, который сообщал бы менеджеру ресурсов о переходе в сон, нет вовсе.
Показания на живом сервере
Ядро, на котором сейчас работает сервер, держит оба яруса включёнными по умолчанию. Его счётчики, снятые с живого устройства:
cluster_entries 2150931system_entries 3534083cpu0 state2 usage=2633034 time=38688859635 us
Два миллиона отключений кластера и три с половиной миллиона входов с обоими погашенными кластерами, а cpu0 провёл 38 688 из 40 800 секунд работы сервера в самом глубоком состоянии, какое у него есть, — около 95 % времени.
Температура сдвинулась ещё раз, меньше, чем в первый, но больше, чем я ожидал. Один только поядерный простой опустил среднесуточную до 46.0–47.9 °C. С работающими кластерными и системными уровнями она держится на 43.3 °C, а самые холодные замеры доходят до 40.3 — то есть на три градуса ниже всего, что телефон показывал раньше, на той же полке, в той же комнате, за той же работой.
Три градуса — это и есть то, во что обходились общие блоки: два L2, L3 и когерентная шина оставались запитаны, пока четыре простаивающих ядра к ним не обращались. Перевести это в ватты не получится, пока телефон на зарядке: счётчик заряда такого тока не видит, а прогоны без кабеля я снимал ещё до этой работы. Так что цифра пока только в градусах.
Путь простоя на этом SoC теперь делает всё то же, что и сток.
В итоге
Телефон всё это время делал свою работу. Все сервисы работали, ничего не падало, в логах ни одной ошибки, и при этом большую часть своей мощности он тратил впустую. Причиной была не чья-то ошибка в коде. Причин было две. Драйвер, намеренно выключенный для целого семейства SoC, потому что он вешал устройства. И набор параметров состояний, выведенных на бумаге и ни разу не проверенных на том железе, которое они описывают. И то и другое — обычная ситуация для старого устройства на mainline-ядре.
То, что телефон загружается, поднимает WiFi и держит сеть, ещё не значит, что система полностью готова. Иногда не работает то, что не ожидаешь, что будет сломано. В моем случае это приводило к сильному нагреву.
Ядро: github.com/arttttt/linux-op3t
Скрипты сборки и прошивки: github.com/arttttt/oneplus3t-pmos-server
Спасибо!
ссылка на оригинал статьи https://habr.com/ru/articles/1067224/