pw_kvs#

Lightweight, persistent key-value store

Stable C++17 Code Size Impact: ~12 kB

#include <cstddef>

#include "pw_kvs/flash_test_partition.h"
#include "pw_kvs/key_value_store.h"
// Not a required dep; just here for demo comms
#include "pw_sys_io/sys_io.h"

// Create our key-value store (KVS). Sector and entry vals for this
// demo are based on @pigweed//pw_kvs:fake_flash_64_aligned_partition
constexpr size_t kMaxSectors = 6;
constexpr size_t kMaxEntries = 64;
static constexpr pw::kvs::EntryFormat kvs_format = {
  .magic = 0xd253a8a9,  // Prod apps should use a random number here
  .checksum = nullptr
};
pw::kvs::KeyValueStoreBuffer<kMaxEntries, kMaxSectors> kvs(
  &pw::kvs::FlashTestPartition(),
  kvs_format
);

kvs.Init();  // Initialize our KVS
std::byte in;
pw::sys_io::ReadByte(&in).IgnoreError();  // Get a char from the user
kvs.Put("in", in);  // Save the char to our key-value store (KVS)
std::byte out;
kvs.Get("in", &out);  // Test that the KVS stored the data correctly
pw::sys_io::WriteByte(out).IgnoreError();  // Echo the char back out
cc_binary(
    name = "app",
    srcs = ["app.cc"],
    # ...
    deps = [
        # ...
        "@pigweed//pw_kvs",
        "@pigweed//pw_kvs:fake_flash_64_aligned_partition",
        # ...
    ]
    # ...
)

pw_kvs is a flash-backed, persistent key-value storage (KVS) system with integrated wear leveling. It’s a relatively lightweight alternative to a file system.

Get started

Integrate pw_kvs into your project

Reference

pw_kvs API reference

Design

How pw_kvs manages state, does garbage collection, etc.

Code size analysis

The code size impact of using pw_kvs in your project

Get started#

Add @pigweed//pw_kvs to your target’s deps:

cc_binary(
  # ...
  deps = [
    # ...
    "@pigweed//pw_kvs",
    # ...
  ]
)

Add $dir_pw_kvs to the deps list in your pw_executable() build target:

pw_executable("...") {
  # ...
  deps = [
    # ...
    "$dir_pw_kvs",
    # ...
  ]
}

Link your library to pw_kvs:

add_library(my_lib ...)
target_link_libraries(my_lib PUBLIC pw_kvs)

Use pw_kvs in your C++ code:

#include "pw_kvs/key_value_store.h"

// ...

Implement the flash memory and flash partition interfaces for your hardware. See pw_kvs/flash_memory.h.

Reference#

Moved: pw_kvs

Design#

pw::kvs::KeyValueStore (“the KVS”) stores key and value data pairs. The key-value pairs are stored in flash partition as a key-value entry (KV entry) that consists of a header/metadata, the key data, and value data. KV entries are accessed through Put(), Get(), and Delete() operations.

Key-value entries#

Each key-value (KV) entry consists of a header/metadata, the key data, and value data. Individual KV entries are contained within a single flash sector; they do not cross sector boundaries. Because of this the maximum KV entry size is the partition sector size.

KV entries are appended as needed to sectors, with append operations spread over time. Each individual KV entry is written completely as a single high-level operation. KV entries are appended to a sector as long as space is available for a given KV entry. Multiple sectors can be active for writing at any time.

When an entry is updated, an entirely new entry is written to a new location that may or may not be located in the same sectore as the old entry. The new entry uses a transaction ID greater than the old entry. The old entry remains unaltered “on-disk” but is considered “stale”. It is garbage collected at some future time.

State#

The KVS does not store any data/metadata/state in flash beyond the KV entries. All KVS state can be derived from the stored KV entries. Current state is determined at boot from flash-stored KV entries and then maintained in RAM by the KVS. At all times the KVS is in a valid state on-flash; there are no windows of vulnerability to unexpected power loss or crash. The old entry for a key is maintained until the new entry for that key is written and verified.

Each KV entry has a unique transaction ID that is incremented for each KVS update transaction. When determining system state from flash-stored KV entries, the valid entry with the highest transaction ID is considered to be the “current” entry of the key. All stored entries of the same key with lower transaction IDs are considered old or “stale”.

Updates/rewrites of a key that has been previously stored is done as a new KV entry with an updated transaction ID and the new value for the key. The internal state of the KVS is updated to reflect the new entry. The previously stored KV entries for that key are not modified or removed from flash storage, until garbage collection reclaims the “stale” entries.

Garbage collection is done by copying any currently valid KV entries in the sector to be garbage collected to a different sector and then erasing the sector.

Flash sectors#

Each flash sector is written sequentially in an append-only manner, with each following entry write being at a higher address than all of the previous entry writes to that sector since erase. Once information (header, metadata, data, etc.) is written to flash, that information is not modified or cleared until a full sector erase occurs as part of garbage collection.

Individual KV entries are contained within a single flash sector; they do not cross sector boundaries. Flash sectors can contain as many KV entries as fit in the sector.

Sectors are the minimum erase size for both Flash memory and Flash partitions. Partitions may have a different logical sector size than the memory they are part of. Partition logical sectors may be smaller due to partition overhead (encryption, wear tracking, etc) or larger due to combining raw sectors into larger logical sectors.

Storage layers#

The flash storage used by the KVS is comprised of two layers, flash memory and flash partitions.

Flash memory#

pw::kvs::FlashMemory is the lower storage layer that manages the raw read/write/erase of the flash memory device. It is an abstract base class that needs a concrete implementation before it can be used.

pw::kvs::FakeFlashMemory is a variant that uses RAM rather than flash as the storage media. This is helpful for reducing physical flash wear during unit tests and development.

Flash partitions#

pw::kvs::FlashPartition is a subset of a pw::kvs::FlashMemory. Flash memory may have one or multiple partition instances that represent different parts of the memory, such as partitions for KVS, OTA, snapshots/crashlogs, etc. Each partition has its own separate logical address space starting from zero to size bytes of the partition. Partition logical addresses do not always map directly to memory addresses due to partition encryption, sector headers, etc.

Partitions support access via pw::kvs::NonSeekableWriter and pw::kvs::SeekableReader. The reader defaults to the full size of the partition but can optionally be limited to a smaller range.

pw::kvs::FlashPartition is a concrete class that can be used directly. It has several derived variants available, such as pw::kvs::FlashPartitionWithStats and pw::kvs::FlashPartitionWithLogicalSectors.

Alignment#

Writes to flash must have a start address that is a multiple of the flash write alignment. Write size must also be a multiple of flash write alignment. Write alignment varies by flash device and partition type. Reads from flash do not have any address or size alignment requirement; reads always have a minimum alignment of 1.

Flash partitions may have a different alignment than the Flash memory they are part of, so long as the partition’s alignment is a multiple of the alignment for the memory.

Allocation#

The KVS requires more storage space than the size of the key-value data stored. This is due to the always-free sector required for garbage collection and the “write and garbage collect later” approach it uses.

The KVS works poorly when stored data takes up more than 75% of the available storage. It works best when stored data is less than 50%. Applications that need to do garbage collection at scheduled times or that write very heavily can benefit from additional flash store space.

The flash storage used by the KVS is multiplied by the amount of Redundancy used. A redundancy of 2 will use twice the storage, for example.

Redundancy#

The KVS supports storing redundant copies of KV entries. For a given redundancy level (N), N total copies of each KV entry are stored. Redundant copies are always stored in different sectors. This protects against corruption or even full sector loss in N-1 sectors without data loss.

Redundancy increases flash usage proportional to the redundancy level. The RAM usage for KVS internal state has a small increase with redundancy.

Garbage collection#

Storage space occupied by stale Key-value entries is reclaimed and made available for reuse through a garbage collection process. The base garbage collection operation is done to reclaim one sector at a time.

The KVS always keeps at least one sector free at all times to ensure the ability to garbage collect. This free sector is used to copy valid entries from the sector to be garbage collected before erasing the sector to be garbage collected. The always-free sector is rotated as part of the KVS wear leveling.

Garbage collection can be performed manually, by invoking the methods below, or it can be configured to happen automatically.

Wear leveling (flash wear management)#

Wear leveling is accomplished by cycling selection of the next sector to write to. This cycling spreads flash wear across all free sectors so that no one sector is prematurely worn out.

The wear leveling decision-making process follows these guidelines:

  • Location of new writes/rewrites of KV entries will prefer sectors already in-use (partially filled), with new (blank) sectors used when no in-use sectors have large enough available space for the new write.

  • New (blank) sectors selected cycle sequentially between available free sectors.

  • The wear leveling system searches for the first available sector, starting from the current write sector + 1 and wraps around to start at the end of a partition. This spreads the erase/write cycles for heavily written/rewritten KV entries across all free sectors, reducing wear on any single sector.

  • Erase count is not considered in the wear leveling decision-making process.

  • Sectors with already written KV entries that are not modified will remain in the original sector and not participate in wear-leveling, so long as the KV entries in the sector remain unchanged.

Code size analysis#

The following size report details the memory usage of KeyValueStore and FlashPartition.

Label

Segment

Delta

KeyValueStore

FLASH

+336

[section .rodata]

+12

main

-26

pw::kvs::FlashPartition::Erase()

-2

pw::stream::CalculateSeek()

-2

pw::kvs::FlashPartition::size_bytes()

DEL

-26

pw::kvs::FlashPartition::IsErased()

DEL

-16

_GLOBAL__sub_I_base_with_only_flash.cc

-2

pw::kvs::FakeFlashMemory::~FakeFlashMemory()

-2

__llvm_libc_21_0_0_git::internal::exit()

DEL

-8

__aeabi_memmove

NEW

+576

pw::kvs::KeyValueStore::InitializeMetadata()

NEW

+368

pw::kvs::KeyValueStore::Init()

NEW

+308

pw::kvs::internal::EntryCache::Find()

NEW

+308

pw::kvs::internal::Sectors::Find()

NEW

+288

pw::kvs::KeyValueStore::WriteEntry()

NEW

+268

pw::kvs::internal::Sectors::FindSectorToGarbageCollect()

NEW

+264

pw::kvs::KeyValueStore::UpdateEntriesToPrimaryFormat()

NEW

+252

pw::kvs::KeyValueStore::FullMaintenanceHelper()

NEW

+228

pw::kvs::internal::EntryCache::AddNewOrUpdateExisting()

NEW

+200

pw::kvs::KeyValueStore::GarbageCollectSector()

NEW

+200

pw::kvs::KeyValueStore::RemoveDeletedKeyEntries()

NEW

+196

pw::kvs::internal::Entry::VerifyChecksumInFlash()

NEW

+172

pw::kvs::KeyValueStore::GetSectorForWrite()

NEW

+172

pw::kvs::internal::EntryCache::RemoveEntry()

NEW

+168

pw::kvs::KeyValueStore::AppendEntry()

NEW

+168

pw::kvs::internal::Entry::CalculateChecksumFromFlash()

NEW

+152

pw::AlignedWriter::Write()

NEW

+152

pw::kvs::KeyValueStore::FullMaintenanceHelper()::$_0::operator()()

NEW

+144

pw::kvs::KeyValueStore::FixedSizeGet()

NEW

+144

pw::kvs::KeyValueStore::PutBytes()

NEW

+132

pw::kvs::KeyValueStore::CopyEntryToSector()

NEW

+132

pw::kvs::internal::Entry::Copy()

NEW

+128

pw::kvs::KeyValueStore::WriteEntryForNewKey()

NEW

+124

pw::kvs::internal::Entry::Read()

NEW

+122

pw::kvs::KeyValueStore::RelocateEntry()

NEW

+120

pw::kvs::KeyValueStore::GetStorageStats()

NEW

+116

pw::kvs::KeyValueStore::LoadEntry()

NEW

+116

pw::kvs::KeyValueStore::ReadEntry()

NEW

+116

pw::kvs::KeyValueStoreBuffer<>::KeyValueStoreBuffer()

NEW

+114

pw::kvs::KeyValueStore::AddRedundantEntries()

NEW

+112

pw::kvs::KeyValueStore::CheckForErrors()

NEW

+112

pw::kvs::KeyValueStore::RepairCorruptSectors()

NEW

+104

pw::kvs::KeyValueStore::UpdateKeyDescriptor()

NEW

+104

pw::kvs::internal::Entry::ValueMatches()

NEW

+100

pw::kvs::internal::Entry::CalculateChecksum()

NEW

+98

pw::AlignedWrite<>()

NEW

+98

pw::kvs::KeyValueStore::Get()

NEW

+98

pw::kvs::KeyValueStore::ScanForEntry()

NEW

+96

pw::kvs::KeyValueStore::EnsureEntryRedundancy()

NEW

+88

pw::Vector<>::operator[]()

NEW

+86

pw::kvs::internal::Entry::Entry()

NEW

+84

pw::kvs::internal::Entry::ReadValue()

NEW

+80

pw::kvs::internal::Entry::AddPaddingBytesToChecksum()

NEW

+76

pw::kvs::KeyValueStore::RelocateKeyAddressesInSector()

NEW

+76

pw::kvs::internal::EntryCache::AddNew()

NEW

+72

pw::kvs::KeyValueStore::CreateOrUpdateKeyDescriptor()

NEW

+70

pw::AlignedWriter::Flush()

NEW

+68

pw::kvs::KeyValueStore::EnsureFreeSectorExists()

NEW

+66

pw::kvs::KeyValueStore::GetAddressesForWrite()

NEW

+64

pw::kvs::KeyValueStore::CreateEntry()

NEW

+60

pw::kvs::internal::Entry::Write()

NEW

+58

pw::AlignedWriter::AddBytesToBuffer()

NEW

+58

pw::kvs::KeyValueStore::KeyValueStore()

NEW

+58

pw::kvs::internal::EntryMetadata::RemoveAddress()

NEW

+56

pw::kvs::internal::Entry::VerifyChecksum()

NEW

+56

pw::kvs::internal::Entry::size()

NEW

+54

pw::kvs::KeyValueStore::FixErrors()

NEW

+52

pw::AlignDown()

NEW

+50

pw::kvs::KeyValueStore::WriteEntryForExistingKey()

NEW

+48

pw::AlignUp()

NEW

+48

std::__2::basic_string_view<>::compare()

NEW

+46

pw::kvs::internal::Sectors::Reset()

NEW

+44

_GLOBAL__sub_I_with_kvs.cc

NEW

+44

pw::kvs::KeyValueStore::FindEntry()

NEW

+44

pw::kvs::internal::Sectors::AddressInSector()

NEW

+42

pw::kvs::internal::EntryCache::Iterator<>::operator*()

NEW

+40

pw::AlignedWriterBuffer<>::AlignedWriterBuffer<>()

NEW

+40

pw::kvs::KeyValueStore::ValueSize()

NEW

+40

pw::kvs::internal::Entry::Tombstone()

NEW

+40

pw::kvs::internal::Entry::descriptor()

NEW

+40

pw::kvs::internal::EntryCache::FindIndex()

NEW

+40

pw::kvs::internal::EntryMetadata::Reset()

NEW

+40

pw::kvs::internal::Hash()

NEW

+40

pw::kvs::internal::Sectors::NextWritableAddress()

NEW

+38

pw::kvs::ChecksumAlgorithm::Verify()

NEW

+38

pw::kvs::KeyValueStore::GarbageCollect()

NEW

+38

pw::kvs::internal::EntryCache::ResetAddresses()

NEW

+38

pw::kvs::internal::EntryCache::addresses()

NEW

+36

pw::kvs::FlashPartition::Input::DoRead()

NEW

+36

pw::kvs::KeyValueStore::Repair()

NEW

+36

pw::kvs::internal::Entry::Valid()

NEW

+36

pw::kvs::internal::EntryCache::present_entries()

NEW

+34

memcmp

NEW

+34

pw::AlignedWriter::AlignedWriter()

NEW

+34

pw::kvs::FlashPartition::Output::DoWrite()

NEW

+34

pw::kvs::internal::Sectors::FindSpaceDuringGarbageCollection()

NEW

+32

_ZNSt3__2eqB8nn210000IcNS_11char_traitsIcEEEEbNS_17basic_string_viewIT_T0_EES6_

NEW

+32

pw::kvs::KeyValueStore::FindExisting()

NEW

+32

pw::kvs::internal::(anonymous namespace)::Contains<>()

NEW

+32

pw::kvs::internal::EntryCache::AddAddressIfRoom()

NEW

+32

pw::kvs::internal::Sectors::FromAddress()

NEW

+32

pw::kvs::internal::Sectors::WearLeveledSectorFromIndex()

NEW

+30

pw::Vector<>::emplace_back<>()

NEW

+30

pw::kvs::FlashPartition::AppearsErased()

NEW

+30

pw::kvs::internal::Entry::Update()

NEW

+26

pw::Vector<>::Append()

NEW

+26

pw::kvs::internal::Entry::ReadKey()

NEW

+26

pw::kvs::internal::EntryFormats::Find()

NEW

+26

pw::kvs::internal::Sectors::FindSpace()

NEW

+24

pw::kvs::internal::Entry::ReadKey<>()

NEW

+22

pw::kvs::internal::Sectors::BaseAddress()

NEW

+20

_ZN2pw3kvs13KeyValueStore3PutIjTnPNSt3__29enable_ifIXntsr14ConvertsToSpanIT_EE5valueEvE4typeELPv0EEENS_6StatusERKNS3_17basic_string_viewIcNS3_11char_traitsIcEEEERKS5_

NEW

+20

pw::kvs::KeyValueStore::Get<>()

NEW

+18

_ZNSt3__26__findB8nn210000IPPKN2pw3kvs8internal16SectorDescriptorES7_PS4_NS_10__identityEEET_SA_T0_RKT1_RT2_

NEW

+12

pw::Padding()

NEW

+12

pw::Vector<>::assign()

NEW

+12

pw::kvs::FlashPartition::Input

NEW

+12

pw::kvs::FlashPartition::Output

NEW

+12

pw::kvs::internal::Entry::next_address()

NEW

+12

pw::kvs::internal::EntryFormats::KnownMagic()

NEW

+12

pw::kvs::internal::Sectors::set_last_new_sector()

NEW

+10

_ZNSt3__24findB8nn210000IPPKN2pw3kvs8internal16SectorDescriptorEPS4_EET_S9_S9_RKT0_

NEW

+10

pw::kvs::KeyValueStore::HeavyMaintenance()

NEW

+8

__aeabi_memmove4

NEW

+8

kvs_format

+10,144

RAM

DEL

-4

test_partition

DEL

-3

is_erased

+3

is_set

NEW

+992

kvs

NEW

+4

kvs_entry_count

+992

FlashPartition

FLASH

+464

[section .rodata]

+24

[section .text]

+64

main

+2

pw::protobuf::WriteLengthDelimitedKeyAndLengthPrefix()

+2

pw::stream::CalculateSeek()

-16

__pre_init_runtime_init_per_core_irq_priorities

+6

operator delete()

+8

vClearInterruptMaskFromISR

NEW

+184

pw::kvs::FakeFlashMemory::Write()

NEW

+154

pw::kvs::FlashPartition::Erase()

NEW

+152

pw::kvs::FlashPartition::Write()

NEW

+116

pw::kvs::FlashPartition::FlashPartition()

NEW

+108

pw::kvs::FlashPartition::IsRegionErased()

NEW

+100

pw::kvs::FakeFlashMemory::Erase()

NEW

+96

pw::kvs::FlashError::Check()

NEW

+68

pw::kvs::FakeFlashMemory::Read()

NEW

+68

pw::kvs::FakeFlashMemoryBuffer<>::FakeFlashMemoryBuffer()

NEW

+60

pw::kvs::FlashMemory::FlashMemory()

NEW

+56

pw::kvs::FakeFlashMemory::FakeFlashMemory()

NEW

+56

pw::kvs::FlashPartition::CheckBounds()

NEW

+52

pw::kvs::FlashPartition::Read()

NEW

+48

pw::kvs::FakeFlashMemory

NEW

+48

pw::kvs::FakeFlashMemoryBuffer<>

NEW

+48

pw::kvs::FlashMemory

NEW

+48

pw::kvs::FlashPartition

NEW

+44

pw::kvs::FakeFlashMemory::FlashAddressToMcuAddress()

NEW

+32

_GLOBAL__sub_I_fake_flash_test_partition.cc

NEW

+28

pw::kvs::FlashPartition::size_bytes()

NEW

+26

pw::kvs::FlashPartition::IsErased()

NEW

+24

__aeabi_uidivmod

NEW

+22

pw::kvs::FlashPartition::PartitionToFlashAddress()

NEW

+16

_GLOBAL__sub_I_base_with_only_flash.cc

NEW

+12

pw::kvs::FakeFlashMemory::~FakeFlashMemory()

NEW

+12

pw::kvs::FakeFlashMemoryBuffer<>::~FakeFlashMemoryBuffer()

NEW

+12

pw::kvs::FlashPartition::~FlashPartition()

NEW

+8

pw::kvs::FlashTestPartition()

NEW

+6

pw::kvs::FlashMemory::FlashAddressToMcuAddress()

NEW

+6

pw::kvs::FlashMemory::SelfTest()

NEW

+6

pw::kvs::FlashPartition::sector_size_bytes()

NEW

+4

pw::kvs::FakeFlashMemory::Disable()

NEW

+4

pw::kvs::FakeFlashMemory::Enable()

NEW

+4

pw::kvs::FakeFlashMemory::IsEnabled()

NEW

+4

pw::kvs::FlashMemory::~FlashMemory()

NEW

+4

pw::kvs::FlashPartition::Init()

NEW

+4

pw::kvs::FlashPartition::sector_count()

NEW

+2

__cxa_pure_virtual

+2,296

RAM

-3

is_set

NEW

+384

pw::kvs::(anonymous namespace)::test_flash

NEW

+24

pw::kvs::(anonymous namespace)::test_partition

NEW

+4

test_partition

NEW

+3

is_erased

+412