【OS】Shell挑战性任务实验报告

Shell实验报告

本次Shell挑战性任务我认为相较于之前,由于增加了内建指令和外部指令的区分,需要更加关注进程方面的问题。但是要得分并不困难,测试点的强度并不高,有一些进程相关的问题不去考虑也没有问题,这可能也是为了让大家更积极地参与到挑战性任务中吧。不过对于大家来说,这自然是好事情。本实验报告结构总体沿添加的各项指令展开,在这之中我也将更加注重对进程相关问题的分析。

内建指令与外部指令

在正式开始添加指令之前,我认为有必要搞明白内建指令和外部指令的区别。
内建指令是shell自身就可以完成的指令,不需要调用外部的文件,当然也不需要fork子进程,在shell主进程中执行即可。本次实验中的内建指令有:cd、pwd、declare、unset、history。而Lab6中我们完成的shell,由于执行的指令均为外部指令,在sh.c的main中,均是fork了一个子进程执行runcmd函数。因此我们要添加内建指令,就需要在这里下功夫。
外部指令我们在Lab6中已经有所接触,如ls、cat、echo等,执行这些命令离不开spawn函数,通过syscall_exofork()创建一个子进程,在该子进程中打开相应的外部命令二进制文件执行外部命令。因此,对于shell主进程来说,真正执行外部命令的进程,是主进程的孙子进程。
通过以上分析,对于单独的内建指令或者外部指令,我们已经捋顺清楚。我们也可以想到,那我们是不是只修改runcmd函数就好了呢?只需要把内建指令的识别分支放在外部指令执行之前,执行完后直接return即可。比如:

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
29
// user/sh.c runcmd()
if (strcmp(argv[0], "cd") == 0) {

if(argc > 2) {

printf("Too many args for cd command\n");

} else {

cd_command(argv[1]);

}

return 0;

}
else if{...}
...
int child = spawn(argv[0], argv);
close_all();
if (child >= 0) {
wait(child);
} else {
debugf("spawn %s: %d\n", argv[0], child);
}
if (rightpipe) {
wait(rightpipe);
}
exit();

但是当二者组合起来,我们需要考虑的问题就多了起来,会发现事情并不是这么简单。考虑以下该混合指令

1
declare a=2 | cat

把该命令拆开来看,该命令中管道左边的pwd为内建指令,而cat为外部指令,根据parsecmd()中的处理逻辑,左边的指令会继续在主进程中向下执行,而对于右侧指令的执行,会首先在主进程中fork一个子进程然后继续递归调用parsecmd()。但是这样会在主进程中声明一个局部变量a=2,这和bash等主流shell的行为是不符的,正确的行为是将该指令整体作为一条指令,无论管道的各部分指令是何种类型,都fork一个子进程,各部分指令都在子进程中执行。因此我们不能只是简单的看到内建指令就在主进程中执行。
不过这一点在评测中似乎并没有体现,在指导书中没有明确说明,不过依据我们使用的主流Bash Shell,这一点还是需要额外注意的。这里提供一篇文章,其中阐述了这一问题。

支持更多指令

该部分要求支持touch,mkdir,rm这三个外部指令和exit内建指令。
新建的外部指令需要我们创建user/touch.c,user/mkdir.c,user/rm.c文件,然后在new.mk中将其对应的二进制文件加入USERAPPS:

1
2
3
4
5
6
7
INITAPPS  +=

USERLIB +=

USERAPPS += touch.b \
mkdir.b \
rm.b \

touch

  • 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

我们的文件系统已经提供了创建文件的接口方法file_creat(),同时我们可以阅读fs/serv.c中的serve_open()方法,我们可以得知,当请求模式中包含O_CREAT时,会调用file_create()创建文件,所以我们只需要使用open方法打开对应path的文件即可,如果无法打开,那么open会帮助我们创建。

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
29
30
31
#include  <lib.h>

int touch(const char *path) {

int r;

if ((r = open(path, O_CREAT | FTYPE_REG)) > 0) {

return 0;

}

if (r < 0) {

printf("touch: cannot touch '%s': No such file or directory\n", path);

}

return r;

}

int main(int argc, char **argv) {

char* path = argv[1];

int t = touch(path);

return t;

}

(我使用的IDE对于C代码的缩进处理有些混乱)

mkdir

  • mkdir:
  • mkdir <dir>:若目录已存在则输出 mkdir: cannot create directory '<dir>': File exists,若创建目录的父目录不存在则输出 mkdir: cannot create directory '<dir>': No such file or directory,否则正常创建目录。
  • mkdir -p <dir>:当使用 -p 选项时忽略错误,若目录已存在则直接退出,若创建目录的父目录不存在则递归创建目录。

在MOS和其他操作系统,如Linux等,文件和目录在定义和结构上没有区别,仅仅只是f_type不同,因此mkdir和touch创建文件部分的方法我们都使用open。
但是open并无法创建目录,这个问题也非常好解决,我们修改一下serve_open函数就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// fs/serv.c
// If mode include O_TRUNC, set the file size to 0

if (rq->req_omode & O_TRUNC) {

if ((r = file_set_size(f, 0)) < 0) {

ipc_send(envid, r, 0, 0);

}

}

// 这里我们判断一下请求模式中有没有指出O_MKDIR
if(rq->req_omode & O_MKDIR){

f->f_type=FTYPE_DIR;

}

// Fill out the Filefd structure

ff = (struct Filefd *)o->o_ff;
...

我们使用O_MKDIR来标识是否要创建目录,这里其实有些取巧,O_MKDIR的使用本来是希望我们增加serve_mkdir、fsipc_mkdir等等从文件系统到用户接口的函数,但是我们就不这样大费周折了,直接把其整合到serve_open的逻辑中。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include  <lib.h>

int mkdir(const char *path,int haveP) {

// printf("path:%s\n",path);

int r;

if ((r = open(path, O_RDONLY)) > 0) {

// printf("open successful\n");

if(!haveP) {

printf("mkdir: cannot create directory '%s': File exists\n", path);

return -1;

} else {

return 0;

}

} else {

// printf("open not successful\n");

char tmp[1024];

strcpy(tmp,path);

char *last_slash = tmp;

for (char *p = tmp; *p; ++p) {

if (*p == '/') last_slash = p;

}

if (last_slash != tmp) {

*last_slash = '\0';

const char *t = (const char*)tmp;

if((r=open(t,O_RDONLY))<=0) {

if(!haveP){

printf("mkdir: cannot create directory '%s': No such file or directory\n", path);

return -2;

} else {

mkdir(t,haveP);

}

}

}

r = open(path,O_CREAT|O_MKDIR);

if(r>0){

return 0;

}

return r;

}

}

int main(int argc, char **argv) {

int haveP = (strcmp(argv[1],"-p")==0)?1:0;

char* path = (haveP)? argv[2]:argv[1];

int t = mkdir(path,haveP);

return t;

}

同时要注意mkdir中的参数,这是比touch多出来的部分,注意处理即可。

rm

  • 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>:如果对应文件或文件夹存在则删除,否则直接退出。

rm的处理相对简单,我们在Lab中实现的MOS已经为我们提供了删除文件的接口,还是注意参数的处理即可。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include  <lib.h>

int g_path(char *path, char *abs_path) {
//将相对路径path转化为绝对路径abs_path
}

int rm(const char*path,int r,int f){

struct Stat st;

int exist = (open(path,O_RDONLY) > 0);

char t_path[1024];

strcpy(t_path,path);

char t_rpath[1024];

g_path(t_path,t_rpath);

const char *rpath = (const char*)t_rpath;

stat(path, &st);

if(r) { //能递归删除目录

if(exist>0){ //存在该文件

open(rpath, O_WRONLY|O_TRUNC);

remove(rpath);

} else { //不存在,看有没有参数f

if(f) {

return 0;

} else {

printf("rm: cannot remove '%s': No such file or directory\n",path);

return -1;

}

}

} else {

if(st.st_isdir){

printf("rm: cannot remove '%s': Is a directory\n",path);

} else {

if(exist>0){

open(rpath, O_WRONLY|O_TRUNC);

remove(rpath);

} else {

printf("rm: cannot remove '%s': No such file or directory\n",path);

return -1;

}

}

}

return 0;

}

int main(int argc, char **argv) {

int r=0;

int f=0;

if(argc>2) { //有选项参数

if(strcmp(argv[1],"-r")==0){

r=1;

f=0;

} else {

r=1;

f=1;

}

}

char* path = (r)? argv[2]:argv[1];

int t=rm(path,r,f);

return t;

}

这里很大一部分篇幅是针对相对路径转化为绝对路径,同时我们注意到我们在touch和mkdir中向open中传入的路径为原始的路径,同时这里的open其实也可以传入原始路径,这一点我们在支持相对路径的部分介绍原因。

exit

较为简单,没什么好说的。

支持相对路径

与路径有关的相关指令包括cd,pwd,mkdir,touch,rm等,于是我们考虑有没有什么方法能够让我们减少一点工作量。同时我们注意到指导书中的提示:

父进程能够把工作路径传递给子进程
很容易联想到进程的写时复制机制,因此,我们可以通过在进程块中维护和获取当前工作目录。

定义和初始化工作目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// include/env.h
struct Env {
...
...
char r_path[256];
};
// kern/env.c
int env_alloc(struct Env **new, u_int parent_id) {
...
...
strcpy(e->r_path,"/");
/* Step 5: Remove the new Env from env_free_list. */
/* Exercise 3.4: Your code here. (4/4) */
LIST_REMOVE(e, env_link);
*new = e;
return 0;
}

这样使得进程初始进程为根目录。

增加调用接口

添加系统调用和用户态接口,使得我们在用户态下能够获取和修改当前的工作目录。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// include/syscall.h
enum {
...
SYS_get_rpath,
SYS_set_rpath,
MAX_SYSNO,
};

// kern/syscall_all.c
int sys_set_rpath(char *newPath) {
if (strlen(newPath) > 1024) { return -1; }
strcpy(curenv->r_path, newPath);
return 0;
}

int sys_get_rpath(char *dst) {
if (dst == 0) { return -1; }
strcpy(dst, curenv->r_path);
return 0;
}

void *syscall_table[MAX_SYSNO] = {
...
[SYS_get_rpath] = sys_get_rpath,
[SYS_set_rpath] = sys_set_rpath,
};

// user/include/lib.h
int syscall_set_rpath(char *newPath);
int syscall_get_rpath(char *dst);
int chdir(char *newPath);
int getcwd(char *path);

// user/lib/syscall_lib.c
int syscall_set_rpath(char *newPath) {
return msyscall(SYS_set_rpath, newPath);
}

int syscall_get_rpath(char *dst) {
return msyscall(SYS_get_rpath, dst);
}

// user/lib/file.c
int chdir(char *newPath) {
return syscall_set_rpath(newPath);
}

int getcwd(char *path) {
return syscall_get_rpath(path);
}

在添加完系统调用和用户态接口后,我们还需要新建进程时工作目录的传递。

传递工作目录

无论什么样的用户态接口,想要创建子进程,都必须调用内核态中的sys_exfork,因此我们对该函数做出修改即可。

1
2
3
4
5
6
// kern/syscall_all.c
int sys_exofork(void) {
...
strcpy(e->r_path, curenv->r_path);
return e->env_id;
}

至此我们已经完成了所有准备工作,只需要自己写一个将相对路径转化为绝对路径的方法即可,这部分并不困难,根据大模型的辅助很容易就能有一个。把这个方法放到sh.c,在执行cd时调用即可。

新增更多指令支持相对目录

完成了cd、pwd指令,我们还需要让新增的三条更多指令支持相对路径。可以发现mkdir和touch均调用open函数,因此我们修改open函数在其中把相对路径转化为绝对路径即可,我们只需要把准备好的转化函数复制到user/lib/file.c中即可。对于rm,我们要调用remove方法,该方法本身不支持相对路径,但是也没什么必要再去修改remove相关的方法了,直接把转化方法复制到rm.c里面调用一下再把绝对路径放到remove中即可。

不带.b后缀指令

比较容易,我们对spawn(user/lib/spawn.c)函数做出修改即可,只需要在使用open打开之前判断一下spawn参数中的prog是否以.b结尾,如果不是,将其为prog添加.b后缀。在这个过程之后,就可以正常打开prog了。

注意:修改后缀时不要忘记结尾的‘\0’

指令自由输入和快捷键

这一部分做起来比较容易,主要是对sh.c中的readline函数做出修改,大模型在这部分的表现还是很好的,自行修改调整即可。

实现注释功能

也比较简单,_gettoken中检测到有#出现就将其改为‘\0’然后返回即可。也就是遇到井号就把其变成命令的结尾。

实现历史指令

我的实现可能有点投机倒把,这部分在最开始就交给大模型去完成,大模型给出的实现是维护一个全局变量结构体,去储存历史指令和要编辑的指令索引。然后每有一条新指令,就维护结构体,同时将更新后的结构体中的内容全部写到.history文件下。
我一开始没有注意这里,看到其功能正常就没多管,后来写到追加重定向时突然想到了这里的处理,其实使用追加重定向去增加.history中的历史指令是更好的。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//这是我新增的user/include/shell.h头文件,里面定义了一些宏变量、环境变量结构、shell状态结构以及用到的函数
#ifndef _SHELL_H_
#define _SHELL_H_
#include <lib.h>
#define MAX_PATH_LEN 256
#define MAX_VAR_NAME_LEN 16
#define MAX_VAR_VALUE_LEN 16
#define MAX_HISTORY_SIZE 20
#define MAX_LINE_LEN 1024

// 环境变量结构
typedef struct {
char name[MAX_VAR_NAME_LEN];
char value[MAX_VAR_VALUE_LEN];
int is_readonly; // 是否为只读变量
} env_var_t;

// Shell状态结构
typedef struct {
env_var_t vars[100]; // 环境变量数组
int var_count; // 环境变量数量
char history[MAX_HISTORY_SIZE][MAX_LINE_LEN]; // 历史命令
int history_count; // 历史命令数量
int history_index; // 当前历史命令索引
} shell_state_t;

// 函数声明

void init_shell(void);
int cd_command(char *path);
int pwd_command(void);
int declare_command(int argc, char **argv,env_var_t* partVars,int* partVarCounts);
int unset_command(char *name,env_var_t* partVars,int* partVarCounts);
int exit_command(void);
int history_command(void);
void add_to_history(char *cmd);
char *get_history(int index);
void save_history(void);
void load_history(void);
char *expand_variables(char *str, env_var_t* partVars, int* partVarCounts);
int execute_command(char *cmd);
int parse_path(char *path, char *abs_path);
int mkdir_command(int argc, char **argv);
int touch_command(int argc, char **argv);
int rm_command(int argc, char **argv);

#endif
// user/sh.c
#include <args.h>
#include <lib.h>
#include <shell.h>
#include <mmu.h>
#define WHITESPACE " \t\r\n"
#define SYMBOLS "<|>&;()"
shell_state_t shell_state; // 添加shell状态变量

至此我认为与进程关系不大的新增部分已经结束,接下来的部分就要更加关注新增功能和进程的关联

实现环境变量管理

这部分我认为是最能体现与进程关联的,指导书中指出:

当执行declare指令时,需要以<var>=<val>(<var>为环境变量名,<val>为对应的值)输出当前Shell中的所有环境变量,评测不会对输出顺序进行测试。子Shell对环境变量的修改不会影响父Shell,且上述指令不能正确执行时返回非零值。
也就是说子进程能从父进程深克隆一份环境变量,但是父进程中的局部变量不会被子进程克隆走,同时子进程对于变量的操作也不会反应到父进程中去。
这便是提醒我们创建进程时的写时复制机制。我们可以将环境变量写在在主进程定义的shell_state中,在创建子进程时会自动复制一份。我们需要考虑的问题是在哪里存储局部变量。
我采用的方法十分粗暴:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
int  main(int  argc, char  **argv)  {
...
init_shell();
env_var_t partVar[100];
int partVarCount=0;
for (;;) {
if (interactive) {
printf("\n$ ");
}
readline(buf, sizeof buf);
if (buf[0] == '#') {
continue;
}
if (echocmds) {
printf("# %s\n", buf);
}
// 添加命令到历史记录
if (buf[0] != '\0') { // 只添加非空命令
add_to_history(buf);
}
char first_word[64];
strcpy(buf,expand_variables(buf,partVar,&partVarCount));
// printf("%s\n",buf);
if (!has_pipe(buf)) {
get_first_word(buf, first_word);
if (is_builtin(first_word)) {
// 直接在主进程执行内建命令
runcmd(buf,partVar,&partVarCount,0);
continue;
}
}
// 如果不是内建命令,则创建子进程执行外部命令
if ((r = fork()) < 0) {
user_panic("fork: %d", r);
}
if (r == 0) {
env_var_t partVar_t[100];
int partVarCount_t=0;
if(!has_logic(buf)){
runcmd(buf,partVar_t,&partVarCount_t,0); // 子进程执行外部命令
} else {
runcmd(buf,partVar_t,&partVarCount_t,1); // 含有逻辑执行
}
exit(); // 这里似乎不会被执行到
} else {
wait(r); // 父进程等待子进程结束
}
}
return 0;
}

我向runcmd函数中传入了有关局部变量储存的参数,父子进程操作的是不同的局部变量,由此实现子进程看不到父进程的局部变量。

实现反引号

这里涉及到了之前提到的内建指令和外部指令以及混合指令的问题。和管道与重定向类似,含有反引号的混合指令的目的只是为了获得反引号内指令的输出,而不是真正去实现反引号内指令对应的效果,因此,反引号内的指令不应该对主进程的状态产生任何影响,因此,反引号内的指令无论是何种指令,都应该在子进程中执行。
有了这样的认识,我们的思路就很清晰了。
首先在_gettoken中增加对反引号的识别和处理:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
if  (*s  ==  '`')  {

*s = 0; // 将开头的反引号设为字符串结束

char *start = s + 1; // 命令开始位置

while (*start && *start != '`') {

start++;

}

if (*start == 0) {

debugf("unmatched backtick\n");

return 0;

}

*start = 0; // 将结束的反引号设为字符串结束

char *result = process_backtick(s + 1); // 执行命令

if (result) {

char *rest_of_line = start + 1;

size_t result_len = strlen(result);

size_t rest_len = strlen(rest_of_line);

char *dest = s + result_len;

char *src = rest_of_line;

if ((void *)dest > (void *)src) {

// 自行实现memmove的逻辑,处理内存重叠

// 从后向前复制

for (size_t i = rest_len + 1; i > 0; --i) {

dest[i - 1] = src[i - 1];

}

} else {

// 无重叠或安全重叠,可以从前向后复制

memcpy(dest, src, rest_len + 1);

}

memcpy(s, result, result_len);

*p1 = s;

*p2 = s + result_len;

} else {

s[0] = '\0';

*p1 = s;

*p2 = s;

}

return 'w';

}

这样便可以将反引号内的指令执行结果代替反引号及反引号包裹的部分,转化为不含反引号的指令执行。
对于反引号内指令的执行,我们只需要创建子进程和管道,让管道捕获子进程的输出即可。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
char*  process_backtick(char  *s)  {

int p[2];

if (pipe(p) < 0) {

debugf("pipe: %d\n", p[0]);

return NULL;

}

int pid = fork();

if (pid < 0) {

debugf("fork: %d\n", pid);

return NULL;

}

if (pid == 0) { // 子进程执行命令

dup(p[1], 1); // 将标准输出重定向到管道写端

close(p[0]); // 关闭读端

close(p[1]);

env_var_t partVar_t[100];

int partVarCount_t=0;

//printf("%s",s);

runcmd(s,partVar_t,&partVarCount_t,0); // 执行命令

exit();

}

// 父进程读取输出

close(p[1]); // 关闭写端

int pos = 0;

char c;

while (read(p[0], &c, 1) == 1) { // 一个字符一个字符地读取

if (pos >= MAX_BACKTICK_OUTPUT - 1) break; // 防止缓冲区溢出

backtick_buffer[pos++] = c;

}

backtick_buffer[pos] = '\0';

// 去除末尾的空白字符

while (pos > 0 && strchr(WHITESPACE, backtick_buffer[pos - 1])) {

pos--;

}

backtick_buffer[pos] = '\0';

close(p[0]);

wait(pid); // 等待子进程结束

return backtick_buffer;

}

实现一行多指令

与之前的管道、重定向、反引号等混合指令不同,一行多指令的目的就是发挥每一条指令的作用。因此组成一行多指令的各个子指令都应该按照主进程的流程执行。
处理方法是首先提取出各个子指令,然后在main函数中让其依次执行相应流程。
但是这里我为了偷懒并没有这样做,因为我观察到测试数据中一行多指令的部分,其子指令都不会对shell状态做出修改,因此我就让除了第一个以外的子指令都去子进程中执行了。因为这样非常好写,可以类似管道的写法使用递归的方式去写。只需要在parsecmd中添加一种情况即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// user/sh.c parsecmd()
case ';':

r = fork();

*rightpipe=r;

if (r == 0) {

return argc;

} else {

wait(r);

close(0);close(1);

dup(opencons(), 1);dup(1,0);

return parsecmd(argv, rightpipe);

}

break;

我这样自然不是好的处理方法,有点欺骗自己,告诉自己没有想到进程这一层次,毕竟这个做这个的时候已经快要放假了,心里也想着能简则简,于是就掩耳盗铃。

runcmd和main有关进程的结构调整

在开始指令条件执行之前,我觉得有必要说一下到目前为止的runcmd和main有关进程的结构调整。所有调整都是为了最基础的原则服务,只考虑非混合指令的情况下,如果是内建指令,就在主进程中执行,如果是外部指令,就要fork一个子进程,然后子进程调用spawn创建一个孙子进程让其执行外部命令。
所以我的main中会先判断是否是非混合指令(!has_pipe),并且根据其第一个单词判断其是否是内建指令,如果是,则直接调用runcmd执行,如果不满足以上条件,那么就使用子进程执行。
runcmd中检查到内建指令就会调用相应的函数,调用结束后直接返回。

实现条件指令执行

和一行多指令类似,按说我们也应该分清在主进程执行还是在子进程执行,同样,在这里我也投机倒把了。在一行多指令的基础上,我们需要获得每一条指令执行后的返回值。
我们考虑使得exit能够返回子进程执行的值,但是如果将exit函数直接改成能返回值,那么所有调用exit的地方我们都要做出修改。于是我在user/lib/libos.c中做出如下修改:

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
29
// user/lib/libos.c
void exit(void) {

exit_r(0);

}

void exit_r(int ret) {

// After fs is ready (lab5), all our open files should be closed before dying.

#if !defined(LAB) || LAB >= 5

close_all();

#endif

syscall_ipc_try_send(env->env_parent_id, ret, 0, 0);

syscall_env_destroy(0);

user_panic("unreachable code");

}
// user/include/lib.h

// libos
void exit(void) __attribute__((noreturn));
void exit_r(int ret) __attribute__((noreturn));

这样我们通过调用exit_r即可将子进程的返回值通过进程间通信IPC的方法返回给父进程。
这个时候我们发现,如果在runcmd中将所有return全部换成exit_r,在执行单独的内建指令时,会导致主进程错误退出,这是我们不想要的结果,因此在runcmd中继续引入一个控制参数,控制是执行exit_r还是return。同样,我们在main中也需要判断是否包含逻辑运算,从而决定传入的控制参数。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// user/sh.c runcmd()
if (strcmp(argv[0], "cd") == 0) {

if(argc > 2) {

printf("Too many args for cd command\n");

if(r){

exit_r(-1);

}

} else {

int tp = cd_command(argv[1]);

if(r){

exit_r(tp);

}

}

return 0;

}
//其他内建指令
...
...
// 处理外部命令

int child = spawn(argv[0], argv);

close_all();

if(r) {

if (child >= 0) {

u_int whom;

int status = ipc_recv(&whom, 0, 0);

wait(child);

if (rightpipe) {

u_int whom_pipe;

int pipe_status = ipc_recv(&whom_pipe, 0, 0);

wait(rightpipe);

exit_r(pipe_status);

}

exit_r(status);

} else {

debugf("spawn %s: %d\n", argv[0], child);

exit_r(child);

}

} else {

if (child >= 0) {

wait(child);

} else {

debugf("spawn %s: %d\n", argv[0], child);

}

if (rightpipe) {

wait(rightpipe);

}

exit();

}
// user/sh.c main()

...
// 如果不是内建命令,则创建子进程执行外部命令
if ((r = fork()) < 0) {

user_panic("fork: %d", r);

}

if (r == 0) {

env_var_t partVar_t[100];

int partVarCount_t=0;

if(!has_logic(buf)){

runcmd(buf,partVar_t,&partVarCount_t,0); // 子进程执行外部命令

} else {

runcmd(buf,partVar_t,&partVarCount_t,1); // 含有逻辑执行

}

exit(); // 这里似乎不会被执行到

} else {

wait(r); // 父进程等待子进程结束

}

}

return 0;

同时我们在_gettoken中增加对&&和||的识别,将其识别为一个token,然后在parsecmd中增加对他们的处理即可。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// user/sh.c parsecmd()

case AND_TOKEN:

r = fork();

if (r == 0) {

return argc;

} else {

u_int whom;

int status = ipc_recv(&whom, 0, 0);

wait(r);

if (status == 0) {

close(0); close(1);

dup(opencons(), 1); dup(1, 0);

return parsecmd(argv, rightpipe);

} else {

// skip until || or end

while((c = gettoken(0, &t)) != 0 && c != OR_TOKEN);

if (c == OR_TOKEN) {

return parsecmd(argv, rightpipe);

}

return 0;

}

}

break;

case OR_TOKEN:

r = fork();

if (r == 0) {

return argc;

} else {

u_int whom;

int status = ipc_recv(&whom, 0, 0);

wait(r);

if (status != 0) {

close(0); close(1);

dup(opencons(), 1); dup(1, 0);

return parsecmd(argv, rightpipe);

} else {

// skip until && or end

while((c = gettoken(0, &t)) != 0 && c != AND_TOKEN);

if (c == AND_TOKEN) {

return parsecmd(argv, rightpipe);

}

return 0;

}

}

break;

实现追加重定向

这部分和进程没啥关系,纯属是前面忘了写。
思路也很简单,观察普通重定向可知,其通过O_TRUNC模式将打开的文件内容截断为0,我们想要追加,就不应该截断,而应打开时将写入位置定位在文件末尾:

1
2
3
4
5
// fs/serv.c serve_open()
if (o->o_mode & O_APPEND) {
struct Fd *fff = (struct Fd *) ff;
fff->fd_offset = f->f_size; // redirect the file pointer
}

以上便是Shell挑战性任务的主要内容。总结一下,为了完成挑战性任务,我们应该对MOS的运行逻辑在学习过程中掌握清晰,知道各层函数是怎么调用的,是在什么状态下调用的,这将极大帮助我们完成该挑战性任务。基于挑战性任务的内容来讲,内建指令和外部指令不同的执行逻辑是一个重难点部分,需要仔细思考。从通过评测的角度来讲,如果像Lab6中所有指令都交由fork的子进程执行,那么在会修改shell全局状态的指令可能会出问题(说可能是因为如果把变量指针传入函数进行修改似乎也不是不行,但是太过丑陋,不像是一个正常的操作系统该做出来的事)。因此要判断一下是内建指令还是外部指令。到这里已经可以通过评测了。而对于混合指令,由于我犯懒,进行了投机倒把,我将所有混合指令的每个局部指令都给放到了子进程中执行,也就是在has_pipe中判断有没有><&|;等。这样做对于一行多指令和指令条件执行是不对的,对于管道、重定向、追加重定向等是没有问题的。由于25春季学期的shell挑战性任务没有对这部分内容进行评测,我就没有对这部分再去精心雕琢,但是不保证后续的课程中会不会出现此类问题相应的测试点!

本篇内容至此结束,有些地方可能写的有些简略,因为我的身体最近一直不太舒服,脖子后面长了一个火疖子让我非常疼,脖子损失了两个自由度,坐在电脑前面写一会儿就会肩膀脖子痛,所以部分内容可能不清晰。不过我认为重要的地方应该都着重说明了。


【OS】Shell挑战性任务实验报告
http://example.com/2025/06/25/【OS】Shell挑战性任务实验报告/
作者
mRNA
发布于
2025年6月25日
许可协议