組み込みシステムは、多くの場合、完全なデータベースサーバのオーバーヘッドなしでローカルデータ管理を必要とします。 C の軽量データベースエンジンを実装すると、開発者はメモリ、パフォーマンス、ストレージを直接制御できます。この記事では、単純に埋め込まれたデータベースエンジンの設計と実装、データ構造、CRUD 操作、インデックス作成、およびリソースの制約のある環境のための永続的な戦略について説明します。

組み込みデータベースエンジンのコア要件

埋め込まれたデータベースエンジンは、RAM、フラッシュ、および処理速度の限界以内に動作しなければなりません。典型的な要件には、決定的な動作、最小限のコードのフットプリント、および外部の依存関係はありません。エンジンは、基本的な操作をサポートする必要があります。インサート、取得、更新、削除、および検索。多くの埋め込まれたデータベースは、EEPROM、SPIフラッシュ、またはSDカードなどの非揮発性メモリにデータを保存し、データを保存する必要があります。

適切なデータ構造を選択すると、最初の設計決定です。配列は、静的サイズで単純に制限されています。リンクリストは、動的成長を可能にし、ポインタオーバーヘッドを追加します。バランスの取れたパフォーマンスのために、固定サイズのレコードプールを使用してハイブリッドアプローチは、フリーリストがうまく機能することができます。 [SQLiteのデザイン原則]]は、はるかに単純なエンジンであっても、有用な洞察を提供します。

記録記憶層の設計

記憶層は、レコードがメモリ上やディスク上にレイアウトされた方法を管理する。 共通パターンは、各レコードを固定長構造として扱うことで、ポインタの算数を簡素化し、直接インデックス化が可能である。 可変長レコードはフラグメンテーションを複雑化し、メモリマネージャが必要である。

レコードプール付き固定長レコード

レコードの最大数(例、])を定義し、静的配列を割り当てます。 スロットが使用されるビットマップまたはフリーリストトラック。 レコードが削除されると、そのスロットはプールに戻ります。 このアプローチは、動的割り当てを避け、O(1)割り当て時間を保証します。

#define MAX_RECORDS 256
typedef struct {
 int id;
 char name[32];
 float value;
 int active; // 1 if slot in use
} Record;

Record pool[MAX_RECORDS];

持続的なストレージのために、プールはファイルやフラッシュの領域によってバックアップすることができます。 起動時に、エンジンは、非揮発性メモリからRAMにプールを読み、シャットダウン(または定期的に)、それが戻ってそれを書き込みます。

データ整合性チェック

破損を検知するために、各レコードに簡単なチェックサムフィールドを追加します。 [CRC-32[]]は、組み込みシステム、エラー検出強度の複雑さのバランスをとるための良い選択です。

基本的なCRUD操作の実装

レコードプールを定義して、レコードをインサート、検索、更新、削除する機能を実行します。検索操作は、多くの場合、パフォーマンスボトルネックであり、ネイブ線形検索は、小規模なデータベース(数百レコード)のみで使用できます。

リスト管理機能の不整形

インデックスのフリーリストを維持します。 インサートでは、フリーリストからインデックスをポップアップし、レコードを埋め、それをアクティブにマークします。 無料のリスト自体は、整数の配列を使用して、単純なスタックすることができます。

int free_list[MAX_RECORDS];
int free_count = MAX_RECORDS;
for (int i = 0; i < MAX_RECORDS; i++) free_list[i] = i;

int db_insert(int id, const char* name, float value) {
 if (free_count == 0) return -1; // no space
 int idx = free_list[--free_count];
 pool[idx].id = id;
 strncpy(pool[idx].name, name, sizeof(pool[idx].name)-1);
 pool[idx].value = value;
 pool[idx].active = 1;
 return idx;
}

調査および更新

シンプルな検索は、アクティブなレコードだけをチェックし、プール上で反復します。更新のために、レコードを検索し、フィールドを変更し、必要に応じて、レコードが削除された場合、フリーリストを再確認します。

int db_find_by_id(int id) {
 for (int i = 0; i < MAX_RECORDS; i++) {
 if (pool[i].active && pool[i].id == id) return i;
 }
 return -1;
}

void db_update(int idx, float new_value) {
 if (idx >= 0 && idx < MAX_RECORDS && pool[idx].active)
 pool[idx].value = new_value;
}

高度なコンセプト: インデックス化と永続化

レコードが成長するにつれて、線形検索は高価になります。 ソートされたキーポインタやバイナリ検索ツリーのような単純なインデックスを追加すると、検索時間を短縮できます。 組み込みシステムの場合、 バイナリ検索でソートされた静的な配列は、インサートが不十分である場合に十分です。

バイナリ検索でソートインデックス

検索キー(例、ID)でソートされたレコードインデックスの並列配列を保持します。新しいレコードを投入すると、バイナリインサートを使用してソートされた配列にインデックスをインサートします。その後、検索はバイナリ検索でO(log n)になります。削除はインデックス配列をシフトする必要がありますが、小さなデータベースではこれで許容されます。

ファイルI/Oによる持続的なストレージ

ファイルのシステムなしでマイクロコントローラでは、未加工フラッシュメモリ書き込みが一般的です。 Linuxベースの組み込みシステムでは、標準POSIX ]//がうまく機能します。 シンプルなファイル形式を使用してください。 ヘッダー(数値、バージョン、レコードカウント)を書き込み、生プール配列に従って続きます。 クラッシュレジリエンスのために、書き込みヘッドログ(WAL)は役立ちますが、基本的なエンジンは、FATFLT1つのフラッシュファイル(FATFAT)に十分な容量が必要です。 [FATFAT]FATFAT: [FAT]FATFATFAT]は、FATFATFATFATFAT:[FATFATFAT]は、FATFATFATFATFATFATFATFATFATFATF]のフラッシュファイルと、または、FATFATFATFATFATFATFATFATFATFATFATFATのフラッシュファイルと、FATは、Fのフラッシュオプションが異なるオプションが異なるフラッシュファイルと入力されたファイルと、または、または、FATFATFATFATF

void db_save(const char* filename) {
 FILE* fp = fopen(filename, "wb");
 if (!fp) return;
 fwrite(pool, sizeof(pool), 1, fp);
 fclose(fp);
}

void db_load(const char* filename) {
 FILE* fp = fopen(filename, "rb");
 if (!fp) return;
 fread(pool, sizeof(pool), 1, fp);
 fclose(fp);
 // Rebuild free-list from pool
 free_count = 0;
 for (int i = 0; i < MAX_RECORDS; i++) {
 if (!pool[i].active) free_list[free_count++] = i;
 }
}

制約と取引オフの処理

組み込みデータベースエンジンは、機能とリソースの使用状況を常にトレードオフに直面しています。 どの機能が含まれているかを選択すると、アプリケーションによって異なります。

  • [ACID準拠 - 通常は必要ありません。 単純な原子は、ほとんどのセンサーデータロギングのためのサッフィを書きます。
  • Indexing - インサートコストを追加しますが、読み込みをスピードアップします。 書き込みヘビーのワークロードのために、インデックスをスキップします。
  • [Concurrency] - ほとんどの埋め込まれたシステムは単一のスレッドを実行します。 RTOSを使用する場合、ミュートを実行します。
  • []メモリーフットプリント] – 静的割当は動的よりも安全です。 バッファサイズに[の定数を使用してください。
  • []パワーロス[] - フラッシュストレージの場合、頻繁な小さな書き込みを避けます。 バッチ更新とダブルバッファスキームを使用します。

実用例:温度のロガー データベース

毎分読み出しを記録し、24時間ローカルに保存するIoT温度センサーを検討してください。データベースエンジンは1440レコード(1分あたり)を処理しなければなりません。各レコードには、タイムスタンプ(Unix epoch)、温度(float)、センサーIDが含まれる場合があります。 256スロット付きの固定レコードプールを使用して、あまりにも小さいです。 ここでは、が必要です。 レコードあたり28バイト(1レコードあたり4 + 4 + 4 + 4 + 4 + 4オーバーヘッド)、40 KB、および40 MB、および40 MB、RAMを使用することができます。

エンジンはリングバッファーのファッションでデータを保存することができます。プールがいっぱいになると、最も古いレコードは上書きされます。次の書き込みスロットと最も古いアクティブレコードの「テール」の「ヘッド」ポインタを実装します。これは、フリーリストロジックを避け、O(1)インサートを提供します。レコードがクロノロジー順に保存されている場合、検索はタイムスタンプのバイナリ検索で最適化できます。

typedef struct {
 uint32_t timestamp;
 float temp_c;
 uint8_t sensor_id;
 uint8_t active; // not needed if using ring buffer
} TemperatureRecord;

#define MAX_LOGS 1440
TemperatureRecord logs[MAX_LOGS];
uint16_t head = 0; // next write position
uint16_t count = 0; // number of valid records

読書をインサート: [に書きます, 増分 ] modulo ]]]], 増分 ]]). 特定のタイムスタンプを検索します: カウント = = = = MAX LOGS の場合, ログは、ヘッドからヘッドへ 1 (ラップ) までの連続したシーケンスです。 仮想開始を計算した後にバイナリ検索を使用してください。 このパターンは、非常に軽量で、および追加のワークフローに使用されます。 [FLTR1]

ターゲットハードウェアのテストと最適化

常に、実際の組み込みハードウェアでデータベースエンジンをテストします。エミュレータは、特にフラッシュ書き込みサイクルとパワーロスシナリオのタイミング制約を欠きます。プロファイリングツールを使用してRAMの使用を監視し、エッジケースを検証します。フルストレージ、破損したデータ、中書き込みをリセットします。シンプルなテストハーネスは、何千ものランダムインサート、検索、および削除を実行し、ゴールデンモデルと比較します。

  • []フラッシュウェアレベリング - EEPROMまたはNORフラッシュに書き込むと、合計が数百千に書き込みます。 摩耗層の丸い緩衝は寿命を延ばします。
  • [] 電源異常安全[] - コミットマークを使用してください:レコードの完全なバッチ後にフラグバイトを書きます。 再起動時に、フラグを確認してください。 欠落した場合は、最後のバッチを破棄し、以前の状態に戻します。
  • []メモリープール] - ディープリカーションを避けます。 関数呼び出しスタックの浅いままにします。 ファイルI/Oの静的バッファを使用します。

コンテンツ

組み込みシステム用のCの基本的なデータベースエンジンを構築することは、リソース制限されたデバイス内のデータを管理するための実用的なアプローチです。固定レコードプールやリングバッファなどの単純なデータ構造に焦点を当てることで、開発者は効率的なCRUD操作を最小限のオーバーヘッドで実現します。オプションのインデックス作成と基本的なパージメントを追加すると、信頼できるローカルデータストアに単純な配列が変わります。この技術は、ここに小さなセンサーロガーからより洗練されたシステムまでスケールを記述し、より大きなデータベースがSQLkeiteやDB Berleyなどの大規模データベースをどのように動作するかを理解するための基礎を提供します。