代码、内容参考来自于包括《操作系统真象还原》、《一个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:一个操作系统的实现


