shell-Challenge-实验报告
shell实现
相对路径实现
在进程控制块中添加工作目录域
char env_work_dir[1024];
,并通过以下两个系统调用进行读取与修改1
2
3int syscall_read_work_dir(void * dstva);
int syscall_write_work_dir(void* srcva);
// 工作目录初值为"/",并且fork出子进程继承父进程工作目录实现相对路径与绝对路径的转换函数
void walk_realtive_path(char* work_dir, char* cd_arg, char* ans)
; 无论传入的是相对路径还是绝对路径最终都转换为绝对路径输出传入当前工作目录(系统调用获得), 传入的路径参数,最终转换为的绝对路径写入ans中;
(一个难写的字符串替换函数)
实现open的相对路径支持
1
2
3
4
5
6
7
8
9
10int open(const char *path_0, int mode) {
char path[1024]; // absolute path
char cur_work_dir[1024];
char path_00[1024];
strcpy(path_00,path_0);
syscall_read_work_dir(cur_work_dir);
walk_realtive_path(cur_work_dir, path_00, path);
//...
}int cd(int argc, char **argv);
函数中实现内建指令cd的功能,其在main函数中被调用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19...
int r = open(argv[1], O_RDONLY); // 尝试打开对应文件或者目录
if(r < 0){ // 如果文件或者目录不存在,直接返回
printf("cd: The directory '%s' does not exist\n", argv[1]);
return 1;
}
char work_dir[1024] ;
syscall_read_work_dir(work_dir);
walk_realtive_path(work_dir, argv[1], ans); // 相对路径转换为绝对路径
struct Stat st;
stat(ans, &st);
if(st.st_isdir){ // 判断是否为目录
syscall_write_work_dir(ans);
close(r);
// printf("the new dir is %s\n", ans);
return 0;
}
...int pwd(int argc, char **argv);
中实现pwd内建指令的功能;但是最后为了顺利完成第四题,我最后将其改为了一般指令
更多指令实现
mkdir
指令实现O_MKDIR
文件打开模式的实现1
2
3
4
5
6//在fs/serve.c/serve_open函数中
if ((rq->req_omode & O_MKDIR) && (r = file_create(rq->req_path,FTYPE_DIR ,&f)) < 0 &&
r != -E_FILE_EXISTS) {
ipc_send(envid, r, 0, 0);
return;
}如果没有
-p
参数1
2
3
4
5
6
7
8
9
10
11
12
13if((fd = open(path, O_RDONLY)) >= 0){ //若存在直接返回
close(fd);
user_panic("mkdir: cannot create directory '%s': File exists\n", path);
return -1;
}
fd = open(path, O_MKDIR); //以mkdir形式打开文件
if (fd == -10) {
user_panic("mkdir: cannot create directory '%s': No such file or directory\n", path);
} else if (fd < 0) {
user_panic("other error when mkdir %s, error code is %d\n", path, fd);
} else {
close(fd);
}如果存在
-p
参数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
26if((fd = open(path, O_RDONLY)) >= 0){
close(fd);
return;
}
int i = 0;
char str[1024];
for (; path[i] != '\0'; ++i) { //尝试打开或者创建逐级目录
if (path[i] == '/') {
str[i] = '\0';
fd = open(str, O_MKDIR);
if (fd >= 0) {
close(fd);
} else {
user_panic("other error when mkdir %s, error code is %d\n", path, fd);
}
}
str[i] = path[i];
}
str[i] = '\0';
fd = open(str, O_MKDIR);
if (fd >= 0) {
close(fd);
} else {
user_panic("other error when mkdir %s, error code is %d\n", path, fd);
}
touch
指令实现;1
fd = open(path, O_CREAT);
rm
指令的实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19void rm(char *path){
int fd;
// case1 : no exist
if((fd = open(path, O_RDONLY)) < 0){
if(f_mode == 0){
user_panic("rm: cannot remove '%s': No such file or directory\n", path);
}
return ;
}
close(fd);
struct Stat st;
stat(path, &st);
if(st.st_isdir && r_mode == 0){
user_panic("rm: cannot remove '%s': Is a directory\n", path);
}
remove(path);
}
环境变量管理
在
env.h
中定义环境变量结构体1
2
3
4
5
6
7
8
9
10
11struct Var{
char name[VAR_NAME_LEN]; // 环境变量名
char value[VAR_VALUE_LEN]; // 环境变量值
unsigned int is_global; // 1=全局变量(可以被子shell继承), 0=局部变量
unsigned int is_readOnly; // 1=只读变量,不可以被重新赋值或者修改
u_int shell_id; // 拥有该环境变量的shell的id
int valid; // 为1代表有效
};
我采用数组存储所有环境变量
1
2
3 struct Var var_table[VAR_NUM]; // 环境变量存储表
static int var_num = 0; // 当前所有进程存在的var个数
static int shell_num = 0; // 当前出现过的shell的进程个数 --- 主要用于为每个进程创建新的shell_id使用
实现以下5个系统调用
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/**
* 在shell进程启动时候调用,继承父shell的全局变量
* 返回当前shell的shell_id
*/
int syscall_init_curenv_var(){
return init_curenv_var();
}
/**
* 修改(若存在且非只读)或者创建单个环境变量
*/
int syscall_declare_write(int shell_id, void * name, void *value, int is_global, int is_readOnly){
return declare_write(shell_id, name, value, is_global, is_readOnly);
}
/**
* 删除环境变量(检查只读权限);-1:只读不可删 或者 0:正常删除
*/
int syscall_declare_unset(int shell_id, void*name){
return declare_unset(shell_id, name);
}
/**
* 查询环境变量name的值,将其写入value
*/
int syscall_declare_get(int shell_id, void*name, void *value){
return declare_get(shell_id, name, value);
}
/**
* 获取当前shell的所有变量到var_array中
* 返回当前shell的环境变量个数
*/
int syscall_delcare_getall(int shell_id, char var_array[1024][50]){
return delcare_getall(shell_id, var_array);
}sh.c
中的调用在
main
函数一开始便调用int syscall_init_curenv_var();
并将返回的当前进程的shell_id
保存下来void expand_variables(char *arg);
该函数主要对于parse_cmd
解析得到的每个参数进行检查,如果引用了环境变量则进行替换int declare_cmd(int argc, char **argv)
在该函数中实现declare
指令的功能:环境变量添加或者修改或者打印输出所有环境变量;调用方式与cd相同int unset_cmd(int argc, char **argv);
unset
内置指令的实现函数,调用方式同上。
输入指令优化
输入指令优化主要是包括上下左右键、BackSpace键,ctrl快捷键等
主要是在readline中实现
1 | ... |
历史指令以及上下回显
实现文件的追加模式打开
1
2
3
4if(o->o_mode & O_APPEND){
struct Fd *fff = (struct Fd*) ff;
fff->fd_offset = f->f_size; // 修改追加模式打开文件的指针位置到文件末尾
}指令保存:在readline中读取到回车键时候则当前指令输入完毕,需要将当前指令保存到
.mos_history
中以追加模式打开,每次都将新指令追加写到最后,并在其中补充’\n’;利用换行符分隔各条指令
history
指令实现history指令中默认只保存并输出20条历史指令,但是我们的实现方式中是保存了所有指令;
在
sh.c
中维护全局静态变量static int hiscount, curline;hiscount
存储已经出现保存了多少条历史指令,curline
代表当前缓冲区所处理指令是第几条指令;通过
hiscount-20
可以知道从第几条指令开始是最后20条指令,输出即可;读取到一个回车代表读取了一条指令上下键指令回显,以上箭头为例
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} else if (temp == 'A') { // up arrow
printf("\033[B");
// 如果存在上一条指令
if(curline != 0 && curline > hiscount - 20){
buf[len] = '\0';
//如果缓冲显示区显示的是正在输入的行,则将其保存到curIn里面
if(curline == hiscount) {
strcpy(curIn, buf);
}
// 清空当前行
if (i != 0) { printf("\033[%dD", i); }
for(int j = 0; j < len; j++) { printf(" ");}
if (len != 0) { printf("\033[%dD", len); }
// 读取curline上一行指令到buf里面并打印输出
if((r = readHistoryLine(--curline, buf)) < 0) {
printf("can't read targetLine\n");
break;
}
printf("%s", buf);
i = strlen(buf);
len = strlen(buf);
}
}
追加重定向、条件执行、反引号
追加重定向
在
parsecmd
中遇到重定向符号时,以追加模式打开对应文件;并将其映射到进程写端
1 | case append_redir : |
反引号
其主要通过以下两个函数实现主体功能:
int replaceBackquoteCommands(char* cmd);
处理cmd字符串中的反引号,其通过递归查找反引号组,利用下面函数运行反应号内指令,并用输出结果替换原来的反引号int executeCommandAndCaptureOutput(char *cmd, char *output, int maxLen);
执行cmd对应的指令,并将指令运行结果保存到output
中
在
sh.c
中的main函数对读取的字符串调用replaceBackquoteCommands()
函数
条件执行
在
parsecmd
中解析到||
或者&&
时候,fork出一个子进程先执行第一部分指令fork出来的子进程通过spawn装载并运行,并在进程结束前将指令运行结果通过ipc发送到fork出来子进程
fork子进程根据当前指令是否为条件指令选择是否发送ipc消息到父进程;如果是条件执行,则将运行结果发送到父进程
父进程根据子进程的ipc消息判断是否需要执行后续指令
此处需要判断后续是否会出现连续的
&&
或者||
在user_panic与libmain中实现指令运行结果向shell发送
实验难点
- 在实现指令优化时候,对于屏幕输出管理较为困难;需要了解ESC转义序列等,且需要确保屏幕输出缓冲区的完全清空与输出
- 在反引号处理与条件执行时候,涉及到新开子进程;对于新开的子进程,实现进程间消息通信与输入输出重定向较为复杂;需要对dup函数理解到位,避免控制台输入输出不正确
实验体会
这次实验不仅加深我对shell的实现原理的理解,更重要的是回顾复习从文件系统到进程管理,以及终端控制的相关知识,每个模块的实现都加深了我对操作系统各组件协同工作的理解,提升了对复杂系统问题的分析能力和处理能力。