Nachos Lab06 Shell实现

思路分析

源码分析

在Nachos中提供一个Shell程序的雏形code/test/shell.c。这个程序干的事情很简单,通过输出--字符串用于标识等待输入,然后将读取的输入信息送入Exec系统调用中执行。该程序会反复执行上述操作。

其中涉及到了两个文件ConsoleInputConsoleOutput,查看其定义可知其在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系统调用接口

  • System

code/machine/sysdep.cc中导入C语言的stdlib.h库。System用于执行一个系统命令。

1
2
3
4
5
6
7
8
9
10
11
/*
* code/machine/sysdep.h
*/
extern void System(char * comd);

/*
* code/machine/sysdep.cc
*/
void System(char *comd) {
system(comd);
}
  • Chdir

Chdir用于切换工作目录。chdir函数位于C语言的unistd.h库。

1
2
3
4
void Chdir(char * path) {
int r = chdir(path);
// ASSERT(r == 0);
}
  • Mkdir

mkdir用于创建目录。mkdir函数位于C语言的sys/stat.h库。由于mkdir还需要一个mode参数用于指定新目录的权限,这里不考虑用户输入,而是使用定值S_IRWXU代表着用户有读写执行的权限。

1
2
3
void Mkdir(char *name) {
mkdir(name, S_IRWXU);
}
  • Rmdir

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
/*
* code/userprog/syscall.h
*/
#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
/*
* code/test/start.s
*/
.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
/*
* code/test/shell.c
*/
OpenFileId input = ConsoleInput;
OpenFileId output = ConsoleOutput;

解决方案:修改WriteRead系统调用的实现。作如下判断,对于标准输入流和标准输出流,则采取调用Unix系统调用的封装函数的方式处理。之所以不用OpenFile,而是直接调WriteFileReadPartial是为了处理stdin和stdout不支持lseek方法的问题。

1
2
3
4
5
6
7
8
9
// Write
if (openFileId == ConsoleInput || openFileId == ConsoleOutput) {
WriteFile(openFileId, in, size);
}

// Read
if (openFileId == ConsoleInput || openFileId == ConsoleOutput) {
r = ReadPartial(openFileId, out, size);
}

参考资料

[1] 百度文库. nachos Lab7实习报告[EB/OL]. https://wenku.baidu.com/view/a3e1f237a31614791711cc7931b765ce04087a54.html?re=view

Author

Chaos Chen

Posted on

2021-01-09

Updated on

2023-06-30

Licensed under

Commentaires