一、 实验目的
-
理解Shell程序的原理、底层逻辑和Shell依赖的数据结构等
-
在操作系统内核MiniEuler上实现一个可用的Shell程序
-
能够根据相关原理编写一条可用的Shell指令
二、 实验过程
首先从底层出发,实现Shell程序
1.在src/include目录下新建prt_shell.h头文件:
这个板块中主要定义了shell能够显示的最大长度以及文件路径的最大值,然后定义了ShellCB控制块,用于Shell的管理,其中包含对用户输入、输入命令的历史的管理以及维护当前工作目录等,此处的ShellBuf是作为Shell的缓冲区,输入的指令会存放在缓冲区内,经过解析得到最终应该执行的操作。
2.向src/bsp目录下的print.c文件中的PRT_UartInit 添加初始化代码,使其支持接收数据中断。
由于我们的操作系统内核MiniEuler不能使用标准输入输出流,所以需要通过串口交互的方式来实现往Shell的缓冲区中写入字符,在此板块中首先定义了一系列串口的配置位和掩码,如:TXE是串口的第9位(从0开始),在实验二中也进行过类似的操作,接着定义了一个用于UART串口接收数据时使用的信号量sem_uart_rx,最后进行串口的初始化,首先禁用UART,清空中断状态,设定中断mask(允许接收中断),并设置波特率相关寄存器(UARTIBRD和UARTFBRD)。
然后读取Line Control Register(LCR)的当前配置,将其与一组掩码进行或操作来设置数据位、奇偶校验和停止位的配置,然后写回寄存器。这里使用的配置是8个数据位、无奇偶校验、1个停止位,并启用FIFO。最后通过设置控制寄存器(UART Control Register)来启用UART,并使能接收和发送。串口配置完成后,调用OsGicIntSetConfig,OsGicIntSetPriority,OsGicClearInt,OsGicEnableInt函数来配置UART接收中断,最后创建数据接收信号量。
3.在src/bsp目录中的print.c文件中实现 OsUartRxHandle()处理接收中断
该中断处理函数首先读取UART的状态寄存器,然后检查flag中的第4位(接收FIFO空标志)是否为0,为0则表示UART接收到的字符非空,将接收到的字符读入shell的缓冲区,使用Offset表示写入字符在缓冲区中的偏移(用于定位),当偏移超出shell缓冲区的最大长度时,将其重置为0,从而实现循环缓冲区的效果,最后调用PRT_SemPost函数,发送信号量sem_uart_rx,通知其他可能在等待UART接收数据的任务已经读入新的字符。
4.在src/bsp目录下的prt_exc.c文件中修改中断激活函数:
当接收到的中断处理号为33时,调用接受处理函数
5.在src/kernel/task目录下的prt_task.c文件中加入display函数
这个函数主要是用于后续Shell的top指令对应的实际操作函数,依次遍历g_runQueue队列,按照优先级打印出所有任务。
6.在src/kernel/tick目录下的prt_tick.c文件中加入display函数
用于后续Shell的tick指令的实际操作,打印出当前已经进行的时钟中断次数。
7.在src/shell目录下新建shmsg.c文件
OS_SEC_TEXT void ShellTask(uintptr_t param1, uintptr_t param2, uintptr_t param3, uintptr_t param4)
{
U32 ret;
char ch;
char cmd[SHELL_SHOW_MAX_LEN];
U32 idx;
ShellCB *shellCB = (ShellCB *)param1;
while (1) {
PRT_Printf("\nminiEuler # ");
idx = 0;
for(int i = 0; i < SHELL_SHOW_MAX_LEN; i++)
{
cmd[i] = 0;
}
while (1){
PRT_SemPend(sem_uart_rx, OS_WAIT_FOREVER);
// 读取shellCB缓冲区的字符
ch = shellCB->shellBuf[shellCB->shellBufReadOffset];
cmd[idx] = ch;
idx++;
shellCB->shellBufReadOffset++;
if(shellCB->shellBufReadOffset == SHELL_SHOW_MAX_LEN)
shellCB->shellBufReadOffset = 0;
PRT_Printf("%c", ch); //回显
if (ch == '\r'){
// PRT_Printf("\n");
if(cmd[0]=='t' && cmd[1]=='o' && cmd[2]=='p'){
OsDisplayTasksInfo();
} else if(cmd[0]=='t' && cmd[1]=='i' && cmd[2]=='c' && cmd[3]=='k'){
OsDisplayCurTick();
}
break;
}
}
}
}
这段代码的功能很简单,首先输出Shell命令行的提示符“minieuler #”,然后根据Shell缓冲区内的字符(即为输入的指令)进行相应的操作,本实验已经实现的有top指令和tick指令,功能实现在前文中已经详细解释,top指令按照优先级打印出队列中的所有任务,tick指令打印出当前已经执行的时钟中断数,新增加的指令会在后文的作业中详细阐述。
OS_SEC_TEXT U32 ShellTaskInit(ShellCB *shellCB)
{
U32 ret = 0;
struct TskInitParam param = {0};
_// task 1_
_// param.stackAddr = 0;_
param.taskEntry = (TskEntryFunc)ShellTask;
param.taskPrio = 9;
_// param.name = "Test1Task";_
param.stackSize = 0x1000; _//__固定4096,参见prt_task_init.c的OsMemAllocAlign_
param.args[0] = (uintptr_t)shellCB;
TskHandle tskHandle1;
ret = PRT_TaskCreate(&tskHandle1, ¶m);
if (ret) {
return ret;
}
ret = PRT_TaskResume(tskHandle1);
if (ret) {
return ret;
}
}
这个函数主要目的是使用给定的参数初始化并启动一个新的ShellTask任务。
至此,Shell的底层初始化已经全部完成。
三、 测试及分析
能够正常运行本实验自带的两条Shell指令
四、 Lab9作业
在实现完Shell的底层原理之后,我们还需要在main函数中启动Shell程序:
引用外部文件中定义的一系列初始化文件,同时定义本实验中的shellCB控制块
然后调用初始化函数对Shell进行初始化,就可以顺利启动Shell程序了
这里实现了三条额外的指令:第一条是Shell程序中不可或缺的help指令,可以打印出当前所有的可用指令以及指令用途:
第二条是清屏操作Clear:
第三条是退出操作exit:
演示:
五、心得体会
通过这个实验,我更深入地理解了命令行Shell的工作原理和底层实现,也顺利实现自己写的两条指令;至此,操作系统课程的所有实验落下帷幕,我本人是感慨万分的,从最开始的什么都看不懂、到处找资料、实验代码一看一整天、实验环境一配一整天,到顺利完成所有的实验,实现了一个自己的简单操作系统内核:MiniEuler,还是成就感满满的。很感谢有这个机会能接触到这么底层的实验,让原本遥不可及的操作系统变得咫尺可得,我也同样明白这只是前行的一小步,未来的学习道阻且长。