手写操作系统(五十九)-实现命令行界面

代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。

1.read调用

之前的sys_read只能从文件中获取数据,还不能从标准输入设备键盘中读取数据。

修改fs/fs.c的sys_read函数

/* 从文件描述符fd指向的文件中读取count个字节到buf,若成功则返回读出的字节数,到文件尾则返回-1 */
int32_t sys_read(int32_t fd, void* buf, uint32_t count) {
    ASSERT(buf != NULL);
    int32_t ret = -1;
    if (fd < 0 || fd == stdout_no || fd == stderr_no) {
        printk("sys_read: fd error\n");
    } else if (fd == stdin_no) {
        char* buffer = buf;
        uint32_t bytes_read = 0;
        while (bytes_read < count) {
            *buffer = ioq_getchar(&kbd_buf);
            bytes_read++;
            buffer++;
        }
        ret = (bytes_read == 0 ? -1 : (int32_t)bytes_read);
    } else {
        uint32_t _fd = fd_local2global(fd);
        ret = file_read(&file_table[_fd], buf, count);
    }
    return ret;
}

else if就是标准输入stdin_no的处理,若发现fd是stdin_no,下面就通过while 和ioq_getchar(&kbd_buf),每次从键盘缓冲区kbd_buf中获取1个字符,直到获取了count个字符为止。kbd_buf是我们存储键盘输入的环形缓冲区,它定义在keyboard.c中。

修改/device/keyboard.h

extern struct ioqueue kbd_buf;

 

fs.c记得导入

#include "keyboard.h"

 

修改lib/user/syscall.h 的 enum SYSCALL NR 中添加 SYS_READ ,以及read函数声明。

/* 用来存放子功能号 */
enum SYSCALL_NR {
    SYS_GETPID,
    SYS_WRITE,
    SYS_MALLOC,
    SYS_FREE,
    SYS_FORK,
    SYS_READ
};
int32_t read(int32_t fd, void* buf, uint32_t count);

 

在lib/user/syscall.c中添加read的系统调用

/* 从文件描述符fd中读取count个字节到buf */
int32_t read(int32_t fd, void* buf, uint32_t count) {
    return _syscall3(SYS_READ, fd, buf, count);
}

 

修改/userprog/syscall-init.c的 syscall_init 函数中添加初始化

/* 初始化系统调用 */
void syscall_init(void) {
    put_str("syscall_init start\n");
    syscall_table[SYS_GETPID]  = sys_getpid;
    syscall_table[SYS_WRITE]   = sys_write;
    syscall_table[SYS_MALLOC]  = sys_malloc;
    syscall_table[SYS_FREE]    = sys_free;
    syscall_table[SYS_FORK]    = sys_fork;
    syscall_table[SYS_READ]    = sys_read;
    put_str("syscall_init done\n");
}

 

2.putchar和clear系统调用

通过putchar系统调用来实现输出单个字符,可以直接用console_put_char实现

通过clear系统调用来实现实现清屏,接下来实现clear

在/lib/kernel/print.S添加

global cls_screen
cls_screen:
   pushad
   ;;;;;;;;;;;;;;;
    ; 由于用户程序的cpl为3,显存段的dpl为0,故用于显存段的选择子gs在低于自己特权的环境中为0,
    ; 导致用户程序再次进入中断后,gs为0,故直接在put_str中每次都为gs赋值.
   mov ax, SELECTOR_VIDEO          ; 不能直接把立即数送入gs,须由ax中转
   mov gs, ax

   mov ebx, 0
   mov ecx, 80*25
 .cls:
   mov word [gs:ebx], 0x0720         ;0x0720是黑底白字的空格键
   add ebx, 2
   loop .cls
   mov ebx, 0

 .set_cursor:              ;直接把set_cursor搬过来用,省事
;;;;;;; 1 先设置高8位 ;;;;;;;;
   mov dx, 0x03d4           ;索引寄存器
   mov al, 0x0e            ;用于提供光标位置的高8位
   out dx, al
   mov dx, 0x03d5           ;通过读写数据端口0x3d5来获得或设置光标位置
   mov al, bh
   out dx, al

;;;;;;; 2 再设置低8位 ;;;;;;;;;
   mov dx, 0x03d4
   mov al, 0x0f
   out dx, al
   mov dx, 0x03d5
   mov al, bl
   out dx, al
   popad
   ret

 

对应/lib/kernel/print.h添加声明

void cls_screen(void);

 

修改lib/user/syscall.h 的 enum SYSCALL NR 中添加功能号 ,以及read函数声明。

/* 用来存放子功能号 */
enum SYSCALL_NR {
    SYS_GETPID,
    SYS_WRITE,
    SYS_MALLOC,
    SYS_FREE,
    SYS_FORK,
    SYS_READ,
    SYS_PUTCHAR,
    SYS_CLEAR
};
void putchar(char char_asci);
void clear(void);

 

在lib/user/syscall.c中添加putchar和clear的系统调用

/* 输出一个字符 */
void putchar(char char_asci) {
    _syscall1(SYS_PUTCHAR, char_asci);
}

/* 清空屏幕 */
void clear(void) {
    _syscall0(SYS_CLEAR);
}

 

在/fs/fs.c调用

/* 向屏幕输出一个字符 */
void sys_putchar(char char_asci) {
   console_put_char(char_asci);
}

在/fs/fs.h添加声明

void sys_putchar(char char_asci);

 

修改/userprog/syscall-init.c的 syscall_init 函数中添加初始化

/* 初始化系统调用 */
void syscall_init(void) {
    put_str("syscall_init start\n");
    syscall_table[SYS_GETPID]  = sys_getpid;
    syscall_table[SYS_WRITE]   = sys_write;
    syscall_table[SYS_MALLOC]  = sys_malloc;
    syscall_table[SYS_FREE]    = sys_free;
    syscall_table[SYS_FORK]    = sys_fork;
    syscall_table[SYS_READ]    = sys_read;
    syscall_table[SYS_PUTCHAR] = sys_putchar;
    syscall_table[SYS_CLEAR]   = cls_screen;
    put_str("syscall_init done\n");
}

 

3.实现简单shell

新建/shell/shell.c

#include "shell.h"
#include "stdint.h"
#include "fs.h"
#include "file.h"
#include "syscall.h"
#include "stdio.h"
#include "global.h"
#include "assert.h"
#include "string.h"

#define cmd_len 128    // 最大支持键入128个字符的命令行输入
#define MAX_ARG_NR 16      // 加上命令名外,最多支持15个参数

/* 存储输入的命令 */
static char cmd_line[cmd_len] = {0};

/* 用来记录当前目录,是当前目录的缓存,每次执行cd命令时会更新此内容 */
char cwd_cache[64] = {0};

/* 输出提示符 */
void print_prompt(void) {
   printf("[Dreams@localhost %s]$ ", cwd_cache);
}

/* 从键盘缓冲区中最多读入count个字节到buf。*/
static void readline(char* buf, int32_t count) {
   assert(buf != NULL && count > 0);
   char* pos = buf;
   while (read(stdin_no, pos, 1) != -1 && (pos - buf) < count) { // 在不出错情况下,直到找到回车符才返回
      switch (*pos) {
       /* 找到回车或换行符后认为键入的命令结束,直接返回 */
     case '\n':
     case '\r':
        *pos = 0;     // 添加cmd_line的终止字符0
        putchar('\n');
        return;

     case '\b':
        if (buf[0] != '\b') {     // 阻止删除非本次输入的信息
           --pos;     // 退回到缓冲区cmd_line中上一个字符
           putchar('\b');
        }
        break;

     /* 非控制键则输出字符 */
     default:
        putchar(*pos);
        pos++;
      }
   }
   printf("readline: can`t find enter_key in the cmd_line, max num of char is 128\n");
}

/* 简单的shell */
void my_shell(void) {
   cwd_cache[0] = '/';
   while (1) {
      print_prompt(); 
      memset(cmd_line, 0, cmd_len);
      readline(cmd_line, cmd_len);
      if (cmd_line[0] == 0) {    // 若只键入了一个回车
     continue;
      }
   }
   panic("my_shell: should not be here");
}

用户键入命令的命令是以字符串形式提交的,因此其长度是有限制的。

cmd_len 表示命令字符串最大的长度,用户输入的命令字符串长度不能超过128个字符。

MAX_ARG_NR表示最大支持的参数个数,定义为16。

数组cmd_line用来存储键入的命令。 数组cwd_cache用来存储当前目录名,主要是在命令提示符中,它由以后实现的cd命令来维护。

函数print_prompt用于输出命令提示符,也就是命令行中显示的主机名等。

readline函数接受2个参,缓冲区buf和读入的字符数,功能是从键盘缓冲区中最多读入count个 字节到 buf。 字符指针pos指向缓冲区buf,我们后面将通过pos往buf中写数据。 函数体中主要是个while循环,每次通过read系统调用读入1个字符到pos中,也就是buf中。 不能一次读入cmd_len个字符,否则用户输入不足cmd_len个字符,无法得到及时的处理,那么用户键入的内容并不是立即反映到屏幕上。

然后通过switch结构判断读入的字符,也就是*pos的值,然后采取不同的处理方法。 前三个case是处理控制键,分别是回车换行符及退格键。 如果*pos的值是字符\n或\r,这表示用户键入了回车,表示命令输入结束,所以使*pos为0,也就是添加了字符串结束标识\0,然后调用putchar在屏幕上输出换行符\n,模拟用户的键盘动作。

if判断是阻止删除非本次输入的信息,如果没有代码if (buf[0] != ‘\b’)的话,按下的退格键会将命令提示符及之前的内容删除,这就错了。 pos会减1,指向buf中前一个字符,并且通过putchar输出b,在屏幕上模拟删除。 如果不是这些控制字符,默认就直接输出。

my_shell函数就是所实现的简单shell,函数中先将当前工作目录缓存cwd_cache置为根目录,然后通过 while 语句,循环调用 print_prompt 输出命令提示符,然后调用 readline 获取用户输入。

对应shell.h

#ifndef __KERNEL_SHELL_H
#define __KERNEL_SHELL_H
void print_prompt(void);
void my_shell(void);
#endif

 

新建/lib/user/assert.c

#include "assert.h"
#include "stdio.h"
void user_spin(char* filename, int line, const char* func, const char* condition) {
   printf("\n\n\n\nfilename %s\nline %d\nfunction %s\ncondition %s\n", filename, line, func, condition);
   while(1);
}

对应/lib/user/assert.h

#ifndef __LIB_USER_ASSERT_H
#define __LIB_USER_ASSERT_H

#define NULL ((void*)0)

void user_spin(char* filename, int line, const char* func, const char* condition);
#define panic(...) user_spin(__FILE__, __LINE__, __func__, __VA_ARGS__)

#ifdef NDEBUG
   #define assert(CONDITION) ((void)0)
#else
   #define assert(CONDITION)  \
      if (!(CONDITION)) {     \
     panic(#CONDITION);   \
      }    

#endif/*NDEBUG*/

#endif/*__LIB_USER_ASSERT_H*/

如此一来,我们的assert判断出错,将会通过printf内的write系统调用正常进入内核态

 

修改/device/keyboard.c的intr_keyboard_handler函数

将下面的put_char注释掉,这是之前的打印,现在不用了

......
/*只处理ASCII码不为0的键*/
if(cur_char){
    /*若kbd_buf未满并且待加入的cur_char不为0,则将其加入到缓冲区区kbd_buf中*/
    if(!ioq_full(&kbd_buf)){
        //put_char(cur_char);     //临时的
        ioq_putchar(&kbd_buf,cur_char);
    }
    return;
}
......

 

修改main.c文件

#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall-init.h"
#include "syscall.h"
#include "stdio.h"
#include "memory.h"
#include "dir.h"
#include "fs.h"
#include "assert.h"
#include "shell.h"

void init(void);

int main(void) {
    put_str("I am kernel\n");
    init_all();
    cls_screen();
    console_put_str("[Dreams@localhost /]$ ");
    while(1);
    return 0;
}

/* init进程 */
void init(void) {
    uint32_t ret_pid = fork();
    if(ret_pid) {  // 父进程
        while(1);
    } else {      // 子进程
        my_shell();
    }
    panic("init: should not be here");
}

my_shell是在fork之后的子进程中。

主线程执行 完init_all之后,调用了cls_screen和用console_put_str输出了命令提示符,目的是清掉屏幕上的硬盘、分 区等初始化信息。

执行结果:

 

接着实现快捷键

Ctrl+u是清除输入,清除本行的输入

Ctrl+l是清空屏幕,相当于clear命令,但不会清除本次输入

我们之前在键盘驱动程序 keyboard.c 中已经可以处理了,但是操作系统虽说是由中断驱动的,但中断过多的话,系统会被拖累得效率骤降。而键盘驱动程序是中断处理程序,每按下一个键就会产生两个中断(分别是通码和断码产生的中断),所以尽可能让中断处理程序简洁。

修改/device/keyboard.c的intr_keyboard_handler函数

只改变if(cur_char)逻辑,就不贴出其他代码了

加入快捷键ctrl+l和ctrl+u的处理

/*只处理ASCII码不为0的键*/
if(cur_char){


    /*****************  快捷键ctrl+l和ctrl+u的处理 *********************
     * 下面是把ctrl+l和ctrl+u这两种组合键产生的字符置为:
     * cur_char的asc码-字符a的asc码, 此差值比较小,
     * 属于asc码表中不可见的字符部分.故不会产生可见字符.
     * 我们在shell中将ascii值为l-a和u-a的分别处理为清屏和删除输入的快捷键*/
    if ((ctrl_down_last && cur_char == 'l') || (ctrl_down_last && cur_char == 'u')) {
        cur_char -= 'a';
    }
    /****************************************************************/

    /*若kbd_buf未满并且待加入的cur_char不为0,则将其加入到缓冲区区kbd_buf中*/
    if(!ioq_full(&kbd_buf)){
        //put_char(cur_char);     //临时的
        ioq_putchar(&kbd_buf,cur_char);
    }
    return;
}

变量cur_char中存储的是按键的ASCII码,我们在keyboard.c将”ctrl+1″和”ctrl+u”组合键也转换为ASCII码,不过此时cur_char中存储的是字符1或字符u的ASCII码值减去字符a的ASCII码值的差。

在ASCI码表中,ASCI码值为十进制0~31和127的字符是控制字符,它们不可见,因此字符1和字符 u 的 ASCII码值减去 a 的 ASCII 后的差会落到控制字符中,但并不是所有的控制字符都可占用,对于系统中已经处理的控制字符必须要保留。 比如退格键’\b’、换行符’\n’,和回车符’\r’的ASCII码分别是8、10和13,已经在shell.c中针对它们做出了处理,因此要定义其他快捷键的话,要将这三个控制键的ASCII码跨过去。

修改shell/shell.c的readline函数

/* 从键盘缓冲区中最多读入count个字节到buf。*/
static void readline(char* buf, int32_t count) {
   assert(buf != NULL && count > 0);
   char* pos = buf;

   while (read(stdin_no, pos, 1) != -1 && (pos - buf) < count) { // 在不出错情况下,直到找到回车符才返回
      switch (*pos) {
       /* 找到回车或换行符后认为键入的命令结束,直接返回 */
     case '\n':
     case '\r':
        *pos = 0;     // 添加cmd_line的终止字符0
        putchar('\n');
        return;

     case '\b':
        if (cmd_line[0] != '\b') {    // 阻止删除非本次输入的信息
           --pos;     // 退回到缓冲区cmd_line中上一个字符
           putchar('\b');
        }
        break;

     /* ctrl+l 清屏 */
     case 'l' - 'a': 
        /* 1 先将当前的字符'l'-'a'置为0 */
        *pos = 0;
        /* 2 再将屏幕清空 */
        clear();
        /* 3 打印提示符 */
        print_prompt();
        /* 4 将之前键入的内容再次打印 */
        printf("%s", buf);
        break;

     /* ctrl+u 清掉输入 */
     case 'u' - 'a':
        while (buf != pos) {
           putchar('\b');
           *(pos--) = 0;
        }
        break;

     /* 非控制键则输出字符 */
     default:
        putchar(*pos);
        pos++;
      }
   }
   printf("readline: can`t find enter_key in the cmd_line, max num of char is 128\n");
}

ctrl+l键处理为清屏操作,这分为四步来完成。 先将pos指向的字符置为0,也就是字符串结束符”\0″。 接着调用clear系统调用清屏,此时屏幕上空空如也。 然后调用print_prompt函数重新输出命令提示符,也就是此时屏幕上出现了”[Dreams@localhost /]$”,最后把buf中的字符串,也就是用户刚刚键入的字符通过printf打印出来。

快捷键”ctrl+u”实现原理是通过while循环连续输出退格符,然后使指针pos逐步递减,并将对应位置为 0,直到 pos 指向了 buf 的起始处。

 

接下来就是让shell对输入的命令作出反应

修改shell/shell.c

#define MAX_ARG_NR 16      // 加上命令名外,最多支持15个参数

/* 存储输入的命令 */
static char cmd_line[MAX_PATH_LEN] = {0};
char final_path[MAX_PATH_LEN] = {0};      // 用于洗路径时的缓冲

/* 用来记录当前目录,是当前目录的缓存,每次执行cd命令时会更新此内容 */
char cwd_cache[MAX_PATH_LEN] = {0};
/* 分析字符串cmd_str中以token为分隔符的单词,将各单词的指针存入argv数组 */
static int32_t cmd_parse(char* cmd_str, char** argv, char token) {
    assert(cmd_str != NULL);
    int32_t arg_idx = 0;
    while(arg_idx < MAX_ARG_NR) {
        argv[arg_idx] = NULL;
        arg_idx++;
    }
    char* next = cmd_str;
    int32_t argc = 0;
    /* 外层循环处理整个命令行 */
    while(*next) {
        /* 去除命令字或参数之间的空格 */
        while(*next == token) {
            next++;
        }
        /* 处理最后一个参数后接空格的情况,如"ls dir2 " */
        if (*next == 0) {
            break;
        }
        argv[argc] = next;

        /* 内层循环处理命令行中的每个命令字及参数 */
        while (*next && *next != token) {     // 在字符串结束前找单词分隔符
            next++;
        }

        /* 如果未结束(是token字符),使tocken变成0 */
        if (*next) {
            *next++ = 0;    // 将token字符替换为字符串结束符0,做为一个单词的结束,并将字符指针next指向下一个字符
        }

        /* 避免argv数组访问越界,参数过多则返回0 */
        if (argc > MAX_ARG_NR) {
            return -1;
        }
        argc++;
    }
    return argc;
}

char* argv[MAX_ARG_NR];    // argv必须为全局变量,为了以后exec的程序可访问参数
int32_t argc = -1;
/* 简单的shell */
void my_shell(void) {
    cwd_cache[0] = '/';
    while (1) {
        print_prompt();
        memset(final_path, 0, MAX_PATH_LEN);
        memset(cmd_line, 0, MAX_PATH_LEN);
        readline(cmd_line, MAX_PATH_LEN);
        if (cmd_line[0] == 0) {  // 若只键入了一个回车
            continue;
        }
        argc = -1;
        argc = cmd_parse(cmd_line, argv, ' ');
        if (argc == -1) {
            printf("num of arguments exceed %d\n", MAX_ARG_NR);
            continue;
        }

        int32_t arg_idx = 0;
        while(arg_idx < argc) {
            printf("%s ", argv[arg_idx]);
            arg_idx++;
        }
        printf("\n");
    }
    panic("my_shell: should not be here");
}

cmd_parse函数接受3个参数,用户键入的原始命令串cmd_str、参数字符串数组argv、分隔符token,功能是分析字符串cmd_str中以token为分隔符的单词,将解析出来的单词的指针存入argv数组。

在函数开头while 循环用来清空数组 argv。while(*next)的 while处理整个命令行 cmd_str。在循环内部的第 1个循环用于跨过 cmd_str 中的空格。 if (*next == 0)if判断,如果 next 已经指向了 cmd_str的结尾就退出循环,这是为了避免在最后一个参数后出现空格的情况。

“argv[argc] = next”每找出一个字符串就将其在cmd_str 中的起始 next存储到 argv 数组。

while (*next && *next != token)的 while 循环用于处理命令行中的每个命令字及参数,在字符串未结束的情况下,遇到分隔符token后就退出,随后在if (*next)判断跳出上个while循环的原因是遇到了token字符,还是遇到了结束符0,如果是token字符,则将指针next指向的token字符置为0,也就是人为添加结束字符”\0″,使数组argv中的每个元素(字符串)从 cmd_str 中找到结束边界,并使next 指向下一个字符。

修改shell/shell.h,加上声明

#ifndef __KERNEL_SHELL_H
#define __KERNEL_SHELL_H
#include "fs.h"
void print_prompt(void);
void my_shell(void);
extern char final_path[MAX_PATH_LEN];
#endif

 

执行结果

 

4.参考

郑钢著操作系统真象还原

田宇著一个64位操作系统的设计与实现

丁渊著ORANGE’S:一个操作系统的实现

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇