Путешествие демона iOS

обратите внимание, что эта статья основана на устройствах iOS с джейлбрейком до версии 9.0.3

Контекст

Недавно у меня был небольшой проект, который работает с демоном iOS.

Требование состоит в том, что мы используем сервер S3 с открытым исходным кодом, а демон iOS может загружать файлы через AWS S3 iOS SDK.

Часть кодирования проста, но при тестировании мы обнаружили одну серьезную проблему: демон продолжает падать даже при загрузке файлов размером 9 МБ. Отчет о сбое не генерируется, только из системного журнала мы видим, что демон разбился и после сообщения от jetsam.

Затем я попытался подключить отладочный сервер и использовать lldb для отладки моего демона. Когда я вручную ввожу код для выполнения функции загрузки, lldb предупреждает меня через несколько секунд:

прекращено из-за проблем с памятью

Я был совершенно сбит с толку, но думаю, это из-за чтения файлов внутри AWS SDK.

Как AWS SDK для iOS загружает файлы

AWS SDK для iOS обрабатывает загрузку через AWSS3TransferManager и AWSS3TransferManagerUploadRequest.

У него также есть свойство AWSS3TransferManagerMinimumPartSize для управления минимальным размером детали.

if (fileSize > AWSS3TransferManagerMinimumPartSize) {
    return [weakSelf multipartUpload:uploadRequest fileSize:fileSize cacheKey:cacheKey];
} else {
    return [weakSelf putObject:uploadRequest fileSize:fileSize cacheKey:cacheKey];
}

Если файл больше минимального размера части, он вызовет

(AWSTask )multipartUpload:(AWSS3TransferManagerUploadRequest)uploadRequest fileSize:(unsigned long long) fileSize cacheKey:(NSString )cacheKey

вместо.

Внутри multipartUpload() он разрежет файл на мелкие кусочки и пересохранит их в файлы в папке:

[NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]]]

Он использует NSFileHandle для разделения файла. Оказалось, что в конечном итоге это может привести к проблемам с памятью, хотя у меня нет веских доказательств.

Я попытался добавить журнал в системный журнал для печати операций NSFileHandle, и я всегда вижу, что после вызова NSData *partData = [fileHandle readDataOfLength:dataLength] или [fileHandle closeFile] демон аварийно завершает работу и перезапускается.

Настройка AWSS3TransferManagerMinimumPartSize

Поскольку мы видели AWSS3TransferManagerMinimumPartSize, я сначала решил уменьшить значение до 2 МБ, и вроде все нормально. Затем я изменяю его на 3 МБ, сначала он работает нормально, но после загрузки нескольких частей демон падает и снова перезапускается, повторяя функцию загрузки снова и снова, и одна важная вещь: я обнаружил, что она не падает на той же итерации: например , иногда он может загрузить 20 частей, но после сбоя и перезапуска он может загрузить только 17 частей. Это дает мне намек на то, что демон должен быть уничтожен намеренно, особенно ядром iOS (благодаря знаниям об убийцах OOM в Linux, которые я получил, когда работал в EMC).

Поэтому я думаю, что у демона должен быть какой-то предел памяти, установленный ядром, но как мы можем его изменить, если это возможно?

AFNetworking действительно потрясающий

Прежде чем использовать S3 для загрузки, я тестировал загрузку файлов с помощью AFNetworking. Я помню, что он может загружать некоторые двоичные файлы, такие как 20+, также используя многокомпонентную загрузку, что меня смущает.

См. также:  Рекомендации по Node.js: кеширование и REST

Затем я пишу небольшой сервер Django для получения файлов из интерфейса AFNetworking:

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:configuration];
NSURLSessionDataTask *dataTask = [manager POST:serverURL parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
    [formData appendPartWithFileURL:fileURL name:uploadName error:nil];
} success:^(NSURLSessionDataTask * _Nonnull task, id  _Nonnull responseObject) {
} failure:^(NSURLSessionDataTask * _Nonnull task, NSError * _Nonnull error) {
}];
[dataTask resume];

Результат поразителен: на мой сервер Django загружается файл размером 200 МБ. Это меня еще больше озадачивает, как может один работать с 200 МБ, а другой не работает с 20 МБ?

Загрузка S3 в приложении

Я решил написать простое приложение, чтобы попробовать, как S3 SDK ведет себя в обычном приложении. После нескольких строк кода я обнаружил, что приложение работает просто отлично. Это дает еще один намек: демон и приложение обрабатываются совершенно по-разному.

Некоторые теории до сих пор

Учитывая приведенные выше факты, у нас есть несколько теорий:

  1. Демон не настолько ограничен, как мы думали ранее. Поскольку AFNetworking может выполнять эту работу, S3 SDK теоретически тоже должен.
  2. NSFileHandle может иметь утечку и запускать ядро ​​для уничтожения (если мы не протестируем его в приложении)
  3. AWS S3 iOS SDK может иметь серьезную проблему с памятью из-за злоупотребления continueWithSuccessBlock и неправильной обработки и освобождения больших объектов NSData в каждом запросе, состоящем из нескольких частей. Я лично думаю, что зло в рекурсивных блоках. Когда вы используете его правильно, это очень мощно, но когда вы допустили ошибку, это будет ужасно узнать. Разработчики AWS могут тестировать только в приложении и рады видеть, что все UT прошли, не осознавая этой ситуации, с которой я столкнулся.

Маловероятно, что у Apple может быть проблема с утечкой NSFileHandle. Согласно документации, это просто оболочка файлового дескриптора. Итак, мы сначала исключаем № 2 и оставляем его позади.

Что касается #3, то, хотя в нем действительно есть ошибки, я не могу его исправить по очевидной причине: нет времени выяснять, где они держат объекты, когда должны их освободить. Это также может означать, что ошибок нет, просто процесс приложения более терпим, чем демон.

Наш бэкэнд-инженер предлагает отказаться от S3, вместо этого мы могли бы иметь небольшой сервер, чтобы получить файлы и перенести их на S3 позже. Какое-то время у меня были те же мысли: отказаться от расследования и обвинить AWS в его ужасном SDK. Но я решаю сделать еще одну попытку для № 1

Сохранить день

На самом деле я уже начал работать над теорией №1, когда нашел интересные факты об AWS SDK. Я думаю, что если бы я смог узнать, как увеличить лимит памяти демона в iOS, проблема исчезла бы.

Но как? Как всем нам известно, iOS не является общедоступной, не говоря уже о том, что у разработчика приложений не может быть возможности подумать о запуске демона на iOS. За исключением тех SDK, фреймворков и общих знаний и концепций ОС, с которыми мы работаем в повседневной жизни, мы на самом деле ничего не знаем о внутреннем устройстве iOS для обычного разработчика приложений.

См. также:  Как собрать .ipa для React Native?

Но благодаря джейлбрейк-сообществу мы можем заглянуть внутрь iOS.

Вновь навестить старого друга и пионера

Когда я работал в EMC, я прошел недельный курс по отладке ядра Linux. лектор – Джонатан Левин, который, на мой взгляд, является первооткрывателем и настоящим мастером. Он знает буквально все о ядрах ОС среди Windows, Linux, OS X (позднее это macOS) и iOS.

Пока я проходил его обучение, я узнал, что он написал книгу под названием «Внутреннее устройство OS X и iOS». В то время я был одержим ядрами Linux и знал, что в OS X есть кровь Unix. Меня очень интересовала реализация Apple после OS X, поэтому я купил эту книгу и прочитал половину из них.

К счастью, он продолжает копаться во внутренностях iOS и опубликовал одну статью.

Обработка нехватки памяти в iOS и Mavericks

Из статьи узнал некоторые факты:

  1. Демон и другие сервисы имеют ограничение памяти, установленное ядром iOS, и jstsam является убийцей, что доказывает то, что мы видели в системном журнале в начале.
  2. /System/Library/LaunchDaemons/com.apple.jetsamproperties.{MODEL}.plist, где {MODEL} может быть N51 (5s), J71 (iPad Air) и т. д. устанавливает значение приоритета, ограничение памяти и т. д.
  3. memorystatus_control — это системный вызов, который не задокументирован, но его можно найти в kern_memorystatus.h:
  4. Представленный где-то в xnu 2107 (то есть уже в iOS 6, но только в OS X 10.9), этот (недокументированный) системный вызов позволяет вам контролировать как состояние памяти, так и джетсам (последнее на iOS).

Хорошо, теперь у нас есть дикая догадка, как заставить это работать.

Но сначала нам нужно знать, действительно ли наш демон ограничен джетсамом?

Получение состояния памяти

К счастью, Джонатан уже написал программу под названием mlist.c. Он может распечатать memorystatus_priority_entry для каждого процесса в macOS/iOS. Тем не менее, он устарел и нацелен на iOS 6, но сейчас мы на iOS 9. Итак, что нам теперь делать?

Помните, что информацию такого рода можно найти на opensource.apple.com, и первое, что нам нужно сделать, это проверить версию XNU iOS 9. Вызов uname -a на iPhone показывает:

Версия ядра Darwin 15.0.0: четверг, 20 августа, 13:11:14 PDT 2015; корень:xnu-3248.1.3

Это 3248.1.3! Посмотрим, что у нас есть на XNU: ближайшая версия — xnu-3248.20.55, правда она намного новее (1.3 до 20.55), но интерфейсы не должны сильно измениться. Покопавшись в xnu-3248.20.55/bsd/sys/kern_memorystatus.h, мы обнаружили:

typedef struct memorystatus_priority_entry {
        pid_t pid;
        int32_t priority;
        uint64_t user_data;
        int32_t limit;
        uint32_t state;
} memorystatus_priority_entry_t;

Кажется, memorystatus_priority_entry должно работать на iOS 9. Теперь приступим к делу (просто позаимствуем код из mlist.c) (не забудьте скопировать kern_memorystatus.h вместе с):

mtool.c
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "kern_memorystatus.h"

#define NUM_ENTRIES 1024

char *state_to_text(int State)
{
    // Convert kMemoryStatus constants to a textual representation
    static char returned[80];

    sprintf (returned, "0x%02x ",State);

    if (State & kMemorystatusSuspended) strcat(returned,"Suspended,");
    if (State & kMemorystatusFrozen) strcat(returned,"Frozen,");
    if (State & kMemorystatusWasThawed) strcat(returned,"WasThawed,");
    if (State & kMemorystatusTracked) strcat(returned,"Tracked,");
    if (State & kMemorystatusSupportsIdleExit) strcat(returned,"IdleExit,");
    if (State & kMemorystatusDirty) strcat(returned,"Dirty,");

    if (returned[strlen(returned) -1] == ',')
        returned[strlen(returned) -1] = '\0';

    return (returned);
}

int main (int argc, char **argv)
{
    struct memorystatus_priority_entry memstatus[NUM_ENTRIES];
    size_t  count = sizeof(struct memorystatus_priority_entry) * NUM_ENTRIES;

    // call memorystatus_control
    int rc = memorystatus_control (MEMORYSTATUS_CMD_GET_PRIORITY_LIST,    // 1 - only supported command on OS X
                                   0,    // pid
                                   0,    // flags
                                   memstatus, // buffer
                                   count); // buffersize

    if (rc < 0) { perror ("memorystatus_control"); exit(rc);}

    int entry = 0;
    for (; rc > 0; rc -= sizeof(struct memorystatus_priority_entry))
    {
        printf ("PID: %5d\tPriority:%2d\tUser Data: %llx\tLimit:%2d\tState:%s\n",
                memstatus[entry].pid,
                memstatus[entry].priority,
                memstatus[entry].user_data,
                memstatus[entry].limit,
                state_to_text(memstatus[entry].state));
        entry++;   
    }
}

Примечание. Эта программа может работать непосредственно на macOS, но для запуска этой программы на iOS нам нужно настроить инструмент, чтобы заставить его работать.

См. также:  LeetCode #590: Обход N-арного дерева в обратном порядке

Теперь мы находим наш демон-процесс и распечатываем его запись о приоритете состояния памяти:

iOS-06:~ root# mtool |grep 32167
PID: 32167      Priority: 0     User Data: 0    Limit: 6        State:0x18 Tracked,IdleExit

Чего ждать? Приоритет 0, а Лимит всего 6? Вы — четкая цель для джетсама! Это также напоминает мне, что когда я меняю AWSS3TransferManagerMinimumPartSize на 2 МБ или 1 МБ, демон продолжает уничтожаться, поэтому мы знаем, что AWS S3 SDK действительно не освобождает ресурсы при загрузке. Пришло время исправить это, разработчики AWS! Посмотрите на своего соседа AFNetworking :)

Как насчет нашего приложения для загрузки S3?

iOS-06:~ root# ps -ax|grep S3
32604 ??         0:00.58 /var/mobile/Containers/Bundle/Application/FF27DD3B-099E-4047-A31A-826868050209/S3Uploader.app/S3Uploader
32606 ttys002    0:00.01 grep S3
iOS-06:~ root# mtool |grep 32604
PID: 32604      Priority:10     User Data: 10100        Limit:650       State:0x00

I think we have the answer: iOS apps could have Limit 650 and AWS S3 SDK could live in this happy land.

Giving more juice

The final work would be, changing daemon memory limit, e.g. 650. Thanks to Apple, SET_MEMLIMIT_PROPERTIES is exported on iOS 9. Code is simple:

#include "kern_memorystatus.h"
int pid = (int)getpid();
// call memorystatus_control
memorystatus_memlimit_properties_t memlimit;
memlimit.memlimit_active = 650;
memlimit.memlimit_inactive = 650;
DDLog(@"setting memory limit for MIWorker pid:%d", pid);
int rc = memorystatus_control (MEMORYSTATUS_CMD_SET_MEMLIMIT_PROPERTIES,
                               pid,  // pid
                               0,  // flags
                               &memlimit,  // buffer
                               sizeof(memlimit));  // buffersize

DDLog(@"DONE: setting memory limit for MIWorker pid:%d, rc:%d", pid, rc);

И проверьте еще раз:

iOS-06:~ root# mtool |grep 32900
PID: 32900      Priority: 0     User Data: 0    Limit:650       State:0x18 Tracked,IdleExit

Оно работает! Затем попробуйте загрузить файл размером 215 МБ, успех! Работает как часы.

Заключение, но не конец

Хотя код загрузки S3 очень прост и понятен, мы столкнулись с серьезными проблемами с iOS jetsam. К счастью, Джонатан спас меня, дав правильное направление. Мне было очень больно и весело играть с кодом C, Objective-C, и это напомнило мне старые времена, когда EMC танцевала с ядром.

Будущая работа

Измените plist, чтобы установить ограничение памяти для демонов.

Для дальнейшего изучения, вот реализация kern_memorystatus.c (XNU 3248.20.55)

Понравилась статья? Поделиться с друзьями:
IT Шеф
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: