Chinaunix首页 | 论坛 | 博客
  • 博客访问: 444411
  • 博文数量: 72
  • 博客积分: 3186
  • 博客等级: 中校
  • 技术积分: 1039
  • 用 户 组: 普通用户
  • 注册时间: 2009-03-07 16:53
文章分类

全部博文(72)

文章存档

2012年(1)

2011年(5)

2010年(10)

2009年(56)

我的朋友

分类: 嵌入式

2009-10-14 17:25:35

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中以一种清晰的思路,一一的将问题化解为逐个的小问题,并一步一步很有条理的解析。函数间层层的调用,最终将一个大问题化解为诸个小问题来处理。这种思想正是现在自己最缺乏的。好好消化这些代码以提高自己。
阅读(1700) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~