Chap.3 OS-挑战性任务shell
实验内容
Shell 挑战性任务
- 本次 Lab6 挑战性任务需要同学们在 MOS 原有的 shell(mosh) 上实现新的功能,该任务会提供若干任务,完成所有任务后即可参加自动化评测获取挑战性任务分数。
- 实现不带.b后缀指令
- 实现指令条件执行
- 实现更多指令
- 实现反引号
- 实现注释功能
- 实现历史指令
- 实现一行多指令
- 实现追加重定向
- 实现引号支持
- 实现前后台任务管理
功能实现详解
实现不带.b后缀指令
你需要实现不带
.b后缀的指令,但仍需兼容带有.b后缀的指令,如ls与ls.b都应能够正确列出当前目录下的文件。
- 对于实现不带.b的指令:
- 指令都是通过
sh.c中的spawn(prog, argv)来实现的prog就是指向程序的字符串,比如"ls.b", "echo.b"- 因此如果
prog是"ls"或者"echo"等不带.b后缀的字符串的话 - 就新建一个字符串
- 然后把
prog拷贝给prog_n - 然后给
prog_n加上”.b”后缀 - 然后
fd = open(prog_n, O_RDONLY)就可以打开这个程序的二进制文件并且加载了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//////////////// 1. ls不带.b /////////////////
char prog_n[1024];
strcpy(prog_n, prog);
int mylen1 = strlen(prog_n);
char myendchar = prog_n[mylen1-1];
if (myendchar != 'b') {
prog_n[mylen1] = '.';
prog_n[mylen1+1] = 'b';
prog_n[mylen1+2] = '\0';
}
/////////////////////////////////////////////
// Step 1: Open the file 'prog' (the path of the program).
// Return the error if 'open' fails.
int fd;
if ((fd = open(prog_n, O_RDONLY)) < 0) {
return fd;
}
实现指令条件执行
你需要实现 Linux shell 中的
&&与||。 对于command1 && command2,command2被执行当且仅当command1返回 0;对于command1 || command2,command2被执行当且仅当command1返回非 0 值。
注: 评测中保证不出现括号。并且需要注意的是,在 bash 中
&&与||的优先级相同,按照从左到右的顺序求值。
例如cmd1 || cmd2 && cmd3,若cmd1返回 0,则cmd1执行后cmd2不会被执行,cmd3会被执行;若cmd1返回非 0 且cmd2返回非 0,则cmd3将不会被执行。
提示:你可能需要修改 MOS 中对用户进程exit的实现,使其能够返回值。
- 首先要清楚shell的调用流程
首先在
user/init.c中spwanl("sh.b","sh",NULL),开启了运行sh.b程序的进程然后sh.b中的
main()函数负责不断从标准输入逐行读取输入的指令到char *buf然后使用
runcmd(buf)来运行指令runcmd(buf)中调用parsecmd(argv, &rightpipe)来解析指令parsecmd(argv, &rightpipe)中不断循环调用gettoken()来解析命令gettoken()实际上调用_gettoken()来判断每次解析到的单词是是什么类型- 一共有三种类型
0:代表解析解结束t:代表解析到特殊字符- 这里对于
<,|,>,&,;,(,)直接返回ascii码就可以 - 对于
||和&&,也选择在_gettoken()中完成- 具体方式是识别到
|或者&的时候,再多走一步判断下一个字符是不是|或者& - 对于
||,我们返回1 - 对于
&&,我们返回2 - 因为在ascii码表中这两个值被保留
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18///////////////// 2. 判断||和&& /////////////////
if (t == '|' && (*s) == '|') {
t = 1;
*p1 = s;
*s++ = 0;
*p2 = s;
} else if (t == '&' && (*s) == '&') {
t = 2;
*p1 = s;
*s++ = 0;
*p2 = s;
} else if (t == '>' && (*s) == '>') {
t = 3;
*p1 = s;
*s++ = 0;
*p2 = s;
}
///////////////////////////////////////////////
- 具体方式是识别到
- 这里对于
'w':代表解析到一个单词
- 一共有三种类型
然后再
parsecmd()中增加能解析指令的case情况case 1:代表读取到了||case 2:代表读取到了&&
- 然后弄清楚管道的进程创建
管道命令
ls | cat是由
sh.b先fork()出一个子进程- 子进程去进一步
parsecmd(),从而spawn出cat命令 sh.b返回原进程,spawn出ls命令- 然后通过管道开关读写端实现
ls把输出传递给cat
- 子进程去进一步
条件指令可以采用一样的形式,只不是管道中,越靠前的指令由父进程创建,越靠后的指令由子进程创建
条件指令中我们需要获取靠前指令的执行结果,并且返回给后面的指令,因为前面的指令要用子进程创建,然后子进程给父进程发送信息,父进程再决定后面的指令可不可以执行
首先
fork()出一个子进程,设置need_ipc_send,然后返回- 去
spawn一个命令 - 这个命令通过修改
libos.c中的libmain,给子进程发送了直接结果的信息 - 然后子进程通过
if(child>0){ ipc_recv(0,0,0); }来等待spawn出的命令发送信息 - 等到了之后,进入
if(need_ipc_send)来给父进程发送信息
- 去
然后回到
parsecmd()中case 2的父进程分支父进程在此处
ipc_recv(0,0,0)等待子进程发送信息结合
env->env_ipc_value和现在是case 1还是case 2来决定是否要跳过下一个指令,直到找到下一个case 1或者case 2跳过的方法就是在case内部调用
c = gettoken(0,&t) != 0, 循环直到有c == 1或者c == 2然后设置
need_ipc_recv为1,这样返回runcmd的时候,才会进入结合condi_or或者condi_and来判断是否可以spawn这个指令然后设置
condi_or或者condi_and其中一个为1然后继续
parsecmd()这个时候往下走一层
如果这一层只有一条指令,就会直接在
case 'w'中返回,最终指令由父进程创建并执行如果这一层有两条指令,就会进入
case 2,然后fork()新的子进程去执行中间那条指令,父进程继续ipc_recv(0,0,0)等待而且由于跳过了不允许执行的指令,因此有子进程的
spawn出来的都是可以执行的指令,这个时候这些指令执行的返回值==0或者!=0再由子进程发送给父进程父进程再决定下一层的指令是创建还是跳过
1
2
3
4
5
6
7
8
9
10char *argv[MAXARGS];
int rightpipe = 0;
int need_ipc_send = 0;
int need_ipc_recv = 0;
int condi_or = 0;
int condi_and = 0;
int bg = 0;
int backquote = 0;
int backpipe = 0;
int argc = parsecmd(argv, &rightpipe, &need_ipc_send, &need_ipc_recv, &condi_or, &condi_and, &bg, &backquote, &backpipe);
1
2
3
4
5
6
7
8
9
10
11
12
13
14int child = -1;
if (need_ipc_recv != 0) {
if (condi_or && env->env_ipc_value != 0) {
child = spawn(argv[0], argv);
} else if (condi_and && env->env_ipc_value == 0) {
child = spawn(argv[0], argv);
}
} else {
child = spawn(argv[0], argv);
// debugf("创建echo子进程:%x\n",child);
if (bg == 2) {
syscall_add_jobs(child,news);
}
}
实现更多指令
你需要实现
touch,mkdir,rm指令,只需要考虑如下情形:
touch:touch <file>:创建空文件file,若文件存在则放弃创建,正常退出无输出。 若创建文件的父目录不存在则输出touch: cannot touch '<file>': No such file or directory。 例如touch nonexistent/dir/a.txt时应输出touch: cannot touch 'nonexistent/dir/a.txt': No such file or directory。mkdir:mkdir <dir>:若目录已存在则输出mkdir: cannot create directory '<dir>': File exists,若创建目录的父目录不存在则输出mkdir: cannot create directory '<dir>': No such file or directory,否则正常创建目录。mkdir -p <dir>:当使用-p选项时忽略错误,若目录已存在则直接退出,若创建目录的父目录不存在则递归创建目录。rm:rm <file>:若文件存在则删除<file>,否则输出rm: cannot remove '<file>': No such file or directory。rm <dir>:命令行输出:rm: cannot remove '<dir>': Is a directory。rm -r <dir>|<file>:若文件或文件夹存在则删除,否则输出rm: cannot remove '<dir>|<file>': No such file or directory。rm -rf <dir>|<file>:如果对应文件或文件夹存在则删除,否则直接退出。touch
- 先调用
user/lib/file.c中的open(path, O_RDONLY)打开文件,如果打开成功就说明文件已经存在,退出 - 如果打开失败,就再次调用
open(path, O_CREAT)来创建这个文件,如果创建失败就打印提示信息1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19if (argc != 2)
{
debugf("touch:缺少文件名\n");
return -1;
}
int fd = open(argv[1], O_RDONLY);
if (fd >= 0) {
debugf("touch:文件已存在\n");
return -1;
}
fd = open(argv[1], O_CREAT);
if (fd < 0) {
printf("touch: cannot touch \'%s\': No such file or directory\n",argv[1]);
return -1;
}
close(fd);
return 0;
- 先调用
mkdir
- 首先可能的指令类型有两种
mkdir xmkdir -p x-p表示忽略错误信息,包括目录已存在时不打印提示信息,路劲不存在时递归创建路径
- 首先判断
argv[1]是不是"-p\0"- 然后使用
stat(path, &stat_buf)函数判断目录是否已经存在- 已经存在 + 无
-p选项:打印提示信息 - 已经存在 +
-p选项:退出
- 已经存在 + 无
- 如果不存在
先直接
mkdir(path)mkdir(path)函数是自己写的,在user/lib/file.c中,并且需要在user/include/lib.h中声明这个函数就是调用
file.c中的open()函数open(path, O_MKDIR)了一下- 实际上是与文件系统服务进程通信,调用了
fs/serv.c中的serve_open()函数 - 首先模仿
O_CREAT的方法,直接复制一份,然后改成O_MKDIR - 然后创建完成后,如果是
O_MKDIR,就要把f->f_type赋值为FTYPE_DIR
- 实际上是与文件系统服务进程通信,调用了
这样可以完成单层目录的创建
因为
serv.c中的serve_open()中调用了fs/fs.c的file_create(),file_create()调用了walk_path()而
walk_path()的原理是,如果路径中有一环不存在就会返回负值,因此文件系统服务进程自带的file_create()不能完成递归创建目录所以需要自己写一个
descend_mkdir()函数该函数的递归出口是
fd = mkdir(path), 如果fd >= 0就是创建成功,说明此时在创建缺失路径的最顶层如果
fd < 0,就要去除最底层的目录,然后调用descend_mkdir()创建他的上一级目录,然后再重新调用descend_mkdir()创建自己最后根据有没有
-p选项来选择在创建失败的时候是否要递归创建目录1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23void descend_mkdir(char *path) {
int fd = mkdir(path);
if (fd > 0) {
return;
}
if (fd < 0) {
// 1. find last '\'
char newpath[1024];
strcpy(newpath, path);
int len = strlen(newpath);
for(int i = len - 1;i >= 0;i--) {
if(newpath[i] == 47) {
newpath[i] = '\0';
break;
}
}
// 2. create father
descend_mkdir(newpath);
// 3. create self
descend_mkdir(path);
}
}
- 然后使用
- 首先可能的指令类型有两种
rm
- 首先根据可能的情况
rm xrm -r xrm -rf x- 判断是否有
-r或者-rf,然后对没有选项,有-r选项,有-rf选项分别处理
- 有
-r选项user/lib/file.c中的remove(path)函数实际上与文件系统服务进程进行通信,然后调用fs/fs.c中的file_remove()函数- 该函数可以删除文件,也可以删除目录
- 因此直接调用
remove(argv[2])即可实现递归删除 - 如果删除失败则打印提示信息
1
2
3
4
5
6
7if (isr) {
r = remove(argv[2]);
if (r < 0) {
printf("rm: cannot remove \'%s\': No such file or directory\n",argv[2]);
return -1;
}
}
- 有
-rf选项- 直接删除即可
- 无论成功失败都不打印提示信息
1
2
3
4else if (isrf) {
remove(argv[2]);
return 0;
}
- 没有选项
- 要先判断需要删除的是否是目录
- 首先
fd = open (path, O_RDONLY)打开该文件,获取文件描述符下标 - 然后
fd_lookup(fd, &fd_)寻找文件描述符,需要引入<fd.h>库 - 然后把
&fd_强制转换为struct Filefd *ffd; - 然后获取
ffd->f_file.f_type,判断ffd->f_file.f_type == FTYPE_DIR - 如果是目录就删除失败,打印提示信息
- 否则就说明是文件类型,删除即可,删除失败打印提示信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29else {
if (argc != 2) {
debugf("rm:缺少文件名\n");
return -1;
}
// struct Stat stat_buf;
// if (stat(argv[1], &stat_buf) >= 0) {
// printf("rm: cannot remove \'%s\': Is a directory\n", argv[1]);
// return -1;
// }
int fd = open(argv[1],O_RDONLY);
if (fd >= 0) {
struct Fd *fd_;
fd_lookup(fd, &fd_);
struct Filefd *fdd = (struct Filefd*)fd_;
if (fdd->f_file.f_type == FTYPE_DIR) {
printf("rm: cannot remove \'%s\': Is a directory\n", argv[1]);
return -1;
}
}
r = remove(argv[1]);
if (r < 0) {
printf("rm: cannot remove \'%s\': No such file or directory\n",argv[1]);
return -1;
}
}
- 首先根据可能的情况
实现反引号
你需要使用反引号实现指令替换。只需要考虑
echo进行的输出,你需要将反引号内指令执行的所有标准输出替换为echo的参数。例如:echo `ls | cat | cat | cat`
- 识别反引号
- 子进程执行反引号内指令
- 传递管道
- 为
backquote而且rightpipe == 0的时候再复制dup(p_quote[1],1),把写端从stdout改成p_quote[1]1
2
3
4
5// parsecmd()中
if ((r = fork()) == 0) {
*backquote = 1;
return parsecmd(argv, rightpipe, need_ipc_send, need_ipc_recv, condi_or, condi_and, bg, backquote, &p_quote[1]);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// runcmd()中
if (child >= 0) {
/* code */
/////////////////// backquote ///////////////////
if (rightpipe == 0 && backquote == 1) {
// 2. 管道
dup(backpipe, 1);
close(backpipe);
close(backpipe);
}
/////////////////////////////////////////////////
/* code */
} else {
/* code */
}
- 父进程读取执行结果
- 父进程从
dup(p_quote[0],0)把从stdin读改成从p_quote[0]读 - 读取的时候因为管道只有32字节
- 因此需要使用
while(1) { read(p_quote[0], tempbuf + i_buf, 1024 - i_buf); }一段一段的读取,不能使用read()一次性读完 - 读完之后作为
argv[argc++] = tempbuf加入到参数中 - 最后还要使父进程的
gettoken走到下一个'`'1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20else {
// 1. 父进程读取执行结果,加入到argv
close(p_quote[1]);
char tempbuf[1024] = {0};
int i_buf = 0;
while (1) {
int len = read(p_quote[0], tempbuf + i_buf, 1024 - i_buf);
if (len <= 0) {
break;
}
i_buf += len;
}
tempbuf[i_buf] = 0;
close(p_quote[0]);
// 2. 先等待子进程完成
wait(r);
argv[argc++] = tempbuf;
// 3. 跳到下一个'`'
while((c = gettoken(0, &t)) != '`') { }
}
- 父进程从
实现注释功能
你需要使用
#实现注释功能,例如ls | cat # this is a comment meow,ls | cat会被正确执行,而后面的注释则会被抛弃。
- 在
parsecmd中,识别到case #直接结束指令解析并返回即可1
2case '#':
return argc;
实现历史指令
你需要实现
shell中保存历史指令的功能,可以通过Up和Down选择所保存的指令并执行。你需要将历史指令保存到根目录的.mosh_history文件中(一条指令一行),为了评测的方便,我们设定$HISTFILESIZE=20(bash 中默认为 500),即在.mosh_history中至多保存最近的 20 条指令。你还需要支持通过history命令输出.mosh_history文件中的内容。
注:在 bash 中,
history为shell built-in command,我们规定需要将history实现为built-in command。
你需要将当前执行的指令先存入
.mosh_history中,例如:
echo `ls | cat`echo meow # commenthistoryhistory | cat
当历史指令为空时,依次执行上述四条指令后,后两条指令会分别输出
echo `ls | cat`echo meow # commenthistory
与
echo `ls | cat`echo meow # commenthistoryhistory | cat
使用
Up能够切换到上一条指令(如果上一条指令存在),使用Down能够切换到下一条指令(如果下一条指令存在)。能够选择的指令范围为:用户当前输入的指令与.mosh_history文件中保存的所有指令。例如在执行了上述四条指令后,用户输入了echo,此时Up应该将指令切换至history | cat,再次进行三次Up后切换至echo `ls | cat`,此时再次Up应保留在该指令(因为已经不存在上一条指令);再进行四次Down后将切换回echo,此时再次Down应保留在该指令(因为不存在下一条指令)。
- 使用内核实现历史指令,历史指令在内核中
- 首先考虑
history | cat的情况- 所有
history的识别和执行要在parsecmd之后,这样有"|"的时候才会输出到p[1] - 而不是直接进以来就识别
history
- 所有
- 然后所有的指令退出前要加入到历史指令中
- 在
exit()前syscall_history_add()一下 - 对于内建指令,更早
exit(),也要更早history_add() - 对于
ls | cat | cat这种指令,会在顶层echo ... 123的exit()之前history_add() - 因此只要是
bakcquote == 1,那就不需要history_add()
- 在
- 对于
history指令本身- 根据需求,需要在打印历史指令之前就
history_add()
- 根据需求,需要在打印历史指令之前就
实现一行多指令
你需要实现使用 ; 将多条指令隔开从而从左至右依顺序执行每条指令的功能。例如:
ls;ls | cat; echo nihao,mosh; echo `ls; echo meow`
ls ; ls- 第一个
ls开一个子进程去执行; 第二个ls父进程执行1
2
3
4
5
6
7
8
9case ';':;
int temp_r = 0;
if ((temp_r = fork()) == 0) {
return argc;
} else {
wait(temp_r);
return parsecmd(argv, rightpipe, need_ipc_send, need_ipc_recv, condi_or, condi_and, bg, backquote, backpipe);
}
break;
实现追加重定向
你需要实现 shell 中 >> 追加重定向的功能,例如:
ls >> file1; ls >> file1
最后文件 file1 中将会有两次 ls 指令的输出。
- 识别
>>,由于ascii中3被保留,这里返回3;在parsecmd的case 3中做进一步处理 - 然后找到
>> file1后面这个字符串所指的文件int temp_fd = open(t, O_APPEND | O_RDWR);O_APPEND这个权限在fs的serv.c中的serve_open实现- 就是把
ff->f_fd.fd_offset = f->f_size; - 识别到
O_APPEND打开模式的时候,把文件描述符的偏移量改为文件大小,这样打开之后就自动定位在文件的末尾
- 就是把
- 如果打开失败说明文件不存在
- 先
open(t, O_CREAT)创建这个文件 - 然后
open(t, O_APPEND | O_RDWR)再次打开1
2
3
4
5
6
7
8
9
10
11case 3:;
// 1. 获取文件
c = gettoken(0, &t);
int fd_temp = open(t, O_APPEND | O_RDWR);
if (fd_temp < 0) {
fd_temp = open(t, O_CREAT);
fd_temp = open(t, O_APPEND | O_RDWR);
// debugf("两次打开文件%s\n",t);
} else {
// debugf("一次就打开文件%s\n",t);
}
- 先
- 然后把
stdout改成指向这个文件,指完之后要关闭文件- 也就是
dup(temp_fd, 1); close(temp_fd);1
2
3
4// 2. 修改输出路径
dup(fd_temp, 1);
close(fd_temp);
break;
- 也就是
实现引号支持
你需要实现引号支持,比如
echo "ls >",shell 在解析时需要将双引号内的内容看作是单个字符串。
- 在
gettoken的时候,如果是识别到'\"'- 就直接一直读取到下一个
'\"' - 期间内的字符串打包成
'w'返回1
2
3
4
5
6
7
8
9
10
11
12
13if (*s == '\"') {
*s++ = 0;
*p1 = s;
s++;
while(*s != '\"') {
s++;
}
*s++ = 0;
*p2 = s;
return 'w';
}
- 就直接一直读取到下一个
实现前后台任务管理
- 你需要支持 mosh 运行后台进程,当命令的末尾添加上
&符号时,该命令应该在后台执行。- 实现
jobs指令列出当前 shell 中所有后台任务的状态。你需要为任务创建 ID(每次启动 mosh 时,任务从 1 开始编号,每个新增任务编号应加 1),并且通过jobs指令输出包括:任务 ID(job_id)、任务的运行状态(status:可能的取值为Running,Done)、任务的进程 ID(env_id)与运行任务时输入的指令(cmd)。请以printf("[%d] %-10s 0x%08x %s", job_id, status, env_id, cmd)的格式进行输出。- 实现
fg将后台任务带回前台继续运行,用户通过fg <job_id>的方式将对应任务带回前台。- 实现
kill指令,用户通过kill <job_id>来实现结束后台任务。
在
fg或kill指令中,若job_id对应的后台任务不存在则输出printf("fg: job (%d) do not exist\n", job_id),若job_id对应的 ID 为envid的进程状态不为Running则输出printf("fg: (0x%08x) not running\n", envid)。
例如:
sleep 10&sleep 60 &jobs# wait for about 10 seconds...jobs
依次执行上述指令,则第一个
jobs应输出(其中进程 ID 的值可能与你本地运行的输出结果不同):[1] Running 0x00003805 sleep 10&[2] Running 0x00005006 sleep 60 &
第二个jobs应输出:[1] Done 0x00003805 sleep 10&[2] Running 0x00005006 sleep 60 &
- 通过
parsecmd中增加case &,来修改background值- 有
background值直接不ipc_recv(0,0,0),当前sh.b直接exit()1
2
3
4if (bg == 1) {
syscall_add_history(news);
exit();
}
- 有
- 对于后台程序
- 开一个子进程托管
- 先关信息记录在内核中
1
2
3
4
5
6
7
8
9// parsecmd()中
case '&':
*bg = 1;
if (fork() == 0) {
*bg = 2;
return argc;
} else {
return argc;
}1
2
3
4
5
6// runcmd()中
child = spawn(argv[0], argv);
// debugf("创建echo子进程:%x\n",child);
if (bg == 2) {
syscall_add_jobs(child,news);
}
- 然后
fg和kill指令就调用内核指令去找到这个进程fg的话就让当前sh.b等待,直到子进程运行完毕1
2
3
4
5
6
7
8
9
10else if (isfg(s) == 0) {
int job_id = atoi(s+3);
int envid = syscall_find_envid(job_id);
if (envid != -1) {
wait(envid);
}
syscall_add_history(news);
exit();
}kill的话直接杀死进程,结束sh.b1
2
3
4
5
6
7else if (iskill(s) == 0) {
int job_id = atoi(s+5);
syscall_kill_job(job_id);
syscall_add_history(news);
exit();
}
- Title: Chap.3 OS-挑战性任务shell
- Author: Toryn
- Created at : 2024-06-28 09:59:54
- Updated at : 2024-06-28 10:46:31
- Link: https://linboyan-trc.github.io/2024/06/28/Chap-3-OS-challenge-shell/
- License: This work is licensed under CC BY-NC-SA 4.0.