임베디드 시스템은 종종 전체 데이터베이스 서버의 오버 헤드없이 로컬 데이터 관리가 필요합니다. 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으로 비휘발성 메모리에서 풀을 읽으며, 폐쇄 (또는 정기적으로) 다시 작성합니다.

Data Integrity Checks에 대한 정보

손상을 감지하기 위해 각 레코드에 간단한 체크섬 필드를 추가하십시오. ]CRC-32은 임베디드 시스템의 좋은 선택이며 오류 감지 강도와 복잡성을 갖추는 것입니다.

Basic CRUD 운영

레코드 풀 정의, 삽입, 찾기, 업데이트 및 삭제 레코드를 구현하는 기능을 구현합니다. 검색 작업은 종종 성능 목, 그래서 네이티브 선형 검색은 작은 데이터베이스에만 허용됩니다 (몇 백 레코드).

Free-List 관리

인덱스의 무료 목록 유지. 삽입에, 인덱스를 무료로 목록에서 팝업, 기록을 채우고, 그것을 활성화합니다. 무료 목록 자체는 정수의 배열을 사용하여 간단한 스택이 될 수 있습니다.

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;
}

고급 개념 : 색인 및 Persistence

기록의 수로 성장, 선형 검색 비싼. 간단한 인덱스를 추가-키 포인터의 정렬 된 배열 또는 바이너리 검색 트리-임프로브 검색 시간. 임베디드 시스템에 대 한, 이진 검색 키에 정렬 된 정적 배열은 종종 삽입이 불순하면 충분.

검색 키 (예, ID)로 분류 된 레코드 인덱스의 병렬 배열을 유지. 새로운 레코드를 삽입 할 때, 바이너리 삽입을 사용하여 정렬 된 배열에 인덱스를 삽입합니다. 그런 다음 검색은 O (log n) 바이너리 검색을 통해. 삭제는 인덱스 어레이를 이동해야하지만, 작은 데이터베이스에 대한이 허용됩니다.

File I/O를 사용하는 Persistent Storage

파일 시스템없이 마이크로 컨트롤러에서, 원시 플래시 메모리 쓰기는 일반적입니다. 리눅스 기반 임베디드 시스템에서, 표준 POSIX // 잘 작동. 간단한 파일 형식을 사용: 헤더를 작성 (마법 번호, 버전, 기록 카운트) 원시 풀 배열에 의해 후. 충돌 탄력, 쓰기 머리 로그 (WAL) 도움이 될 수 있습니다, 하지만 기본 엔진에 대한 (RTC)는 하나의 플래시 시스템 [FLT].FLT:1].FLT:2.

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 컴포지트 – 보통 필요하지 않습니다. 간단한 원자는 대부분의 센서 데이터 로깅에 대한 suffice를 작성합니다.
  • Indexing – 삽입 비용 추가, 읽기 속도. 쓰기 무거운 작업 부하에 대 한, 인덱스 건너뛰기.
  • Concurrency – 대부분의 임베디드 시스템은 단일 스레드를 실행합니다. RTOS를 사용하는 경우 레버리지 뮤텍스트.
  • Memory 발자국 – 정적 배지는 역동적인 보다 더 안전합니다. 버퍼 크기에 대한 상수도를 사용하십시오.
  • Power loss – 플래시 스토리지를 위해, 빈번한 작은 쓰기를 피합니다. 배치 업데이트 및 더블 버퍼 계획을 사용합니다.

Practical 예제: 온도 로거 데이터베이스

기록 보관소에는 매일 분마다 기록되는 IoT 온도 센서가 고려되며 24 시간 동안 로컬로 저장합니다. 데이터베이스 엔진은 1440 레코드 (분당 1개)를 처리해야 합니다. 각 기록은 타임탬프 (유닉스 epoch), 온도 (float) 및 센서 ID를 포함할 수 있습니다. 고정 레코드 풀을 사용하여 256 슬롯이 너무 작습니다. 여기에는 가 필요합니다. 레코드당 28 바이트 (4+4+4+4+4+4)로 풀은 40KB의 많은 마이크를 사용하여 많은 양의 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

읽기 삽입: , increment ] modulo , increment ] (]). 특정 타임스탬프 검색 : count == MAX LOGS가 있다면 로그는 head-1 (wrap)에 연속적인 순서입니다. 가상 시작을 컴퓨팅 후 바이너리 검색을 사용합니다. 이 패턴은 매우 가벼워지기 쉬운 패턴을 제공합니다.[FLT]]] [FLT:FLT:]]]] [FLT:]]]]]

Target Hardware에 대한 테스트 및 최적화

항상 실제 임베디드 하드웨어에 데이터베이스 엔진을 테스트합니다. 에뮬레이터는 타이밍 제약을 놓고, 특히 플래시 쓰기 사이클과 전력 손실 시나리오를 사용합니다. 프로파일링 도구와 RAM 사용량을 모니터하고 가장자리 사례를 확인 : 전체 저장, 손상된 데이터, 초기 글쓰기. 간단한 테스트 하네스는 수천의 임의 삽입, 검색 및 삭제를 실행하면서 황금 모델에 비교합니다.

  • 플래시웨어 레벨링 – EEPROM 또는 NOR 플래시로 작성하면, 총 100만에 기록됩니다. 착용층의 원형 버퍼는 수명을 연장합니다.
  • Power-fail safe – 커밋 마크를 사용합니다: 레코드의 전체 배치 후 플래그 바이트를 작성합니다. 재시작시, 플래그를 확인; 누락된 경우, 마지막 배치를 삭제하고 이전 상태로 돌려줍니다.
  • Memory Pooling – 깊은 재순환을 피하십시오. 함수 호출 스택을 유지하십시오. I/O 파일을 위한 정체되는 완충기를 사용하십시오.

관련 기사

C에서 기본 데이터베이스 엔진을 내장한 시스템은 리소스 제한 장치에서 데이터를 관리하는 데 실질적인 접근법입니다. 고정 레코드 풀과 링 버퍼와 같은 간단한 데이터 구조에 집중함으로써 개발자는 최소한의 오버 헤드와 효율적인 CRUD 작업을 달성합니다. 옵션 색인 및 기본 지속률을 추가하면 신뢰할 수있는 로컬 데이터 저장소에 간단한 어레이를 전환합니다. 이 기술은 여기에 작은 센서 로거에서 더 정교한 시스템에 이르기까지 규모를 설명하고 SQLite 또는 Berkehood와 같은 더 큰 임베디드 데이터베이스를 이해하기위한 기반을 제공합니다.