К сожалению все описанное применимо только ко внутренней карте памяти.
Служба Media Storage в OS Android отвечает за индексацию всех медиа-файлов на внутренней / внешней карте. Когда mediaserver натыкается на большое скопление файлов, он глубоко и надолго уходит в индексацию найденного добра, пытаясь найти среди этих файлов что-то похожее на медиа-контент, потребляя при этом немалый процент энергии и процессорного времени, что может продолжатся по нескольку часов. Для того, чтобы это не происходило, в Android-е предусмотрен механизм .nomedia файлов, — каталоги содержащие такой файл, должны быть пропущены mediaserver-ом. По непонятным причинам, на многих современных прошивках, начиная с Jelly Bean, mediaserver просто игнорирует данные файлы, продолжая свою нелегкую и бесполезную работу по индексации внутренних ресурсов приложений. Также бывает случается, что перезагрузка устройства приводит к тому, что mediaserver начинает индексацию с нуля, что еще сильнее усугубляет ситуацию.
Небольшой дискламер: повторяя действия описанные в данном посте, вы делаете все на свой страх и риск, автор не несет ответственности за испорченные устройства, потерянные данные, потраченное время. Вы также можете лишится гарантии, т.к. потребуется модификация прошивки системы.
Приведу пару конкретных примеров. На моем телефоне, на внутренней карте памяти находится каталог AnkiDroid содержащий порядка 20 тысяч gif/mp3 файлов внутри, — ресурсы приложения AnkiDroid. Когда mediaserver натыкался на это чудо, он уходил в глубоких жор батареи, который продолжатся часами (до 5-7 часов). Все еще сильнее усугубляется тем, что я немного параноик, и использую шифрование внутренней памяти, так что любой ввод-вывод — это еще и нагрузка на CPU, по шифрованию/дешифрированию данных. Более того, когда mediaserver занят своей грязной работой, любая попытка что-то прочитать с карты, тоже безбожно тормозит. Так больше жить было нельзя, я решил действовать.
Отличительной особенностью Galaxy S3 (равно как и Nexus 4, и многих других современников) от многих смартфонов «предыдущего поколения», является то, что в нем объединен раздел /data, с данными приложений, и «внутренняя карта памяти», — по сути это один и тот же раздел отформатированный в ext4. То, что выглядит как «внутренняя карта памяти», на самом деле хранится в каталоге /data/media.
Однако тут возникает одна проблема, не разрешимая в контексте ext4 файловой системы: карты памяти обычно отформатированы в FAT/exFAT, и как следствие, на карте памяти отсутствуют такие понятия, как права на файлы, владельцы файлов, итп, — любой файл может быть прочитан или записан любым приложением имеющим доступ к SD карте. Однако ext4 — это обычная linux/unix FS, так что если какой то файл будет создан по умолчанию с правами 644, то в дальнейшем, изменить этот файл сможет только приложение, создавшее его. Такая не стыковка наверняка породит множество проблем в приложениях. Вторая проблема — то, что FAT не чувствителен к регистру в имени файлов, так что если приложение пытается открыть файл «records.db», а файл реально называется «Records.db», файл не смотря на разницу в регистре, должен все также открываться и читаться.
Чтобы обойти все эти не стыковки, в Android создали сервис «sdcard», который посредством fuse (Filesystem in Userspace) реализует некоторую «прокси файловую систему» и делает ext4 похожей на FAT. Данный сервис «монтирует» каталог /data/media в новую точку монтирования — /storage/sdcard0, и те же файлы из /data/media, в новой точке монтирования ведут себя так, как будто бы это настоящий FAT.
Немного поискав исходники этого сервиса, быстро находим их в официальном репозитории Android-а, вот версия кода из Android JellyBean (4.1): линк
Когда я открыл этот файл, у меня был легкий шок, когда я увидел функцию с циклом обработки запросов:
void handle_fuse_requests(struct fuse *fuse) { unsigned char req[256 * 1024 + 128]; int len; for (;;) { len = read(fuse->fd, req, sizeof(req)); if (len < 0) { if (errno == EINTR) continue; ERROR("handle_fuse_requests: errno=%d\n", errno); return; } handle_fuse_request(fuse, (void*) req, (void*) (req + sizeof(struct fuse_in_header)), len); } }
Да, да, и еще раз да. Оно исключительно одно поточное! Внутри функции handle_fuse_request
находится всего лишь огромный switch, с обработкой конкретных сообщений, и отсылкой ответа драйверу fuse. На не зашифрованном устройстве данный одно поточный код может еще и не является ужасным бутылочным горлышком, т.к. сама по себе флеш-память — не очень быстрая, однако на устройствах с включенным шифрованием все координально меняется, — немалая часть времени ввода-вывода тратится на шифрование, и следовательно данный сервис не использует все возможности 4х ядер процессора, отсюда и дикие тормоза при доступе к карте памяти при активном mediaserver-е. И более того, единственный работающий поток в sdcard, при активном mediaserver-е, оказывается «заспамлен» запросами от mediaserver-а, которые обрабатываются по принципу FIFO, и как следствие — запросы другого приложения, даже если другому приложению всего лишь надо прочитать 2 байта из файла, идут все по той же длинной очереди, что занимает огромное время.
Моим первым порывом было все «взять и переписать». Однако посмотрев новую версии этого файла из ветки 4.2, я обнаружил что в Android 4.2 этот файл уже был переписан, так что теперь он использует потоки, так что нужно только как то завести новую версию файла в Android 4.1. Это как минимум снизило бы тормоза системы когда работает mediaserver. У меня не было 100% уверенности, что Samsung не внес никаких изменений в sdcard.c в своей прошивке, но попытаться стоило.
Для того, чтобы получить полные исходники сервиса, делаем git clone репозитория, и переключаемся на ветку с 4.2:
git clone https://android.googlesource.com/platform/system/core cd core git checkout jb-mr1.1-release
Исходники самого сервиса sdcard лежат в core/sdcard.
Далее для сборки потребуется Android NDK, его можно скачать и установить отсюда.
Для того, чтобы этот код можно было собрать при помощи NDK, создаем каталог «проекта», внутри создаем подкаталог «jni», куда в свою очередь копируем все исходники сервиса sdcard. Плюс необходимо в каталоге jni создать файл Application.mk
с вот таким содержимым:
APP_ABI := armeabi-v7a APP_PLATFORM := android-16
После этого, находясь в каталоге jni, либо каталогом выше, консольной командой ndk-build пытаемся скомпилировать этот код.
Попытка компиляции завершится с ошибками, т.к во первых у нас нет файла «android_filesystem_config.h», плюс в коде не объявлены константы AID_SDCARD_RW
, UTIME_NOW
и UTIME_OMIT
. AID_SDCARD_RW
— это идентификатор группы пользователей, — эту группу будут иметь создаваемые файлы. Посмотрев содержимое /data/media, легко находим, что это значение на Galaxy S3 установлено в 1015. UTIME_OMIT
и UTIME_NOW
— это две стандартные API константы, относящиеся к вызову utimensat, их значение я просто скопировал из системных заголовочных файлов своей хост-системы. Создав в папке проекта файл android_filesystem_config.h, и поместив туда все эти константы, а также поменяв путь файлу android_filesystem_config.h в sdcard.c, добиваемся успешной компиляции sdcard.c, но теперь код спотыкается на линковке: не находит символы pread64
, pwrite64
и вышеупомянутый utimensat
. Видимо Google решили немного ограничить API доступный стандартно через NDK, однако это легко обходится, достаточно вытянуть полноценный libc.so
с устройства и положить в нужную папку:
adb pull /system/lib/libc.so
Полученный libc.so
следует скопировать в каталог platforms/android-14/arch-arm/usr/lib
в каталоге NDK, тем самым затерев штатный libc.so
, идущий с NDK (на всякий случай скопируйте оригинал куда нибудь).
После этих всех манипуляций, наконец ndk-build завершается без ошибок, хотя и с некоторыми варнингами, которые можно проигнорировать.
После того, как сборка sdcard завершилась успешно, разведаем, какие аргументы передаются sdcard, и не изменились ли они, при переходе на 4.2. Штатно, на Galaxy S3, сервис sdcard инициализируется из файла /init.smdk4x12.rc
, в котором имеется следующая секция:
service sdcard /system/bin/sdcard /data/media 1023 1023 class late_start
Сравнив с кодом sdcard.c из Android 4.2, видно что добавлен новый аргумент — точка монтирования, — этот код убираем, чтобы соответствовало ожиданиям скрипта /init.smdk4x12.rc
. Сам скрипт модифицировать куда сложнее, — он расположен в initrd
, который вместе с ядром лежат в boot
разделе, так что модификация этого скрипта требует модификации и пересборки initrd
, с последующей пересборкой boot.img
, и прошивкой оного. Также в sdcard.c я сразу поменял значение константы DEFAULT_NUM_THREADS
, штатно оно было 2, я поменял на 3, — дабы использовать больше ядер, но не все, оставив одно ядро «для других задач».
После всех этих переделок, свеже-собранный sdcard заливается в систему (например через adb push sdcard /путь/куда/положить
).
Останавливаем системный сервис sdcard (тут следует быть осторожным — некоторым приложениям такая неожиданная кончина карты памяти может прийтись не по душе, — лучше сделать бэкап важных данных):
adb shell su # при необходимости setprop ctl.stop sdcard
И далее, в том-же терминале, пробуем запустить новый sdcard, для проверки работоспособности, перед тем, как собственно переписывать системный sdcard:
(при необходимости надо сделать chmod 755 sdcard) /путь/к/файлу/sdcard /data/media 1023 1023
В моем случае сервис запустился без ошибок, и в /storage/sdcard0 появились файлы и папки внутренней карты памяти.
Далее, дело за малым, — перезаписать системный sdcard, и запустить его штатно. Останавливаем тестовый sdcard (ctrl+c), перезаписываем:
mount -o remount -w /system cp /system/bin/sdcard /system/bin/sdcard.old cp /путь/к/sdcard /system/bin/sdcard chmod 755 /system/bin/sdcard chown 0:0 /system/bin/sdcard setprop ctl.start sdcard
В некоторых системах «упрощенный» синтаксис команды mount может не сработать, потребуется писать полностью все аргументы — тип fs, устройство и точку монтирования, вроде такого: mount -t ext4 -o remount /dev/block/mmcblk0p4 /system
.
Далее я перезагрузил свой телефон, убедился (через OS Monitor), что mediaserver снова начал поедать CPU, и попробовал посмотреть содержимое внутренней карты памяти File Explorer-ом, — открылось замечательно, без тормозов по 30 секунд при переходе от папки к папке. Т.е. результат есть, концепция работает. Также потребление CPU, при работе mediaserver-а, выросло с 18-20% до 30-35%, что означает лишь одно: много-поточный код работает, и mediaserver в результате закончит работу раза в 2 быстрее. (Тут стоит добавить важный комментарий: далеко не все процессорное время расходуется именно mediaserver-ом, — много отжирает сам sdcard, но это вызвано лишь тем, что ему «засчитывается» время потраченное ядром на крипто-операции).
После того, как промежуточный результат был достигнут, следующим шагом было ограничение доступа mediaserver-у к файлам, которые ему индексировать явно не следует. Начал я с того, что модифицировал sdcard, таким образом чтобы если запрос на чтение каталога приходил от mediaserver-а, и каталог при этом содержит .nomedia
, то отдавать пустой список, вместо реального содержимого каталога. Однако, как мне показалось, в итоге mediaserver начал безбожно сбоить: начались проблемы с индексацией реальных медиа-файлов, таких как снимки отснятые камерой (они стали недоступны в галерее), итп. Не став разбираться, что это такое, решил идти другим путем. На всякий случай, оставив ново-реализованную функциональность, но поменяв строку ".nomedia"
на ".forbid"
, т.е. если мне потребуется спрятать содержимое какого-то каталога от mediaserver-а, достаточно будет создать пустой файл .forbid
. Однако это было не самым надежным способом, — были подозрения, что так тоже будет сбоить.
Главные мои опасения были по поводу того, что возможно mediaserver — не единственный, кто рыщет по каталогам, и когда то, что видит mediaserver, не согласуется с данными других сервисов, он впадает в ступор. Значит надо спрятать содержимое каталога ото всех приложений, кроме приложения, создавшего данный каталог. Т.к. в OS Android каждое приложение имеет свой уникальный uid, осуществить такую фильтрацию — не сложно, несколько изменений в коде, и теперь, если в каталоге существует файл .allowuid
, то содержимое каталога будет видно только процессам с UID равным значению, записанному в данном файле. Плюсом данного подхода является то, что если какое-то приложение знает точный URI ресурса (например переданного с Intent-ом), — оно сможет открыть спрятанный файл без каких либо проблем, так что данная модификация не должна как либо повлиять на функциональность системы.
Собственно сами изменения — затронули главным образом функции handle_opendir
и handle_readdir
, а так-же структуру dirhandle
. В первой функции добавлен код проверки наличия файлов, во второй — собственно проверка на «права доступа», модифицированный sdcard.c можно посмотреть тут.
Компилируем, деплоим, испытываем:
$ adb shell $ cd /storage/sdcard0 $ ls ABBYY Alarms Android AnkiDroid ... $ echo 11 > .allowuid $ ls $ id uid=2000(shell) gid=2000(shell) ... $ echo 2000 > .allowuid $ ls ABBYY Alarms Android AnkiDroid ...
Бинго! Похоже, что работает. Осталось выяснить uid приложения AnkiDroid:
$ su # ls -ldn /data/data/*anki* drwxr-x--x 10217 10217 2013-05-26 02:07 com.ichi2.anki
И записать число 10217 в /storage/sdcard0/AnkiDroid/.allowuid:
echo 10217 > /storage/sdcard0/AnkiDroid/.allowuid
Приложение AnkiDroid при этом так-же осталось абсолютно работоспособным.
Все, в системе наступил мир и покой, — mediaserver пришел в себя, и больше не расходует батарейку как ненормальный, а система получила приятный бонус в виде более оптимизированного под много-поточность сервиса sdcard.
Полный код проекта sdcard, вместе с собранным под S3 бинарником можно найти тут.
P.S. Это мой первый пост на хабре, рекомендации и критика — приветствуются.
ссылка на оригинал статьи http://habrahabr.ru/post/180985/
Добавить комментарий