热爱开源,喜欢分析操作系统架构
分类: C/C++
2012-11-04 19:34:47
介绍完了finish shell的数据类型,我现在开始说明语法解释器的运行流程。finish shell初始化函数finsh_system_init完成的功能很简单,主要有4个方面:
调度器启动后,但tshell成为运行态最高优先级后,系统就会运行finsh_thread_entry,最先调用的是finsh_init,它主要是对shell里面的一些变量和全局变量进行初始化设置,比如finish_parse、globe_node_talbe,global_variable之类的,后面都会说到,目前先按下不表。
随后,tshell会被rx_sem阻塞住,直finish shell注册的usart接收中断后,调finsh_rx_ind释放rx_sem去唤醒tshell去接收串口数据。串口数据是一个字节一个字节的读取的,并对每个字节进行分析。其他一些关于历史记录、自动补全的shell功能我就不提,重点是每当系统接收到一个回车符(windows和posix的回车符是不同的,rt_thread已经有了针对的程序分支),就认为用户输入了一段完整的命令,开始调用finsh_run_line来进行语法分析。这段命令会以‘;’为分隔符,被分成一句一句的命令,而这一句一句的命令就会被语法解释器分解成上文提过的各种数据类型,从而被系统的虚拟机识别,最后运行。而组织这些数据类型的最关键的结构就是finish_parse:
命令语句的读取和命令语句的执行不一定以相同的顺序执行,就拿我上一篇举的例子
命令读取自然是从开始到‘;’结束,但是命令的的执行确不是这样的。我们按顺序读取2+3×(2-1),但命令是最先从2-1开始执行的,因此在读取和执行之间需要相应的转换措施 。不仅仅如此,对于这个命令,系统要清楚的知道‘int’是向内存空间声明变量,‘=’是赋值运算,‘2’是常量,‘+’是加法运算,从文本到它对应的实际含义同样也需要一定的转换机制。而finish shell提供的语法解释器恰好完成了这样的工作。
finish_parse是这个语法解释器的核心,你可以把它想象成工厂的流水生产线。parse_string是命令流水线,finsh_token是加工车床。随着position的递增,推动命令一个字符一个字符进入finish_token。而finish_token加工命令不是以字符为单位的,它是以token为单位的,比如‘int’是一个token,‘a’,‘2’,‘=‘也都是token。每当finish_token从流水线上获取一个token后,它就停止流水线开始加工产品了。通过finish_token的加工,文本字符的命令被识别,重组生成finish_node这个半成品零件。我们的虚拟机不识别文本的‘2’,但它能够识别加工后的finish_node——2。这样我们的生产线将命令原料以token为单位分割,加工成一个个finish_node,但这样我们只是解决了之前提及的第二个问题,执行顺序的问题还是需要其他的方式解决。
由于我们完成了文本到finish_node的加工,我们可以从finish_node获取那些符号变量,比如‘=’‘+’‘()’之类的,c对符号运算有一个优先级表,finish_token会按照优先级表,利用符号将剩下的finish_node组织起来拼接起来,从而完成我们虚拟机可以直接运行的命令指令。这样语法解释器的功能就完成了。
本篇我主要讲述finsh_token是如何分割和加工命令。首先finsh_token从line从获取整句命令,再用token_trim_space消除前面的空格和制表符,然后用token_get_string提供token的名称。token类型不外乎是关键词、变量、常量和符号。而变量名称是一由字母数字和下划线组成的开头不为数字的组合,关键词一共就那么几个,剩下的不是符号的就是常量。当然常量有多种类型,符号也有多种,但总的来说识别过程到不是很复杂,看看源码相信很快就能明白。不管要注意,只有常量的值会存放在finish_token的value中,毕竟符号没有值,变量只是个名称,只有常量才有value。
token类型识别后,我们需要将它转为响应的node。读这个识别的过程不可谓不辛苦,涉及十几个函数,又是循环又是递归,真的要介绍起来实在不知如何下手。原谅我偷个懒,绕开繁琐的过程,只是从转换好的结果说起吧。
token类型如果是符号的话,其转换过程是最为简单的。毕竟符号只是代表了单纯一个操作,没有涉及数据内容。生成符号node的函数也非常简单,调finsh_node_allocate(type)就搞定了。符号node里的相关内容只有一个,那就是node_type也就是参数type。而具体的type,可以从finsh_token_type中找到。
稍微复杂一点的是常量,比如说‘2’,它的node里node_type=finsh_token_type_value_int,而value.int=2。Value放的是常量的值,node_type放的是常量的类型。
最为复杂的是变量,常量的类型我们可以从名称中判断出来,但我们却不能从变量名称中了解到这个变量究竟是数字、字符串抑或是函数。幸好按照c语言,一个变量再使用前必须要声明,在finish shell中,声明变量的有三种方案。第一种是静态的编译时申请的,主要是通过FINSH_FUNCTION_EXPORT来声明函数变量,用FINSH_VAR_EXPORT来声明其他的变量。第二种是系统运行时声明的,通过调用finsh_sysvar_append和finsh_syscall_append,来动态添加。而其他的变量可以通过输入关键词声明+名称来完成声明,比如说上面命令里的‘int a’。除了之前提过的SYS_VAR和SYS_CALL,finsih shell还有一个VAR变量类,VAR对应的是这类的变量声明,它对应的内存区为global_variable。就拿‘int a‘来说,系统调用了finsh_var_insert函数从global_variable表中找出空闲的VAR,并将a的名称‘a‘和变量类型‘int’赋给它。当然这个VAR还有一个value的值,声明的时候是不用管它的,就像c语言里,声明变量时不用考虑变量的值,道理是一样的。
每当读取一个变量名称时,finish shell会调用finsh_node_new_id来在全局域中找之前的声明变量,一旦变量名有符合的全局变量,就将建立相应的node,并将其与找到的全局变量的内存地址对应起来。全部变量声明的类型一共有三个,global_variable存放的是VAR变量,sysvar_table和global_sysvar_list放的是SYS_VAR变量, syscall_table和global_syscall_list放的是SYS_CALL函数变量。但找到对应的变量后,finsh_node_new_id会分配以node,其node_type=ID,id_type为变量类型。根据id_type,其变量的值id分为finish_var,finish_sysvar和finish_syscall,而id对应着变量的名称、描述符和指向变量实体的指针。
至于node的分配,和VAR分配的原理一样,也是从一块全局表中取出的。这样静态分配的坏处就是分配的变量数目是有限的,一旦超出预设值,就不能分配了。
到这里finsh_parse就把原来文本形式的命令加工了一个个散列的符号、常量和变量的node。这样的node是不能直接交给虚拟机的,还需要进行重组。而所谓的重组就是以符号node为茎,以常量和变量node为叶,以符号优先级为序,拼接的过程,这个我们下篇再讲。