本文分析了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如下示意图:
cmd line buffer
Shell内有一个console_input数组buf用于存放console输入的cmd line1
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 line1
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的流转过程:
- shell初始化时将cmd line buf全部加入到avail_queue
- console从avail_queue取出一个cmd line buffer,将收到数据放入
- console接收到回车后,将cmd line buffer加入到cmds_queue中
- shell thread一直读cmds_queue,发现有cmd line后取出处理
- 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按下面三步进行:
- parser参数
- 查找命令
- 执行命令
参数
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种:
- 内部命令
- 独立命令
- 模块命令
查找执行命令时依照内部命令->独立命令->模块命令的方式进行查找,首次找到后就不会再进行查找,例如独立命令和模块命令内有相同的命令,则只会执行独立命令。
内部命令
内部命令internal_commands以static变量的形式被直接放到.data段,查找的时候在internal_commands搜寻即可
内部命令只有help, select, exit三条,具体流程比较简单,这里不做分析1
2
3
4
5
6
7
8
9
10
11static 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
6const 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
4SECTION_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
7static 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
6static struct shell_module (__shell__name) __used
__attribute__((__section__(".shell_module_"))) = {
.module_name = "sample",
.commands = commands,
.prompt = NULL
}
查找
module的存放位置和方式和前面的cmd一致,只是放在shell_module_段内以__shell_module_start为开始
模块命令的查找状态有两种
已经select模块,那么直接从default_module中查找
1
2
3
4
5
6
7get_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);
}没有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
23static 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
8typedef 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