Настройка и портирование SQLite


Наилучший подход к сборке SQLite для большинства приложений — это использование файла с объединённым кодом sqlite3.c и заголовочного файла sqlite3.h, которые должны быть совместимы и компилироваться на любой системе Unix или Windows без необходимости в изменениях или особых параметрах компилятора

Вся рубрика SQLite: уроки, инструменты и примеры
Схема сборки SQLite из sqlite3.c и sqlite3.h через compiler в app binary
Для обычного проекта достаточно совместимой пары sqlite3.c и sqlite3.h: отдельные флаги и замена подсистем нужны только при понятной инженерной причине

Большинство приложений работает с SQLite в стандартной конфигурации, без каких-либо специальных параметров компиляции. Многие разработчики могут проигнорировать этот документ и просто собрать SQLite из объединённого файла без дополнительных знаний или каких-либо особых действий

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

  • Замена встроенной подсистемы мьютексов альтернативной реализацией
  • Полное отключение всех мьютексов для использования в однопоточных приложениях
  • Перенастройка подсистемы выделения памяти для использования распределителя памяти, отличного от реализации malloc() из стандартной библиотеки
  • Перестройка подсистемы выделения памяти таким образом, чтобы она вообще никогда не вызывала malloc(), а вместо этого удовлетворяла все запросы на память с использованием буфера памяти фиксированного размера, переданного SQLite при запуске
  • Замена интерфейса к файловой системе альтернативной реализацией — иными словами, переопределение всех системных вызовов, которые SQLite выполняет для работы с диском, на совершенно другой набор системных вызовов
  • Переопределение других интерфейсов операционной системы, например вызовов для получения времени по UTC или местного времени

В SQLite есть три отдельные подсистемы, которые можно изменить или переопределить во время компиляции. Подсистема мьютексов используется для сериализации доступа к ресурсам SQLite, разделяемым между потоками. Подсистема выделения памяти используется для выделения памяти, необходимой объектам SQLite и кэшу базы данных

Наконец, подсистема виртуальной файловой системы (Virtual File System, VFS) используется для обеспечения переносимого интерфейса между SQLite и базовой операционной системой, и особенно файловой системой. Эти три подсистемы принято называть «интерфейсными» подсистемами SQLite

Карта настраиваемых подсистем SQLite: mutex, memory и VFS
Три точки нестандартной настройки SQLite: мьютексы, выделение памяти и VFS. Чем ниже уровень замены, тем больше ответственность за тесты на целевой платформе

Большинство разработчиков могут использовать встроенные реализации по умолчанию и собирать SQLite без особых параметров или опций времени компиляции, однако некоторые высокоспециализированные приложения могут извлечь выгоду из замены или изменения одной или нескольких встроенных подсистем SQLite

Настройка или замена подсистемы мьютексов

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

-DSQLITE_THREADSAFE=0

Мьютексы дёшевы, но не бесплатны, поэтому производительность будет выше при полном отключении мьютексов. Размер результирующей библиотеки также будет немного меньше. Отключение мьютексов во время компиляции — рекомендуемая оптимизация для приложений, где это имеет смысл

Если приложение использует SQLite как динамически подключаемую библиотеку, его стоит проверить на предмет отключения мьютексов с помощью API sqlite3_threadsafe(). Приложения, использующие SQLite из нескольких потоков, должны удостовериться, что они не подключились к версии SQLite с отключёнными мьютексами

Мьютексы SQLite также можно отключить во время выполнения с помощью интерфейса sqlite3_config(). Чтобы полностью отключить все мьютексы, приложение может вызвать:

sqlite3_config(SQLITE_CONFIG_SINGLETHREAD);

Хотя отключение мьютексов во время выполнения менее эффективно, чем на этапе компиляции, это всё равно может дать определённый прирост производительности

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

sqlite3_config(SQLITE_CONFIG_MULTITHREAD);
sqlite3_config(SQLITE_CONFIG_MEMSTATUS, 0);

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

Сравнение режимов потоков SQLite THREADSAFE=0, SINGLETHREAD, MULTITHREAD и SERIALIZED
Mutex-настройки SQLite дают выигрыш только тогда, когда приложение само строго контролирует потоки и не делит один connection между параллельными операциями

SQLite использует pthreads для реализации мьютексов в Unix и требует рекурсивного мьютекса. Большинство современных реализаций pthreads поддерживают рекурсивные мьютексы, но не все. Для систем, которые не поддерживают рекурсивные мьютексы, рекомендуется, чтобы приложения работали только в однопоточном режиме

Если это невозможно, SQLite предоставляет альтернативную реализацию рекурсивного мьютекса, построенную поверх стандартных «быстрых» мьютексов pthreads. Эта альтернативная реализация должна работать корректно при условии, что pthread_equal() является атомарной операцией и процессор имеет когерентный кэш данных. Альтернативная реализация рекурсивного мьютекса включается следующим ключом командной строки компилятора:

-DSQLITE_HOMEGROWN_RECURSIVE_MUTEX=1

По этой теме полезно отдельно посмотреть EXPLAIN QUERY PLAN: план выполнения SQL-запроса в SQLite, чтобы расширить контекст и сравнить подходы

По этой теме полезно отдельно посмотреть Создание Flutter-приложения с SQLite, BLoC и Streams, чтобы расширить контекст и сравнить подходы

Настройка или замена подсистемы выделения памяти

По умолчанию SQLite получает необходимую ему память для объектов и кэша из реализации malloc()/free() стандартной библиотеки. Также ведётся работа с экспериментальными распределителями памяти, которые удовлетворяют все запросы на память из единого буфера памяти фиксированного размера, переданного SQLite при запуске приложения. Дополнительная информация об этих экспериментальных распределителях памяти будет предоставлена в будущей редакции этого документа

SQLite поддерживает возможность указания приложением альтернативного распределителя памяти во время выполнения путём заполнения экземпляра объекта sqlite3_mem_methods указателями на процедуры альтернативной реализации, а затем регистрации новой альтернативной реализации с помощью интерфейса sqlite3_config(). Например:

sqlite3_config(SQLITE_CONFIG_MALLOC, &my_malloc_implementation);

SQLite делает копию содержимого объекта sqlite3_mem_methods, поэтому объект можно изменять после того, как вызов sqlite3_config() вернёт управление. Я нахожу этот подход особенно удобным в тех случаях, когда нужно подключить собственный пул памяти с жёсткими ограничениями на потребление — достаточно один раз зарегистрировать реализацию и больше не думать об этом

Добавление новых виртуальных файловых систем

Начиная с версии 3.5.0 (2007-09-04), SQLite поддерживает интерфейс, называемый виртуальной файловой системой (Virtual File System), или «VFS». Этот объект несколько неточно назван, поскольку на самом деле он является интерфейсом ко всей базовой операционной системе, а не только к файловой системе

Одной из интересных особенностей интерфейса VFS является то, что SQLite может поддерживать несколько VFS одновременно. Каждое соединение с базой данных должно выбрать единственный VFS для своего использования при первом открытии соединения с помощью sqlite3_open_v2(). Но если процесс содержит несколько соединений с базой данных, каждое из них может выбрать другой VFS. VFS можно добавлять во время выполнения с помощью интерфейса sqlite3_vfs_register()

Стандартные сборки SQLite для Unix, Windows и OS/2 включают VFS, подходящий для целевой платформы. Сборки SQLite для других операционных систем по умолчанию не содержат VFS, но приложение может зарегистрировать один или несколько во время выполнения

На практике именно этот механизм чаще всего используется при портировании SQLite на встраиваемые платформы — я встречал реализации VFS для RTOS-систем, где стандартные файловые примитивы Unix попросту недоступны

Портирование SQLite на новую операционную систему

Для портирования SQLite на новую операционную систему — операционную систему, не поддерживаемую по умолчанию, — приложение должно предоставить:

  • работающую подсистему мьютексов (но только если оно многопоточное),
  • работающую подсистему выделения памяти (при условии, что в стандартной библиотеке отсутствует malloc()), и
  • работающую реализацию VFS.

Всё это можно предоставить в одном вспомогательном файле кода на C, а затем скомпоновать его со стандартным файлом кода sqlite3.c для получения работающей сборки SQLite для целевой операционной системы. Помимо альтернативных подсистем мьютексов и выделения памяти и нового VFS, вспомогательный файл кода на C должен содержать реализации следующих двух процедур:

  • sqlite3_os_init()
  • sqlite3_os_end()

Файл кода sqlite3.c содержит реализации по умолчанию VFS, а также функций sqlite3_initialize() и sqlite3_shutdown(), подходящие для Unix, Windows и OS/2. Чтобы предотвратить загрузку одного из этих компонентов по умолчанию при компиляции sqlite3.c, необходимо добавить следующую опцию времени компиляции:

-DSQLITE_OS_OTHER=1

Ядро SQLite вызовет sqlite3_initialize() на раннем этапе. Вспомогательный файл кода на C может содержать реализацию sqlite3_initialize(), которая регистрирует подходящий VFS, а также, возможно, инициализирует альтернативную систему мьютексов (если мьютексы необходимы) или выполняет любую необходимую инициализацию подсистемы выделения памяти

Схема портирования SQLite на новую операционную систему через SQLITE_OS_OTHER, sqlite3_os_init, VFS, mutex и memory
При SQLITE_OS_OTHER=1 стандартный OS-слой отключается: приложение должно зарегистрировать VFS и, при необходимости, дать свои mutex и memory реализации

Ядро SQLite никогда не вызывает sqlite3_shutdown(), но эта функция является частью официального API SQLite и не предоставляется иным образом при компиляции с -DSQLITE_OS_OTHER=1, поэтому вспомогательный файл кода на C, вероятно, должен предоставлять её для полноты

Типичные ошибки при настройке и портировании

Работая с нестандартными конфигурациями SQLite, я раз за разом вижу одни и те же проблемы, которые стоит обозначить явно

Чек-лист ошибок при нестандартной сборке SQLite: THREADSAFE=0, MULTITHREAD, SQLITE_OS_OTHER, custom malloc и homegrown mutex
Большинство проблем в нестандартной сборке появляется не в SQL-запросах, а на границе mutex, memory и VFS

Отключение мьютексов в многопоточном приложении. Флаг -DSQLITE_THREADSAFE=0 полностью убирает всю защиту потоков. Если приложение использует SQLite из нескольких потоков, это приведёт к повреждению данных без каких-либо явных ошибок на этапе компиляции. Перед отключением мьютексов всегда стоит проверить архитектуру приложения

Смешение соединений между потоками при SQLITE_CONFIG_MULTITHREAD. Эта конфигурация не запрещает использование SQLite из нескольких потоков — она лишь снимает защиту на уровне отдельного соединения. Два потока, одновременно работающие с одним и тем же объектом соединения, вызовут неопределённое поведение

Забытый вызов sqlite3_os_init() при портировании. При компиляции с -DSQLITE_OS_OTHER=1 ядро SQLite ожидает, что приложение само предоставит реализацию sqlite3_os_init(). Если функция не определена или не регистрирует VFS, первый же вызов sqlite3_open() завершится ошибкой

Изменение объекта sqlite3_mem_methods после регистрации без повторного вызова sqlite3_config(). SQLite копирует содержимое объекта в момент вызова sqlite3_config(). Последующие изменения структуры не влияют на зарегистрированную реализацию — нужен повторный вызов

Отсутствие рекурсивных мьютексов в pthreads. На некоторых встраиваемых платформах реализация pthreads не поддерживает рекурсивные мьютексы. В этом случае необходимо либо переходить в однопоточный режим, либо явно включать флаг -DSQLITE_HOMEGROWN_RECURSIVE_MUTEX=1

Ответы на эти вопросы могут быть для вас полезными

Нужно ли что-то специально настраивать, чтобы использовать SQLite в однопоточном приложении? Нет, SQLite работает корректно в однопоточном режиме без каких-либо изменений. Однако для повышения производительности рекомендуется добавить флаг -DSQLITE_THREADSAFE=0 при компиляции — это полностью исключает накладные расходы на мьютексы

Можно ли использовать несколько VFS одновременно в одном процессе? Да. SQLite поддерживает регистрацию нескольких VFS через sqlite3_vfs_register(). Каждое соединение с базой данных выбирает конкретный VFS при открытии через sqlite3_open_v2(). Разные соединения в одном процессе могут использовать разные VFS

Что произойдёт, если скомпилировать SQLite с -DSQLITE_OS_OTHER=1 и не предоставить реализацию VFS? SQLite не будет содержать никакого VFS по умолчанию. Первый же вызов sqlite3_open() или sqlite3_open_v2() завершится ошибкой, поскольку ядро не найдёт зарегистрированного VFS для работы с файловой системой

Как проверить во время выполнения, скомпилирован ли SQLite с поддержкой потоков? Для этого предназначен API sqlite3_threadsafe(). Он возвращает ненулевое значение, если поддержка потоков включена, и ноль, если мьютексы были отключены при компиляции флагом -DSQLITE_THREADSAFE=0

Можно ли заменить распределитель памяти уже после инициализации SQLite? Нет. Альтернативный распределитель памяти необходимо регистрировать через sqlite3_config(SQLITE_CONFIG_MALLOC, …) до первого вызова sqlite3_initialize(). После инициализации изменение распределителя не поддерживается

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x