Разбор memorykiller механизма в Android
Все, кто хоть немного сталкивались с операционной системой Android – знают что там нет как такового механизма закрытия приложений.
Приложение сворачивается и висит в памяти, пока есть доступные системные ресурсы. Когда со свободной памятью становится туго, то в ядре запускается memorykiller, играет в считалочку и прибивает самый «несчастливый» процесс. При этом система руководствуется некоторыми определенными критериями, такими как «ценность процесса», «упитанность», «критичность немедленного освобождения памяти» и т.д. Вроде всё ясно и понятно, но что конкретно конкретно стоит за этим механизмом непонятно совершенно. Лично я в сети так и не нашел внятного и подробного описания алгоритма работы меморикиллера и значения его настроек. Чтож, попробуем разобраться самостоятельно.
MemoryKiller – это модуль ядра, разработанный Google специально для Андройда, он является коллегой стандартного линуксового OOM Killer и использует некоторые его функции.
Конфигурация.
/sys/module/lowmemorykiller/parameters/debug_level
/sys/module/lowmemorykiller/parameters/cost
/sys/module/lowmemorykiller/parameters/adj
/sys/module/lowmemorykiller/parameters/minfree
- debug_level – Уровень отладки, от 0 до 5-ти. По умолчанию 2 (пишет только об убитых процессах).
- cost – Этот параметр не представляет особого интереса.
Lowmemorykiller вызывается через механизм «утилизации страниц сокращаемых кешей» (chrink_caches() ) и соответственно память, занятая приложениями, рассматривается как некий абстрактный кеш – данные в котором могут быть сброшены при необходимости. Но чем выше цена этих данных тем более неохотно система будет пытаться их очистить. Грубо говоря чем выше это значение, тем реже будет вызываться наш убивец, но вызов memorykiller еще не гарантирует того что какой-либо процесс будет убит. Значение по умолчанию 32 (для обычных кешей цена = 2) - adj – список минимальных приоритетов (важности) приложении, выбранный приоритет зависит от следующего параметра.
- minfree – список из значений минимально доступной памяти (значения указаны в страницах по 4кб), в зависимости от которой выбирается минимальный приоритет.
- Списки adj и minfree могут содержать до 6 элементов, значения слева направо должны увеличиваться. Если количество элементов в списках не равно, то лишние элементы в бОльшем списке игнорируются.
В наследство от OOM Killer каждое приложение имеет параметр oom_adj (/proc/$PID/oom_adj), который характеризует важность приложения и может принимать значения от -17 до 15. В случае значения -17 процесс не будет рассматриваться как кандидатура на уничтожение. Значение 15 имеют самые «ненужные» процессы.
Оценка доступной памяти.
Доступная память оценивается по 2м параметрам – количество не занятых страниц (NR_FREE_PAGES) и количество страниц, занятых под дисковый кеш (NR_FILE_PAGES). Причем оценка производится не совместно. Т.е. система ищет минимальное значение из minfree, которое будет больше и свободных страниц и страниц занятых под кеш, а не их суммы(!!!).
Если такое значение найдено, то из списка adj выбирается соответствующее значение (по положению в списке). Далее будут рассматриваться кандидаты на вылет со значением oom_adj >= adj.
Текущие значения NR_FREE_PAGES и NR_FILE_PAGES можно посмотреть в системе тут adb shell cat /proc/zoneinfo :
Node 0, zone Normal
<<.......>>
nr_free_pages 3936
nr_inactive_anon 8217
nr_active_anon 7469
nr_inactive_file 942
nr_active_file 4196
nr_unevictable 71
nr_mlock 0
nr_anon_pages 15604
nr_mapped 5238
nr_file_pages 5293
<<.....>>
В поисках жертвы.
После определения необходимости чисток и минимальных критериев система начинает искать подходящую жертву по следующим критериям (критерии идут в порядке важности):
- Учитываются только процессы с приоритетом oom_adj >= adj
- При прочих равных условиях выбирается процесс с максимальным oom_adj (минимальной ценностью)
- Среди процессов с одинаковой «ценностью» выбирается процесс занимающий больше памяти (оценка производится функцией get_mm_rss())
- Если процесс попадающий под все эти условия найден, то ему отсылается прощальный SIGKILL. За один вызов lowmemorykiller убивается только 1 процесс.
Примечания.
За назначение «ценности» приложения (параметра oom_adj) по всей видимости отвечает фреймворк, а его алгоритм раздачи «плюшек» уже совсем другая история, которая тем не менее так же любопытна.
Повторюсь, lowmemorykiller вызывается в механизме утилизации страничных кадров, когда система пытается высвободить занятые ресурсы, под новые запросы памяти. В системе он регистрируется как callback-функция усечения кеша ( register_shrinker() ), за частоту вызова отвечает параметр cost, устанавливающий цену нашего «мнимого кеша».
Оценка размера приложения на основании get_mm_rss(), мне кажется не совсем верной. Т.к. учитываются и общие, с другими процессами, страницы памяти. Которые не будут освобождены при уничтожении процесса. На мой взгляд тут будет целесообразнее учитывать только приватные страницы, эксклюзивно принадлежащие процессу.
LowMemoryKiller не учитывает доступные страницы в файле подкачки. Стандартный Android не использует swap (частично из-за этого и был разработан memorykiller), а вот у не оффициальных сборок это может вызвать некоторые проблемы (особенно при использовании compcache).

Очень интересная статья. Как раз сейчас пытаюсь исправить слишком агрессивный (такой стал после обновления Android 2.1 -> 2.2) memorykiller. Он прибивает висящие в фоне и нужные процессы, хотя памяти достаточно (в minfree 1536, 2048, 4096, 5120, 5632, 6144 а различные мониторы и менеджеры процессов на телефоне показывают свободной памяти около 100 мб, и такой размер система постоянно держит свободным). Такое ощущение что неправильно определяется свободной памяти. Интересно, а этот модуль сам определяет,что мало памяти и надо какие то приложения прибивать, или другой модуль определяет что памяти мало и запускает memorykiller?
Ну я бы не стал очень доверять показаниям менеджеров процессов, для меня большая загадка откуда они берут эти цифры.
Советую посмотреть доступную память там, где её смотрит сам мемори киллер (adb shell cat /proc/zoneinfo) или на худой конец adb shell free .
Еще хочу отметить что 6144 страницы это 24 мегабайта.
Какое устройство и какая прошивка у вас? Используется ли свап?
P.S. с управлением процессами во фреймворке я пока не разобрался до конца, там всё куда запутанней. Но насколько я понял фреймворк тоже имеет полномочия завершать приложения не дожидаясь пока это сделает мемори киллер, хотя не уверен до конца.
Сейчас менеджер процессов в телефоне показывает свободной 107 Мб.
часть из cat /proc/zoneinfo :
nr_free_pages 6739
nr_file_pages 21467
в сумме 28206 или 110 Мб – что как раз соответствует показаниям менеджера процессов.
У меня HTC Legend, прошивка – модифицированная. Свапа нет. Ядро тоже модифицированное, но пробовал стоковое от HTC – та же фигня. Еще пробовал взять lowmemorykiller.c от ядра 2.1 версии и собрать с ним, но эффекта это не дало. Или я неправильно адаптировал lowmemorykiller от 2.1 – с языком С вообще не знаком, не программист я, раньше только баловался на дельфях
Кажется я разобрался почему система прибивает системные приложения в фоне. Смотрел в logcat момент когда происходит убивание процесса системой, и в этот момент в логе писалось что то вроде ActivityManager: No longer want %имя приложения%.
Поискал в исходниках ядра фразу «No longer want» – не нашел.
Такая фраза есть в исходниках фреймворка, файл ActivityManagerService.java причем там есть переменная:
318 // The maximum number of hidden processes we will keep around before
319 // killing them; this is just a control to not let us go too crazy with
320 // keeping around processes on devices with large amounts of RAM.
321 static final int MAX_HIDDEN_APPS = 15;
Еще сравнил функционал ActivityManagerService.java от Froyo и от Eclair – в Eclair такого кода вообще нет (когда фреймворк убивает процесс).
Попробую разобрать фреймфорк и отредактировать параметр MAX_HIDDEN_APPS, но сомневаюсь что получится… Я так понимаю у HTC он модифицированный, и собрать из исходников не получится.
P.S. ссылка на ActivityManagerService.java из 2.2 – http://android.git.kernel.org/?p=platform/frameworks/base.git;a=blob;f=services/java/com/android/server/am/ActivityManagerService.java;h=0c11940b1b83f34b0d3a29e19c9ffa373b06440b;hb=refs/heads/froyo-release
из 2.1 – http://android.git.kernel.org/?p=platform/frameworks/base.git;a=blob;f=services/java/com/android/server/am/ActivityManagerService.java;h=20ccfdcbaaf0b13940880ba1ecfc72926eaa79ef;hb=refs/heads/eclair-release
Посмотрел, в ява части конечно всё довольно грустно….
Просто пропатчить фреймворк вам не удасться, придётся перебирать все пакеты фреймоврка и приложений – это долго и муторно.
Мне не совсем понятно чем не устраивает лимит на 15приложений которые висят и никак не используются? Как негативно это влияет на ходовые качества?
То что пересобирать все пакеты придется это я понимаю, только не понял их подписывать еще надо или нет, еще погуглю на эту тему. Кстати что меня удивило – название переменных в декомпилированном фреймворке (smali файлах) остались те же самые как в исходниках. Есть еще мысль собрать services.jar в который входит ActivityManagerService.java, незнаю пока как.
А этот лимит в 15 приложений это вообще плохая затея. Потому что в список этих приложений входят сервисы, системные процессы, контент-провайдеры и пр: звонилка, контакты, сообщения, оболчка Sense и т.д.) В результате даже без пользовательских приложений набирается даже больше 15-ти и получаем такую фигню (в logcat):
I/ActivityManager( 155): No longer want com.android.vending (pid 13822): hidden #16
W/ActivityManager( 155): Scheduling restart of crashed service com.android.vending/.licensing.LicensingService in 14944ms
I/ActivityManager( 155): No longer want com.maxmpz.audioplayer (pid 13813): hidden #16
W/ActivityManager( 155): Scheduling restart of crashed service com.maxmpz.audioplayer/.player.HeadsetMicroService in 14879ms
Система убивает сервисы, которые затем сразу же перезапускаются, и этот процесс похоже бесконечен.
Ну и с пользовательской точки зрения неудобства: например в android 2.1 открытые в браузере вкладки и их содержимое хранилось в памяти ну очень долго, если не бесконечно, бывали случаи когда я через неделю открывал браузер и там всё было ровно на том месте где я в прошлый раз остановился. Сейчас на 2.2 через час-два браузер всё «забывает», да и не только он, стандартный сенсовский плеер тоже быстро забывал позицию воспроизведения, меня это особенно раздражало – слушаю длинные сеты. Заменил его на poweramp – он сохраняет позицию в своих настройках, и даже после убийства процесса воспроизводит с прошлой позиции.
Мессенджеры опять же, если зайти в сеть, и свернуть приложение – через некоторое время процесс умирает, но не всегда.