Zephyr Shell分析

本文分析了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

#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中配置

CONFIG_CONSOLE_SHELL_MAX_CMD_QUEUED=5

一个console_input用于保存一条cmd line

#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中配置

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个参数,命令和参数之间,参数之间都以空格分割, 如下

#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三条,具体流程比较简单,这里不做分析

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_段中

#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”);***展开为

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_预留位置

#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内

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包含

    __data_rom_start = LOADADDR(_DATA_SECTION_NAME);

#include <linker/common-ram.ld>

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

#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定义,一次定义一个模块的所有命令

#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 \
	}

例如下面代码:

static struct shell_cmd commands[] = {
        { "ping", shell_cmd_ping, NULL },
        { "params", shell_cmd_params, "print argc" },
        { NULL, NULL, NULL }
};

SHELL_REGISTER("sample", commands);

展开为

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中查找
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);
}
  1. 没有select,会先根据module名找到module,再从module cmd中到到cmd
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就是你定义命令是自己填进去的

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