我做嵌入式软件,但我认为这不是一个嵌入式问题。我不(因技术原因不能)使用像MySQL这样的数据库,只使用C或C ++结构。
是否存在如何处理从程序版本到版本的这些结构的布局变化的一般原理?
我们来看一本地址簿。从程序版本x到x + 1,如果:
这些只是一些简单的例子;我不是在寻找那些答案,而是寻找通用解决方案。
显然,我需要一些硬编码逻辑来处理每个变化。
如果有人没有从版本x升级到x + 1但等待x + 2怎么办?我应该尝试组合更改,还是只应用x - > x + 1后跟x + 1 - > X + 2?
如果版本x + 1有问题,我们需要回滚到s / w的先前版本,但已经“升级”了数据结构,该怎么办?
我倾向于TLV(http://en.wikipedia.org/wiki/Type-length-value),但可以看到很多潜在的麻烦。
这不是什么新鲜事,所以我只是想知道其他人是怎么做的......
答案 0 :(得分:11)
我确实有一些代码,如果有必要,可以从两个较短的段中将较长的字符串拼凑在一起。呸。这是我保持一些数据兼容12年后的经验:
定义目标 - 有两个:
向版本0添加版本支持 - 至少编写版本标头。保持(可能很多)旧的读者代码,可以原始地解决第一种情况。如果您不想实施案例2,请立即开始拒绝新数据 !
如果您只需要案例1,而且随着时间的推移而预期的变化相当小,那么您已经设置好了。无论如何,在第一次发布之前完成的这两件事可以为您节省许多麻烦。
序列化期间转换 - 在运行时,只将数据保留在内存中的“新格式”中。在持久性限制下进行必要的转换和测试(在读取时转换为最新版本,在编写时实现向后兼容性)。这将版本问题隔离在一个地方,有助于避免难以追踪的错误。
保留所有版本的测试数据集。
存储可用类型的子集 - 将实际序列化数据限制为几种数据类型,例如int,string,double。在大多数情况下,额外的存储大小由减少的代码大小组成,支持这些类型的更改。 (但这并不总是可以在嵌入式系统上进行权衡)。
e.g。不存储短于原生宽度的整数。 (当您需要存储长整型数组时,可能需要这样做)。
添加断路器 - 存储一些密钥,允许您故意使旧代码显示此新数据不兼容的错误消息。您可以使用作为错误消息一部分的字符串 - 然后您的旧版本可能会显示它不知道的错误消息 - “您可以使用我们网站上的ConvertX工具导入此数据”在本地化方面不是很好应用程序但仍然优于“UngültigesFormat”。
不要直接序列化结构 - 这是逻辑/物理分离。我们混合使用两种,都有其优点和缺点。如果没有一些运行时开销,这些都无法实现,这几乎可以限制您在嵌入式环境中的选择。无论如何,在持久性过程中不要使用固定的数组/字符串长度,这应该已经解决了一半的麻烦。
(A)正确的序列化机制 - 我们使用bianry序列化程序,允许在存储时启动“块”,它具有自己的长度标题。在读取时,会跳过额外的数据,并且缺少数据的默认初始化(这简化了序列化代码中大量实现“读取旧数据”。)块可以嵌套。这就是你在物理方面所需要的一切,但需要一些糖衣来完成常见任务。
(B)使用不同的内存中表示 - 内存中的表示基本上可以是map<id, record>
,其中id woukld可能是整数,record
可以是
我最初写道,所以这些人不会问我每个格式兼容性问题,虽然实现有许多缺点(我希望我能认识到今天的清晰度问题......)它可以解决< / p>
默认情况下,查询非现有值将返回默认/零初始化值。当你在访问数据时记住这一点,并且在添加新数据时,这有很大帮助:想象版本1将自动计算“foo长度”,而在版本2中,用户可以覆盖该设置。值为零 - 在“计算类型”或“长度”中应表示“自动计算”,并且您已设置。
以下是您可以预期的“更改”方案:
为了实施案例2,您还需要考虑:
表示不快。那是很多。但它并不像看起来那么复杂。
答案 1 :(得分:4)
关系数据库人们使用了一个巨大的概念。
它被称为将体系结构分解为“逻辑”和“物理”层。
你的结构既是逻辑层又是物理层,它们被混合成一个难以改变的东西。
您希望程序依赖于逻辑层。您希望您的逻辑层依次映射到物理存储。这使您可以在不破坏事情的情况下进行更改。
您无需重新发明SQL即可完成此任务。
如果您的数据完全存在于内存中,那么请考虑一下。将物理文件表示与内存中表示分离。以一些“通用”,灵活,易于解析的格式(如JSON或YAML)编写数据。这允许您以通用格式读取并构建高度版本特定的内存中结构。
如果您的数据已同步到文件系统,则还有更多工作要做。再看一下RDBMS的设计理念。
不要对简单的无脑struct
进行编码。创建一个“记录”,将字段名称映射到字段值。它是名称 - 值对的链接列表。这很容易扩展,可以添加新字段或更改值的数据类型。
答案 2 :(得分:3)
如果您在谈论C API中的结构使用时,请参阅一些简单的指南:
这是一个示例 - 我有一个引导加载程序,它在程序映像中的固定偏移处查找结构,以获取有关可能已闪存到设备中的图像的信息。
加载程序已经过修改,它支持结构中的其他项目以进行一些增强。但是,较旧的程序映像可能会闪烁,而较旧的映像使用旧的struct格式。由于上面的规则从一开始就遵循,新的加载器完全能够处理。这很容易。
如果结构进一步修改并且新图像在具有旧加载器的设备上使用新的结构格式,那么该加载器也将能够处理它 - 它只是不会对增强做任何事情。但是由于没有(或将要)删除任何字段,旧的加载器将能够执行其设计的任何操作,并使用具有更新信息的配置结构的较新映像执行此操作。
如果您在谈论的是一个包含有关字段等元数据的实际数据库,那么这些指南并不适用。
答案 3 :(得分:2)
您正在寻找的是前向兼容的数据结构。有几种方法可以做到这一点。这是低级方法。
struct address_book
{
unsigned int length; // total length of this struct in bytes
char items[0];
}
其中'items'是一个结构的可变长度数组,描述了它自己的大小和类型
struct item
{
unsigned int size; // how long data[] is
unsigned int id; // first name, phone number, picture, ...
unsigned int type; // string, integer, jpeg, ...
char data[0];
}
在你的代码中,你通过一些智能投射迭代这些项目(address_book-&gt;长度将告诉你何时结束)。如果您点击了一个您不知道的ID或您不知道如何处理的类型的项目,您只需跳过该数据(从item-&gt; size)跳过它,然后继续下一个。这样,如果某人在下一个版本中发明了新数据字段或删除了一个,您的代码就能够处理它。您的代码应该能够处理有意义的转换(如果员工ID从整数变为字符串,它应该可以将其作为字符串处理),但您会发现这些情况非常罕见,并且通常可以使用公共代码处理
答案 4 :(得分:2)
我过去曾经在资源非常有限的系统中处理过这种情况,通过在PC上进行翻译作为s / w升级过程的一部分。您可以提取旧值,转换为新值,然后更新就地数据库吗?
对于简化的嵌入式数据库,我通常不会直接引用任何结构,但会在任何参数周围放置一个非常轻量级的API。这允许您更改API下面的物理结构,而不会影响更高级别的应用程序。
答案 5 :(得分:1)
最近我正在使用bencoded数据。这是bittorrent使用的格式。很简单,您可以直观地检查它,因此它比二进制数据更容易调试并且紧凑。我借用了高质量的C ++ libtorrent中的一些代码。对于你的问题,它很简单,因为当你读回它时,检查字段是否存在。而且,对于gzip压缩文件,它就像在做:
ogzstream os(meta_path_new.c_str(), ios_base::out | ios_base::trunc);
Bencode map(Bencode::TYPE_MAP);
map.insert_key("url", url.get());
map.insert_key("http", http_code);
os << map;
os.close();
要读回来:
igzstream is(metaf, ios_base::in | ios_base::binary);
is.exceptions(ios::eofbit | ios::failbit | ios::badbit);
try {
torrent::Bencode b;
is >> b;
if( b.has_key("url") )
d->url = b["url"].as_string();
} catch(...) {
}
我过去使用过Sun的XDR格式,但现在我更喜欢这种格式。使用perl,python等其他语言阅读也更容易。
答案 6 :(得分:1)
在结构中嵌入版本号,或者像Win32一样使用版本号并使用大小参数 如果传递的结构不是最新版本,则修复结构。
大约10年前,我为电脑游戏保存游戏系统编写了类似的系统。我实际上将类数据存储在一个单独的类描述文件中,如果我发现版本号不匹配,那么我将通过类描述文件,找到类,然后根据描述升级二进制类。这显然需要在新的类成员条目中填写默认值。它工作得很好,它也可以用来自动生成.h和.cpp文件。
答案 7 :(得分:1)
我同意S.Lott的观点,即最佳解决方案是将您要做的事情的物理和逻辑层分开。您实际上是将您的接口和实现组合到一个对象/结构中,这样做就会错过一些抽象的力量。
但是,如果您必须使用单个结构,那么您可以采取一些措施来帮助简化操作。
1)实际上需要某种版本号字段。如果您的结构发生变化,您将需要一种简单的方法来查看它并知道如何解释它。沿着这些相同的路线,有时将结构的总长度存储在某个结构域中是有用的。
2)如果要保持向后兼容性,您需要记住,代码将在内部引用结构字段作为结构基址的偏移量(来自结构的“前面”)。如果您想避免破坏旧代码,请确保将所有新字段添加到结构的 back 并保留所有现有字段(即使您不使用它们)。这样,旧代码就能够访问结构(但最后会忘记额外的数据),新代码可以访问所有数据。
3)由于您的结构可能会改变大小,因此不要依赖sizeof(struct myStruct)
来始终返回准确的结果。如果您遵循上面的#2,那么您可以看到您必须假设未来结构可能会变大。对sizeof()
的调用计算一次(在编译时)。使用“结构长度”字段可以确保当您(例如)memcpy
复制整个结构的结构时,包括您不知道的末尾的任何额外字段。
4)绝不删除或缩小字段;如果你不需要它们,请将它们留空。不要改变现有字段的大小;如果您需要更多空间,请创建一个新字段作为旧字段的“长版本”。这可能会导致数据重复问题,因此请务必仔细考虑您的结构,并尝试规划字段,以便它们足够大以适应增长。
5)不要在结构中存储字符串,除非您知道将它们限制为某个固定长度是安全的。相反,只存储指针或数组索引并创建一个字符串存储对象来保存可变长度的字符串数据。这也有助于防止字符串缓冲区溢出覆盖结构的其余数据。
我参与过的几个嵌入式项目都使用此方法修改结构,而不会破坏向后/向前兼容性。它有效,但它远非最有效的方法。不久之后,你最终会浪费空间与过时/废弃的结构字段,重复的数据,零碎存储的数据(这里的第一个字,那里的第二个字)等等。如果你被迫在现有的框架内工作,那么这可能为你工作。但是,使用界面抽象出物理数据表示将更加强大/灵活,而且不那么令人沮丧(如果您有使用这种技术的设计自由)。
答案 8 :(得分:0)
您可能需要了解Boost Serialization库如何处理该问题。