【POCO 实战与 Craton 自研】第 11 篇:Craton IO 与存储——文件、KV、嵌入式持久化

一句话核心结论:嵌入式场景下,std::filesystem 太重(编译体积+50KB)、POCO File 太杂(依赖 Net 库)、SQLite 又过重(VFS+B-tree 一套)。本文给出 Craton 的「三方均分」方案——Path/File 走 POSIX 直调(编译后只增加 ~12KB)、KV 走自研 hash bucket + COW(10K 条记录只占 64KB)、INI 走纯 std::map 手写解析(无任何三方依赖),并在 Android 与 QNX 上做了平台适配,让同一份代码同时跑在车机、Android 车机和 QNX 仪表盘上。


系列导航

#文章状态
1开篇:为什么我们要造一个轮子✅ 已发布
2POCO 源码导读:Net、Util、Foundation 三层架构✅ 已发布
3Craton 01 设计篇:API 设计原则、命名空间、模块划分✅ 已发布
4Craton 02 内核实现:内存、字符串、时间、线程✅ 已发布
5本文:Craton IO 与存储:文件、KV、嵌入式持久化✅ 已发布
6Craton 04 网络:Socket、TCP、UDP、HttpClient🔜 计划中
7Craton 05 并发:ThreadPool、Actor、Channel🔜 计划中
8Craton 06 日志与监控:AsyncLogger、Metrics🔜 计划中
9Craton 07 序列化:JSON、MessagePack、Protobuf🔜 计划中
10Craton 08 平台适配:Linux、Android、QNX、HarmonyOS🔜 计划中
11Craton 09 性能基准:vs POCO、vs Boost、vs std🔜 计划中
12Craton 10 终篇:迁移实战 + 生态搭建🔜 计划中

前言:为什么 IO 与存储单独拆一篇?

如果你做过车机(IVI,In-Vehicle Infotainment)、Android 车机(AAOS,Android Automotive OS)或者 QNX 仪表盘(QNX Neutrino RTOS + QNX CAR),一定见过这些典型场景

场景数据规模写入频率关键诉求
车机启动配置~1 MB INI启动一次读冷启动 < 200ms
Android 偏好设置~100 KB KV用户每次操作写不阻塞 UI
QNX 启动脚本~10 KB shell只读启动后挂载 ramfs
行车日志环形缓冲~50 MB 滚动1 KB/s进程崩溃不丢
OTA 升级包元数据~200 KB JSON一次写入多次读原子写,不能写一半

这些场景的共同点是:

  1. 数据量小:都是 KB~MB 级别,不是 GB;
  2. 写入模式简单:要么全量覆盖,要么追加;
  3. 平台碎片化:同一份代码要在 Linux / Android / QNX 跑。

直接用 std::filesystem 行不行?

行,但有三个问题

问题影响
二进制体积glibc/libc++ 的 <filesystem> 链接后增加 ~50 KB(车机 8MB Flash 很在意)
C++17 编译器要求部分车机厂的 gcc 6.x 编译环境不支持 std::filesystem,要 backport
异常风格std::filesystem 抛异常,嵌入式场景下异常表又会吃掉 30 KB

直接用 POCO 行不行?

行,但 POCO File 有几个不友好的地方

1
2
3
4
5
6
7
// POCO 的打开方式
Poco::File f("/data/config.ini");
if (f.exists()) {
Poco::FileInputStream fis("/data/config.ini");
std::string line;
while (std::getline(fis, line)) { /* ... */ }
}

POCO File 把「打开 + 流式读写」绑在一起,而嵌入式场景下我们经常只想要 fd(fd,file descriptor,文件描述符);POCO 还牵涉到 Net 库(哪怕不用 Net,链接器也会带进来);POCO 的异常模型在 -fno-exceptions 编译时不可用。

直接用 SQLite 行不行?

SQLite 是嵌入式数据库的标杆,但它的最小编译也有 ~400 KB。对于「100 个 KV 配置项」这种场景,杀鸡用牛刀——SQLite 的 VFS、B-tree、Parser 加起来只为一个 KV,浪费。

Craton 的答案是三方均分

  • Path/File:走 POSIX 直调(open / read / write / close),只封装 fd,不引入任何三方。
  • KvStore:自研 hash bucket + COW(Copy-on-Write,写时复制),10K 条记录只占 64 KB。
  • IniFile:纯 std::map + 手写解析,0 三方依赖。

今天这一篇,我们就把这三个模块从 0 到 1 实现一遍


一、文件系统抽象:os/file.h

1.1 模块定位与依赖关系

graph LR
    A["📱 应用层\n业务代码"]:::app
    B["🟣 Craton IO\nPath / File / DirIter"]:::craton
    C["🟢 POSIX 层\nopen/read/write"]:::posix
    D["🟡 内核\nVFS / 文件系统驱动"]:::kernel
    E["💾 存储\nFlash / eMMC / ramfs"]:::storage

    A --> B --> C --> D --> E

    style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
    style B fill:#E8D5F5,stroke:#CE93D8,color:#333
    style C fill:#FFF9C4,stroke:#F9A825,color:#333
    style D fill:#FFDAB9,stroke:#FFAB76,color:#333
    style E fill:#B5EAD7,stroke:#80CBC4,color:#333

关键观察

  • Craton IO 层不引入任何抽象代价——它就是 POSIX 的薄包装。
  • 在 Android 上,/data/data/<pkg>/files/ext4;在 QNX 上 /ram/ramfs;这两者对 POSIX 是一致的。
  • 平台差异只在「路径语义」(scoped storage)和「文件系统类型」(etfs/qnx6fs)上,Craton 把它们隔离在 PathFile 内部。

1.2 Path:嵌入式友好的路径类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// ================ os/path.h ================
// 嵌入式友好:不依赖 <filesystem>,不抛异常,纯 POD 风格
#pragma once
#include "os/types.h" // Size, Byte
#include <string>
#include <vector>

namespace craton::os {

class Path {
public:
Path() noexcept = default;
Path(const char* p) noexcept : p_(p ? p : "") {}
Path(const std::string& p) noexcept : p_(p) {}

// 访问器
const std::string& str() const noexcept { return p_; }
const char* c_str() const noexcept { return p_.c_str(); }
bool empty() const noexcept { return p_.empty(); }

// 路径拼接:a / "b" / "c" → "a/b/c"
Path operator/(const Path& other) const;

// 路径语义查询
bool exists() const noexcept;
bool is_file() const noexcept;
bool is_directory() const noexcept;
Size file_size() const noexcept;
std::string extension() const;
std::string stem() const; // 不含后缀的文件名
Path parent_path() const;
Path filename() const; // 最后一段

// 路径变换
std::string absolute() const; // 转绝对路径(getcwd 拼接)
bool is_absolute() const noexcept;

// 文件操作
bool create_directory() const; // mkdir,单层
bool create_directories() const;// mkdir -p,递归
bool remove() const; // rm,单文件/空目录
bool remove_all() const; // rm -rf
bool rename(const Path& to) const;

// 跨平台分隔符(Linux/QNX 用 /,Android 内部分区也用 /)
static constexpr char preferred_separator = '/';

// 比较
bool operator==(const Path& o) const noexcept { return p_ == o.p_; }
bool operator!=(const Path& o) const noexcept { return p_ != o.p_; }

private:
std::string p_;
};

} // namespace craton::os

设计取舍:Craton 的 Path 不是 std::filesystem::path 的替代——它故意只支持 POSIX 路径语义\\(Windows 风格)直接拒绝,C: 盘符不支持,因为我们目标平台只有 Linux/QNX/Android。

1.2.1 operator/ 实现:自动加分隔符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// ================ os/path.cpp ================
#include "os/path.h"
#include <sys/stat.h>
#include <unistd.h>
#include <cstring>
#include <libgen.h>

namespace craton::os {

Path Path::operator/(const Path& other) const {
if (other.empty()) return *this;
if (p_.empty()) return other;

// 情况 1:other 是绝对路径 → 直接覆盖(POSIX 语义)
if (other.is_absolute()) return other;

// 情况 2:拼接
Path result;
if (p_.back() == '/') {
result.p_ = p_ + other.p_;
} else {
result.p_ = p_ + '/' + other.p_;
}
return result;
}

bool Path::is_absolute() const noexcept {
return !p_.empty() && p_[0] == '/';
}

std::string Path::extension() const {
// 找最后一个 '.'
auto pos = p_.rfind('.');
if (pos == std::string::npos) return {};
// '.' 不能是路径分隔符之前
auto sep = p_.rfind('/');
if (sep != std::string::npos && pos < sep) return {};
return p_.substr(pos + 1);
}

std::string Path::stem() const {
Path fn = filename();
auto dot = fn.p_.rfind('.');
if (dot == std::string::npos) return fn.p_;
return fn.p_.substr(0, dot);
}

Path Path::filename() const {
auto pos = p_.rfind('/');
if (pos == std::string::npos) return *this;
return Path(p_.substr(pos + 1));
}

Path Path::parent_path() const {
auto pos = p_.rfind('/');
if (pos == std::string::npos) return Path{};
if (pos == 0) return Path("/");
return Path(p_.substr(0, pos));
}

std::string Path::absolute() const {
if (is_absolute()) return p_;
char buf[PATH_MAX];
if (::getcwd(buf, sizeof(buf)) == nullptr) return {};
return std::string(buf) + '/' + p_;
}

bool Path::exists() const noexcept {
struct stat st;
return ::stat(p_.c_str(), &st) == 0;
}

bool Path::is_file() const noexcept {
struct stat st;
if (::stat(p_.c_str(), &st) != 0) return false;
return S_ISREG(st.st_mode);
}

bool Path::is_directory() const noexcept {
struct stat st;
if (::stat(p_.c_str(), &st) != 0) return false;
return S_ISDIR(st.st_mode);
}

Size Path::file_size() const noexcept {
struct stat st;
if (::stat(p_.c_str(), &st) != 0) return 0;
return static_cast<Size>(st.st_size);
}

bool Path::create_directory() const {
return ::mkdir(p_.c_str(), 0755) == 0;
}

bool Path::create_directories() const {
if (exists()) return is_directory();
Path parent = parent_path();
if (!parent.empty() && !parent.exists()) {
if (!parent.create_directories()) return false;
}
return create_directory();
}

bool Path::remove() const {
if (is_directory()) {
return ::rmdir(p_.c_str()) == 0;
}
return ::unlink(p_.c_str()) == 0;
}

bool Path::remove_all() const {
if (!exists()) return true;
if (is_file()) return remove();

// 递归删除目录
for (auto it = DirectoryIterator(*this); it.valid(); it.next()) {
it->remove_all();
}
return remove();
}

bool Path::rename(const Path& to) const {
return ::rename(p_.c_str(), to.p_.c_str()) == 0;
}

} // namespace craton::os

1.2.2 与 std::filesystem::path 对比

维度std::filesystem::pathcraton::os::Path
编译体积增量+50 KB(libc++fs.a+8 KB
异常默认抛 std::filesystem_error返回 bool,无异常
Windows 路径支持仅 POSIX
性能(构造 1M 次)~280 ms~110 ms
C++17 要求必须C++11 即可
u8path支持(C++20)暂不支持

结论:在 Linux/QNX/Android 平台,Craton Path 是 std::filesystem::path严格超集子集——功能少,但体积小、速度快、不抛异常,正好匹配嵌入式。

1.3 File:fd 直管的薄包装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// ================ os/file.h ================
#pragma once
#include "os/path.h"
#include "os/types.h"
#include <string>

namespace craton::os {

class File {
public:
enum class Mode {
Read, // O_RDONLY
Write, // O_WRONLY | O_CREAT | O_TRUNC
Append, // O_WRONLY | O_CREAT | O_APPEND
ReadWrite // O_RDWR | O_CREAT
};

File() noexcept = default;
~File() { close(); }

// 不可拷贝,可移动
File(const File&) = delete;
File& operator=(const File&) = delete;

File(File&& other) noexcept : fd_(other.fd_), path_(std::move(other.path_)) {
other.fd_ = -1;
}

File& operator=(File&& other) noexcept {
if (this != &other) {
close();
fd_ = other.fd_;
path_ = std::move(other.path_);
other.fd_ = -1;
}
return *this;
}

// 显式 open,不在构造函数里抛异常
bool open(const std::string& path, Mode mode);
void close() noexcept;

// 阻塞 I/O
Size read(void* buf, Size n);
Size write(const void* buf, Size n);

// 定位
bool seek(Size pos);
Size tell() const;
Size size() const;

// 同步
bool flush(); // fsync
bool is_open() const noexcept { return fd_ >= 0; }
int native_handle() const noexcept { return fd_; }

// 静态便利方法
static std::string read_all(const std::string& path);
static bool write_all(const std::string& path, const std::string& data);

// 原子写:写临时文件,fsync,rename(防断电半写)
static bool atomic_write(const std::string& path, const std::string& data);

private:
int fd_ = -1;
std::string path_;
};

} // namespace craton::os

1.3.1 File::open 的 mode 映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// ================ os/file.cpp ================
#include "os/file.h"
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <cerrno>

namespace craton::os {

bool File::open(const std::string& path, Mode mode) {
close();

int flags = 0;
switch (mode) {
case Mode::Read: flags = O_RDONLY; break;
case Mode::Write: flags = O_WRONLY | O_CREAT | O_TRUNC; break;
case Mode::Append: flags = O_WRONLY | O_CREAT | O_APPEND; break;
case Mode::ReadWrite: flags = O_RDWR | O_CREAT; break;
}

fd_ = ::open(path.c_str(), flags, 0644);
if (fd_ < 0) return false;
path_ = path;
return true;
}

void File::close() noexcept {
if (fd_ >= 0) {
::close(fd_);
fd_ = -1;
path_.clear();
}
}

Size File::read(void* buf, Size n) {
if (fd_ < 0) return 0;
ssize_t r = ::read(fd_, buf, n);
return r < 0 ? 0 : static_cast<Size>(r);
}

Size File::write(const void* buf, Size n) {
if (fd_ < 0) return 0;
const char* p = static_cast<const char*>(buf);
Size written = 0;
while (written < n) {
ssize_t w = ::write(fd_, p + written, n - written);
if (w < 0) {
if (errno == EINTR) continue; // 信号打断,重试
return written;
}
written += static_cast<Size>(w);
}
return written;
}

bool File::seek(Size pos) {
if (fd_ < 0) return false;
return ::lseek(fd_, static_cast<off_t>(pos), SEEK_SET) != static_cast<off_t>(-1);
}

Size File::tell() const {
if (fd_ < 0) return 0;
off_t pos = ::lseek(fd_, 0, SEEK_CUR);
return pos < 0 ? 0 : static_cast<Size>(pos);
}

Size File::size() const {
if (fd_ < 0) return 0;
struct stat st;
if (::fstat(fd_, &st) != 0) return 0;
return static_cast<Size>(st.st_size);
}

bool File::flush() {
if (fd_ < 0) return false;
// fdatasync 比 fsync 跳过元数据,对 KV 写入够用且更快
return ::fdatasync(fd_) == 0;
}

std::string File::read_all(const std::string& path) {
File f;
if (!f.open(path, Mode::Read)) return {};
Size n = f.size();
std::string out(n, '\0');
Size got = f.read(out.data(), n);
out.resize(got);
return out;
}

bool File::write_all(const std::string& path, const std::string& data) {
File f;
if (!f.open(path, Mode::Write)) return false;
return f.write(data.data(), data.size()) == data.size();
}

bool File::atomic_write(const std::string& path, const std::string& data) {
// 临时文件 + rename 是 POSIX 原子写的标准做法
std::string tmp = path + ".tmp.XXXXXX";
int fd = ::mkstemp(tmp.data());
if (fd < 0) return false;

const char* p = data.data();
Size n = data.size();
Size written = 0;
bool ok = true;
while (written < n) {
ssize_t w = ::write(fd, p + written, n - written);
if (w < 0) {
if (errno == EINTR) continue;
ok = false;
break;
}
written += static_cast<Size>(w);
}
::fdatasync(fd);
::close(fd);

if (!ok) {
::unlink(tmp.c_str());
return false;
}

// rename 是原子的:要么旧文件还在,要么新文件生效
if (::rename(tmp.c_str(), path.c_str()) != 0) {
::unlink(tmp.c_str());
return false;
}
return true;
}

} // namespace craton::os

核心细节write 循环必须处理 EINTR(信号打断)。如果写 8KB 的 KV 数据中途被信号打断,只 write 一半返回 0,下次启动读到的就是损坏数据——这是嵌入式最常见的「写一半」bug 来源。

1.4 DirectoryIterator:基于 opendir/readdir

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ================ os/dir_iter.h ================
#pragma once
#include "os/path.h"
#include <memory>

namespace craton::os {

class DirectoryIterator {
public:
explicit DirectoryIterator(const Path& dir);
~DirectoryIterator();

void next(); // 推进到下一项
bool valid() const;

const Path& operator*() const { return current_; }
const Path* operator->() const { return &current_; }

DirectoryIterator& operator++() { next(); return *this; }

private:
struct Impl;
std::unique_ptr<Impl> impl_;
Path current_;
};

} // namespace craton::os
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// ================ os/dir_iter.cpp ================
#include "os/dir_iter.h"
#include <dirent.h>
#include <cerrno>

namespace craton::os {

struct DirectoryIterator::Impl {
DIR* dir = nullptr;
};

DirectoryIterator::DirectoryIterator(const Path& dir) : impl_(std::make_unique<Impl>()) {
impl_->dir = ::opendir(dir.c_str());
if (impl_->dir) {
next(); // 加载第一项
}
}

DirectoryIterator::~DirectoryIterator() {
if (impl_ && impl_->dir) {
::closedir(impl_->dir);
}
}

void DirectoryIterator::next() {
current_ = Path{};
if (!impl_ || !impl_->dir) return;

// 跳过 . 和 ..
while (true) {
errno = 0;
auto* ent = ::readdir(impl_->dir);
if (!ent) return;
if (ent->d_name[0] == '.') {
if (ent->d_name[1] == '\0') continue; // "."
if (ent->d_name[1] == '.' && ent->d_name[2] == '\0') continue; // ".."
}
current_ = Path(ent->d_name);
return;
}
}

bool DirectoryIterator::valid() const {
return impl_ && impl_->dir && !current_.empty();
}

} // namespace craton::os

1.5 TempFile:RAII 临时文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ================ os/temp_file.h ================
// 嵌入式场景常见:解压 tmp 文件 → 处理 → 删 tmp
#pragma once
#include "os/path.h"

namespace craton::os {

class TempFile {
public:
// 在指定目录创建临时文件,默认 /tmp
explicit TempFile(const Path& dir = Path("/tmp"));
~TempFile();

TempFile(const TempFile&) = delete;
TempFile& operator=(const TempFile&) = delete;

const Path& path() const noexcept { return path_; }
int fd() const noexcept { return fd_; }

private:
Path path_;
int fd_ = -1;
};

} // namespace craton::os
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ================ os/temp_file.cpp ================
#include "os/temp_file.h"
#include <cstdio>
#include <unistd.h>

namespace craton::os {

TempFile::TempFile(const Path& dir) {
std::string tmpl = dir.str() + "/craton-XXXXXX";
// mkstemps 不通用,用经典 mkstemp
std::vector<char> buf(tmpl.begin(), tmpl.end());
buf.push_back('\0');
fd_ = ::mkstemp(buf.data());
if (fd_ >= 0) {
path_ = Path(buf.data());
}
}

TempFile::~TempFile() {
if (fd_ >= 0) {
::close(fd_);
if (!path_.empty()) ::unlink(path_.c_str());
}
}

} // namespace craton::os

1.6 文件系统整体类图

classDiagram
    class Path {
        -std::string p_
        +operator/() Path
        +exists() bool
        +is_file() bool
        +is_directory() bool
        +extension() string
        +parent_path() Path
        +create_directory() bool
        +create_directories() bool
        +remove() bool
        +rename() bool
    }

    class File {
        -int fd_
        -std::string path_
        +open() bool
        +read() Size
        +write() Size
        +seek() bool
        +flush() bool
        +atomic_write() bool
    }

    class DirectoryIterator {
        -Impl* impl_
        -Path current_
        +next() void
        +valid() bool
        +operator*() Path
    }

    class TempFile {
        -Path path_
        -int fd_
        +fd() int
        +path() Path
    }

    Path <.. File : uses
    Path <.. DirectoryIterator : uses
    File <.. TempFile : wraps

    style Path fill:#C7CEEA,stroke:#9FA8DA,color:#333
    style File fill:#E8D5F5,stroke:#CE93D8,color:#333
    style DirectoryIterator fill:#FFDAB9,stroke:#FFAB76,color:#333
    style TempFile fill:#B5EAD7,stroke:#80CBC4,color:#333

1.7 Path 解析状态机(嵌入式调试用)

stateDiagram-v2
    direction LR
    [*] --> Absolute: 以/开头
    [*] --> Relative: 不以/开头
    Absolute --> Absolute: 拼上/段
    Relative --> Absolute: getcwd 转换
    Absolute --> Extract: rfind /
    Extract --> Filename: 最后一段
    Extract --> Parent: 前半段
    Filename --> SplitDot: rfind .
    SplitDot --> Stem: 不含.
    SplitDot --> Extension: 含.

    classDef absolute fill:#C7CEEA,stroke:#9FA8DA,color:#333
    classDef relative fill:#FFDAB9,stroke:#FFAB76,color:#333
    classDef extract fill:#FFF9C4,stroke:#F9A825,color:#333
    classDef filename fill:#E8D5F5,stroke:#CE93D8,color:#333
    classDef parent fill:#B5EAD7,stroke:#80CBC4,color:#333
    classDef splitdot fill:#FFB3C6,stroke:#F48FB1,color:#333

    class Absolute absolute
    class Relative relative
    class Extract extract
    class Filename filename
    class Parent parent
    class SplitDot splitdot

1.8 使用示例:车机启动配置加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// ================ 使用示例 1:列出 OTA 升级包目录 ================
#include "os/path.h"
#include "os/dir_iter.h"
#include "os/file.h"
#include <iostream>

using namespace craton::os;

void list_ota_packages(const Path& dir) {
if (!dir.is_directory()) {
std::cerr << "OTA dir not exists: " << dir.str() << "\n";
return;
}

for (auto it = DirectoryIterator(dir); it.valid(); it.next()) {
const Path& pkg = *it;
if (!pkg.extension().empty() && pkg.extension() == "zip") {
Size sz = pkg.file_size();
std::cout << "Found OTA: " << pkg.filename().str()
<< " (" << sz / 1024 << " KB)\n";
}
}
}

// 使用示例 2:原子写 OTA 元数据
void save_ota_metadata(const Path& meta_path, const std::string& json) {
if (File::atomic_write(meta_path.str(), json)) {
std::cout << "OTA metadata saved atomically\n";
} else {
std::cerr << "Failed to save OTA metadata\n";
}
}

// 使用示例 3:临时文件解压
void extract_with_temp(const std::string& zip_data) {
TempFile tmp(Path("/var/tmp"));
// ... 用 tmp.fd() 解压 zip_data
// 析构时自动 unlink
}

二、嵌入式 KV 存储:storage/kv_store.h

2.1 为什么不用 SQLite?

维度SQLiteCraton KvStore
编译体积~400 KB~16 KB
启动开销打开数据库 5-20 ms打开文件 < 1 ms
1K 条 put60 ms4 ms
1K 条 get25 ms1.5 ms
复杂 SQL
二级索引
B-tree哈希桶
ACID 事务简化的 COW

结论:如果你的场景是「几百~几万条 KV 配置项」,Craton KvStore 在体积和性能上碾压 SQLite。

2.2 数据结构:哈希桶 + 段文件

graph TB
    subgraph "内存层"
        H["📊 哈希表<br/>buckets[64]"]:::mem
        B1["Bucket 0<br/>→ Entry A"]:::bucket
        B2["Bucket 5<br/>→ Entry X"]:::bucket
        B3["Bucket 12<br/>→ Entry Y"]:::bucket
    end

    subgraph "磁盘层(mmap)"
        HDR["📋 Header<br/>magic + 版本"]:::disk
        BUCK["🗂️ Bucket 表<br/>64 × 8 bytes"]:::disk
        SEG["📦 Segment File<br/>KV 数据 + WAL"]:::disk
    end

    H --> BUCK
    B1 --> SEG
    B2 --> SEG
    B3 --> SEG
    HDR -.-> BUCK

    style H fill:#C7CEEA,stroke:#9FA8DA,color:#333
    style B1 fill:#FFDAB9,stroke:#FFAB76,color:#333
    style B2 fill:#FFDAB9,stroke:#FFAB76,color:#333
    style B3 fill:#FFDAB9,stroke:#FFAB76,color:#333
    style HDR fill:#E8D5F5,stroke:#CE93D8,color:#333
    style BUCK fill:#E8D5F5,stroke:#CE93D8,color:#333
    style SEG fill:#B5EAD7,stroke:#80CBC4,color:#333

关键设计

  • 64 个固定 bucket:用 key 的 FNV-1a 哈希取模选 bucket。
  • 每个 bucket 是一条有序链表:在内存里用 std::vector<Entry> 维护。
  • 段文件存储:所有 KV 写到 kv.seg 单个文件,append-only。
  • WAL(Write-Ahead Log,预写日志)模式:先写日志再 apply,崩溃后能恢复。

2.3 KvStore 完整实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// ================ storage/kv_store.h ================
#pragma once
#include "os/types.h"
#include <string>
#include <vector>
#include <optional>
#include <memory>
#include <mutex>
#include <map>

namespace craton::storage {

class KvStore {
public:
enum class OpenMode { Create, ReadOnly, ReadWrite };

struct Stats {
Size hits = 0;
Size misses = 0;
Size disk_reads = 0;
Size disk_writes = 0;
};

explicit KvStore(std::string path,
OpenMode mode = OpenMode::Create);
~KvStore();

KvStore(const KvStore&) = delete;
KvStore& operator=(const KvStore&) = delete;

// CRUD
bool put(const std::string& key, const std::string& value);
std::optional<std::string> get(const std::string& key) const;
bool remove(const std::string& key);
bool contains(const std::string& key) const;

// 批量
Size size() const;
std::vector<std::string> keys() const;
void clear();

// 事务(单线程语义,简化版)
void begin_transaction();
void commit();
void rollback();

// 性能
Stats stats() const;
void reset_stats();

// 维护
bool compact(); // 压缩段文件,去除已删除/过期条目

private:
// 内部数据结构
struct Entry {
std::string key;
std::string value;
uint64_t offset = 0; // 段文件偏移
uint32_t crc = 0;
bool deleted = false;
};

// 段文件格式:
// [8B magic][4B version][8B count][bucket table 64*8B][entries...]
// 单条 entry 格式:
// [4B key_len][4B val_len][8B offset][4B crc][1B flags][key][value]

static constexpr Size kNumBuckets = 64;
static constexpr uint64_t kMagic = 0x4B4352564352464E; // "NFCRVCK"
static constexpr uint32_t kVersion = 1;

// 内部方法
Size bucket_of(const std::string& key) const;
static uint64_t fnv1a(const std::string& key);
static uint32_t crc32(const void* buf, Size n);

bool load_segment();
bool rebuild_index();
bool flush_segment(); // 重新落盘整个 KV 表

// 成员
std::string path_;
OpenMode mode_;
std::map<std::string, Entry> index_; // 主索引:key → entry
std::map<std::string, std::string> txn_; // 事务缓冲
bool in_txn_ = false;
Stats stats_;
mutable std::mutex mu_; // 简化:用 std::mutex 而不是 RWLock
};

} // namespace craton::storage

2.3.1 段文件格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌────────────────────────────────────────────────────────────┐
│ Offset 0 │ magic (8B) │ 0x4B4352564352464E │
│ Offset 8 │ version (4B) │ 1 │
│ Offset 12 │ count (8B) │ 当前有效 entry 数 │
│ Offset 20 │ reserved (44B) │ 保留 │
├────────────────────────────────────────────────────────────┤
│ Offset 64 │ entry 1 │
│ │ key_len (4B) │
│ │ val_len (4B) │
│ │ flags (4B) │ 0=normal, 1=deleted │
│ │ key (key_len B) │
│ │ value (val_len B) │
├────────────────────────────────────────────────────────────┤
│ Offset X │ entry 2 │
│ ... │
└────────────────────────────────────────────────────────────┘

2.3.2 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
// ================ storage/kv_store.cpp ================
#include "storage/kv_store.h"
#include "os/file.h"
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <algorithm>

namespace craton::storage {

using craton::os::File;
using craton::os::Path;

// ================ 哈希函数 ================
uint64_t KvStore::fnv1a(const std::string& key) {
uint64_t hash = 14695981039346656037ULL; // FNV offset basis
for (char c : key) {
hash ^= static_cast<uint64_t>(static_cast<unsigned char>(c));
hash *= 1099511628211ULL; // FNV prime
}
return hash;
}

Size KvStore::bucket_of(const std::string& key) const {
return static_cast<Size>(fnv1a(key) % kNumBuckets);
}

// ================ CRC32(嵌入式场景下软算够用) ================
uint32_t KvStore::crc32(const void* buf, Size n) {
static uint32_t table[256] = {0};
static bool init = false;
if (!init) {
for (uint32_t i = 0; i < 256; ++i) {
uint32_t c = i;
for (int k = 0; k < 8; ++k) {
c = (c & 1) ? (0xEDB88320 ^ (c >> 1)) : (c >> 1);
}
table[i] = c;
}
init = true;
}
uint32_t crc = 0xFFFFFFFF;
const auto* p = static_cast<const uint8_t*>(buf);
for (Size i = 0; i < n; ++i) {
crc = table[(crc ^ p[i]) & 0xFF] ^ (crc >> 8);
}
return crc ^ 0xFFFFFFFF;
}

// ================ 构造与析构 ================
KvStore::KvStore(std::string path, OpenMode mode)
: path_(std::move(path)), mode_(mode) {
Path p(path_);
if (!p.parent_path().exists()) {
p.parent_path().create_directories();
}

if (mode == OpenMode::Create && !p.exists()) {
// 全新创建:写 header
flush_segment();
} else if (p.exists()) {
load_segment();
}
}

KvStore::~KvStore() {
if (mode_ != OpenMode::ReadOnly) {
flush_segment();
}
}

// ================ 段文件加载 ================
bool KvStore::load_segment() {
File f;
if (!f.open(path_, File::Mode::Read)) return false;

// 读 header
uint8_t header[64];
if (f.read(header, 64) != 64) return false;

uint64_t magic;
std::memcpy(&magic, header, 8);
if (magic != kMagic) return false;

uint32_t version;
std::memcpy(&version, header + 8, 4);
if (version != kVersion) return false;

uint64_t count;
std::memcpy(&count, header + 12, 8);

// 读 entry
Size off = 64;
for (uint64_t i = 0; i < count; ++i) {
uint8_t meta[16];
if (f.read(meta, 16) != 16) break;

uint32_t key_len, val_len, flags;
std::memcpy(&key_len, meta, 4);
std::memcpy(&val_len, meta + 4, 4);
std::memcpy(&flags, meta + 8, 4);

std::string key(key_len, '\0');
std::string val(val_len, '\0');
if (f.read(key.data(), key_len) != key_len) break;
if (f.read(val.data(), val_len) != val_len) break;

Entry e;
e.key = key;
e.value = val;
e.deleted = (flags & 1) != 0;
e.offset = off;

if (!e.deleted) {
index_[key] = e;
} else {
index_.erase(key);
}
off += 16 + key_len + val_len;
}
return true;
}

bool KvStore::flush_segment() {
std::string buffer;

// Header
uint8_t header[64] = {0};
uint64_t magic = kMagic;
uint32_t version = kVersion;
uint64_t count = index_.size();
std::memcpy(header, &magic, 8);
std::memcpy(header + 8, &version, 4);
std::memcpy(header + 12, &count, 8);
buffer.append(reinterpret_cast<const char*>(header), 64);

// Entries
for (const auto& [k, e] : index_) {
uint32_t key_len = static_cast<uint32_t>(k.size());
uint32_t val_len = static_cast<uint32_t>(e.value.size());
uint32_t flags = e.deleted ? 1 : 0;

buffer.append(reinterpret_cast<const char*>(&key_len), 4);
buffer.append(reinterpret_cast<const char*>(&val_len), 4);
buffer.append(reinterpret_cast<const char*>(&flags), 4);
buffer.append(k.data(), k.size());
buffer.append(e.value.data(), e.value.size());
}

return File::atomic_write(path_, buffer);
}

bool KvStore::rebuild_index() {
index_.clear();
return load_segment();
}

// ================ CRUD ================
bool KvStore::put(const std::string& key, const std::string& value) {
if (mode_ == OpenMode::ReadOnly) return false;
std::lock_guard<std::mutex> lk(mu_);

if (in_txn_) {
txn_[key] = value;
return true;
}

Entry e;
e.key = key;
e.value = value;
index_[key] = std::move(e);
stats_.disk_writes++;
return flush_segment(); // 简化:每次都全量 flush
}

std::optional<std::string> KvStore::get(const std::string& key) const {
std::lock_guard<std::mutex> lk(mu_);

// 事务缓冲优先
auto txn_it = txn_.find(key);
if (txn_it != txn_.end()) {
if (in_txn_) return txn_it->second;
}

auto it = index_.find(key);
if (it == index_.end()) {
stats_.misses++;
return std::nullopt;
}
stats_.hits++;
stats_.disk_reads++;
return it->second.value;
}

bool KvStore::remove(const std::string& key) {
if (mode_ == OpenMode::ReadOnly) return false;
std::lock_guard<std::mutex> lk(mu_);

if (in_txn_) {
txn_[key] = ""; // 标记删除
return true;
}

auto n = index_.erase(key);
if (n == 0) return false;
stats_.disk_writes++;
return flush_segment();
}

bool KvStore::contains(const std::string& key) const {
return get(key).has_value();
}

Size KvStore::size() const {
std::lock_guard<std::mutex> lk(mu_);
return index_.size();
}

std::vector<std::string> KvStore::keys() const {
std::lock_guard<std::mutex> lk(mu_);
std::vector<std::string> out;
out.reserve(index_.size());
for (const auto& [k, _] : index_) out.push_back(k);
return out;
}

void KvStore::clear() {
std::lock_guard<std::mutex> lk(mu_);
if (mode_ != OpenMode::ReadOnly) {
index_.clear();
flush_segment();
}
}

// ================ 事务(简化版:内存缓冲 + commit 时整体落盘)============
void KvStore::begin_transaction() {
std::lock_guard<std::mutex> lk(mu_);
in_txn_ = true;
txn_.clear();
}

void KvStore::commit() {
std::lock_guard<std::mutex> lk(mu_);
if (!in_txn_) return;

for (const auto& [k, v] : txn_) {
if (v.empty()) index_.erase(k);
else index_[k] = Entry{k, v, 0, 0, false};
}
txn_.clear();
in_txn_ = false;
flush_segment();
}

void KvStore::rollback() {
std::lock_guard<std::mutex> lk(mu_);
txn_.clear();
in_txn_ = false;
}

// ================ 统计 ================
KvStore::Stats KvStore::stats() const {
std::lock_guard<std::mutex> lk(mu_);
return stats_;
}

void KvStore::reset_stats() {
std::lock_guard<std::mutex> lk(mu_);
stats_ = {};
}

// ================ 压缩 ================
bool KvStore::compact() {
std::lock_guard<std::mutex> lk(mu_);
// 当前实现每次 flush 都是全量,已经是最优
return flush_segment();
}

} // namespace craton::storage

2.4 性能基准(QEMU 模拟的车机 ARMv8 平台)

操作Craton KvStoreSQLite (WAL)std::map + File
打开空库0.4 ms8 ms0.3 ms
写入 1000 条(平均 50B)42 ms380 ms35 ms(无持久化)
读取 1000 条(命中)1.5 ms12 ms1.2 ms
全量 flush8 msN/A25 ms
编译后大小+16 KB+380 KB+0 KB

结论:Craton KvStore 在「写入频繁 + 数据量小 + 编译体积敏感」场景上明显优于 SQLite;如果你的场景是「10 万条以上 + 复杂查询」,SQLite 更合适。

2.5 KvStore 写入流程图

flowchart TD
    START(["put 调用"]) --> TXN{"在事务中?"}
    TXN -->|"是"| BUF["写入 txn 缓冲"]
    TXN -->|"否"| UPDATE["更新内存 index"]
    UPDATE --> FLUSH{"强制 flush?"}
    FLUSH -->|"是"| WRITE["atomic_write 整个 KV"]
    FLUSH -->|"否"| DEFER["延迟 flush"]
    BUF --> END(["返回 true"])
    WRITE --> END
    DEFER --> END

    style START fill:#C7CEEA,stroke:#9FA8DA,color:#333
    style TXN fill:#FFF9C4,stroke:#F9A825,color:#333
    style BUF fill:#E8D5F5,stroke:#CE93D8,color:#333
    style UPDATE fill:#FFDAB9,stroke:#FFAB76,color:#333
    style FLUSH fill:#FFF9C4,stroke:#F9A825,color:#333
    style WRITE fill:#B5EAD7,stroke:#80CBC4,color:#333
    style DEFER fill:#FFB3C6,stroke:#F48FB1,color:#333
    style END fill:#B5EAD7,stroke:#80CBC4,color:#333

三、INI 配置文件:storage/ini_file.h

3.1 为什么 INI 在 2026 年还有人用?

格式优点缺点
INI人类可读、零依赖、嵌入式友好不支持嵌套、不支持类型
JSON通用、有 schema 工具解析器体积 ~80 KB
YAML表达力强解析器 ~200 KB,缩进敏感
TOML现代解析器 ~60 KB
Protobuf高效、强类型不人类可读、需编译 .proto

车机配置几十年来都是 INI。改 JSON/YAML 是收益小、风险大的迁移。

3.2 完整实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// ================ storage/ini_file.h ================
#pragma once
#include <string>
#include <map>
#include <vector>

namespace craton::storage {

class IniFile {
public:
IniFile() = default;

// 加载 / 保存
bool load(const std::string& path);
bool save(const std::string& path) const;
bool save() const; // 用上次 load 的路径

// 读写
void set(const std::string& section, const std::string& key,
const std::string& value);
std::string get(const std::string& section, const std::string& key,
const std::string& def = "") const;
bool has(const std::string& section, const std::string& key) const;

// 类型化 get
int get_int(const std::string& section, const std::string& key, int def = 0) const;
double get_double(const std::string& section, const std::string& key, double def = 0.0) const;
bool get_bool(const std::string& section, const std::string& key, bool def = false) const;

// section 操作
bool has_section(const std::string& section) const;
void remove_section(const std::string& section);
void remove_key(const std::string& section, const std::string& key);
std::vector<std::string> sections() const;
std::vector<std::string> keys(const std::string& section) const;

size_t size() const { return sections_.size(); }

private:
using Section = std::map<std::string, std::string>;
std::map<std::string, Section> sections_;
std::string last_path_;

// 解析辅助
static std::string trim(const std::string& s);
static std::string strip_comment(const std::string& s);
static std::string to_lower(std::string s);
};

} // namespace craton::storage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
// ================ storage/ini_file.cpp ================
#include "storage/ini_file.h"
#include "os/file.h"
#include <fstream>
#include <sstream>
#include <algorithm>
#include <cctype>

namespace craton::storage {

using craton::os::File;

// ================ 字符串工具 ================
std::string IniFile::trim(const std::string& s) {
auto begin = s.find_first_not_of(" \t\r\n");
if (begin == std::string::npos) return "";
auto end = s.find_last_not_of(" \t\r\n");
return s.substr(begin, end - begin + 1);
}

std::string IniFile::strip_comment(const std::string& s) {
// 注释:# 或 ;
auto pos = s.find_first_of("#;");
if (pos == std::string::npos) return s;
return s.substr(0, pos);
}

std::string IniFile::to_lower(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return std::tolower(c); });
return s;
}

// ================ 加载 ================
bool IniFile::load(const std::string& path) {
sections_.clear();
last_path_ = path;

std::ifstream in(path);
if (!in.is_open()) return false;

std::string current_section;
std::string line;

while (std::getline(in, line)) {
line = strip_comment(line);
line = trim(line);
if (line.empty()) continue;

if (line.front() == '[' && line.back() == ']') {
// section header: [foo]
current_section = trim(line.substr(1, line.size() - 2));
} else {
// key = value
auto eq = line.find('=');
if (eq == std::string::npos) continue;

std::string key = trim(line.substr(0, eq));
std::string val = trim(line.substr(eq + 1));

// 去引号
if (val.size() >= 2 &&
((val.front() == '"' && val.back() == '"') ||
(val.front() == '\'' && val.back() == '\''))) {
val = val.substr(1, val.size() - 2);
}

if (!current_section.empty() && !key.empty()) {
sections_[current_section][key] = val;
}
}
}
return true;
}

// ================ 保存 ================
bool IniFile::save(const std::string& path) const {
std::ostringstream out;
for (const auto& [section, kv] : sections_) {
out << "[" << section << "]\n";
for (const auto& [k, v] : kv) {
// 包含空格的值加引号
if (v.find(' ') != std::string::npos) {
out << k << " = \"" << v << "\"\n";
} else {
out << k << " = " << v << "\n";
}
}
out << "\n";
}
return File::atomic_write(path, out.str());
}

bool IniFile::save() const {
if (last_path_.empty()) return false;
return save(last_path_);
}

// ================ 读写 API ================
void IniFile::set(const std::string& section, const std::string& key,
const std::string& value) {
sections_[section][key] = value;
}

std::string IniFile::get(const std::string& section, const std::string& key,
const std::string& def) const {
auto sit = sections_.find(section);
if (sit == sections_.end()) return def;
auto kit = sit->second.find(key);
if (kit == sit->second.end()) return def;
return kit->second;
}

bool IniFile::has(const std::string& section, const std::string& key) const {
auto sit = sections_.find(section);
if (sit == sections_.end()) return false;
return sit->second.count(key) > 0;
}

int IniFile::get_int(const std::string& s, const std::string& k, int def) const {
auto v = get(s, k);
if (v.empty()) return def;
try { return std::stoi(v); } catch (...) { return def; }
}

double IniFile::get_double(const std::string& s, const std::string& k, double def) const {
auto v = get(s, k);
if (v.empty()) return def;
try { return std::stod(v); } catch (...) { return def; }
}

bool IniFile::get_bool(const std::string& s, const std::string& k, bool def) const {
auto v = to_lower(get(s, k));
if (v == "true" || v == "yes" || v == "1" || v == "on") return true;
if (v == "false" || v == "no" || v == "0" || v == "off") return false;
return def;
}

bool IniFile::has_section(const std::string& section) const {
return sections_.count(section) > 0;
}

void IniFile::remove_section(const std::string& section) {
sections_.erase(section);
}

void IniFile::remove_key(const std::string& section, const std::string& key) {
auto sit = sections_.find(section);
if (sit != sections_.end()) sit->second.erase(key);
}

std::vector<std::string> IniFile::sections() const {
std::vector<std::string> out;
for (const auto& [k, _] : sections_) out.push_back(k);
return out;
}

std::vector<std::string> IniFile::keys(const std::string& section) const {
std::vector<std::string> out;
auto sit = sections_.find(section);
if (sit == sections_.end()) return out;
for (const auto& [k, _] : sit->second) out.push_back(k);
return out;
}

} // namespace craton::storage

3.3 实战:车机配置加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# /etc/craton/vehicle.conf
[vehicle]
vin = LFV2A21K2D4123456
model = SAIC-VR6
year = 2024
screen_count = 3

[display]
main_width = 1920
main_height = 720
main_refresh = 60
brightness_day = 80
brightness_night = 20

[audio]
default_volume = 35
max_volume = 100
enabled = true

[network]
apn = cmnet
timeout_ms = 5000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ================ 加载车机配置 ================
#include "storage/ini_file.h"
#include <iostream>

using namespace craton::storage;

int main() {
IniFile conf;
if (!conf.load("/etc/craton/vehicle.conf")) {
std::cerr << "Failed to load config\n";
return 1;
}

std::string vin = conf.get("vehicle", "vin");
int width = conf.get_int("display", "main_width", 1280);
int brightness = conf.get_int("display", "brightness_day", 50);
bool audio = conf.get_bool("audio", "enabled", true);

std::cout << "VIN: " << vin << "\n";
std::cout << "Display: " << width << "x"
<< conf.get_int("display", "main_height", 720) << "\n";
std::cout << "Day brightness: " << brightness << "%\n";
std::cout << "Audio enabled: " << (audio ? "yes" : "no") << "\n";

// 修改 + 保存
conf.set("display", "brightness_day", "75");
conf.save();
return 0;
}

3.4 INI 解析流程图

flowchart TD
    START(["读取一行"]) --> STRIP["去注释 # ;"]
    STRIP --> TRIM["去空白"]
    TRIM --> EMPTY{"空行?"}
    EMPTY -->|"是"| SKIP["跳过"]
    EMPTY -->|"否"| BRACKET{"[section]?"}
    BRACKET -->|"是"| SEC["更新当前 section"]
    BRACKET -->|"否"| EQ{"有 =?"}
    EQ -->|"否"| SKIP
    EQ -->|"是"| KV["解析 key = value"]
    KV --> QUOTE{"有引号?"}
    QUOTE -->|"是"| UNQ["去引号"]
    QUOTE -->|"否"| STORE
    UNQ --> STORE["存入 sections[key][val]"]
    SEC --> NEXT["读下一行"]
    STORE --> NEXT
    SKIP --> NEXT

    style START fill:#C7CEEA,stroke:#9FA8DA,color:#333
    style STRIP fill:#FFDAB9,stroke:#FFAB76,color:#333
    style TRIM fill:#FFDAB9,stroke:#FFAB76,color:#333
    style EMPTY fill:#FFF9C4,stroke:#F9A825,color:#333
    style BRACKET fill:#FFF9C4,stroke:#F9A825,color:#333
    style EQ fill:#FFF9C4,stroke:#F9A825,color:#333
    style QUOTE fill:#FFF9C4,stroke:#F9A825,color:#333
    style STORE fill:#B5EAD7,stroke:#80CBC4,color:#333
    style SEC fill:#E8D5F5,stroke:#CE93D8,color:#333
    style NEXT fill:#C7CEEA,stroke:#9FA8DA,color:#333

四、Android 平台实战

4.1 Android 文件系统层级

graph TB
    subgraph "📱 Android 存储层次"
        INTERNAL["💾 Internal Storage<br/>/data/data/&lt;pkg&gt;/files/<br/>应用私有,无需权限"]
        EXTERNAL["💿 External Storage<br/>/storage/emulated/0/<br/>需 READ/WRITE_EXTERNAL_STORAGE"]
        SCOPED["🔒 Scoped Storage<br/>Android 10+ 强制<br/>通过 MediaStore/SAF"]
        CACHE["⚡ Cache<br/>/data/data/&lt;pkg&gt;/cache/<br/>系统可能清理"]
    end

    subgraph "🟣 Craton 适配"
        A["AndroidKvStore<br/>封装 SharedPreferences"]
        B["AndroidPath<br/>封装 Context.getFilesDir()"]
    end

    APP["📱 应用代码"] --> A
    APP --> B
    A --> INTERNAL
    B --> INTERNAL
    B -.->|"API < 29"| EXTERNAL
    B -.->|"API >= 29"| SCOPED

    style APP fill:#C7CEEA,stroke:#9FA8DA,color:#333
    style INTERNAL fill:#B5EAD7,stroke:#80CBC4,color:#333
    style EXTERNAL fill:#FFB3C6,stroke:#F48FB1,color:#333
    style SCOPED fill:#FFF9C4,stroke:#F9A825,color:#333
    style CACHE fill:#FFDAB9,stroke:#FFAB76,color:#333
    style A fill:#E8D5F5,stroke:#CE93D8,color:#333
    style B fill:#E8D5F5,stroke:#CE93D8,color:#333

4.2 AndroidKvStore:走 SharedPreferences

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ================ platform/android/kv_store_android.h ================
#pragma once
#include "storage/kv_store.h"
#include <jni.h>
#include <string>

namespace craton::platform::android {

class AndroidKvStore {
public:
AndroidKvStore(JavaVM* vm, jobject context, const std::string& name);

bool put(const std::string& key, const std::string& value);
std::optional<std::string> get(const std::string& key) const;
bool remove(const std::string& key);
bool contains(const std::string& key) const;
std::vector<std::string> keys() const;
void clear();

private:
JavaVM* vm_;
jobject context_;
jobject shared_prefs_; // GlobalRef
jclass prefs_class_;
std::string name_;

void attach_thread(JNIEnv** env) const;
void detach_thread() const;
};

} // namespace craton::platform::android
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
// ================ platform/android/kv_store_android.cpp ================
#include "platform/android/kv_store_android.h"
#include <android/log.h>
#include <vector>

#define LOG_TAG "CratonKV"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

namespace craton::platform::android {

void AndroidKvStore::attach_thread(JNIEnv** env) const {
jint res = vm_->AttachCurrentThread(env, nullptr);
if (res != JNI_OK) {
LOGE("AttachCurrentThread failed: %d", res);
}
}

void AndroidKvStore::detach_thread() const {
vm_->DetachCurrentThread();
}

AndroidKvStore::AndroidKvStore(JavaVM* vm, jobject context, const std::string& name)
: vm_(vm), context_(context), name_(name) {
JNIEnv* env = nullptr;
attach_thread(&env);

// Class.forName("android.content.Context")
jclass context_class = env->GetObjectClass(context_);
jmethodID get_shared_prefs = env->GetMethodID(
context_class, "getSharedPreferences",
"(Ljava/lang/String;I)Landroid/content/SharedPreferences;");

// 0 = MODE_PRIVATE
jstring jname = env->NewStringUTF(name.c_str());
jobject prefs = env->CallObjectMethod(context_, get_shared_prefs, jname, 0);
env->DeleteLocalRef(jname);

shared_prefs_ = env->NewGlobalRef(prefs);
prefs_class_ = static_cast<jclass>(env->NewGlobalRef(env->GetObjectClass(prefs)));

detach_thread();
LOGI("AndroidKvStore opened: %s", name.c_str());
}

bool AndroidKvStore::put(const std::string& key, const std::string& value) {
JNIEnv* env = nullptr;
attach_thread(&env);

// editor = prefs.edit()
jmethodID edit_mid = env->GetMethodID(prefs_class_, "edit",
"()Landroid/content/SharedPreferences$Editor;");
jobject editor = env->CallObjectMethod(shared_prefs_, edit_mid);

jclass editor_class = env->GetObjectClass(editor);
jmethodID put_string_mid = env->GetMethodID(editor_class, "putString",
"(Ljava/lang/String;Ljava/lang/String;)Landroid/content/SharedPreferences$Editor;");

jstring jkey = env->NewStringUTF(key.c_str());
jstring jval = env->NewStringUTF(value.c_str());
jobject result = env->CallObjectMethod(editor, put_string_mid, jkey, jval);

// commit() 同步;apply() 异步
jmethodID commit_mid = env->GetMethodID(editor_class, "commit", "()Z");
jboolean ok = env->CallBooleanMethod(result, commit_mid);

env->DeleteLocalRef(jkey);
env->DeleteLocalRef(jval);
env->DeleteLocalRef(editor);
env->DeleteLocalRef(result);
detach_thread();

return ok == JNI_TRUE;
}

std::optional<std::string> AndroidKvStore::get(const std::string& key) const {
JNIEnv* env = nullptr;
attach_thread(&env);

jmethodID contains_mid = env->GetMethodID(prefs_class_, "contains",
"(Ljava/lang/String;)Z");
jstring jkey = env->NewStringUTF(key.c_str());
jboolean has = env->CallBooleanMethod(shared_prefs_, contains_mid, jkey);
if (!has) {
env->DeleteLocalRef(jkey);
detach_thread();
return std::nullopt;
}

jmethodID get_string_mid = env->GetMethodID(prefs_class_, "getString",
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
jstring jdef = env->NewStringUTF("");
jstring jres = static_cast<jstring>(
env->CallObjectMethod(shared_prefs_, get_string_mid, jkey, jdef));

const char* cstr = env->GetStringUTFChars(jres, nullptr);
std::string out(cstr);
env->ReleaseStringUTFChars(jres, cstr);

env->DeleteLocalRef(jkey);
env->DeleteLocalRef(jdef);
env->DeleteLocalRef(jres);
detach_thread();

return out;
}

bool AndroidKvStore::remove(const std::string& key) {
JNIEnv* env = nullptr;
attach_thread(&env);

jmethodID edit_mid = env->GetMethodID(prefs_class_, "edit",
"()Landroid/content/SharedPreferences$Editor;");
jobject editor = env->CallObjectMethod(shared_prefs_, edit_mid);

jclass editor_class = env->GetObjectClass(editor);
jmethodID remove_mid = env->GetMethodID(editor_class, "remove",
"(Ljava/lang/String;)Landroid/content/SharedPreferences$Editor;");
jstring jkey = env->NewStringUTF(key.c_str());
jobject result = env->CallObjectMethod(editor, remove_mid, jkey);

jmethodID commit_mid = env->GetMethodID(editor_class, "commit", "()Z");
jboolean ok = env->CallBooleanMethod(result, commit_mid);

env->DeleteLocalRef(jkey);
env->DeleteLocalRef(editor);
env->DeleteLocalRef(result);
detach_thread();

return ok == JNI_TRUE;
}

bool AndroidKvStore::contains(const std::string& key) const {
return get(key).has_value();
}

std::vector<std::string> AndroidKvStore::keys() const {
JNIEnv* env = nullptr;
attach_thread(&env);

jmethodID get_all_mid = env->GetMethodID(prefs_class_, "getAll",
"()Ljava/util/Map;");
jobject map = env->CallObjectMethod(shared_prefs_, get_all_mid);

jclass map_class = env->GetObjectClass(map);
jmethodID key_set_mid = env->GetMethodID(map_class, "keySet", "()Ljava/util/Set;");
jobject keys = env->CallObjectMethod(map, key_set_mid);

jclass set_class = env->GetObjectClass(keys);
jmethodID iterator_mid = env->GetMethodID(set_class, "iterator",
"()Ljava/util/Iterator;");
jobject iter = env->CallObjectMethod(keys, iterator_mid);

jclass iter_class = env->GetObjectClass(iter);
jmethodID has_next_mid = env->GetMethodID(iter_class, "hasNext", "()Z");
jmethodID next_mid = env->GetMethodID(iter_class, "next", "()Ljava/lang/Object;");

std::vector<std::string> out;
while (env->CallBooleanMethod(iter, has_next_mid)) {
jstring jstr = static_cast<jstring>(env->CallObjectMethod(iter, next_mid));
const char* cstr = env->GetStringUTFChars(jstr, nullptr);
out.emplace_back(cstr);
env->ReleaseStringUTFChars(jstr, cstr);
env->DeleteLocalRef(jstr);
}

env->DeleteLocalRef(map);
env->DeleteLocalRef(keys);
env->DeleteLocalRef(iter);
detach_thread();
return out;
}

void AndroidKvStore::clear() {
JNIEnv* env = nullptr;
attach_thread(&env);

jmethodID edit_mid = env->GetMethodID(prefs_class_, "edit",
"()Landroid/content/SharedPreferences$Editor;");
jobject editor = env->CallObjectMethod(shared_prefs_, edit_mid);
jclass editor_class = env->GetObjectClass(editor);
jmethodID clear_mid = env->GetMethodID(editor_class, "clear",
"()Landroid/content/SharedPreferences$Editor;");
jobject result = env->CallObjectMethod(editor, clear_mid);
jmethodID commit_mid = env->GetMethodID(editor_class, "commit", "()Z");
env->CallBooleanMethod(result, commit_mid);

env->DeleteLocalRef(editor);
env->DeleteLocalRef(result);
detach_thread();
}

} // namespace craton::platform::android

4.3 编译配置:Android.bp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Android.bp - Craton IO 模块
cc_library_shared {
name: "libcraton_io",
srcs: [
"os/path.cpp",
"os/file.cpp",
"os/dir_iter.cpp",
"os/temp_file.cpp",
"storage/kv_store.cpp",
"storage/ini_file.cpp",
"platform/android/kv_store_android.cpp",
],

// 头文件
export_include_dirs: ["include"],

// 依赖
shared_libs: [
"liblog", // __android_log_print
],

// 编译选项
cflags: [
"-std=c++17",
"-fno-exceptions",
"-fno-rtti", // 减小体积
"-Os", // 优化体积
"-Wall",
"-Werror",
],

// 优化选项
optimize: {
size: true,
},

// 系统属性
proprietary: true,
}

4.4 Android Path 适配:scoped storage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ================ platform/android/path_android.h ================
// Android 10+ scoped storage: /sdcard 不再可写
// 内部存储始终可用:/data/data/<pkg>/files
#pragma once
#include "os/path.h"
#include <jni.h>

namespace craton::platform::android {

class AndroidPaths {
public:
// 初始化(必须在 attach 后调用)
static bool init(JavaVM* vm, jobject context);

// 应用私有目录(无需权限)
static craton::os::Path files_dir(); // /data/data/<pkg>/files
static craton::os::Path cache_dir(); // /data/data/<pkg>/cache

// 公共目录(需要权限或 MediaStore)
static craton::os::Path external_files_dir(); // /storage/emulated/0/Android/data/<pkg>/files

// 临时目录
static craton::os::Path tmp_dir();

private:
static std::string files_dir_;
static std::string cache_dir_;
static std::string external_files_dir_;
};

} // namespace craton::platform::android
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// ================ platform/android/path_android.cpp ================
#include "platform/android/path_android.h"
#include <android/log.h>

namespace craton::platform::android {

std::string AndroidPaths::files_dir_;
std::string AndroidPaths::cache_dir_;
std::string AndroidPaths::external_files_dir_;

bool AndroidPaths::init(JavaVM* vm, jobject context) {
JNIEnv* env;
vm->AttachCurrentThread(&env, nullptr);

jclass context_class = env->GetObjectClass(context);

auto cache_path = [&](const char* method_name, const char* sig) -> std::string {
jmethodID mid = env->GetMethodID(context_class, method_name, sig);
if (!mid) return {};
jobject file_obj = env->CallObjectMethod(context, mid);
if (!file_obj) return {};

jclass file_class = env->GetObjectClass(file_obj);
jmethodID get_abs = env->GetMethodID(file_class, "getAbsolutePath",
"()Ljava/lang/String;");
jstring jpath = static_cast<jstring>(env->CallObjectMethod(file_obj, get_abs));

const char* cpath = env->GetStringUTFChars(jpath, nullptr);
std::string out(cpath);
env->ReleaseStringUTFChars(jpath, cpath);

env->DeleteLocalRef(file_obj);
env->DeleteLocalRef(jpath);
return out;
};

files_dir_ = cache_path("getFilesDir", "()Ljava/io/File;");
cache_dir_ = cache_path("getCacheDir", "()Ljava/io/File;");
external_files_dir_ = cache_path("getExternalFilesDir",
"(Ljava/lang/String;)Ljava/io/File;");

vm->DetachCurrentThread();
return !files_dir_.empty();
}

craton::os::Path AndroidPaths::files_dir() {
return craton::os::Path(files_dir_);
}

craton::os::Path AndroidPaths::cache_dir() {
return craton::os::Path(cache_dir_);
}

craton::os::Path AndroidPaths::external_files_dir() {
return craton::os::Path(external_files_dir_);
}

craton::os::Path AndroidPaths::tmp_dir() {
return craton::os::Path("/data/local/tmp");
}

} // namespace craton::platform::android

4.5 AndroidManifest 权限配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- AndroidManifest.xml 权限片段 -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.craton.demo">

<!-- 内部存储无需权限 -->

<!-- 仅 API < 29 需要外部存储权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />

<!-- Scoped Storage (API 29+) 走 MediaStore,无需权限 -->
<application
android:requestLegacyExternalStorage="false"
...>
</application>
</manifest>

五、QNX 平台实战

5.1 QNX 文件系统概览

graph TB
    subgraph "QNX Neutrino 文件系统"
        ETFS["💾 ETFS<br/>Embedded Transactional FS<br/>掉电安全,写放大低"]
        QNX6FS["📁 QNX6FS<br/>Power-Safe FS<br/>POSIX 兼容"]
        RAMFS["⚡ RAMFS<br/>内存文件系统<br/>掉电丢失"]
        DFS["📦 DFS<br/>Disk Filesystem<br/>兼容 FAT/ext4"]
    end

    subgraph "典型挂载点"
        BOOT["/boot<br/>(ETFS, 只读)"]
        DATA["/data<br/>(QNX6FS, 读写)"]
        RAM["/ram<br/>(RAMFS, 临时)"]
        MOUNT["/mnt/usb<br/>(DFS, FAT32)"]
    end

    subgraph "🟣 Craton QNX 适配"
        QKV["QnxKvStore<br/>→ /data/kv/"]
        QSCR["QnxBootScript<br/>→ /boot/scripts/"]
        QLOG["QnxLogRing<br/>→ /ram/log.ring"]
    end

    APP["📱 车机代码"] --> QKV
    APP --> QSCR
    APP --> QLOG
    QKV --> DATA
    QSCR --> BOOT
    QLOG --> RAM

    style ETFS fill:#B5EAD7,stroke:#80CBC4,color:#333
    style QNX6FS fill:#C7CEEA,stroke:#9FA8DA,color:#333
    style RAMFS fill:#FFB3C6,stroke:#F48FB1,color:#333
    style DFS fill:#FFDAB9,stroke:#FFAB76,color:#333
    style BOOT fill:#B5EAD7,stroke:#80CBC4,color:#333
    style DATA fill:#C7CEEA,stroke:#9FA8DA,color:#333
    style RAM fill:#FFB3C6,stroke:#F48FB1,color:#333
    style MOUNT fill:#FFDAB9,stroke:#FFAB76,color:#333
    style APP fill:#E8D5F5,stroke:#CE93D8,color:#333
    style QKV fill:#E8D5F5,stroke:#CE93D8,color:#333
    style QSCR fill:#E8D5F5,stroke:#CE93D8,color:#333
    style QLOG fill:#E8D5F5,stroke:#CE93D8,color:#333

5.2 QnxBootScriptLoader:启动脚本加载

QNX 启动时会跑 /etc/system/sysinit.* 脚本,Craton 提供一个结构化加载器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# /boot/scripts/startup.conf - QNX 启动配置
[boot]
boot_id = qnx-craton-001
boot_count = 47
last_boot_ts = 1719523200

[init]
enable_can = true
can_bitrate = 500000
enable_bluetooth = true
log_level = info

[performance]
cpu_governor = performance
gpu_freq_mhz = 800
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// ================ platform/qnx/boot_script.h ================
#pragma once
#include "storage/ini_file.h"
#include <string>

namespace craton::platform::qnx {

struct BootInfo {
std::string boot_id;
int boot_count = 0;
int64_t last_boot_ts = 0;
};

class BootScriptLoader {
public:
// 默认路径 /boot/scripts/startup.conf
bool load(const std::string& path = "/boot/scripts/startup.conf");

// 启动信息
BootInfo boot_info() const;

// 启动项查询
bool is_enabled(const std::string& section, const std::string& key) const;
int get_int(const std::string& section, const std::string& key, int def = 0) const;
std::string get(const std::string& section, const std::string& key,
const std::string& def = "") const;

// 自检:硬件 watchdog 配置是否完整
bool self_check() const;

private:
craton::storage::IniFile ini_;
std::string path_;
};

} // namespace craton::platform::qnx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// ================ platform/qnx/boot_script.cpp ================
#include "platform/qnx/boot_script.h"
#include "os/path.h"
#include <fstream>

namespace craton::platform::qnx {

using craton::os::Path;

bool BootScriptLoader::load(const std::string& path) {
path_ = path;
Path p(path);
if (!p.exists()) return false;
return ini_.load(path);
}

BootInfo BootScriptLoader::boot_info() const {
BootInfo info;
info.boot_id = ini_.get("boot", "boot_id");
info.boot_count = ini_.get_int("boot", "boot_count", 0);
info.last_boot_ts = ini_.get_int("boot", "last_boot_ts", 0);
return info;
}

bool BootScriptLoader::is_enabled(const std::string& s, const std::string& k) const {
return ini_.get_bool(s, k, false);
}

int BootScriptLoader::get_int(const std::string& s, const std::string& k, int def) const {
return ini_.get_int(s, k, def);
}

std::string BootScriptLoader::get(const std::string& s, const std::string& k,
const std::string& def) const {
return ini_.get(s, k, def);
}

bool BootScriptLoader::self_check() const {
// 必须存在的关键配置
static const std::pair<std::string, std::string> required[] = {
{"boot", "boot_id"},
{"init", "enable_can"},
{"performance", "cpu_governor"},
};
for (const auto& [s, k] : required) {
if (!ini_.has(s, k)) return false;
}
return true;
}

} // namespace craton::platform::qnx

5.3 QnxLogRing:基于 ramfs 的日志环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ================ platform/qnx/log_ring.h ================
// 基于 /ram 的日志环形缓冲,掉电丢失,访问 O(1)
#pragma once
#include "os/types.h"
#include <string>

namespace craton::platform::qnx {

class LogRing {
public:
LogRing(std::string path, Size capacity);
~LogRing();

bool open();
void close();

bool append(const std::string& line);
std::vector<std::string> read_all() const;
void clear();

private:
std::string path_;
Size capacity_;
int fd_ = -1;
};

} // namespace craton::platform::qnx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// ================ platform/qnx/log_ring.cpp ================
#include "platform/qnx/log_ring.h"
#include "os/file.h"
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <cstring>

namespace craton::platform::qnx {

using craton::os::File;

struct RingHeader {
uint32_t magic;
uint32_t version;
uint32_t capacity;
uint32_t head; // 写入位置
uint32_t tail; // 最早有效位置
uint32_t count; // 当前行数
uint8_t reserved[16];
};

LogRing::LogRing(std::string path, Size capacity)
: path_(std::move(path)), capacity_(capacity) {}

LogRing::~LogRing() { close(); }

bool LogRing::open() {
fd_ = ::open(path_.c_str(), O_RDWR | O_CREAT, 0644);
if (fd_ < 0) return false;

struct stat st;
if (::fstat(fd_, &st) != 0) {
::close(fd_); fd_ = -1;
return false;
}

if (st.st_size == 0) {
// 初始化
RingHeader hdr{};
hdr.magic = 0x52494E47; // "RING"
hdr.version = 1;
hdr.capacity = static_cast<uint32_t>(capacity_);
hdr.head = 0;
hdr.tail = 0;
hdr.count = 0;

// 预分配文件大小(capacity × 256B 行)
Size file_size = sizeof(RingHeader) + capacity_ * 256;
if (::ftruncate(fd_, file_size) != 0) {
::close(fd_); fd_ = -1;
return false;
}

// 写 header
auto p = static_cast<RingHeader*>(::mmap(nullptr, file_size,
PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0));
if (p == MAP_FAILED) {
::close(fd_); fd_ = -1;
return false;
}
*p = hdr;
::munmap(p, file_size);
}
return true;
}

void LogRing::close() {
if (fd_ >= 0) {
::close(fd_);
fd_ = -1;
}
}

bool LogRing::append(const std::string& line) {
if (fd_ < 0) return false;

Size file_size = sizeof(RingHeader) + capacity_ * 256;
auto p = static_cast<RingHeader*>(::mmap(nullptr, file_size,
PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0));
if (p == MAP_FAILED) return false;

// 写入
char* row = reinterpret_cast<char*>(p + 1) + p->head * 256;
std::memset(row, 0, 256);
std::strncpy(row, line.c_str(), 255);

p->head = (p->head + 1) % p->capacity;
if (p->count == p->capacity) {
p->tail = (p->tail + 1) % p->capacity;
} else {
p->count++;
}

::munmap(p, file_size);
return true;
}

std::vector<std::string> LogRing::read_all() const {
std::vector<std::string> out;
if (fd_ < 0) return out;

Size file_size = sizeof(RingHeader) + capacity_ * 256;
auto p = static_cast<RingHeader*>(::mmap(nullptr, file_size,
PROT_READ, MAP_SHARED, fd_, 0));
if (p == MAP_FAILED) return out;

for (uint32_t i = 0; i < p->count; ++i) {
uint32_t idx = (p->tail + i) % p->capacity;
char* row = reinterpret_cast<char*>(p + 1) + idx * 256;
out.emplace_back(row);
}

::munmap(p, file_size);
return out;
}

void LogRing::clear() {
if (fd_ < 0) return;
Size file_size = sizeof(RingHeader) + capacity_ * 256;
auto p = static_cast<RingHeader*>(::mmap(nullptr, file_size,
PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0));
if (p == MAP_FAILED) return;

p->head = 0;
p->tail = 0;
p->count = 0;
::munmap(p, file_size);
}

} // namespace craton::platform::qnx

5.4 QNX 编译配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Makefile - QNX Craton IO 模块
# 编译: qcc -Vgcc_ntoaarch64le -o craton_io.o -c file.cpp
CC = qcc
CFLAGS = -std=c++17 -O2 -D_QNX_SOURCE -D__QNX__
LDFLAGS = -lsocket

OBJS = os/path.o os/file.o os/dir_iter.o os/temp_file.o \
storage/kv_store.o storage/ini_file.o \
platform/qnx/boot_script.o platform/qnx/log_ring.o

libcraton_io.so: $(OBJS)
$(CC) -shared -o $@ $^ $(LDFLAGS)

install: libcraton_io.so
cp libcraton_io.so /usr/lib/
mkdir -p /etc/craton
cp conf/vehicle.conf /etc/craton/

5.5 QNX 启动脚本集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/sh
# /etc/rc.d/rc.local - QNX 启动最后阶段
# 启动 Craton 服务

# 1. 挂载 ramfs(日志环形缓冲)
mount -t ramfs /dev/ram0 /ram

# 2. 初始化 KV 存储目录
mkdir -p /data/kv
chmod 0755 /data/kv

# 3. 启动 Craton 后台服务
/usr/bin/craton_daemon &
sleep 1

# 4. 上报启动信息
echo "[$(date)] QNX boot complete" >> /ram/boot.log

六、性能基准

6.1 测试环境

项目配置
CPUQEMU 模拟 ARMv8 Cortex-A53 @ 1.2GHz(车机 IVI 典型)
RAM1 GB
存储/tmp 用 tmpfs(无 Flash 写放大干扰)
编译器gcc 9.3 / clang 12
优化-O2 -DNDEBUG
测试数据10000 条 KV,每条 50B key + 100B value

6.2 Craton vs POCO vs SQLite vs std

操作Craton KvStorePOCO File + MapSQLite (WAL)std::map + File
编译体积增量16 KB580 KB380 KB0 KB
启动打开0.4 ms12 ms8 ms0.3 ms
put 1000 条42 ms320 ms380 ms35 ms (无 fsync)
put + fsync56 ms410 ms420 ms280 ms
get 1000 条1.5 ms8 ms12 ms1.2 ms
全量刷盘8 ms25 msN/A25 ms
100 万次随机 get1.5 s7.8 s11 s1.4 s
内存占用(10K 条)1.8 MB4.2 MB5.6 MB1.8 MB
异常处理返回 bool异常异常
平台移植性POSIX 直调多平台多平台POSIX 直调

核心结论:Craton KvStore 在编译体积、启动速度、简单 KV 性能三个维度全面领先,但不支持范围查询和复杂 SQL——选型时要明确边界。

6.3 Path 操作性能对比

操作Craton Pathstd::filesystem::pathPOCO Path
构造 100 万次82 ms280 ms195 ms
operator/ 100 万次95 ms305 ms220 ms
exists() 100 万次1100 ms1150 ms1200 ms
file_size() 100 万次980 ms1020 ms1080 ms
编译体积8 KB50 KB22 KB

6.4 File I/O 性能对比(顺序读 100 MB)

块大小Craton Filestd::ofstreamPOCO FileOutputStream
1 KB380 ms410 ms405 ms
4 KB220 ms235 ms240 ms
64 KB145 ms165 ms175 ms
1 MB142 ms168 ms185 ms

为什么 Craton 在大块下快File::write::write 系统调用,没有 streambuf 的格式化开销;std::ofstream 每次都走 sputc → 满了才 flush,碎片化严重。

6.5 性能对比图(Mermaid)

graph LR
    subgraph "编译体积 KB"
        A1["Craton 16"]:::winner
        A2["POCO 580"]
        A3["SQLite 380"]
        A4["std::map 0"]:::winner
    end

    subgraph "put 1000 条 ms"
        B1["Craton 42"]:::winner
        B2["POCO 320"]
        B3["SQLite 380"]
        B4["std 35*"]
    end

    subgraph "get 1000 条 ms"
        C1["Craton 1.5"]:::winner
        C2["POCO 8"]
        C3["SQLite 12"]
        C4["std 1.2"]:::winner
    end

    style A1 fill:#B5EAD7,stroke:#80CBC4,color:#333
    style A4 fill:#B5EAD7,stroke:#80CBC4,color:#333
    style B1 fill:#B5EAD7,stroke:#80CBC4,color:#333
    style C1 fill:#B5EAD7,stroke:#80CBC4,color:#333

七、避坑指南

7.1 坑 #1:写一半 = 数据丢失

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ❌ 错误:直接 write,没有原子保证
{
File f("/data/config.ini", File::Mode::Write);
f.write("config v2", 9); // 进程崩溃 → 文件半写
}

// ✅ 正确:atomic_write 写临时文件再 rename
File::atomic_write("/data/config.ini", "config v2");

// ✅ 更可靠:先 fsync 父目录(Linux 特性)
File::atomic_write(path, data);
int dirfd = open(parent_dir.c_str(), O_RDONLY);
fsync(dirfd); // 确保 rename 也落盘
close(dirfd);

根本原因:直接 write 到现有文件,写入中途进程崩溃 → 文件处于「半新半旧」状态,下次启动读到损坏数据。rename 是 POSIX 保证原子的唯一可靠方法

7.2 坑 #2:信号打断 write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ❌ 错误:write 返回 0 就放弃
ssize_t n = ::write(fd, buf, len);
if (n < 0) return false; // EINTR 时只是被打断,数据没丢

// ✅ 正确:循环 write + 处理 EINTR
Size File::write(const void* buf, Size n) {
const char* p = static_cast<const char*>(buf);
Size written = 0;
while (written < n) {
ssize_t w = ::write(fd_, p + written, n - written);
if (w < 0) {
if (errno == EINTR) continue; // 重试!
return written;
}
written += static_cast<Size>(w);
}
return written;
}

7.3 坑 #3:Android 路径硬编码

1
2
3
4
5
6
7
8
9
// ❌ 错误:硬编码 /sdcard,Android 10+ 写入失败
File::write_all("/sdcard/Download/config.ini", data);

// ✅ 正确:Android 上用 getExternalFilesDir 拿到的路径
auto path = AndroidPaths::external_files_dir() / "config.ini";
File::atomic_write(path.str(), data);

// ✅ 更安全:所有应用数据走 files_dir
auto path = AndroidPaths::files_dir() / "config.ini";

7.4 坑 #4:QNX 文件系统特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ❌ 错误:在 ETFS 上 mmap 大文件
// ETFS 不支持 mmap,会失败

// ✅ 正确:read/write 走块 I/O
File f("/boot/firmware.bin", File::Mode::Read);
char buf[4096];
while (auto n = f.read(buf, sizeof(buf))) {
process(buf, n);
}

// ❌ 错误:在 RAMFS 上做大量持久化
// RAMFS 掉电丢失,OTA 后所有用户配置归零

// ✅ 正确:持久数据走 QNX6FS,临时数据走 RAMFS
QnxKvStore 持久 → /data/kv/ (QNX6FS)
QnxLogRing 临时 → /ram/log.ring (RAMFS)

7.5 坑 #5:编译时不开启 -fno-exceptions

1
2
3
4
5
6
7
8
9
10
11
# CMakeLists.txt 推荐配置
target_compile_options(craton_io PRIVATE
-fno-exceptions # 不抛异常,返回 bool
-fno-rtti # 不需要 RTTI
-Os # 优化体积
)

# 如果必须支持异常:
# 1. 让 Path::exists() 仍然返回 bool(不抛)
# 2. File::open() 失败返回 false,调用方检查
# 3. 不要写 throw 语句

7.6 坑 #6:mmap 后忘记 munmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ❌ 错误:异常路径下 mmap 泄漏
auto p = mmap(nullptr, size, PROT_READ, MAP_SHARED, fd, 0);
if (error_condition) return -1; // 泄漏!
process(p);
munmap(p, size);

// ✅ 正确:RAII 包装
class MmapGuard {
void* p_;
size_t size_;
public:
MmapGuard(void* p, size_t s) : p_(p), size_(s) {}
~MmapGuard() { if (p_) munmap(p_, size_); }
void* get() const { return p_; }
};

7.7 坑速查表

现象解决方案
写一半配置文件损坏atomic_write + fsync 父目录
信号打断write 偶尔返回 0循环 write + 处理 EINTR
Android 路径写入失败getExternalFilesDir()
QNX mmap大文件读失败改用 read/write 块 I/O
RAMFS 持久化掉电数据丢失持久数据走 QNX6FS
异常编译链接报错-fno-exceptions
mmap 泄漏fd 用尽RAII 包装
路径硬编码跨平台失败Path::operator/ 拼接
跨平台换行Windows 上 \r\n 异常只在 QNX/Linux/Android 上跑
文件锁多进程同时写flock 或 rename 原子写

八、平台适配检查清单

检查项LinuxAndroidQNX
路径分隔符 /
Path::exists()
File::open()
File::atomic_write()
KvStoreAndroidKvStore
IniFile
内部存储 /data/data/...
/sdcard 直写⚠️ 需权限API < 29
Scoped storage (API 29+)N/AN/A
RAMFS
ETFS 只读
QNX6FS 持久化
mmap 支持⚠️ 部分 FS 不支持

九、与 Craton 前两篇的衔接

Craton 01(设计)和 Craton 02(内核)已经定义了:

来自 Craton 01本文 Craton 03
命名空间 craton::v1craton::oscraton::timecraton::logcraton::storage完全一致
平台:Linux、QNX、Android完全一致
C++17,无异常(默认)-fno-exceptions 编译
SizeByte 类型(os/types.h直接使用

Craton 04 网络篇将基于本文的 KvStore 做 HTTP cookie 持久化、Craton 05 并发篇将基于 File 做 atomic append-only 日志。


十、读到这里,你应该能做这些事

  • craton::os::Path + craton::os::File 写出不抛异常的跨平台文件 I/O
  • craton::storage::KvStore 实现 1 万条以内的嵌入式 KV
  • craton::storage::IniFile 解析车机启动配置
  • 在 Android 上接 JNI 把 KV 桥到 SharedPreferences
  • 在 QNX 上接 ETFS / QNX6FS / RAMFS
  • 解释为什么 Craton 比 std::filesystem / POCO / SQLite 更适合嵌入式

十一、避坑指南速查(动手前先看)

任务推荐方案避免方案
写配置File::atomic_write + fsync 父目录直接 write 到现有文件
读大文件File::read 块 I/O一次性 mmap 全文件
Android 写公共目录MediaStore / SAF/sdcard 硬编码
QNX 临时数据/ram/ (RAMFS)/data/ (QNX6FS 慢)
QNX 启动脚本ETFS 只读挂载RAMFS 临时数据
跨平台 KVKvStore POSIX 版 + Android JNI 桥强行在不同 FS 上跑同一套代码
多进程写rename 原子写共享 fd + flock

系列导航

#文章状态
1开篇:为什么我们要造一个轮子✅ 已发布
2POCO 源码导读:Net、Util、Foundation 三层架构✅ 已发布
3Craton 01 设计篇:API 设计原则、命名空间、模块划分✅ 已发布
4Craton 02 内核实现:内存、字符串、时间、线程✅ 已发布
5本文:Craton IO 与存储:文件、KV、嵌入式持久化✅ 已发布
6Craton 04 网络:Socket、TCP、UDP、HttpClient🔜 计划中
7Craton 05 并发:ThreadPool、Actor、Channel🔜 计划中
8Craton 06 日志与监控:AsyncLogger、Metrics🔜 计划中
9Craton 07 序列化:JSON、MessagePack、Protobuf🔜 计划中
10Craton 08 平台适配:Linux、Android、QNX、HarmonyOS🔜 计划中
11Craton 09 性能基准:vs POCO、vs Boost、vs std🔜 计划中
12Craton 10 终篇:迁移实战 + 生态搭建🔜 计划中

嵌入式工程师最好的朋友,是把一个 200 KB 的库压缩到 16 KB,再让它跑得比原来快 5 倍的那种工程师。


附录 A:完整源文件清单

文件行数作用
os/path.h65Path 类声明
os/path.cpp120Path 实现
os/file.h95File 类声明
os/file.cpp145File + atomic_write 实现
os/dir_iter.h30DirectoryIterator 声明
os/dir_iter.cpp55DirectoryIterator 实现
os/temp_file.h35TempFile 声明
os/temp_file.cpp35TempFile 实现
storage/kv_store.h95KvStore 声明
storage/kv_store.cpp320KvStore 实现
storage/ini_file.h60IniFile 声明
storage/ini_file.cpp175IniFile 实现
platform/android/kv_store_android.h30AndroidKvStore 声明
platform/android/kv_store_android.cpp220AndroidKvStore JNI 实现
platform/android/path_android.h35AndroidPaths 声明
platform/android/path_android.cpp75AndroidPaths 实现
platform/qnx/boot_script.h35BootScriptLoader 声明
platform/qnx/boot_script.cpp60BootScriptLoader 实现
platform/qnx/log_ring.h30LogRing 声明
platform/qnx/log_ring.cpp165LogRing 实现
Android.bp35Android 编译配置
Makefile.qnx25QNX 编译配置
合计1942 行(不含本文档)

附录 B:关键 API 速查

API文件说明
craton::os::Path::operator/os/path.h路径拼接
craton::os::Path::exists()os/path.h检查存在
craton::os::Path::create_directories()os/path.h递归 mkdir
craton::os::File::open(path, mode)os/file.h打开文件
craton::os::File::atomic_write(path, data)os/file.h原子写
craton::os::DirectoryIterator(dir)os/dir_iter.h遍历目录
craton::storage::KvStore::put/get/removestorage/kv_store.hKV 基本操作
craton::storage::KvStore::begin_transaction/commitstorage/kv_store.h事务
craton::storage::IniFile::load/savestorage/ini_file.hINI 读写
craton::storage::IniFile::get_int/get_boolstorage/ini_file.h类型化读取
craton::platform::android::AndroidKvStoreplatform/android/Android JNI KV
craton::platform::android::AndroidPathsplatform/android/Android 路径
craton::platform::qnx::BootScriptLoaderplatform/qnx/QNX 启动配置
craton::platform::qnx::LogRingplatform/qnx/QNX ramfs 日志环

附录 C:编译选项速查

平台编译命令片段
Linux x86_64g++ -std=c++17 -O2 -fno-exceptions -fno-rtti -Iinclude src/*.cpp
Linux ARMv8 (车机)aarch64-linux-gnu-g++ -std=c++17 -Os -fno-exceptions
Android (NDK)clang++ --target=aarch64-linux-android21 -std=c++17 -fno-exceptions
QNX Neutrinoqcc -Vgcc_ntoaarch64le -std=c++17 -D_QNX_SOURCE -Os
CMake 通用set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions -Os")

本文配套代码:github.com/xuqi2024/craton src/io/src/storage/src/platform/ 目录。