Запуск нативных Linux-приложений в Android-окружении на примере TV-приставки NV-501-Wac

HELP-ME-24.COM (Freelance Team), Черноусов Антон

Продолжаем цикл статей по расширению возможностей типовых Andorid-устройств (предыдущая статья нашего цикла доступна по ссылке ).

Запуск Linux-приложений в Android-окружении

Сегодня мы поговорим о том, что же все таки в Android-устройствах есть от Linux и как мы можем это использовать. Сразу скажу, что от Linux там только ядро и сильно урезанный набор системных утилит. Именование каталогов кардинально отличается от привычного в Linux и естественно используется иная модель инициализации служб.

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

После включения вашего устройства загружается ядро, производится ядерный этап настройки оборудования и управление передается бинарному файлу /init. На этом этапе все похоже на загрузку обычного Linux-устройства и последовательность типовая initrd -> init с единственным отличием в том, что initrd представляет собой отдельный раздел на внутренней памяти телефона. Про работу внутренней памяти и разбивке разделов можно много рассказывать и для каждого Android - устройства производитель придумывает свою особую ни на что не похожую схему, поэтому поговорим об этом как нибуть позже, а сейчас мы подошли к первому интересному моменту.

Android мало того, что использует свою собственную ни с чем не совместимую систему инициализации, так еще и раздел где находятся файлы конфигурации init-системы представляет собой ro-файловую систему изменить данные на которой можно только пересобрав прошивку.

В нашем случае (как я уже говорил в прошлой статье), исходных кодов для пересборки прошивки нам не дали, поэтому есть два варианта как мы можем внести изменения в процесс загрузки для запуска наших процессов на уровне системы. Первый, это использование специализированного программного обеспечения эмулирующего работу Linux rc.d-скриптов, но в этом варианте вы работаете на уровне Android-системы, а не уровня инициализации и ваше приложение может быть банально выгружено подсистемой Android (такой метод используйте когда совсем уже нет вариантов как то "прицепится" к init.rc). И второй способ это проанализировать скрипт /init.rc, найти в нем скрипт запускаемый от имени пользователя root и обязательно с раздела /system/, так как раздел system хоть и примонтирован по умолчанию в режиме только для чтения, но может быть легко перемонтирован в режим чтения-записи.

Итак, в нашем случае я выбрал для модификации блок запуска демона sshd:

service sshd /system/bin/start-ssh
class main
#disabled

start-ssh - это обычный bash-скрипт в котороый мы внесем небольшую модификацию:

#!/system/bin/sh
umask 077
# Execute Linux Environment
/system/execute-linux.sh &

Таким образом мы одновременно с демоном ssh запустим необходимые нам команды на системном уровне. На этом этапе есть еще одна деталь на которую необходимо обратить внимание. Вам необходимо дождаться окончания инициализации оборудования прежде чем приступать к выполнению ваших скриптов, для этого я написал небольшой скрипт который отслеживает появления устройства /dev/block/sda1:

#!/system/bin/sh
echo "Start linux environment" > /data/linux.log

FILE="/dev/block/sda1"
NOT_MOUNTED=true

while $NOT_MOUNTED;
do
/system/bin/sleep 15
if [ -b $FILE ];
then
echo "File $FILE exists." >>/data/linux.log
/system/xbin/mount -o rw /dev/block/sda1 /system/linux/sda/
mount -o bind /dev/ /system/linux/sda/LINUX/root-image/dev/
mount -o bind /sys/ /system/linux/sda/LINUX/root-image/sys/
mount -o bind /proc/ /system/linux/sda/LINUX/root-image/proc/
mount -o bind /system/linux/sda/ /system/linux/sda/LINUX/root-image/sda/
/system/xbin/swapon /dev/block/sda2
chroot /system/linux/sda/LINUX/root-image/ /start-environment.sh
NOT_MOUNTED=false
else
echo "File $FILE does not exist." >>/data/linux.log
fi
done

В моем конкретном случае я использую внешнее хранилище в виде жесткого диска на 500 гб, жесткий диск форм-фактора ноутбука, так как питания приставки хватает только для него. К примеру скрипта мы вернемся чуть позже, а сейчас хотелось бы отметить, что скрипт пишется под каждый конкретный случай интеграции и в нашем случае должен после подключения жесткого диска смонтировать раздел подкачки, переопределить ряд системных параметров и передать управление полноценному Linux-окружению (сделать chroot в ARM-версию Ubuntu linux).

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

/system/xbin/swapon /dev/block/sda2

Для этой системы я не смог создать раздело подкачки в виде "файла" и пришлось использовать именно раздел диска и я рекомендую вам обязательно учесть этот момент при подготовке накопителя, так как с высокой долей вероятности и в вашей системе не получится запустить виртуальную память в виде файла. Во вторых, дополнительная операция монтирования:

/system/xbin/mount -o rw /dev/block/sda1 /system/linux/sda/

требуется для того, чтобы мы могли использовать накопитель в виде полноценной файловой системы ext3, а не fuse-точки монтирования как предложено изначально подсистемой Android. В штатном режиме файловая система мало того, что проходит лишние уровни абстракции, так еще и блокируется бит исполнения. Кстати, именно из за блокировки бита исполнения проект "Complete Linux Installer" использует loop-монтирование образа диска для запуска chroot-окружения Linux. В итоге, вместо обычной операции монтирования файловой системы мы имеем матрешку из fuse и loop монтирования и как это сказывается на производительности вы наверное догадываетесь.

Так как мы используем файл подкачки, нам потребуется несколько перенастроить модель работы с памятью. По умолчанию, init-скрипт задает набор параметров работы с виртуальной памятью и эти параметры разработаны специально для Android-модели с ее неоднозначной моделью многозадачности в условиях экономии оперативной памяти памяти, собственно в нашем случае init-скрипт переопределяет следующие параметры:

write /proc/1/oom_adj -16
write /proc/sys/kernel/panic_on_oops 1
write /proc/sys/kernel/hung_task_timeout_secs 0
write /proc/cpu/alignment 4
write /proc/sys/kernel/sched_latency_ns 10000000
write /proc/sys/kernel/sched_wakeup_granularity_ns 2000000
write /proc/sys/kernel/sched_compat_yield 1
write /proc/sys/kernel/sched_child_runs_first 0
write /proc/sys/kernel/randomize_va_space 2
write /proc/sys/kernel/kptr_restrict 2
write /proc/sys/kernel/dmesg_restrict 0
write /proc/sys/vm/mmap_min_addr 32768
write /proc/sys/net/ipv4/ping_group_range "0 2147483647"
write /proc/sys/kernel/sched_rt_runtime_us 950000
write /proc/sys/kernel/sched_rt_period_us 1000000
write /proc/sys/net/core/rmem_max 8388608
write /proc/apanic_console 1
write /proc/sys/vm/overcommit_memory 1
write /proc/sys/vm/min_free_order_shift 4
write /proc/sys/vm/dirty_expire_centisecs 200
write /proc/sys/vm/dirty_background_ratio 5
write /proc/sys/vm/extra_free_kbytes ${sys.sysctl.extra_free_kbytes}

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

echo 0 > /proc/sys/vm/overcommit_memory
echo 15 > /proc/sys/vm/swappiness
echo 15 > /proc/sys/vm/dirty_ratio
echo 3000 > /proc/sys/vm/dirty_expire_centisecs

По завершении настройки выполняется команда echo 3 > /proc/sys/vm/drop_caches для сброса кэша.

После проведенных операций предварительной подготовки приступим к заявленному выше "запуску нативных Linux-приложений в Android-окружении". Для этого мы будем использовать типовую ARM-версию Ubuntu Linux в минимальной конфигурации из наработок проекта "Complete Linux Installer", существенным отличием нашего решения от наработок данного проекта является запуск только отдельных демонов от имени системного пользователя без старта init-подсистемы linux и наше решение больше похоже на запуск docker-контейнеров приложений.

Для работы нам понадобится образ Linux-системы для платформы ARM который можно скачать по адресу https://sourceforge.net/projects/linuxonandroid/files/Ubuntu/14.04/Core/. Этот образ необходимо распаковать, подготовить tar.gz-архив, передать его на TV-приставку и там распаковать на подключенном диске в каталог /system/linux/sda/root-image/. Операции подготовки будем проводить на полноценном Linux-дистрибутиве (можно это конечно проделать и в среде Android, но это потребует слишком много лишних телодвижений).

# unzip ./ubuntu-14.04.CORE.ext4.PREALPHAv1.zip
# mkdir ./tmp-image
# mount ./ubuntu-14.04.CORE.ext4.img ./tmp-image/
# mkdir ./root-image
# rsync -av ./tmp-image/ ./root-image/
# tar -czvf ./root-image.tar.gz ./root-image/

После копирования и распаковки образа на Android TV-приставку проверим, что chroot проходит успешно, для чего выполним все операции подготовки образа и переход в chroot вручную:

# mount -o bind /dev/ /system/linux/sda/root-image/dev/
# mount -o bind /sys/ /system/linux/sda/root-image/sys/
# mount -o bind /proc/ /system/linux/sda/root-image/proc/
# /system/xbin/chroot /system/linux/sda/root-image/ /bin/bash

Если все прошло удачно, то добавляем эти команды (кроме последней) в скрипт /system/execute-linux.sh. Итогом работы скрипта полжна быть передача управления внутрь chroot для запуска сервисов которым требуется полноценное Linux-окружение.

В результате всех проделанных операций мы получаем скрипт /system/execute-linux.sh вида:

#!/system/bin/sh
echo "Start linux environment" > /data/linux.log
FILE="/dev/block/sda1"
NOT_MOUNTED=true
while $NOT_MOUNTED;
do
/system/bin/sleep 15
if [ -b $FILE ];
then
echo "File $FILE exists." >>/data/linux.log
/system/xbin/mount -o rw /dev/block/sda1 /system/linux/sda/
/system/xbin/swapon /dev/block/sda2
echo 0 > /proc/sys/vm/overcommit_memory
echo 15 > /proc/sys/vm/swappiness
echo 15 > /proc/sys/vm/dirty_ratio
echo 3000 > /proc/sys/vm/dirty_expire_centisecs
echo 3 > /proc/sys/vm/drop_caches
mount -o bind /dev/ /system/linux/sda/root-image/dev/
mount -o bind /sys/ /system/linux/sda/root-image/sys/
mount -o bind /proc/ /system/linux/sda/root-image/proc/
/system/xbin/chroot /system/linux/sda/root-image/ /rc.local
NOT_MOUNTED=false
else
echo "File $FILE does not exist." >>/data/linux.log
fi
done

При старте ТВ-приставки будут выполняться команды прописанные в файле rc.local в корне Linux-окружения. Так же для полноценной работы я так же рекомендую создать скрипт /system/bin/linux-env для быстрого перехода в Linux-окружение, эта операция потребуется довольно часто:

#!/system/bin/sh
/system/xbin/chroot /system/linux/sda/root-image/ /bin/bash.env

Обратите внимание, что передача управления осуществляется не bash-интерпретатору, а sh-скрипту который предварительно устанавливает переменные окружения:

#!/bin/bash
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
/bin/bash

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

Оставьте комментарий

Вы должны быть вошедший в чтобы отправить комментарий