Table of Contents
Введение
Статья занудно описывает семантику (ну да, смысл, суть) операций над базами в PalmOS Data Manager. Сие творение было написано как возмущенная реакция на изменения в PalmOS 5.4.9 (начало в этом треде: http://forum.sources.ru/index.php?showtopic=119558 ) . Читать с интонациями писателя Коклюшкина.
Классический Data Manager
В классической PalmOS Data Manager располагает свои базы в кучах памяти и реализован как надстройка над Memory Manager. Это приводит к тому, что хэндлы записей можно рассматривать как обычные хэндлы блоков в динамической куче, которые можно лочить, данные которых можно читать итд. Различия между хэндлами из динамической памяти и из хранилища следующие:
- хэндлы data manager не стоит освобождать и изменять их размер.
- не стоит писать в память, на которую указывает хэндл. Даже если отсутствует защита куч по записи.
Чем плохи эти действия? Тем, что они идут в обход Data Manager и не дают ему возможности изменить, например, размер базы при resize записи с помощью MemHandleResize. В принципе изменить размер можно, но где и когда это аукнется - неизвестно.
Какие операции над записями и ресурсами предлагаются программисту?
Записи
У записей в базе существуют два признака: busy и dirty. Операции доступа к записям изменяют состояние этих битов.
операция | смысл | действия с флагом busy | действия с флагом dirty | действия, если флаг busy выставлен | |||||
DmGetRecord | Возвращает хэндл записи для модификации | выставляет | не трогает | возвращает ошибку | |||||
DmQueryRecord | Возвращает хэндл записи для чтения | не изменяет | не изменяет | не влияет на функцию | |||||
DmReleaseRecord | Освобождает запись | сбрасывает | выставляет по параметру | требует выставленного флага |
Некоторые неверные действия с этими функциями ловится, а некоторые нет.
- Если забыть вызвать DmReleaseResource после DmGetRecord, то запись останется busy и последующие DmGetRecord не смогут получить эту запись
- Вызов DmReleaseResource после DmQueryRecord ловится в отладочном ROM по отсутствию бита busy
- Ничто не мешает сделать DmWrite для записи, полученной по DmQueryRecord. У этой записи не будет выставлен бит dirty и не изменится modification number в заголовке базы.
- Неосвобожденные записи отлавливаются в отладочном ROM во время DmCloseDatabase.
Ресурсы
Базы ресурсов не содержат признаков записей, поэтому существует только две функции доступа к записям
операция | смысл | ||
DmGet(1)Resource | Возвращает хэндл записи | ||
DmReleaseResource | Осовобождает хэндл записи |
Никакой проверки правильного использования пары функций нет.
- Функция DmReleaseResource ничего не делает. Неудивительно, что ее отсутствия в коде можно не замечать очень долго.
Локинг записей и ресурсов
Функции DmGet* возвращают хэндлы. Эти хэндлы можно и нужно лочить для работы с данными. Важно понять, что залоченность этих хэндлов абсолютно безразлична Data Manager. Единственное исключение: после любого сброса функция DmInit разлочивает все записи во всех кучах storage. Все остальное время записи могут быть залочены или разлочены. Освобождение записи или закрытие базы на это не влияет.
Лок ресурса часто применяется программами-резидентами для регистрации процедурного аларма или нотификации. Такой код лочит ресурс code#1 и передает указатель в функцию регистрации. Даже после выхода из программы указатель на обработчик останется валидным.
Защита базы
К сожалению лок записи не спасает от удаления базы. В этом случае у залочившей ресурс программы нет никакой возможности обнаружить, что указатель на ресурс больше не валиден. Для предотвращения такой ситуации в API есть функция DmProtectDatabase, которая защищает базу и не позволяет удалять ее. Эта функция не запрещает модифицировать записи базы, но это уже не так принципиально, вряд ли кто-то полезет изменять записи в чужой базе. А вот удалить незащищенную базу можно из любого файл менеджера.
Защита базы напрямую никак не связана с локом записей, ее можно применять и на базе без залоченных записей. Связь между локом и защитой существует на уровне настоятельных рекомендаций
NVFS. Первая попытка
Реализация Data Manager на базе NVFS отличается от классики тем, что базы больше не хранятся в адресуемой памяти. С первого взгляда кажется, что существующее DM API позволяет сделать прозрачную подзагрузку записей в кэш. Не получилось. Почему?
Записи
Попробуем составить таблицу состояний записи в кэше. Состояния гипотетические и составлены из соображений логики, нежели из реального состояния дел.
статус | описание | может ли быть выгружена при очистке кэша | |||
загружена для модификации | запись загружена функцией DmGetRecord | не может | |||
загружена для чтения | запись загружена функцией DmQueryRecord или DmRecordInfo | может. остается хэндл, указывающий на блок размера 0 | |||
загружена для повторного использования | запись после вызова DmReleaseRecord с dirty=====false | может | |||
грязная | запись после вызова DmReleaseRecord с dirty=====false | не может | |||
сирота | запись после вызова DmQueryRecord и ужатая до блока размером 0 | не может |
Отметим следующие нюансы.
- DmSyncDatabase переводит грязную запись в запись для повторного использования.
- Хэндл, полученный от DmQueryRecord требуется перед использованием проверять на длину. Если она нулевая, то запись стоит перезапросить.
- Не очень понятно время жизни записи, загруженной для чтения с помощью DmQueryRecord. До закрытия файла (скорее всего)? До закрытия программы? Вообще, создается впечатление, что такие записи лучше не использовать.
Как мы видим, в целом все хорошо и предсказуемо. По крайней мере пока.
Ресурсы
Переходя к ресурсам, хочется грязно выругаться. API Resource Manager не позволяет программисту выразить свои намерения также явно, как и в Record API. Наличие опциональной DmReleaseRecord не дает никакой информации о том, была ли запись модифицирована или нет.
Как это делает NVFS? Скорее всего этот уровень отслеживает вызовы DmWrite и неявно выставляет внутренний бит dirty для таких ресурсов.
статус | описание | может ли быть выгружена при очистке кэша | |||
загружена | ресурс загружен функцией DmGet(1)Resource | не может | |||
загружена для повторного использования | запись после вызова DmReleaseResource | может | |||
грязная | измененная запись после вызова DmReleaseResource | не может |
Отметим следующие нюансы.
- DmSyncDatabase переводит грязную запись в запись для повторного использования.
- Для освобождения кэша требуется явно вызвать DmReleaseResource. А все ли и всегда ли его вызывают? Или же иногда ресурсы могут сбрасываться как и записи имени DmQueryRecord?
Локинг
Похоже, что залоченные ресурсы и записи кэш не очищает. Что он с ними делает - неизвестно. На следующие вопросы ответ неизвестен:
- Сохраняет ли NVFS связь между залоченным ресурсом и базой после закрытия оной? Будет ли при повторном открытии базы возвращен тот же хэндл, А если да, то всегда ли?
- Как NVFS обрабатывает release для залоченного ресурса? Скорее всего никак, release обрабатывает только незалоченные ресурсы.
- А может для залоченного ресурса release вообще вреден?
Эти вопросы подводят к тому, что надежность программы-резидента на системе с NVFS под большим вопросом.
Защита
Факт защищенности базы никак не влияет на логику кэша. Точка.
NVFS в PalmOS 5.4.9
К чему привела вышеописанная логика?
- Первыми были побиты разработчики резидентов, которые не лочили ресурсы кода в памяти. Те, которые не поддавались были излечены программой RescoLocker, которая лично лочила ресурсы указанных программ в кэше для надежности.
- Вторыми были побиты авторы резидентов, которые подписывались не процедурами, а через sublaunch. Это было медленно. Для избавления был написан SharkCache, который лочил указанные программы в кэше для ускорения повторных запусков.
- Самые умные посмотрели на результат действия двух предыдущих программ и охнули. Ибо кэш оказался забит всякой ерундой. Программа DBCacheTool пыталась очистить кэш путем создания временной базы нужного размера. Предполагалось, что создание новых записей вытолкнет из кэша “ненужные” записи. Это привело к тому, что процедура очистки кэша стала вызываться чаще и нашлись все ошибки, допущенные при ее создании. С завидной регулярностью эта операция висла или перегружала устройство.
- Естественно, что проверять записи перед каждым использованием DmQueryRecord никто не стал. Поэтому программы регулярно напарывались на пустую запись, будучи морально к этому неготовыми.
Горе-разработчики NVFS посмотрели на это безобразие и решили переделать. Все и сразу.
И сделали следующее:
- DmQueryRecord перестала возвращать пустую запись. Теперь NVFS влез в MemoryManager так, что лок такой пустой записи пересчитывает ее из флеша.
- После закрытия незащищенной базы все записи или ресурсы переводятся в состояние “загружена для повторного использования” независимо от лока и разлочиваются. Это решает проблему “забыл сделать release” или “забыл сделать unlock”. но возникает проблема “забыл защитить”. Наверняка в Resco Locker появится опция “защитить базу”.
Это кардинальная переделка и плоха и хороша. Плоха тем, что полностью поменяла логику кэша, хороша тем, что сделала его более контролируемым.
Заключение
Как же надежно защитить свой резидент от превратностей NVFS? Рекомендуется следующий вариант. Он был проверен на программах YAHM и RusHack и показал высокую стабильность:
Инсталляция резидента:
- DmGet1Resource
- DmReleaseResource (спорный шаг. похоже, что он опционален)
- DmProtectDatabase(true) (лучше делать до закрытия базы, а то вдруг кэш успеет ее убить?)
Запрещение резидента:
- DmGet1Resource
- MemHandleUnlock (за текущий лок)
- MemHandleUnlock (за лок при инсталляции)
- DmProtectDatabase(false)