Zephyr Shell分析

Creative Commons
本作品采用知识共享署名

本文分析了zephyr shell的工作原理,同时提到了和shell相关的一些配置选项

概述

shell已console为交互接口,提供一个CLI,包含以下功能:

  • 管理&组织cmd
  • 自动完成cmd
  • 接收输入module的cmd line并parser出cmd和参数
  • 执行cmd
  • 显示执行cmd的过程和其它信息

Interface

Shell使用console作为interface和用户交互,一般情况下支援uart和telnet两种console。当telnet connect时uart console暂停工作,telnet exit后uart console继续工作。Shell Interface如下示意图:
shell

cmd line buffer

Shell内有一个console_input数组buf用于存放console输入的cmd line

1
2
#define MAX_CMD_QUEUED CONFIG_CONSOLE_SHELL_MAX_CMD_QUEUED
static struct console_input buf[MAX_CMD_QUEUED];

buf的大小CONFIG_CONSOLE_SHELL_MAX_CMD_QUEUED默认是3个, 可在prj.conf中配置

1
CONFIG_CONSOLE_SHELL_MAX_CMD_QUEUED=5

一个console_input用于保存一条cmd line

1
2
3
4
5
6
7
8
9
#define CONSOLE_MAX_LINE_LEN CONFIG_CONSOLE_INPUT_MAX_LINE_LEN
struct console_input {
/** FIFO uses first 4 bytes itself, reserve space */
int _unused;
/** Whether this is an mcumgr command */
u8_t is_mcumgr : 1;
/** Buffer where the input line is recorded */
char line[CONSOLE_MAX_LINE_LEN];
};

cmd line的最大长度由CONFIG_CONSOLE_INPUT_MAX_LINE_LEN决定,默认128,可以在prj.conf中配置

1
CONFIG_CONSOLE_INPUT_MAX_LINE_LEN=64

从console获取cmd line

Shell在初始化的时候创建两个fifo
avail_queue cmd line 空闲buffer fifo, 将空闲buffer从shell送到console
cmds_queue cmd line使用buffer fifo,将含有cmd line的buffer从console送到shell
cmd line buffer的流转过程:

  1. shell初始化时将cmd line buf全部加入到avail_queue
  2. console从avail_queue取出一个cmd line buffer,将收到数据放入
  3. console接收到回车后,将cmd line buffer加入到cmds_queue中
  4. shell thread一直读cmds_queue,发现有cmd line后取出处理
  5. shell thread处理cmd line后将其又加入到avail_queue

printk

当console建立时会使用__printk_hook_install注册console的输出函数,因此shell调用printk最后是通过console输出的。
console的具体工作机制不在本文讨论范围内,之后会写文章分析uart console再详细说明。

Shell CMD

Shell thread从fifo中拿到cmd line按下面三步进行:

  1. parser参数
  2. 查找命令
  3. 执行命令

参数

shell cmd最多支持10个参数,命令和参数之间,参数之间都以空格分割, 如下

1
2
3
#define ARGC_MAX 10
char *argv[ARGC_MAX + 1], **argv_start = argv;
argc = line2argv(line, argv, ARRAY_SIZE(argv));

shell使用line2argv来提取参数到argv数组中,argv[0]放置的cmd,后面放置的是arg

查找命令

shell内部将命令分为3种:

  1. 内部命令
  2. 独立命令
  3. 模块命令

查找执行命令时依照内部命令->独立命令->模块命令的方式进行查找,首次找到后就不会再进行查找,例如独立命令和模块命令内有相同的命令,则只会执行独立命令。

内部命令

内部命令internal_commands以static变量的形式被直接放到.data段,查找的时候在internal_commands搜寻即可
内部命令只有help, select, exit三条,具体流程比较简单,这里不做分析

1
2
3
4
5
6
7
8
9
10
11
static const struct shell_cmd *get_internal(const char *command)
{
static const struct shell_cmd internal_commands[] = {
{ "help", cmd_help, "[command]" }, //显示帮助信息
{ "select", cmd_select, "[module]" }, //选中指定模块为默认模块,代码体现为从__shell_module_start中到对应模块设置到default_module
{ "exit", cmd_exit, NULL }, //退出某个模块,代码体现为将default_module设置为NULL
{ NULL },
};

return get_cmd(internal_commands, command);
}

独立命令

定义

独立命令通过宏SHELL_REGISTER_COMMAND定义,被放到.shell_cmd_段中

1
2
3
4
5
6
7
8
#define SHELL_REGISTER_COMMAND(name, callback, _help) \
\
const struct shell_cmd (__shell__##name) __used \
__attribute__((__section__(".shell_cmd_"))) = { \
.cmd_name = name, \
.cb = callback, \
.help = _help \
}

例如SHELL_REGISTER_COMMAND(“version”, shell_cmd_version,”Show kernel version”);展开为

1
2
3
4
5
6
const struct shell_cmd __shell__shell_cmd_version __used 
__attribute__((__section__(".shell_cmd_"))) = {
.cmd_name = "version",
.cb = shell_cmd_version,
.help = "Show kernel version"
}

查找

在include/linker/linker-defs.h中定义了SHELL_INIT_SECTIONS为段.shell_cmd_预留位置

1
2
3
4
5
6
7
#define	SHELL_INIT_SECTIONS()				\
__shell_module_start = .; \ //shell_module_段起始地址
KEEP(*(".shell_module_*")); \
__shell_module_end = .; \ //shell_module_段起始地址
__shell_cmd_start = .; \ //shell_cmd_段起始地址
KEEP(*(".shell_cmd_*")); \
__shell_cmd_end = .; \ //shell_cmd_段结束地址

SHELL_INIT_SECTIONS被放在include/linker/common-ram.ld, 虽然.shell_cmd_从文件上看是放到ram内,但实际是依配置而定,例如XIP的话最终会被放到FLASH内

1
2
3
4
SECTION_DATA_PROLOGUE(initshell, (OPTIONAL),)
{
SHELL_INIT_SECTIONS()
} GROUP_DATA_LINK_IN(RAMABLE_REGION, ROMABLE_REGION)

common-ram.ld被include/arch/arm/cortex_m/scripts/link.ld包含

1
2
3
    __data_rom_start = LOADADDR(_DATA_SECTION_NAME);

#include <linker/common-ram.ld>

从前面的ld文件分析可以看出通过宏SHELL_REGISTER_COMMAND定义的独立命令会被放到.shell_cmd_段中, shell_cmd_start是它的起始地址,下面代码显示了从shell_cmd_start开始查找独立命令的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define NUM_OF_SHELL_CMDS (__shell_cmd_end - __shell_cmd_start)

static const struct shell_cmd *get_standalone(const char *command)
{
int i;

for (i = 0; i < NUM_OF_SHELL_CMDS; i++) {
if (!strcmp(command, __shell_cmd_start[i].cmd_name)) {
return &__shell_cmd_start[i];
}
}

return NULL;
}

模块命令

定义

模块命令使用宏SHELL_REGISTER定义,一次定义一个模块的所有命令

1
2
3
4
5
6
7
8
9
10
11
#define SHELL_REGISTER(_name, _commands) \
SHELL_REGISTER_WITH_PROMPT(_name, _commands, NULL)

#define SHELL_REGISTER_WITH_PROMPT(_name, _commands, _prompt) \
\
static struct shell_module (__shell__name) __used \
__attribute__((__section__(".shell_module_"))) = { \
.module_name = _name, \
.commands = _commands, \
.prompt = _prompt \
}

例如下面代码:

1
2
3
4
5
6
7
static struct shell_cmd commands[] = {
{ "ping", shell_cmd_ping, NULL },
{ "params", shell_cmd_params, "print argc" },
{ NULL, NULL, NULL }
};

SHELL_REGISTER("sample", commands);

展开为

1
2
3
4
5
6
static struct shell_module (__shell__name) __used 
__attribute__((__section__(".shell_module_"))) = {
.module_name = "sample",
.commands = commands,
.prompt = NULL
}

查找

module的存放位置和方式和前面的cmd一致,只是放在shell_module_段内以__shell_module_start为开始
模块命令的查找状态有两种

  1. 已经select模块,那么直接从default_module中查找

    1
    2
    3
    4
    5
    6
    7
    get_module_cmd(default_module, argv[0])

    static const struct shell_cmd *get_module_cmd(struct shell_module *module,
    const char *cmd_str)
    {
    return get_cmd(module->commands, cmd_str);
    }
  2. 没有select,会先根据module名找到module,再从module cmd中到到cmd

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    static struct shell_module *get_destination_module(const char *module_str)
    {
    int i;

    for (i = 0; i < NUM_OF_SHELL_ENTITIES; i++) {
    if (!strncmp(module_str,
    __shell_module_start[i].module_name, //从__shell_module_start开始查找模块
    MODULE_NAME_MAX_LEN)) {
    return &__shell_module_start[i];
    }
    }

    return NULL;
    }

    module = get_destination_module(argv[0]);
    if (module) { //找到模块后再从module中找到cmd
    cmd = get_module_cmd(module, argv[1]);
    if (cmd) {
    argc--;
    argv_start++;
    }
    }

例如执行kernel version, argv[0]=”kernel”, argv[1]=”version”,会先找到和kernel匹配的module,再从module找到和”version”匹配的cmd

执行命令

无论是那种命令最后通过查找都会得到一个struct shell_cmd,执行命令就是执行这个结构体里面的cb,而这个cb就是你定义命令是自己填进去的

1
2
3
4
5
6
7
8
typedef int (*shell_cmd_function_t)(int argc, char *argv[]);

struct shell_cmd {
const char *cmd_name;
shell_cmd_function_t cb;
const char *help;
const char *desc;
};

总结

shell的整个工作过程可以总结为:
1.通过SHELL_REGISTER_COMMAND和SHELL_REGISTER注册你自定义的命令到段shell_cmd_和shell_module_–>将字符串和自定义函数绑定
2.console接收到cmd line并将cmd line送到shell
3.shell解析cmd line得到模块名,命令字符串和参数
4.shell以模块命和命令字符串为索引在shell_cmd_和shell_module_查找对应的自定义函数
4.调用自定义函数

参考代码

https://github.com/zephyrproject-rtos/zephyr/tree/master/subsys/shell
https://github.com/zephyrproject-rtos/zephyr/tree/master/drivers/console
https://github.com/zephyrproject-rtos/zephyr/blob/master/misc/printk.c