如何为低内存系统设置 flex 和 bison?

How to setup flex and bison for low memory systems?

我喜欢借助 flex 和 bison 构建一个简单的协议解析器。它解析像 "reset"、"led on"、"set uart 115200,N,8" 这样的命令。 解析器应该 运行 在 stm32f4 MCU(48Mhz,256KBytes RAM)上。

在 MCU 运行s Zephyr OS 组织时序和内存。将执行线程的堆栈大小设置得更高一些(大约 5kBytes)后,我可以在没有内存异常的情况下调用 yyparse。 现在我面临一个新问题。似乎 flex 运行s 内存不足:

out of dynamic memory in yyensure_buffer_stack()!
out of dynamic memory in yy_create_buffer()!
out of dynamic memory in yy_create_buffer()!
flex scanner jammed!
fatal flex scanner internal error--end of buffer missed!

有什么办法可以解决这个问题吗?

我的 flexer 设置是:

%option warn noyywrap nodefault
%option 8Bit batch never-interactive
%{
#include <stdlib.h>
#include <stdio.h>

// stack size
#define YYMAXDEPTH 50


#define YY_NO_UNISTD_H
#include "notification_par.h"

#define MY_YY_INPUT(buf,result,max_size) \
    { \
    size_t n; \
    my_yyInput(buf, &n, max_size); \
    result = n; \
    } \

#define YY_INPUT MY_YY_INPUT
#define YY_DECL int yylex()

#define YY_FATAL_ERROR(msg) my_fatal_error( msg )

%}

还有野牛:

%{
#define YY_NO_UNISTD_H

#include <stdio.h>
#include <stdlib.h>

extern int yylex();
extern int yyparse();
extern FILE* yyin;
void yyerror(const char* s);


#include <misc/printk.h>
%}

%union {
    char val[64];
}

我从 uart3 队列读取字符的简单函数:

void my_yyInput( char *buffer, size_t * numBytesRead, size_t bLn ) {
    size_t i;
    *numBytesRead = 0;
    for (i = 0; i < bLn; i++) {
        if ( 0 == k_msgq_get(&uart3.rxQ, buffer, MSEC(100))) {
            buffer++;
            (*numBytesRead)++;
        } else {
            // make sure that we read at least one char
            if (*numBytesRead) return;
        }
    }
}

在开始讨论节省内存之前的重要说明。您覆盖 yy_fatal_error,大概是为了避免使用 fprintf。这很好,但重要的是要注意 yy_fatal_error 被声明为 noreturn,并且 flex 扫描器不期望它为 return。您的实施 必须 遵守此规则;它应该调用 exit 或 Zephyr OS 中的任何等效项;否则,扫描器将在发生致命错误后尝试继续,从而导致更多致命错误或段错误或其他不良后果。 (此外,添加感叹号有点奇怪。它看起来像是您使用的是非标准版本的 flex。)

实际上我对错误 out of dynamic memory in yyensure_buffer_stack()! 有点吃惊,因为该错误是在仅分配了一个指针的位置产生的,大概在您的系统上总共有四个字节。这表明 malloc 根本不起作用,这将被证明是一个问题,因为如果没有一些动态存储分配,flex 扫描器将无法工作。 (如果 malloc 不可用,可以提供替代功能。)

扫描器分配四个字节来存储指向新创建的缓冲区状态对象的指针后,它会继续分配缓冲区状态本身(大约四十个字节),然后分配缓冲区。缓冲区(通常)为 16378 字节(加上两个额外的字节,总共 16380),这对于只有 256k 内存的机器来说是相当多的。这就是您要关注的部分。

实际上,很少需要这么大的缓冲区,特别是因为扫描器通常可以在需要时重新分配缓冲区。缓冲区需要比您希望解析的最大单个标记大一点;尽管对于交互式程序来说,它的长度足以容纳一行输入可能会有所帮助。默认设置对于需要处理长注释和字符串常量的编译器很有帮助,但对于您的应用程序,使用更小的缓冲区可能就足够了。

有两种减少缓冲区大小的基本方法。最简单的方法是在序言中定义一个宏:

#define YY_BUF_SIZE 200

然后您可以使用正常的 YY_INPUT 机制来读取输入,就像您示例中的代码一样。

虽然这很简单,但并不是最有效的。一次读取一个字符(或一次读取几个字符)会导致大量的重新扫描,而且内核队列机制也会增加一些开销,其中 none 是必需的。假设您的协议消息明确地以换行符终止(也就是说,每条消息都以换行符终止并且每个换行符终止一条消息)那么一个更有效的解决方案是将一行输入累积到您自己的缓冲区中,您可以直接从串口做,然后从缓冲区解析整行。

您需要记住的一件事是弹性缓冲区必须以两个 NUL 字节终止。因此,您的输入缓冲区需要比您期望能够处理的最长协议消息长两个字节。

结果可能如下所示:

/* Assume there is an ISR which accumulates characters into line until a newline
 * is received, updating line_read for each character. It needs to stop reading
 * when it reaches MAX_PROTOCOL_SIZE.
 */

#define MAX_PROTOCOL_SIZE 254
char line[MAX_PROTOCOL_SIZE + 2];
int  line_read = 0;

/* When the new line character is read, this function is called. Here I assume
 * that the ISR did not bother to NUL-terminate the input.
 */
void process_line(char* buffer, int buflen) {
  buffer[buflen] = buffer[buflen + 1] = 0; /* Terminate with two NULs */
  YY_BUFFER_STATE flex_buf = yy_scan_buffer(buffer, buflen + 2);
  yyparse();
  yy_delete_buffer(flex_buf);
  yylex_destroy();  /* See note */
}

yylex_destroy 的调用将释放 () 动态分配(如前所述,动态分配很小:指向缓冲区状态的单个指针和缓冲区状态本身,大约 40 字节。您可以重用这些分配;调用 yylex_destroy 的优点是它会重新初始化 flex 状态,如果在没有读取整个输入的情况下解析失败,这会很有用。

缓冲区本身永远不会 malloc'd 或 free'd,因为它是您的缓冲区。它也永远不会重新填充,因此您可以定义 YY_INPUT 不执行任何操作或抛出错误。