vivi进入main之后,分8个步骤来完成相应的工作。每一步完成一个功能。使得程序看起来也比较清晰。
1. misc
main中从第7步开始涉及到命令的相关处理函数了。
/*
* Step 7:
*/
misc();
init_builtin_cmds();
|
misc() 的工作很简单,即能过add_command来添加一个命令。如下:
extern user_command_t cpu_cmd; //这是一个命令结构
int
misc(void)
{
add_command(&cpu_cmd);
return 0;
}
|
vivi 中的单个命令是一个 user_command_t 数据结构,具体定义如下:
typedef struct user_command{
const char *name; //命令名
void (*cmdfunc)(int argc, const char **); //函数指针,即指向该命令的处理函数
struct user_command *next_cmd; //这个指针指向下一个命令结构,用于组成链表
const char *helpstr; //该命令的帮助信息
} user_command_t;
|
我们来具体看一个命令:help
user_command_t help_cmd = {
"help", //命令名,查找命令时就是通过命令名来遍历链表查找。
command_help, // help命令的具体执行函数
NULL, //指向下一个命令,若为最后一个命令则为NULL
"help [{cmd}] \t\t\t-- Help about help?" //帮助,当我们敲击一个命令的帮助时,就显出这一行,vivi中的帮助很简洁吧。
};
|
vivi中的各个命令是通过链表组织起来的,其定义了全局的头指针 head_cmd 和 尾指针
tail_cmd;添加一个命令就是向链表的表尾插入这个命令结构。前面 misc中用到add_command()。来看一下这个函数。
/*
* === FUNCTION ======================================================================
* Name: add_command
* Description: add user command
* =====================================================================================
*/
void
add_command(user_command_t *cmd)
{
if (head_cmd == NULL)
head_cmd = tail_cmd = cmd;
else {
tail_cmd->next_cmd = cmd;
tail_cmd = cmd;
}
}
可以看到,这个函数实际上就是向链表加添加结点。每个结点就是一个命令结构。
|
2. init_builtin_cmds();
int
init_builtin_cmds(void)
{
printk("init built-in commands\n");
add_command(&bon_cmd);
add_command(&reset_cmd);
// add_command(&part_cmd);
// add_command(&load_cmd);
// add_command(&go_cmd);
add_command(&dump_cmd);
// add_command(&call_cmd);
add_command(&boot_cmd);
add_command(&help_cmd);
return 0;
}
这里做的事跟misc中差不多,只不过是加载的命令不同而已。
|
至此,vivi的命令就加载完成了。
3.
/*
* Step 8:
*/
boot_or_vivi();
|
开始步骤8.
回想使用vivi的时候,开发板上电,然后在一段时间内,我们按下键盘中的空格键(或其它非回车键),vivi就进入了一个类似于命令行的状态。这个时候,我们可以在终端minicom里输入命令,然后回车,就可以完成相应的工作。如果不按键,则vivi自动的启动kernel。这个过程看起来如此熟悉而简单,但具体到代码上,它又是如何实现的呢?下面一点一点的展开分析。
static void
boot_or_vivi(void)
{
char c;
int ret;
ulong boot_delay;
/* 这里get_parm_value会从我们的参数中取得boot_delay的延时时间参数,如果这个延时参数为0,则直接调用vivi_shell(),也即进入vivi的命令行。 */
boot_delay = get_param_value("boot_delay", &ret);
if (ret)
boot_delay = DEFAULT_BOOT_DELAY;
if (boot_delay == 0)
vivi_shell();
printk("Press Return to start the LINUX now, any other key for vivi\n");
/* awaitkey()用来等待一段时间,时间长度为boot_delay参数决定。awaitkey()这个函数我们下面再具体分析,这里只了解其功能。 awaitkey()在延时阶段会尝试从键盘获取字符,若获取到字符,则存于c 中,后续再判断是哪个键,若为回车则调用run_autoboot()来启动内核。否则进入vivi_shell(),即vivi的命令行。 */
c = awaitkey(boot_delay, NULL);
if ((c != '\r') && (c != '\n') && (c != '\0')) {
printk("type \"help\" for help.\n");
vivi_shell();
}
run_autoboot();
return;
}
|
awaitkey()位于smc_core.c中,其作用上面已经提到,
在延时阶段会尝试从键盘获取字符,若获取到了字符则立即返回。
__u8
do_getc(vfuncp idler, unsigned long timeout, int *statp)
{
__u8 c, rxstat;
int do_timeout = timeout != 0;
getc_errno = 0;
/*
* SERIAL_CHAR_READY() (UTRSTAT0 & UTRSTAT_RX_READY)
* 0 : empty
* 1 : the buffer register has received a data
* 所以这里只要一获取到字符,则立即调出while循环
*/
while (!SERIAL_CHAR_READY()) {
if (do_timeout) {
if (!timeout)
break;
timeout--;
}
if (idler)
idler();
}
if (do_timeout && timeout == 0) {
//延时时间结束,设置返回参数
c = 0;
rxstat = -1;
} else {
//获取到了字符,设置相应的返回参数
c = SERIAL_READ_CHAR();
rxstat = SERIAL_READ_STATUS();
}
if (rxstat) {
getc_errno = rxstat;
if (statp)
*statp = rxstat;
}
return c;
}
char
awaitkey(unsigned long delay, int *error_p)
{
return (do_getc(NULL, delay, error_p));
}
|
下面我们看一下vivi_shell()。这个是重点。
static void
vivi_shell(void)
{
serial_term();
}
|
serial_term();如下。完成工作为:1. 获取输入 2. 调用函数执行命令
#define MAX_PROMPT_LEN 16
#define CONFIG_SERIAL_TERM_PROMPT "vivi"
/*
* char prompt[16] = "boot";
*/
char prompt[MAX_PROMPT_LEN] = CONFIG_SERIAL_TERM_PROMPT;
void
serial_term(void)
{
char cmd_buf[MAX_CMDBUF_SIZE];
for (;;) {
printk("%s> ", prompt); /* vivi> 这就是vivi的命令行提示符*/
getcmd(cmd_buf, MAX_CMDBUF_SIZE);/* 通过串口来取得我们在pc机终端minicom里输入的字符串,是获取整个字符串,这字符串就是我们输入的命令和参数 */
if (cmd_buf[0])
exec_string(cmd_buf); /* 调用相应函数来执行我们的命令(执行之前需要对我们输入的字符串进行解析) */
}
}
|
字符串是如何获取的呢?无非就是串口的通信,这点我们在之前的分析就已熟悉。但是我们输入的是命令和参数,而且是在minicom里,所以处理起来会有些不同。来看看getcmd()
这个函数我还是有几个地方没有看懂,在下面标了问号的地方。
其他的过程就很简单了。getc()从串口获取字符,然后填入cmd_buf[]中。
void
getcmd(char *cmd_buf, unsigned int len)
{
char curpos = 0; /* current position - index into cmd_buf */
char c;
// int cmd_echo = 1;
/* Clear out the buffer */
memset(cmd_buf, 0, MAX_CMDBUF_SIZE);
for (;;) {
c = getc();
switch (c) {
/*
* 退格
*/
case 0x08:
case 0x06:
case 0x07:
case 0x7E:
case 0x7F:
if (curpos) {
curpos--;
putc(0x08);
putc(' ');
putc(0x08);
}
cmd_buf[curpos] = '\0'; /* ? */
break;
case '\r':
case '\n':
case '\0':
putc('\r');
putc('\n');
goto end_cmd;
case CTL_CH('x'): /* what's this ? */
curpos = 0;
break;
default:
if (curpos < MAX_CMDBUF_SIZE) {
cmd_buf[curpos] = c;
putc(c);
curpos++;
}
break;
}
}
/*
* cmd_buf[]字符串最后要放置一个'\0'。这是在哪里完成的?
*/
end_cmd:
DPRINTK("COMMAND: %s\n", cmd_buf);
}
|
获取了字符串之后,我们还不能直接执行命令。比如“cpu help; help” ,这个字符串中,我们需要面对几个问题。字符串中有空格,用来区分各参数;还有分号,用来分开两个命令。这样的话,我们需要把命令的各参数分别存储起来,把不同的命令也先后处理……
如此这般,我们需要先对字符串进行解析,解析完成之后,分为argc(参数个数) 和 char *argv[] (保存各参数) 。回想一下main(int argc, char **argv),是不是很相似呢!
解析完了之后,再调用execcmd()来执行相应的命令。
exec_string 就是按照这个流程来工作的。
void
exec_string(char *buf)
{
int argc;
char *argv[128];
char *resid;
while (*buf) {
memset(argv, 0, sizeof(argv));
parseargs(buf, &argc, argv, &resid); //解析字符串
if (argc > 0)
execcmd(argc, (const char **)argv); //执行命令
buf = resid; // 注意这里用while循环包起来,判断buf,就可以在一行中执行多个命令了。buf永远指向下一个命令的开始,若只有一个命令,buf自然就是指向该字符串的结束符'\0'
}
}
|
对于字符串的解析,parseargs()这个函数看起来还是有点复杂,一时不知道怎么下手。
写了一个小的测试代码来理解parseargs(),从终端中获取输入的字符串,调用parseargs()解析过后,再把各命令和参数打印到终端上来。调试了几下之后, 对parseargs()的工作情况也逐渐了解了。
static void
parseargs(char *argstr, int *argc_p, char **argv, char **resid)
{
int argc = 0;
char c;
enum ParseState lastState = PS_WHITESPACE;
/* ParseState中主要是列出几个状态标志。比如PS_WHITESPACE即表示输入的是空格或tab */
/* tokenize the argstr */
while ((c = *argstr) != 0) {
enum ParseState newState;
if (c == ';' && lastState != PS_STRING && lastState != PS_ESCAPE) /* 如果有分号&&分号不是出现在双引号""之间&&分号之前不是反斜杠(即'\;'),则说明分号表示一个命令及其参数的结束,此时可能是同一行中有一个以上的命令,立即跳出,下一个命令留到下一次的解析 */
break;
if (lastState == PS_ESCAPE) {
newState = stackedState;
} else if (lastState == PS_STRING) {
if (c == '"') {
newState = PS_WHITESPACE;
*argstr = 0;
} else {
newState = PS_STRING;
}
} else if ((c == ' ') || (c == '\t')) {
/* whitespace character */
*argstr = 0;/* 命令参数之间是以空格或tab分开的,将其置为0,使得各命令参数字符串有了'\0'的结尾 */
newState = PS_WHITESPACE;
} else if (c == '"') {
newState = PS_STRING;
*argstr++ = 0;
argv[argc++] = argstr;
} else if (c == '\\') {
stackedState = lastState;
newState = PS_ESCAPE;
} else {
/* token */
if (lastState == PS_WHITESPACE) {
argv[argc++] = argstr;
}
newState = PS_TOKEN;
}
lastState = newState;
argstr++;
}
argv[argc] = NULL;
if (argc_p != NULL)/* 感觉这里加个判断真是多余,argc_p是前个函数里的局部变量,其地址不可能为0呀 */
*argc_p = argc;
if (*argstr == ';') {
*argstr++ = '\0';
} /* 当一行有多个命令时,解析完一个命令后,argstr在这里就指向了下一个命令开始,下一次解析时就是解析第二个命令了 */
*resid = argstr; /* 这里将下一个命令的首地址返回。若是一个命令的结束,则为字符串的结尾,即*argstr = 0 */
}
|
解析过程对原字符串并没有真正改动(只是将空格、tab、";"等换成'\0'),将char **argv指向字符串中的各个关键点(即参数字符串的首地址)。
字符串解析完了之后,相应的信息就填入了argc 和 argc 中。
比如输入命令: cpu info ;bon
那么第一次解析,只会解析到cpu info ,所以此时 argc = 2, argc是又重指针,argc[0]是字符吕 "cpu"的首地址,argc[1]是字符串info的首地址。然后调用execcmd()执行这个命令。这个命令执行完了之后,又开始解析下一个命令bon,解析完了之后继续用execcmd()去执行它……
来看看命令是如何执行的吧。其实看完user_command_t的结构体各成员之后,应该就不难理解了。这是一个很常用的技巧。
/*
* execute the command
*/
static void
execcmd(int argc, const char **argv)
{
user_command_t *cmd = find_cmd(argv[0]);/* argv[0]指向的是命令名,通过命令名我们就能在命令链表里找到这个命令,find_cmd找到该命令后返回该命令结构的指针。*/
if (cmd == NULL) {
printk("Could not found '%s' command\n", argv[0]);
printk("If you want to know available commands, type 'help'\n");
return;
}
cmd->cmdfunc(argc, argv);/* 命令结构体的第二个成员即为指向处理函数的指针。这理用指向函数的指针直接调用该函数,于是,整个命令的执行就进入到了该命令自己的处理函数中了 */
}
static user_command_t *
find_cmd(const char *cmdname)
{
user_command_t *curr;
curr = head_cmd; /* head_cmd 就是命令链表的头指针 */
while (curr != NULL) {
/* 其实这里的判断,strlen(cmdnamd)并不能很好的实现,比如cpu命令,当然只打入c时,它就会返回第一个以c开头的命令。所以我觉得长度的判断用命令的长度更好一些,即strlen(curr->name) */
if (strncmp(curr->name, cmdname, strlen(cmdname)) == 0)
return curr;
curr = curr->next_cmd;
}
return NULL;
}
经过这么多的分析,一个命令是添加,以及在终端进而输入的字符是如何被识别成命令又如何执行,这个过程有了更深的认识。
当然,看这些代码的同时,也对自己的编程能力汗颜了。关于command命令的这整个处理过程,vivi中以一种清晰的思路,一一的将问题化解为逐个的小问题,并一步一步很有条理的解析。函数间层层的调用,最终将一个大问题化解为诸个小问题来处理。这种思想正是现在自己最缺乏的。好好消化这些代码以提高自己。
阅读(1769) | 评论(0) | 转发(0) |