【问题标题】:Strategy for estimating / calculating buffer space needed by writer function on embedded system估计/计算嵌入式系统编写器功能所需的缓冲区空间的策略
【发布时间】:2011-10-18 11:07:38
【问题描述】:

这本身并不是一个令人眼花缭乱的编程问题,而更像是一个设计模式问题。我原以为这是嵌入式资源有限系统上的一个常见设计问题,但到目前为止我在 SO 上发现的所有问题似乎都不相关(但请指出我可能遗漏的任何相关内容)。

基本上,我正在尝试制定最佳策略来估计某些写入器函数所需的最大缓冲区大小,当该写入器函数的输出不固定时,特别是因为某些数据是可变长度的文本字符串。

这是一个在小型 ARM micro 上运行的 C 应用程序。应用程序需要通过 TCP 套接字发送各种类型的消息。当我想发送一个 TCP 数据包时,TCP 堆栈(Keil RL)为我提供了一个缓冲区(库从它自己的池中分配),我可以将数据包数据有效负载写入其中。缓冲区大小当然取决于 MSS;所以让我们假设它最多为 1460,但它可能会更小。

一旦我有了这个缓冲区,我就把这个缓冲区和它的长度传递给一个编写器函数,该函数又可以调用各种嵌套的编写器函数来构建完整的消息。这种结构的原因是因为我实际上正在生成一个小的 XML 文档,其中每个编写器函数通常会生成一个特定的 XML 元素。每个 writer 函数都想将一些字节写入我分配的 TCP 数据包缓冲区。我只知道给定编写器函数在运行时写入了多少字节,因为一些封装的内容取决于用户定义的可变长度的文本字符串。

有些消息的大小需要(比如)2K 左右,这意味着它们可能会被拆分到至少两个 TCP 数据包发送操作中。这些消息将通过调用一系列编写器函数来构建,这些编写器函数一次生成一百个字节。

在调用每个编写器函数之前,或者可能在编写器函数本身中,我最初需要比较可用的缓冲区空间与编写器函数需要多少;如果没有足够的可用空间,则传输该数据包并稍后继续写入新数据包。

我正在考虑的可能解决方案是:

  1. 最初使用另一个更大的缓冲区将所有内容写入。由于资源限制,这不是首选。此外,我仍然希望有一种方法可以通过算法计算出我的消息编写器函数需要多少空间。

  2. 在编译时,为每个编写器函数生成一个“最坏情况大小”常量。每个 writer 函数通常会生成一个 XML 元素,例如 <START_TAG>[string]</START_TAG>,所以我可以有类似的东西:#define SPACE_NEEDED ( START_TAG_LENGTH + START_TAG_LENGTH + MAX_STRING_LENGTH + SOME_MARGIN )。无论如何,我的所有内容编写器函数都是从函数指针表中挑选出来的,因此我可以将每个编写器函数的最坏情况大小估计常量作为该表中的新列存在。在运行时,我根据该估计常数检查缓冲空间。这可能是我目前最喜欢的解决方案。唯一的缺点是它确实依赖于正确的维护才能使其工作。

  3. 我的编写器函数提供了一种特殊的“虚拟运行”模式,它们在其中运行并计算它们想要写入但不写入任何内容的字节数。这可以通过简单地发送 NULL 来代替函数的缓冲区指针来实现,在这种情况下,函数的返回值(通常表示写入缓冲区的数量)只是表示它想要写入多少。我唯一不喜欢的是,在“虚拟”和“真实”调用之间,基础数据可能 - 至少在理论上 - 会发生变化。一个可能的解决方案是静态捕获底层数据。

提前感谢您的任何想法和 cmets。

解决方案

自从发布问题以来,我实际上已经开始做的事情是让每个内容编写器函数接受一个状态或“迭代”参数,这允许编写器被 TCP 发送函数多次调用。 writer 被调用,直到它标记它没有更多可写。如果 TCP 发送函数在某个迭代后决定缓冲区现在接近满了,它会发送数据包,然后该过程稍后会继续使用新的数据包缓冲区。我认为这种技术与 Max 的回答非常相似,因此我接受了。

关键是在每次迭代中,必须设计一个内容写入器,使其不会向缓冲区写入超过LENGTH 个字节;并且在每次调用 writer 之后,TCP 发送函数将在再次调用 writer 之前检查它在数据包缓冲区中是否还有 LENGTH 空间。如果没有,它会在一个新的数据包中继续。

我做的另一个步骤是认真考虑如何构建邮件标题。很明显,就像我认为几乎所有使用 TCP 的协议一样,必须在应用程序协议中实现一些指示总消息长度的方法。这是因为 TCP 是基于 stream 的协议,而不是基于数据包的协议。这又是让人有点头疼的地方,因为我需要一些预先的方法来了解插入到起始标头中的总消息长度。解决这个问题的简单方法是在每个发送的 TCP 数据包的开头插入一个消息头,而不是仅在应用程序协议消息的开头(当然可能跨越多个 TCP 套接字),并且基本上实现 fragmentation。所以,在标题中,我实现了两个标志:fragment 标志和last-fragment 标志。因此,每个标头中的length 字段只需要说明特定数据包中有效负载的大小。在接收端,从流中读取单个标头+有效负载块,然后重新组装成完整的协议消息。

毫无疑问,这无疑非常简单地说明了 HTTP 和许多其他协议如何通过 TCP 工作。非常有趣的是,只有在我尝试编写一个可在 TCP 上运行的健壮协议时,我才开始意识到真正考虑消息结构的重要性,包括标题、帧等,以便它工作通过流协议。

【问题讨论】:

    标签: c buffer


    【解决方案1】:

    我在一个小得多的嵌入式系统中遇到了一个相关问题,该系统在 PIC 16 微控制器上运行(并且用汇编语言而不是 C 语言编写)。我的“缓冲区大小”始终是两个字节的 UART 传输队列,而我只有一个“编写器”函数,它正在遍历 DOM 并发出其 XML 序列化。

    我想出的解决方案是“彻底解决”问题。 writer 函数变成了一项任务:每次调用它时,它都会写入尽可能多的字节(根据串行数据传输速率可能大于 2),直到传输缓冲区已满,然后返回。但是,它会在状态变量中记住它通过 DOM 的距离。下次调用它时,它会从先前到达的点继续。从循环中重复调用编写器任务。如果没有可用的缓冲区空间,它会立即返回而不改变其状态。它从无限循环中重复调用,该循环充当此任务和系统中其他任务的循环调度程序。每次循环,都有一个等待 TMR0 定时器溢出的延迟。所以每个任务在固定的时间片内只被调用一次。

    在我的实现中,数据由 TxEmpty 中断例程传输,但也可以由另一个任务发送。

    我猜这里的“模式”是程序计数器的一个角色是保持控制流的当前状态,并且这个角色可以从 PC 抽象到另一个数据结构。

    显然,这并不立即适用于您更大、更高级别的系统。但这是看待问题的另一种方式,可能会激发您自己的独特见解。

    祝你好运!

    【讨论】:

    • 嗨 Max,非常感谢您的友好回答,很抱歉我花了这么长时间才正确回复这个问题! (过去我自己做过很多 PIC 汇编器工作——不过现在主要是 C。)我将用我最后所做的来编辑我的原始问题文本。