北航操作系统OS 挑战性任务 Shell增强实现 Lab6
实现不带 .b
后缀指令
首先寻找单个命令执行的位置,找到是spawn()
函数,prog
是主命令。然后根据主命令打开相应可执行文件,于是,我想到了可以在这里对调用的主命令字符串进行替换,尝试在结尾添加.b
来实现不带它的后缀指令。
if ((fd = open(prog, O_RDONLY)) < 0) {
char tmp[10003]={0};
int len=0;
for (len=0;prog[len];++len) tmp[len]=prog[len];
tmp[len++]='.';tmp[len++]='b';
if ((fd = open(tmp, O_RDONLY)) < 0) {
return fd;
}
}
实现指令条件执行
我发现,在sh.c
的parsecmd
函数当中,我们对一行的命令看作是单个命令进行处理。但是在这里,指令条件执行意味着我们需要将一行分为多个指令,这个时候我们需要首先观察gettoken
函数来识别条件执行的&&
和||
,然后再交付runcmd
去挨个执行。这里我记录了一个excu
来判定是否需要执行接下来的一条指令,然后再根据新的结果确定新的excu
的值。
case 'c': // && ||
r = fork();
if (r == 0) {
if (excu) return argc;
else exit(0);
} else {
argc = 0;
syscall_ipc_recv(0);
wait(r);
ret = env->env_ipc_value;
excu = ((ret != 0 && t[0]=='|') || (ret == 0 && t[0] == '&'));
}
break;
这里的fork()
是对spawn()
的行为的模仿,我们叉出一个新的进程来执行当前的指令。
同时,我们需要获取每个指令的返回值。这个返回值在libos.c
的函数当中,所有的主函数在返回的时候都会执行exit()
。我们需要把它设计成有返回值的exit(x)
。我的做法是进程间通信,也就是向父进程使用syscall_ipc_try_send()
发送一个值。
#include <env.h>
#include <lib.h>
#include <mmu.h>
void exit(int ret) {
exit_r(ret);
}
void exit_r(int ret) {
close_all();
syscall_ipc_try_send(env->env_parent_id, ret, 0, 0);
syscall_env_destroy(0);
user_panic("unrerachable");
}
const volatile struct Env *env;
extern int main(int, char **);
void libmain(int argc, char **argv) {
// set env to point at our env structure in envs[].
env = &envs[ENVX(syscall_getenvid())];
// call user main routine
int ret = main(argc, argv);
// exit gracefully
exit_r(ret);
}
在这里,内核的编译过程当中也有其他的代码使用了无返回值的exit()
,想要把它们从编译当中剔除比较困难,所以我手动修改了所有的exit()
。注意,不可以不修改,因为在本作业使用的shell
内核当中,指令可能中途在没有返回值的exit()
返回,而我们必须让每一个指令都有返回值,所以最佳方式就是全部修改。
实现更多指令
touch
指令
touch
命令是创建一个空文件。我们可以使用文件控制块的struct Stat
文件状态来判断一个地址下是否存在文件以及该文件的类型(目录或普通文件)。首先判断父目录是否存在,然后判断该目录是否存在,如果文件可以创建,则把通用函数open
的权限位设为O_CREAT
,从而创建文件。
#include <lib.h>
int flag[256];
void usage(void) {
printf("usage: ls [-dFl] [file...]\n");
exit(1);
}
void parseName(char *path, char *dir, char* name) {
int tot=0;
while (path[tot]) ++tot;
int len=tot;
for (tot=len-1;tot>=0 && path[tot]!='/';--tot);
int t=0;
for (t=0;tot+t+1<len;++t)
name[t]=path[tot+t+1];
name[t]=0;
t=0;
for (t=0;t<tot;++t)
dir[t]=path[t];
if (t>=0) dir[t]='\0';
else dir[0]='.',dir[1]='\0';
}
int main(int argc, char **argv) {
char *path = argv[1];
char dir[10003];
char name[10003];
int i;
ARGBEGIN {
default:
usage();
flag[(u_char)ARGC()]++;
break;
}
ARGEND
parseName(path,dir,name);
struct Stat st;
int r = stat(dir, &st);
if (r < 0 || !st.st_isdir) {
printf("touch: cannot touch '%s': No such file or directory\n",path);
return 1;
}
r = stat(path, &st);
if (r >= 0 && !st.st_isdir) {
return 0;
}
struct Fd fd;
r = open(path, O_CREAT);
if (r < 0) {
user_panic("touch cannot open file %s\n",path);
}
close(r);
return 0;
}
这一部分是较为简单的。
mkdir
指令
这一部分与上一个命令的不同之处在于对于-p
开关需要递归创建文件夹。方法是按照'/'
划分,然后挨个创建目录。
#include <lib.h>
int flag[256];
void usage(void) {
printf("usage: ls [-dFl] [file...]\n");
exit(1);
}
void parseName(char *path, char *dir, char* name) {
int tot=0;
while (path[tot]) ++tot;
int len=tot;
for (tot=len-1;tot>=0 && path[tot]!='/';--tot);
int t=0;
for (t=0;tot+t+1<len;++t)
name[t]=path[tot+t+1];
name[t]=0;
t=0;
for (t=0;t<tot;++t)
dir[t]=path[t];
if (t>=0) dir[t]='\0';
else dir[0]='.',dir[1]='\0';
}
int main(int argc, char **argv) {
char *path;
char dir[10003];
char name[10003];
int i;
ARGBEGIN {
default:
usage();
case 'p':
flag[(u_char)ARGC()]++;
break;
}
ARGEND
path = argv[0];
parseName(path,dir,name);
struct Stat st;
int r = stat(dir, &st);
if (r < 0 || !st.st_isdir) {
if (!flag['p']) {
printf("mkdir: cannot create directory '%s': No such file or directory\n",path);
return 1;
} else {
int i;
for (i=1;path[i];++i) {
if (path[i]=='/') {
path[i]='\0';
r=stat(path, &st);
if (r < 0 || !st.st_isdir) {
r = open(path, O_MKDIR);
close(r);
}
path[i]='/';
}
}
}
}
r = stat(path, &st);
if (r >= 0 && st.st_isdir) {
if (flag['p']) return 0;
else {
printf("mkdir: cannot create directory '%s': File exists\n",path);
return 1;
}
}
r = open(path, O_MKDIR);
if (r<0) user_panic("unreachable\n");
close(r);
return 0;
}
rm
指令
和第一个的区别除了调用设备通用函数remove()
进行删除操作以外,根据开关判断一下要输出什么即可。
#include <lib.h>
int flag[256];
void usage(void) {
printf("usage: ls [-dFl] [file...]\n");
exit(1);
}
void parseName(char *path, char *dir, char* name) {
int tot=0;
while (path[tot]) ++tot;
int len=tot;
for (tot=len-1;tot>=0 && path[tot]!='/';--tot);
int t=0;
for (t=0;tot+t+1<len;++t)
name[t]=path[tot+t+1];
name[t]=0;
t=0;
for (t=0;t<tot;++t)
dir[t]=path[t];
if (t>=0) dir[t]='\0';
else dir[0]='.',dir[1]='\0';
}
int main(int argc, char **argv) {
char *path;
char dir[10003];
char name[10003];
int i;
ARGBEGIN {
default:
usage();
case 'r':
case 'f':
flag[(u_char)ARGC()]++;
break;
}
ARGEND
path = argv[0];
parseName(path,dir,name);
struct Stat st;
int r = stat(path, &st);
if (r >= 0) {
if (st.st_isdir) {
if (!flag['r']) {
printf("rm: cannot remove '%s': Is a directory\n",path);
return 1;
} else {
remove(path);
return 0;
}
} else {
remove(path);
return 0;
}
} else {
if (flag['r'] && flag['f']) {
return 0;
} else if (flag['r']) {
printf("rm: cannot remove '%s': No such file or directory\n",path);
return 1;
} else {
printf("rm: cannot remove '%s': No such file or directory\n",path);
return 1;
}
}
return 0;
}
实现反引号
我们发现,反引号的部分需要我们执行反引号内部的指令并得到它的标准输出。需要注意的是,必须是得到标准输出,因为我们不知道使用反引号的函数是否仍然会重定向。因此我们模仿spawn
和管道通信,叉出一个用于执行反引号内部指令的进程,让这个进程和再叉出一个的进程(该进程负责直接从标准输出中读出内容并写入数组)之间进行管道通信,这样我们就把标准输出变到了数组内部,然后再执行spawn
(实际上是调用runcmd
,因为可能内部仍然是很多句话),数组内的东西就可以作为argv
的参数了。
注意进行管道通信的时候,最好是一个字符一个字符读入而不是一下读入很多个字符,虽然我看到read
设备在管道设备当中的具体实现是会阻塞读入直到管道关闭或者达到目标个数的,但是实操的时候只能读入管道大小个,我不是很清楚为什么,但是一个字符一个字符读入就可以,很奇怪。
case 'f':;
if (excu) {
r = fork();
if (r == 0) {
runcmd(t);
} else if (r > 0) {
syscall_ipc_recv(0);
ret = env->env_ipc_value;
*wt = 1;
return parsecmd(argv,rightpipe,wt);
}
int q[2];
pipe(q);
*rightpipe = fork();
if (*rightpipe > 0) {
dup(q[1],1);
close(q[1]);
close(q[0]);
alloc = 1;
r = fork();
if (r == 0) {
runcmd(t);
} else {
syscall_ipc_recv(0);
ret = env->env_ipc_value;
}
// return argc;
} else if (*rightpipe == 0) {
dup(q[0],0);
close(q[0]);
close(q[1]);
alloc=0;
r = fork();
if (r == 0) {
int len=0;
char tmp[100003] = {0};
do {
read(0, tmp+len, 1);
++len;
} while (tmp[len-1]!=-1 && tmp[len-1]!=0);
tmp[--len]=0;
printf("tmp(%d) = |%s|\n",tmp);
argv[argc++]=tmp;
return argc;
} else {
if (r >= 0 && *wt) wait(r);
return parsecmd(argv, rightpipe, wt);
}
}
}
break;
实现注释功能
非常简单,和达到行末退出一致。
case '#':
if (excu) return argc;
else exit(0);
实现历史指令
我使用的方法,是每次在readline
得到当前一行的指令过后,先把这一行指令写入.mosh_history
文件。在写入的时候,时刻保证文件内部只有20行。在得到这一行的指令之前,输入的时候,就预先把历史命令读入内存数组,然后判断上下字符27[A
和27[B
,进行切换。注意当前没有输入完成的内容也需要当作一行保存起来。回车键敲下,当前行的最终形态写入历史文件。
void readline(char *buf, u_int n) {
char ope[20][600];int size;
read_history(ope,&size);
int r,cur = size;
for (int i = 0; i < n; i++) {
if ((r = read(0, buf + i, 1)) != 1) {
if (r < 0) {
debugf("read error: %d\n", r);
}
exit(1);
}
if (buf[i] == '\b' || buf[i] == 0x7f) {
if (i > 0) {
i -= 2;
} else {
i = -1;
}
if (buf[i] != '\b') {
printf("\b \b");
}
}
if (buf[i] == '\r' || buf[i] == '\n') {
buf[i] = 0;
memcpy(copy_buf,buf,strlen(buf)+1);
return;
}
if (buf[i]==27) {
++i;read(0,buf+i,1);
++i;read(0,buf+i,1);
int la_len=0;
if (cur == size) la_len = i;
else la_len = strlen(ope[cur])-1;
if (buf[i]=='A') {
printf("%c[B",27);
if ((--cur) < (size==20))
cur = (size == 20);
} else if (buf[i] == 'B') {
if ((++cur) > size)
cur = size;
}
int j;for (j=0;j<la_len;++j) printf("\b \b");
i -= 3;
if (cur == size) {
for (j=0;j<=i;++j) printf("%c",buf[j]);
} else {
int len = strlen(ope[cur])-1;
for (j=0;j<len;++j) printf("%c",ope[cur][j]);
}
}
}
debugf("line too long\n");
while ((r = read(0, buf, 1)) == 1 && buf[0] != '\r' && buf[0] != '\n') {
;
}
buf[0] = 0;
copy_buf[0]=0;
}
void read_history(char ope[][600],int *sz) {
int top=0,fd;int size = 0;
fd = open(HISTORY_FILE,O_RDONLY);
if (fd < 0) {
fd = open(HISTORY_FILE,O_CREAT);
close(fd);
fd = open(HISTORY_FILE,O_RDONLY);
}
int la;char c;
while (1) {
la=read(fd,&c,1);
if (la!=1) {
break;
}
if (c=='\r' || c == '\n') {
ope[size][top++]='\n';ope[size][top]='\0';
++size;top=0;
while ((la=read(fd,&c,1)) == 1 && strchr(WHITESPACE, c));
if (la != 1) break;
}
ope[size][top++]=c;
}
*sz = size;
close(fd);
}
void write_history(char *buf) {
char ope[20][600];int size = 0,top=0;
read_history(ope,&size);
remove(HISTORY_FILE);
int fd = open(HISTORY_FILE,O_CREAT);
close(fd);
fd = open(HISTORY_FILE,O_WRONLY);
int i;
for (i=(size==20);i < size;++i) {
write(fd,ope[i],strlen(ope[i]));
}
top = strlen(buf);buf[top++]='\n';buf[top]='\0';
write(fd,buf,top);
close(fd);
}
这一部分是读写历史文件。我把这两部分分开来了,目的是在命令行执行history
指令的时候,可以单独拿出来read_history
然后读出历史。
if (!strcmp(argv[0],"history")) {
int r = fork();
if (r == 0) {
char ope[20][600];int size;
read_history(ope,&size);
//printf("size = %d\n",size);
for (int i=0;i<size;++i)
printf("%s",ope[i]);
} else
wait(r);
return 0;
}
这一段对history
命令的具体处理在runcmd
函数当中,同样是模仿spawn
叉出一个进程处理指令,父进程得到返回值。
实现一行多指令
实现一行多指令的时候,我们一定要把它切成很多条指令。在这里为了保证输入在行上的完整性,我把它放在了parsecmd
函数当中。需要注意的是新的指令需要对应新的返回值,因此需要新的进程去执行。同时,这也可以保证前后进程在重定向上的一致性。(这一点在后台进程当中会用到。)
case ';':
r = fork();
if (r == 0) {
if (excu) return argc;
else exit(0);
} else {
wait(r);
argc = 0;
excu = 1;
}
break;
实现追加重定向
追加重定向和反引号指令是很类似的,区别在于反引号在由管道通信过后,标准输出的内容输入的是数组,而追加重定向的部分,标准输出的内容输入文件。
case 'e':// >>
if (gettoken(0, &t) != 'w') {
debugf("syntax error: >> not followed by word\n");
exit(1);
}
if (excu) {
struct Stat st;
int tt = stat(t, &st);
if (tt < 0 || st.st_isdir) {
fd = open(t,O_CREAT);
close(fd);
}
int q[2];
pipe(q);
//printf("after pipe\n");
*rightpipe = fork();
//printf("father = %d envid = %d\n",env->env_parent_id, env->env_id);
if (*rightpipe > 0) {
// printf("in 1 dup %d %d\n",q[1],1);
dup(q[1],1);
// printf("in 2\n");
close(q[1]);
// printf("in 3\n");
close(q[0]);
alloc = 1;
// printf("env = %d returned \n",env->env_id);
return argc;
} else if (*rightpipe == 0) {
dup(q[0],0);
// printf("begin waiting r = %d\n",env->env_parent_id);
// wait(env->env_parent_id);
// printf("after waiting r = %d\n",r);
// printf("before read\n");
// read(q[0], tmp, 1000000);
// printf("after read |%s|\n",tmp);
close(q[0]);
close(q[1]);
alloc = 0;
r = fork();
if (r == 0) {
int len=0;
char tmp[100003] = {0};
do {
read(0, tmp+len, 1);
++len;
} while (tmp[len-1]!=-1 && tmp[len-1]!=0);
tmp[--len]=0;
fd = open(t,O_RDWR);
char conte[10003];
read(fd, conte, 10000);
// printf("conte = |%s|\n",conte);
// printf("tmp(%d) = |%s|\n",len,tmp);
// char tes[13] = "123123";
write(fd, tmp, len);
// read(fd, conte, 10000);
// printf("conte after = |%s|\n",conte);
close(fd);
exit(0);
} else {
if (r >= 0 && *wt) {
// printf("begin wait");
wait(r);
// printf("end wait");
}
// ret = env->env_ipc_value;
return parsecmd(argv, rightpipe,wt);
// argc = 0;
// excu = 0;
}
//return parsecmd(argv, rightpipe);
}
}
break;
实现引号支持
将引号内部的部分作为argv
的参数即可。
if (*s == '"') {
*s = 0;
++s;
*p1 = s;
while (*s && *s != '"') ++s;
if (!(*s)) exit(1);
*p2 = s;
*s = ' ';
return 'w';
}
实现前后台任务管理
我的实现方式,是在内存当中开辟一个结构体去存储所有的后台信息。关于一个后台程序是否运行,我们通过内核态和用户态共用的进程哈希数组ENVS
来获取相关进程的状态,然后输出。但是每次fork()
之后,内存空间也被赋值为两份,要如何让这两份空间都能更新最外层的进程的数组呢?我想到的办法是进程间通信。由于每次运行正确返回0,运行错误返回1,所以我们可以设计如果运行正确,在最外层的shell
进程fork()
一个新进程,将新添加的指令挨个使用ipc
进程通信传给shell
进程,而首先传输该指令所需要添加的后台进程数的相反数,也就是一个负数,就与前两者区分开了。shell
进程发现如果执行进程得到负数,那么就按照这个负数的绝对值个数以此尝试syscall_ipc_recv(0)
来获取发送的每个新进程的信息。
这一部分是parsecmd
当中的处理:使用wait(wt)
标记该进程是否需要shell
等待,以及alloc
标记上一条指令是否重定向了标准输入输出,0表示重定向了标准输入,1表示重定向了标准输出。如果是,重定向回来。
case '&':
if (excu) {
r = fork();
if (r == 0) {
*wt = 0;
return argc;
} else {
syscall_ipc_recv(0);
wait(r);
++bs_top;
bs[bs_top].job_id = bs_top;
bs[bs_top].status = 0;
bs[bs_top].env_id = env->env_ipc_value;
int len = strlen(copy_buf);
memcpy(bs[bs_top].cmd, copy_buf, len+1);
if (alloc == 0)
dup(1,0);
else
if (alloc == 1)
dup(0,1);
*wt = 1;
return parsecmd(argv, rightpipe, wt);
}
}
break;
主函数当中shell
进程的叉出新进程的操作如下:
if (r == 0) {
int t = bs_top;
int r = _runcmd(buf);
// printf("r=%d t - bs_top = %d\n",r, t - bs_top);
if (r > 0) exit(r);
if (t < bs_top) {
send(t-bs_top);
// printf("sent\n");
for (++t;t<=bs_top;++t) {
send(bs[t].status);
// printf("send status%d\n",bs[t].status);
send(bs[t].env_id);
// printf("send env_id%d\n",bs[t].env_id);
// int len = strlen(bs[t].cmd);
// send(len);
// int i=0;for (i=0;i<len;++i)
}
}
exit(0);
} else {
// printf("recv\n");
syscall_ipc_recv(0);
// printf("recved\n");
int n = - getvalue();
// printf("n = %d\n",n);
if (n > 0) {
while (n--) {
++bs_top;
bs[bs_top].job_id = bs_top;
recv();bs[bs_top].status = getvalue();
// printf("get status %d\n",getvalue());
recv();bs[bs_top].env_id = getvalue();
// printf("get env_id %d\n",getvalue());
// recv();int len = getvalue();
int i;for (i=0;copy_buf[i];++i)
bs[bs_top].cmd[i]=copy_buf[i];
bs[bs_top].cmd[i] = '\0';
}
}
wait(r);
}
获取进程状态的函数如下:
void check_status(struct backstage *cur) {
if (cur->status == 0) {
struct Env *e = &envs[ENVX(cur->env_id)];
if (e->env_status == ENV_FREE || e->env_id != cur->env_id)
cur->status = 1;
}
}
fg
和kill
两个操作函数和jobs
查询函数的具体实现
if (!strcmp(argv[0],"fg") || !strcmp(argv[0],"kill")) {
int id = 0,i;
for (i=0;argv[1][i];++i) id = id * 10 + argv[1][i]-'0';
if (!(1 <= id && id <= bs_top)) {
printf("fg: job (%d) do not exist\n", id);
return 1;
}
/* struct backstage *cur = &bs[map_bs[id]];
if (cur->status == 0) {
int st = envs[ENVX(cur->env_id)].env_status;
if (st != 1) // runnable
cur->status = 1;
}*/
struct backstage *cur = &bs[id];
check_status(cur);
if (cur->status != 0) {
printf("fg: (0x%08x) not running\n", cur->env_id);
}
if (!strcmp(argv[0],"fg")) {//fg
wait(cur->env_id);
} else if (!strcmp(argv[0],"kill")) {
// printf("kill %d!\n",cur->env_id);
//syscall_set_env_status(cur->env_id, ENV_NOT_RUNNABLE);
syscall_env_destroy(cur->env_id);
check_status(cur);
// printf("status(%d) = %s\n",cur->env_id,run_stat[cur->status]);
cur->status = 1;
} else return 1;
return 0;
}
if (!strcmp(argv[0], "jobs")) {
// printf("jobs %d\n",bs_top);
for (int i=1;i<=bs_top;++i) {
check_status(&bs[i]);
printf("[%d] %-10s 0x%08x %s\n", bs[i].job_id, run_stat[bs[i].status], bs[i].env_id, bs[i].cmd);
}
return 0;
}
这里要注意的是,由于修改进程状态只有该进程的父进程和它自己有权限,这是syscall_env_destroy(int)
和syscall_set_env_status()
当中perm
位写明的,所以我的做法是直接修改了syscall_env_destroy
的perm
位为0。事实上应该自己新写一个syscall
比较好,但是这样做在测试数据当中没有出问题,所以使用了。