操作系统专题实践——Linux Shell

1
2
3
4
5
6
7
8
__          __  _                            _                     _     _          _____ _          _ _ 
\ \ / / | | | | | | ( ) / ____| | | | |
\ \ /\ / /__| | ___ ___ _ __ ___ ___ | |_ ___ __ _ ___| |__ |/ ___ | (___ | |__ ___| | |
\ \/ \/ / _ \ |/ __/ _ \| '_ ` _ \ / _ \ | __/ _ \ / _` |/ __| '_ \ / __| \___ \| '_ \ / _ \ | |
\ /\ / __/ | (_| (_) | | | | | | __/ | || (_) | | (_| | (__| | | | \__ \ ____) | | | | __/ | |
\/ \/ \___|_|\___\___/|_| |_| |_|\___| \__\___/ \__, |\___|_| |_| |___/ |_____/|_| |_|\___|_|_|
__/ |
|___/

实验要求

实现具有管道、重定向功能的shell,能够执行一些简 单的基本命令,如进程执行、列目录等

具体要求

  1. 设计一个C语言程序,完成最基本的shell角色:给出 命令行提示符、能够逐次接受命令; 对于命令分成三种

    • 内部命令(例如help命令、exit命令等)
    • 外部命令(常见的ls、cp等,以及其他磁盘上的可执行程 序HelloWrold等)
    • 无效命令(不是上述二种命令)
  2. 具有支持管道的功能,即在shell中输入诸如 “dir | more”能够执行dir命令并将其输出通过管道将 其输入传送给more。

  3. 具有支持重定向的功能,即在shell中输入诸如“dir > direct.txt”能够执行dir命令并将结果输出到direct.txt

  4. 将上述步骤直接合并完成

实验指导

老师给的一些指导

shell的基本工作流程

实验过程

功能设计

展示提示符

这个函数用于显示类似于 [root@localhost tmp]λ 的东西,需要获取用户名,主机名,当前工作目录等内容。

  • 获取用户名

    1
    2
    passwd *pwd = getpwuid(getuid());
    string username(pwd->pw_name);
  • 获取当前目录

    1
    2
    getcwd(char_buf, CHAR_BUF_SIZE);
    string cwd(char_buf);
    • prompt 中目录只显示最近一级,此处用 / 来 split 后取最后一个即可
    • 家目录需要折叠为 ~,这里顺便把家目录地址存到全局变量 home_dir,后续要用到
  • 获取主机名

    1
    2
    gethostname(char_buf, CHAR_BUF_SIZE);
    string hostname(char_buf);
    • 有时 hostname 会是形如 localhost.locald.xxx 的形式,也 split 处理一下
  • 输出之即可

    这里为了区别于普通的terminal,将输出设为了红色。

    1
    cout << RED << "[" << username << "@" << hostname << " " << cwd << "]λ ";

解析命令

  • 为存储解析结果,定义如下四个类:

    • cmd:各种 cmd 的基类
    • exec_cmd:形如 argv[0] argv[1] ... 的普通命令
    • pipe_cmd:管道命令,形如 left: cmd* | right: cmd*
    • redirect_cmd:重定向命令,形如 cmd_: cmd* > (or <) file
  • (最基础的)解析 exec_cmd

    parse_exec_cmd 函数。注意这里使用 string_split_protect 函数来 split 出 argv,这样可以保持被引号引起的带空格的 argument 不被拆分。

  • 解析一条命令

    parse 函数。采用分治法递归地解析命令。

    • 从左到右扫描字符串
    • 如果是普通字符,则读入缓存
    • 如果是重定向符号,将当前缓存解析为 exec_cmd,作为左手边 cmd;继续不断读入直到再次遇到符号或字符串结束,作为右手边 file,构建 redirect_cmd
    • 如果是管道符号,递归地调用 parse 解析右侧剩余,解析结果作为本层递归的右手边,构建 pipe_cmd
  • 解析内建命令

    主要支持 cd 、history 和 quit 命令。

    • 调用 exit(0) 即可实现 quit
    • history 命令根据记录打印即可
    • 对于 cd,考虑如下情况
      • 无参 cd 等价于 cd ~
      • 对于形如 cd ~/some_path 的命令,使用 home_dir 替换 ~
      • 其他情况调用 chdir 即可

执行命令

主要见 run_cmd 函数。该函数接收一个 cmd*,递归地完成其链上所有 cmd 的执行。

  • 对于 exec_cmd

    • 检查别名,替换别名,例如 ll → ls -l

    • 使用

1
execvp
函数执行命令 - 看 [这篇博文](https://blog.csdn.net/yychuyu/article/details/80173039) 了解 exec 族函数,可见 `execvp` 在当前场景最为合适 - 第二个参数是一个末元素为 NULL 的 char**(char*[]),内容为 argv
  • 对于 pipe_cmd

    • 为 pipe_cmd 的 left 和 right 分别 fork 子进程执行,并使用管道让这两个兄弟进程通信

    • 这张图很好地说明了父子进程使用管道通信的方法

      img

    • 依据上图,不难类比出兄弟进程进行 IPC 的方法如下

      • 父进程 pipe
      • 父进程 fork 两次
      • child1 关读端,重定向写端,执行命令,关写端
      • child2 关写端,重定向读端,执行命令,关读端
      • 父进程关闭读、写端,并 wait
  • 对于 redirect_cmd

    • 打开 file,获得 fd
    • 重定向 stdin 或 stdout 到 fd
    • 执行命令
    • 关闭 fd

主函数

在一个死循环中读入当前命令,如果不是 builtin_command,则 fork 子进程进行解析和执行(避免阻塞 ExpShell 自身),执行完成后子进程 exit。

其他细节

  • pipe、open、dup2 等方法返回值小于 0 均表示出现错误,需要触发 panic
  • 对于 wait 方法的状态字,当 WIFEXITED(status) 为 0 时表示子进程异常退出,使用 WEXITSTATUS(status) 可以进一步获得子进程的 exit code

内建指令

cd、quit、history

指令内容

cd

cd主要是使用chdir()函数更换当前目录。同时一个比较重要的点是对家目录的处理。如输入cd即更改目录到家目录,以及用”~”替代家目录。

quit

调用exit(0)退出即可

history

用一个全局的vector记录历史命令,输出即可。

代码

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
int process_builtin_command(string line) {
// 1 - cd
if (line == "cd") {
chdir(home_dir.c_str()); // cd 视为 cd~
return 1;
} else if (line.substr(0, 2) == "cd") {
// 用家目录替换~
string arg1 = string_split(line, WHITE_SPACE)[1];
if (arg1.find("~") == 0)
line = "cd " + home_dir + arg1.substr(1);
// change directory
int chdir_ret = chdir(trim(line.substr(2)).c_str());
if (chdir_ret < 0) {
panic("chdir failed");
return -1;
} else
return 1;
}
// 2 - quit
if (line == "quit") {
cout << "Bye from ExpShell." << BLACK << endl;
exit(0);
}
// 3 - history
if (line == "history") {
for (int i = cmd_history.size() - 1; i >= 0; i--)
cout << "\t" << i << "\t" << cmd_history.at(i) << endl;
return 1;
}
return 0;
}

指令类

基类cmd

设计一个基类,其余类都派生自cmd类。

含有一个数据成员type即指令类型。指令类型共有以下几个:

1
2
3
4
5
#define CMD_TYPE_NULL 0      
#define CMD_TYPE_EXEC 1
#define CMD_TYPE_PIPE 2
#define CMD_TYPE_REDIR_IN 4
#define CMD_TYPE_REDIR_OUT 8

派生类

共有三个派生类 exec_cmd、 pipe_cmd、 redirect_cmd,分别为普通的指令,具有管道的指令和具有重定向的指令。

exec_cmd含有一个表示参数列表的数据成员,pipe_cmd 含有两个cmd类指针作为其两侧的指令,redirect_cmd含有一个文件名和一个fd。

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
// 基类
class cmd {
public:
int type;
cmd() { this->type = CMD_TYPE_NULL; }
};

// 普通指令
// argv[0] ...argv[1~n]
class exec_cmd : public cmd {
public:
vector<string> argv;
exec_cmd(vector<string> &argv) {
this->type = CMD_TYPE_EXEC;
this->argv = vector<string>(argv);
}
};

// 管道指令
// left | right
class pipe_cmd : public cmd {
public:
cmd *left;
cmd *right;
pipe_cmd() { this->type = CMD_TYPE_PIPE; }
pipe_cmd(cmd *left, cmd *right) {
this->type = CMD_TYPE_PIPE;
this->left = left;
this->right = right;
}
};

// 重定向指令
// ls > a.txt; some_program < b.txt
class redirect_cmd : public cmd {
public:
cmd *cmd_;
string file;
int fd;
redirect_cmd() {}
redirect_cmd(int type, cmd *cmd_, string file, int fd) {
this->type = type;
this->cmd_ = cmd_;
this->file = file;
this->fd = fd;
}
};

指令分析

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
cmd *parse(string line) {
line = trim(line);
string cur_read = "";
cmd *cur_cmd = new cmd();
int i = 0;
while (i < line.length()) {
//处理重定向指令
if (line[i] == '<' || line[i] == '>') {
cmd *lhs = parse_exec_cmd(cur_read); // [lhs] < (or >) [rhs] 先提取左边的指令
int j = i + 1;
while (j < line.length() && !is_symbol(line[j]))
j++;
string file = trim(line.substr(i + 1, j - i)); //得到重定向的文件名
cur_cmd = new redirect_cmd(line[i] == '<' ? CMD_TYPE_REDIR_IN
: CMD_TYPE_REDIR_OUT,
lhs, file, -1);
i = j;
} else if (line[i] == '|') {
cmd *rhs = parse(line.substr(i + 1)); //递归解析右边指令
if (cur_cmd->type == CMD_TYPE_NULL)
cur_cmd = parse_exec_cmd(cur_read);
cur_cmd = new pipe_cmd(cur_cmd, rhs);
return cur_cmd;
} else
cur_read += line[i++];
}
if (cur_cmd->type == CMD_TYPE_NULL)
return parse_exec_cmd(cur_read);
else
return cur_cmd;
}

运行指令

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
void run_cmd(cmd *cmd_) {
switch (cmd_->type) {
case CMD_TYPE_EXEC: {
exec_cmd *ecmd = static_cast<exec_cmd *>(cmd_);
// process alias
if (alias_map.count(ecmd->argv[0]) != 0) {
vector<string> arg0_replace =
string_split(alias_map.at(ecmd->argv[0]), WHITE_SPACE); //仍需拆分成多个指令
//用新的拆分后的指令替换原指令
ecmd->argv.erase(ecmd->argv.begin());
for (vector<string>::reverse_iterator it = arg0_replace.rbegin();
it < arg0_replace.rend(); it++) {
ecmd->argv.insert(ecmd->argv.begin(), (*it));
}
}
// prepare vector<string> for execvp
// execp函数参数不能是string,去除指令序列中的空格,在序列最后加入NULL(execp函数参数要求)
vector<char *> argv_c_str;
for (int i = 0; i < ecmd->argv.size(); i++) {
string arg_trim = trim(ecmd->argv[i]);
if (arg_trim.length() > 0) { // skip blank string
char *tmp = new char[MAX_ARGV_LEN];
strcpy(tmp, arg_trim.c_str());
argv_c_str.push_back(tmp);
}
}
argv_c_str.push_back(NULL);
char **argv_c_arr = &argv_c_str[0];
// vscode made wrong marco expansion here
// second argument is ok for char** rather than char *const (*(*)())[]
int execvp_ret = execvp(argv_c_arr[0], argv_c_arr);
if (execvp_ret < 0)
panic("execvp failed");
break;
}
case CMD_TYPE_PIPE: {
pipe_cmd *pcmd = static_cast<pipe_cmd *>(cmd_);
pipe_wrap(pipe_fd);
// fork twice to run lhs and rhs of pipe
if (fork_wrap() == 0) {
// i'm a child, let's satisfy lhs
close(pipe_fd[0]);
dup2_wrap(pipe_fd[1], fileno(stdout)); // lhs_stdout -> pipe_write
// close the original ones
run_cmd(pcmd->left);
close(pipe_fd[1]);
}
if (fork_wrap() == 0) {
// i'm also a child, let's satisfy rhs
close(pipe_fd[1]);
dup2_wrap(pipe_fd[0], fileno(stdin)); // pipe_read -> rhs_stdin
run_cmd(pcmd->right);
close(pipe_fd[0]);
}
// really good. now we have lhs_stdout -> pipe -> rhs_stdin
// if fork > 0, then i'm the father
// let's wait for my children
close(pipe_fd[0]);
close(pipe_fd[1]);
int wait_status_1, wait_status_2;
wait(&wait_status_1);
wait(&wait_status_2);
check_wait_status(wait_status_1);
check_wait_status(wait_status_2);
break;
}
case CMD_TYPE_REDIR_IN:
case CMD_TYPE_REDIR_OUT: {
redirect_cmd *rcmd = static_cast<redirect_cmd *>(cmd_);
if (fork_wrap() == 0) {
// i'm a child, let's satisfy the file being redirected to (or from)
rcmd->fd = open_wrap(rcmd->file.c_str(), rcmd->type == CMD_TYPE_REDIR_IN
? REDIR_IN_OFLAG
: REDIR_OUT_OFLAG);
dup2_wrap(rcmd->fd, rcmd->type == CMD_TYPE_REDIR_IN ? fileno(stdin)
: fileno(stdout));
run_cmd(rcmd->cmd_);
close(rcmd->fd);
}
// if fork > 0, then i'm the father
// let's wait for my children
int wait_status;
wait(&wait_status);
check_wait_status(wait_status);
break;
}
default:
panic("unknown or null cmd type", true, 1);
}
}