Обзор особенностей ядра Андроида

    “А я… карбюратор промываю!”
    Анекдот


    Введение

    В детском садике мы с единомышленниками препарировали кузнечиков в надежде разобраться в их строении. В школе распаивали радиоприёмник “Россия”. В институте дошла очередь до автомобилей, гайки которых были многократно переставлены. Интересы поменялись, но желание “разбирать” иногда просыпается, и сегодня оно направлено на Андроид.

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



    Общая архитектура

    Архитектуру Андроида можно схематично изобразить так:



    Оригинальная схема не содержит информации об особенностях ядра и не акцентирует внимание на Binder-е и системных сервисах. А ведь Binder является “клеем”, связывающим все компоненты системы.

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

    Ядро

    Ядро — центральная часть любого дистрибутива, называемого “Линукс”. Несмотря на доступность “чистого” ядра, многие разработчики (Ubuntu, Fedora, SuSe и т.д.) добавляют к нему свои патчи перед включением в дистрибутив. Андроид идёт той же дорогой, только ценой потери прямой совместимости: на “чистом” ядре он не заведётся. В настоящее время есть намерения включить “андроидизмы” в основную версию ядра, в 2011 году Линус Торвальдс давал на этот процесс 4-5 лет. Успех уже достигнут в рамках включения механизма wakelocks в версии ядра 3.5.

    Рассмотрим “андроидизмы” более подробно.

    Wakelocks

    История данного механизма эпична, потянет на сборник статей “Путь wakelock-ов в Линукс”: их обсуждение заняло порядка 2000 писем в рассылке LKML.

    Настольные компьютеры и ноутбуки имеют устоявшуюся систему энергорежимов (у x86 процессоров таковых несколько): компьютер работает “на полных оборотах”, когда что-то делается, и уходит в энергоэффективный режим, когда система простаивает. Уход в “спящий” режим происходит либо после довольно длительного бездействия, либо вручную, например, при закрытии крышки ноутбука.

    На телефонах требовался другой механизм: основное состояние системы — “спячка”, выход из него осуществляется только в случаях необходимости. Таким образом, система может уснуть, даже если какое-то приложение проявляет активность. В Андроиде был реализован механизм wakelock-ов: если приложение (или драйвер) выполняет что-то важное, что должно дойти до логического завершения, оно “захватывает” wakelock, предотвращая засыпание устройства.

    Попытки портирования механизма wakelock-ов в ядро вызвали сопротивление многих разработчиков. Программисты Андроида решали конкретную проблему, решением которой стал определённый механизм. Условия задачи были весьма узки. Целевая платформа — ARM, поэтому использовались её особенности: ARM-процессоры изначально предполагают частую смену режимов работы “сна” и “бодрствования”, в отличие от x86. В Андроиде приложения общаются с системой управления питанием через PowerManager, а что делать клиентским Линукс-приложениям?

    Разработчики Андроида даже не пытались найти общее решение “на будущее”, которое потом без проблем бы вливалось в основное ядро, не консультировались по этой проблеме с сообществом ядра Линукс. Можно ли их за это винить? Несмотря на все проблемы и обсуждения, как упоминалось выше, в ядре появилось API с идентичной функциональностью autosleep.

    Программистам приложений под Андроид довольно редко приходится сталкиваться с wakelock-ами, так как платформа и драйверы обрабатывают возложенные на них обязательства с учётом “спящего” режима. Тем не менее, вмешаться в этот процесс поможет знакомый PowerManager. Кстати, автору приходит в голову только один сценарий: не дать телефону уснуть при запуске сервиса из BroadcastReceiver-а, что решается вспомогательным классом из Android Support Library WakefulBroadcastReceiver.

    Low Memory Killer

    В стандартном ядре Линукса есть Out of Memory Killer, который на основании параметра badness определяет убиваемый процесс:

    badness_for_task = total_vm_for_task / (sqrt(cpu_time_in_seconds) *
    sqrt(sqrt(cpu_time_in_minutes)))


    Таким образом, чем больше процесс потребляет памяти и чем меньше живёт, тем меньше ему повезёт.

    Все программисты, читавшие документацию или проходившие собеседования, знают, что, во-первых, процесс может быть “убит” и при наличии свободных ресурсов, во-вторых, кандидат на вытеснение выбирается по другим критериям: наличие “живых” Андроид-компонент, видимость пользователю и так далее.

    Механизм довольно простой: каждому процессу присваивается приоритет от -17 до 16, при этом чем выше приоритет, тем выше вероятность убивания процесса, и, в зависимости от количества свободной памяти, выбирается приоритет, начиная с которого процессы будут завершены. Приоритеты описаны в ProcessList.java. Занимательно, что приоритет приложения домашнего экрана HOME_APP_ADJ довольно высок, а я-то думал: почему он постоянно перезапускается?

    Массивы mOomAdj и mOomMinFreeLow/mOomMinFreeHigh как раз задают правила “когда что очистить”:

    private final int[] mOomAdj = new int[] {FOREGROUND_APP_ADJ, VISIBLE_APP_ADJ, PERCEPTIBLE_APP_ADJ, BACKUP_APP_ADJ, CACHED_APP_MIN_ADJ, CACHED_APP_MAX_ADJ};
    
    private final long[] mOomMinFreeHigh = new long[] {49152, 61440, 73728,86016, 98304, 122880};


    Таким образом, приложение домашнего экрана вытесняется при остатке свободной памяти в 73728 КБ на телефоне с экраном 1280x800 и ОЗУ в 700 МБ.
    ProcessList передаёт соответствующие значения в ядро, что можно видеть в его методе updateOomLevels.

    Приоритеты процессам выставляет Activity Manager Service, один из многих системных сервисов, общаться с которым можно через Activity Manager.

    Binder

    Binder, наряду с другими решениями (Files, Sigmals, Sockets, Pipes, Semaphores, Shared Memory и т.д.), решает задачу межпроцессного взаимодействия. Ноги у данного решения растут из проекта OpenBinder, разработчики которого в своё время перешли в команду Андроида.

    Bionic (реализация libc) не использует System V IPC, так как в андроидовском окружении стандартные средства приведут к утечкам ресурсов.

    Особенности:
    1. Управление потоками (мы все помним, что сервис, поддерживающий AIDL, должен работать в многопоточном окружении). Максимальное число потоков — 15 (ProcessState.c, метод open_driver), поэтому не стоит блокировать Binder-потоки в большом количестве без лишней необходимости.
    2. Механизм информирования о смерти процесса, держащего объект Binder “Link to Death”. Например, через него Window Manager узнаёт о смерти приложения и удаляет связанные с ним окна. Также LocationManager при смерти всех своих слушателей перестаёт опрашивать GPS-приёмник. Lowmemorykiller доволен. :)
    3. 2 режима вызова: блокирующий и неблокирующий (oneway). В первом случае вызывающий поток блокируется и ждёт отработки метода в потоке процесса-обработчика. Программисты просто вызывают методы через точку, взаимодействие потоков берёт на себя платформа.
    4. Передача UID и PID для безопасности. Через них системные сервисы определяют, есть ли у вызывающего процесса права совершать запрашиваемые действия.
    5. Для Java-программистов — средства создания Proxy и Stub-ов для конвертирования вызовов Java-методов в транзакции Binder-а.


    Рассмотрим как это работает на примере LocationManager-а.



    Когда мы хотим получить информацию о GPS, происходит следующее:
    1. Наше приложение вызывает соответствующий метод у LocationManager-а.
    2. LocationManager делегирует вызов прокси-объекту, преобразующему Java-методы и объекты в Binder-транзакцию (прокси-объектом у LocationManager-а является mService).
    3. Транзакция посылается драйверу ядра, который перенаправляет её LocationManagerService-у, отнаследованному от .LocationManager.Stub.
    4. .LocationManager.Stub делает обратные действия: разворачивает транзакцию в вызов Java-метода.
    5. .LocationManagerService обрабатывает запрос (используя, например, GPS-драйвер).
    6. Stub-объект пакует ответ в транзакцию, и процесс идёт в обратном направлении.
    7. Драйвер пересылает ответ обратно.
    8. Прокси-объект распаковывает результат вызова метода в Java-объекты.


    Как видим, за вызовом методов системных сервисов скрывается довольно большая логика.

    Ashmem

    Anonymous Shared Memory (ashmem) — механизм разделяемой памяти. В Линуксе, как правило, данный механизм реализован через POSIX SHM. Разработчики Андроида сочли его недостаточно защищённым, что могло сыграть на руку вредоносному ПО. Особенностями ashmem-а являются счётчик ссылок, при обнулении которого разделяемая память может быть освобождена (например, память освобождается при завершении всех процессов, использующих её), и сокращение разделяемого региона при нехватке памяти в системе.

    Ярким примером использования ashmem-а является процесс zygote, в котором загружается стартовая версия Dalvik VM с загруженными базовыми классами и ресурсами, а остальные приложения просто ссылаются на эту память.

    Binder имеет ограничение на размер транзакции в 1МБ (иначе будет выброшено исключение TransactionTooLargeException). Если нам надо передать из одного процесса в другой большой объём данных, можно как раз воспользоваться Ashmem-ом: создать MemoryFile и передать дескриптор файла в другой процесс.

    Logger

    Обычные дистрибутивы, как правило, используют две системы логирования: лог ядра, доступный через команду dmesg, и системные логи, располагающиеся обычно в директории /var/log.

    Система Андроида включает несколько циклических буферов для хранения сообщений пользовательских программ (что продлевает время жизни карт памяти, так как циклы чтения-записи не расходуются впустую) и не имеет дополнительных задержек от работы с сокетами, которые применяются в стандартном syslog-е.



    На диаграмме представлена общая система логирования Андроида. Драйвер логирования предоставляет доступ к каждому буферу через /dev/log/*. Приложения имеют доступ к ним не напрямую, а через библиотеку liblog. С библиотекой liblog общаются классы Log, Slog и EventLog. Команда adb logcat показывает содержимое буфера “main”.

    Заключение

    В данной заметке мы кратко рассмотрели некоторые особенности Андроида как Линукс-системы. За скобками остались некоторые другие части (pmem, RAM console и т.д.), а также такие важные аспекты платформы в целом, как System Service, процесс запуска системы и другие. Если данная тема будет интересна, в следующих статьях мы рассмотрим и их.
    Mail.ru Group
    892.21
    Building the Internet
    Share post

    Comments 17

      +16
      править код Андроида, не будучи сотрудником Google, практически невозможно

      Не совсем верно. Думаю, что вернее фраза править код Андроида, не будучи сотрудником компании, входящей в Open Handset Alliance, практически невозможно.

      Вот, например, первый попавшийся свежий коммит сотрудника Intel в master branch Android.

      Статья хорошая, пишите ещё, а то популярных статей по архитектуре Android можно по пальцам пересчитать, да и редко от одного автора, из-за чего получается, что кто в лес, кто по дрова.
        +1
        Править код никто не мешает никому. Патчи от кого попало в AOSP может и не возьмут, а в CyanogenMod, например — запросто.
          0
          Да, про код совсем неясно высказался :)
          +4
          Очень занимательно. С нетерпением жду продолжения, про остальные составляющие системы.
            +9
            Во-первых, меня не покидает ощущение, что я уже где-то это читал. Я бы всё-таки советовал давать ссылки на источники. В данном случае, мне кажется, что это книга Karim Yaghmour «Embedded Android» и презентации Marakana group (сейчас их можно найти здесь).

            А во-вторых, несотрудники Google тоже имеют право отправлять свои патчи. Если они пройдут ревью, то их включат в основную ветку.
              –1
              Абсолютно верно: отмеченные два источника являлись основными. Надо дописать список литературы.
              +1
              Спасибо, жду продолжения
                +5
                Разработчики Андроида даже не пытались найти общее решение “на будущее”, которое потом без проблем бы вливалось в основное ядро, не консультировались по этой проблеме с сообществом ядра Линукс. Можно ли их за это винить?

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

                Спасибо за статью! Жду продолжения!
                  +2
                  Если данная тема будет интересна, в следующих статьях мы рассмотрим и их.
                  Тема очень интересная.

                  Если кто посоветует интересные документы по Дальвику или в какой книге про Дальвик достаточно глубоко копают, то был бы очень признателен. На Хабре про него все перечитал, листал книги по Андройду, но там подача материала в научно-популярном стиле и очень мало по архитектуре Дальвика.
                    0
                    Тема довольно интересная и не такая уж и простая :) Я бы посоветовал начать вот отсюда, а дальше лучшее, что я нашел в свое время, это вот это. А вообще, лучше всего читать исходники, но, к сожалению, это делать довольно сложно (возможно только мне).
                      0
                      Спасибо, добрый человек!
                    0
                    Boinic (реализация libc)

                    Скорее имелось в виду: Bionic
                      0
                      Wakelocks-то да, отличная хрень. Особенно учитывая их неуправляемость штатными средствами.
                        +1
                        Вот таких вот статей и не хватает. Большое спасибо. Пишите ещё!
                          +1
                          Mail.ru становится тортом!
                          Статья отличная, продолжайте в том же духе! :)
                            0
                            LocationManager делегирует вызов прокси-объекту, преобразующему Java-методы и объекты в Binder-транзакцию (прокси-объектом у LocationManager-а является mService).

                            Можно пару слов о том, как это происходит? Что из себя структурно представляет Binder-транзакция, и в каком виде там существуют Java-объекты? Не возникает ли тормозов/расхода батареи из-за такой сложной логики взаимодействия?
                              +1
                              Наглядно на то, что происходит, можно в любых классах Proxy и Stub, которые создаёт утилита aidl.

                              Java объекты перегоняются в обычный байтовый массив на стороне клиента (средствами Parcel-а и Parcelable-а), из которого вновь формируются объекты на стороне сервиса.

                              Здесь есть описание структуры транзакции. Помимо аргументов вызываемого метода (data.ptr.buffer), содержит идентификатор метода, который необходимо вызвать (code), идентификатор сервиса, в который всем этим данным надо отправиться (target.handle), и другие поля.

                              Все операции простые и быстрые (записать в массив — прочитать из массива), накладные расходы, конечно, есть, но вряд ли соизмеримы со стоимостью выполнения самой логики вызываемых методов. Сам замеров не делал :) Что-то похожее надо делать при любом межпроцессном взаимодейтсвии: «как есть» Java-объекты из одной виртуальной машины в другую не передать.

                            Only users with full accounts can post comments. Log in, please.