思路分析
源码分析
在Nachos中提供一个Shell程序的雏形code/test/shell.c。这个程序干的事情很简单,通过输出--
字符串用于标识等待输入,然后将读取的输入信息送入Exec
系统调用中执行。该程序会反复执行上述操作。
其中涉及到了两个文件ConsoleInput
和ConsoleOutput
,查看其定义可知其在Unix系统中分别对应了标准输入流stdin和标准输出流stdout。因此这也就意味着想要模拟shell必须使用Unix文件系统,除非在Nachos中实现一套完整的输入输出方法。
1 2
| #define ConsoleInput 0 #define ConsoleOutput 1
|
尽管使用的是Unix文件系统,但是被Exec
调用执行的程序仍要自己实现。因此也需要实现或者修改一些Nachos内的系统调用,来执行Linux上的系统调用,从而做到对Unix文件系统执行命令。
另外,Nachos的用户程序,由于没有实现相关的Nachos系统库,也因此没有办法从外部向Nachos用户程序传参。这也就意味着部分命令,没法通过“用户程序 + 系统调用”的方式实现。
总之一句话,因为用的Unix文件系统,命令需要能够对Unix生效,所以在Nachos中调Unix的系统调用。另外,需要修改Shell以满足传参的需求。
Shell实现
前置条件
标准输入/输出流的处理
因为使用了标准输入/输出流,因此需要修改Write和Read系统调用的实现。具体实现方式见困难1 shell程序启动出现Segment Fault的错误。
相关命令设计
系统调用 |
功能 |
命令 |
使用方法 |
SC_Ls |
查看当前目录 |
ls |
ls |
SC_Pwd |
查看当前路径 |
pwd |
pwd |
SC_Cd |
切换目录 |
cd |
cd <path> |
SC_Nf |
新建文件 |
nf |
nf <name> |
SC_Nd |
新建目录文件 |
nd |
nd <name> |
SC_Df |
删除文件 |
df |
df <name> |
SC_Dd |
删除目录 |
dd |
dd <name> |
改动Shell以支持参数输入
虽然没法将参数传递给其他的Nachos用户程序,但是可以直接在Shell程序中解析buffer来处理参数。此外由于Shell没有提供退出的接口,因此自己实现输入”q”来结束一个shell。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| if( i > 0 ) { if (i == 2 && buffer[0] == 'l' && buffer[1] == 's') { Ls(); } else if (i == 3 && buffer[0] == 'p' && buffer[1] == 'w' && buffer[2] == 'd') { Pwd(); } else if (buffer[0] == 'c' && buffer[1] == 'd' && buffer[2] == ' ') { Cd(buffer + 3); } else if (buffer[0] == 'n' && buffer[1] == 'f' && buffer[2] == ' ') { Nf(buffer + 3); } else if (buffer[0] == 'n' && buffer[1] == 'd' && buffer[2] == ' ') { Nd(buffer + 3); } else if (buffer[0] == 'd' && buffer[1] == 'f' && buffer[2] == ' ') { Df(buffer + 3); } else if (buffer[0] == 'd' && buffer[1] == 'd' && buffer[2] == ' ') { Dd(buffer + 3); } else if (buffer[0] == 'q') { Halt(); } else { newProc = Exec(buffer); Join(newProc); } }
|
添加相关Linux系统调用接口
在code/machine/sysdep.cc中导入C语言的stdlib.h库。System用于执行一个系统命令。
1 2 3 4 5 6 7 8 9 10 11
|
extern void System(char * comd);
void System(char *comd) { system(comd); }
|
Chdir用于切换工作目录。chdir函数位于C语言的unistd.h库。
1 2 3 4
| void Chdir(char * path) { int r = chdir(path);
}
|
mkdir用于创建目录。mkdir函数位于C语言的sys/stat.h库。由于mkdir还需要一个mode参数用于指定新目录的权限,这里不考虑用户输入,而是使用定值S_IRWXU
代表着用户有读写执行的权限。
1 2 3
| void Mkdir(char *name) { mkdir(name, S_IRWXU); }
|
rmdir用于删除一个空目录,如果目录非空删除操作会失败。rmdir位于C语言的unistd.h库。
1 2 3
| void Rmdir(char *name) { rmdir(name); }
|
命令程序实现
首先创建相关的系统调用号以及系统接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
#define SC_Ls 11 #define SC_Pwd 12 #define SC_Cd 13 #define SC_Nf 14 #define SC_Nd 15 #define SC_Df 16 #define SC_Dd 17
void Ls(); void Pwd(); void Cd(char *path); void Nf(char *name); void Nd(char *name); void Df(char *name); void Dd(char *name);
|
然后实现用户接口,实现方式比较相似,因此此处仅列举出其中一个的实现方式。以及在code/userprog/exception.cc添加相应的接口和在code/userprog/syscalldep.h做具体的功能实现。
1 2 3 4 5 6 7 8 9 10 11 12
|
.globl Ls .ent Ls Ls: addiu $2,$0,SC_Ls syscall j $31 .end Ls ...
|
Ls
使用c++的函数system来执行一个Linux命令。
1 2 3
| void SysLs() { System("ls"); }
|
Pwd
同样使用c++的函数system来执行一个Linux命令。
1 2 3
| void SysPwd() { System("pwd"); }
|
Cd
利用封装C语言的接口Chdir来实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void SysCd() { int base = machine->ReadRegister(4); int value; int count = 0;
char *path = new char[255]; do { while (!machine->ReadMem(base + count, 1, &value)); path[count] = (char)value; count++; } while (value != '\0');
Chdir(path);
delete[] path; }
|
Nf
直接使用文件系统提供的接口创建文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void SysNf() { int base = machine->ReadRegister(4); int value; int count = 0;
char *name = new char[FileNameMaxLen + 1]; do { while (!machine->ReadMem(base + count, 1, &value)); name[count] = (char)value; count++; } while (value != '\0' && count <= FileNameMaxLen + 1);
fileSystem->Create(name);
delete[] name; }
|
Df
同样,直接使用文件系统提供的接口删除文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void SysDf() { int base = machine->ReadRegister(4); int value; int count = 0;
char *name = new char[FileNameMaxLen + 1]; do { while (!machine->ReadMem(base + count, 1, &value)); name[count] = (char)value; count++; } while (value != '\0' && count <= FileNameMaxLen + 1);
fileSystem->Remove(name);
delete[] name; }
|
Nd
同样利用C函数mkdir实现文件的创建。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void SysNd() { int base = machine->ReadRegister(4); int value; int count = 0;
char *name = new char[FileNameMaxLen + 1]; do { while (!machine->ReadMem(base + count, 1, &value)); name[count] = (char)value; count++; } while (value != '\0' && count <= FileNameMaxLen + 1);
Mkdir(name);
delete[] name; }
|
Dd
利用C函数rmdir来实现文件夹的删除操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void SysDd() { int base = machine->ReadRegister(4); int value; int count = 0;
char *name = new char[FileNameMaxLen + 1]; do { while (!machine->ReadMem(base + count, 1, &value)); name[count] = (char)value; count++; } while (value != '\0' && count <= FileNameMaxLen + 1);
Rmdir(name);
delete[] name; }
|
测试结果
以及执行如下用户程序,来测试shell用户程序的执行。
1 2 3 4 5
| int main() { Create("aaa.txt"); Exit(0); }
|
遇到的困难
困难1 shell程序启动出现Segment Fault的错误
错误原因已经查明如下图所示,currentOffset是一个未初始化变量,因此其内的信息是不可访问的。也即图上的错误Cannot access memory at address 0x5
。
1 2 3 4 5
|
OpenFileId input = ConsoleInput; OpenFileId output = ConsoleOutput;
|
解决方案:修改Write和Read系统调用的实现。作如下判断,对于标准输入流和标准输出流,则采取调用Unix系统调用的封装函数的方式处理。之所以不用OpenFile,而是直接调WriteFile和ReadPartial是为了处理stdin和stdout不支持lseek方法的问题。
1 2 3 4 5 6 7 8 9
| if (openFileId == ConsoleInput || openFileId == ConsoleOutput) { WriteFile(openFileId, in, size); }
if (openFileId == ConsoleInput || openFileId == ConsoleOutput) { r = ReadPartial(openFileId, out, size); }
|
参考资料
[1] 百度文库. nachos Lab7实习报告[EB/OL]. https://wenku.baidu.com/view/a3e1f237a31614791711cc7931b765ce04087a54.html?re=view