为什么实现一个自己 shell?

你是一个 Window/Android 用户,你可以直接用图形化桌面直接双击跑一个程序,没压力吧,轻松吧。
图形化工具为我们屏蔽了底层进程调用的细节。或者你是一个 Linux 用户,你喜欢用 bash 执行 grep
命令来查找文件中的内容,喜欢用 git 来提交代码,喜欢用 ls 来查看目录内容,有时你还会去使用
将多个命令一起使用:cat cat.txt | grep "smelly cat" | wc -c。作为一个 Linux Hacker,你理应
当了解这些图形化工具背后的原理,利用它实现一些更好玩的事情。

TASK

打造一个绝无伦比的 xxx-super-shell (xxx 是你的名字),它能实现下面这些功能:

  • 实现 管道 (也就是 |)
  • 实现 输入输出重定向(也就是 < > >>)
  • 实现 后台运行(也就是 &
  • 实现 cd,要求支持能切换到绝对路径,相对路径和支持 cd -
  • 屏蔽一些信号(如 ctrl + c 不能终止)
  • 界面美观
  • 开发过程记录、总结、发布在个人博客中

要求:

  • 不得出现内存泄漏,内存越界等错误
  • 学会如何使用 gdb 进行调试,使用 valgrind 等工具进行检测

知识要点

  1. 懂得如何使用 shell
  2. 理解 shell 原理
  3. Linux系统编程:进程控制
  4. gdb
  5. valgrind

直接讲解代码

分割命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void parse_args(char* line, char** args) {
char* token = strtok(line, " \t\r\n\a");
args[0] = token;
int i = 1;
if(strcmp(args[0],"ls")==0)
{
args[i++]=(char*)"--color=auto";
}
if(strcmp(args[0],"ll")==0)
{
args[0]=(char*)"ls";
args[i++]=(char*)"-l";
args[i++]=(char*)"--color=auto";
}
while (token != NULL) {
token = strtok(NULL, " \t\r\n\a");
args[i++] = token;
}
COUNT=i-1;
}

这里使用了string.h库中的strtok函数(详情见man手册)来分割字符串,方便之后使用execvp函数来执行命令,这里检测如果命令是ls的话,那么就加上–color=auto,这是为了让ls的结果具有颜色显示的效果,使界面更加美观。并且将ll识别为ls -l

设置后台运行与管道的标志
1
2
3
4
5
6
7
8
9
10
11
12
13
void parse(char *line){
HT=false;
PP=false;
for(int i=0;i<strlen(line);i++){
if(line[i]=='&'){
HT=true;
line[i]=' ';
}
if(line[i]=='|'){
PP=true;
}
}
}

这里的HT和PP都是bool类型,每次进入函数都先熄灭它们两个,如果遍历命令中出现了&或|,那就点亮它们

重定向状态获取
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
char *CheckRedir(char *start)
{
char *end=start+strlen(start)-1;

while(end>=start)
{
if(*end=='>')
{
if(*(end-1)=='>')
{
redir_status=APPEND_REDIR;
*(end-1)='\0';
end++;
while(*end==' ')
++end;
break;
}
redir_status=OUTPUT_REDIR;
*end='\0';
end++;
while(*end==' ')
++end;
break;
}
else if(*end=='<')
{
redir_status=INPUT_REDIR;
*end='\0';
end++;
while(*end==' ')
++end;
break;
}
else
{
end--;
}
}
if(end>=start)
{
return end;
}
else
{
return NULL;
}
}

这里先看一下我的宏

1
2
3
4
5
6
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
#define NONE_REDIR 0

int redir_status=NONE_REDIR;

四种状态分别表示 重定向输入,输出,追加和无重定向工作,初始化状态为无重定向工作

这里观察命令中是否存在>>,>,<等标记从而确定重定向的状态,并返回需要重定向的文件名

找管道和执行管道(初始版/笑哭)
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
int has_pipe(char** args, int* pipe_position) {
for (int i = 0; args[i] != NULL; i++) {
if (strcmp(args[i], "|") == 0) {
*pipe_position = i;
return 1;
}
}
return 0;
}

void run_command_with_pipe(char** args1, char** args2) {
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}

pid_t pid1 = fork();
if (pid1 == -1) {
perror("fork");
exit(EXIT_FAILURE);
}

if (pid1 == 0) {
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
execvp(args1[0], args1);
perror("execvp");
exit(EXIT_FAILURE);
}

pid_t pid2 = fork();
if (pid2 == -1) {
perror("fork");
exit(EXIT_FAILURE);
}

if (pid2 == 0) {
close(pipefd[1]);
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
execvp(args2[0], args2);
perror("execvp");
exit(EXIT_FAILURE);
}

close(pipefd[0]);
close(pipefd[1]);
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 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
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
void DoPipe(char **argv, int count)
{
pid_t pid;
int ret[10];
int number=0;
for(int i=0;i<count;i++)
{
if(!strcmp(argv[i],"|"))
{
ret[number++]=i;
}
}
int cmd_count=number+1;
char* cmd[cmd_count][10];
for(int i=0;i<cmd_count;i++)
{
if(i==0)
{
int n=0;
for(int j=0;j<ret[i];j++)
{
cmd[i][n++]=argv[j];
}
cmd[i][n]=NULL;
}
else if(i==number)
{
int n=0;
for(int j=ret[i-1]+1;j<count;j++)
{
cmd[i][n++]=argv[j];
}
cmd[i][n]=NULL;
}
else
{
int n=0;
for(int j=ret[i-1]+1;j<ret[i];j++)
{
cmd[i][n++]=argv[j];
}
cmd[i][n]=NULL;
}
}
int fd[number][2];
for(int i=0;i<number;i++)
{
pipe(fd[i]);
}
int i=0;
for(i=0;i<cmd_count;i++)
{
pid=fork();
if(pid==0)
break;
}
if(pid==0)
{
if(number)
{
if(i==0)
{
dup2(fd[0][1],1);
close(fd[0][0]);
for(int j=1;j<number;j++)
{
close(fd[j][1]);
close(fd[j][0]);
}
}
else if(i==number)
{
dup2(fd[i-1][0],0);
close(fd[i-1][1]);
for(int j=0;j<number-1;j++)
{
close(fd[j][1]);
close(fd[j][0]);
}
}
else
{
dup2(fd[i-1][0],0);
close(fd[i-1][1]);
dup2(fd[i][1],1);
close(fd[i][0]);
for(int j=0;j<number;j++)
{
if(j!=i&&j!=(i-1))
{
close(fd[j][0]);
close(fd[j][1]);
}
}
}
}
execvp(cmd[i][0],cmd[i]);
perror("execvp");
exit(1);
}
for(i=0;i<number;i++)
{
close(fd[i][0]);
close(fd[i][1]);
}
for(int j=0;j<cmd_count;j++)
wait(NULL);
}
执行命令
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
void run_command(char** args) {
// int pipe_position;
// if (has_pipe(args, &pipe_position)) {
// char* args1[MAX_ARGS];
// char* args2[MAX_ARGS];
// memcpy(args1, args, (pipe_position) * sizeof(char*));
// args1[pipe_position] = NULL;
// memcpy(args2, &args[pipe_position + 1], (MAX_ARGS - pipe_position - 1) * sizeof(char*));
// run_command_with_pipe(args1, args2);
// }
if(PP){
DoPipe(args,COUNT);
}
else {
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
execvp(args[0], args);
perror("execvp");
exit(EXIT_FAILURE);
} else {
wait(NULL);
}
}
}

这里如果PP亮着就说明有管道,就执行dopipe,没有管道就是单一命令,直接fork(),子进程execvp执行命令,父进程wait就行。另外这里注释掉的是我曾经的只能处理一个管道的代码。

主函数
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
int main() {
char line[MAX_LINE];
char* args[MAX_ARGS];
char prior[100]={'\0'};
args[0]="\0";
signal(SIGINT,SIG_IGN); //屏蔽ctrl+c
int x=dup(0),y=dup(1);
while (1) {
printf("[dyx-super-shell]# ");
if (fgets(line, MAX_LINE, stdin) == NULL) {
perror("fgets");
exit(EXIT_FAILURE);
}
// line=readline(line);
if(strcmp(line,"\n")==0||!line)
continue;
line[strlen(line)-1]='\0';
// add_history(line);
parse(line);
char *sep=CheckRedir(line);
parse_args(line, args);
if (args[0] == NULL) {
continue;
}
if (strcmp(args[0], "exit") == 0) {
break;
}
if (strcmp(args[0], "cd") == 0)
{
if (args[1] && strcmp(args[1], "-") == 0)
{
if (prior)
{
char buf[100];
getcwd(buf, 100);
printf("%s\n", prior);
chdir(prior);
strcpy(prior, buf);
}
else
{
printf("bash: cd: OLDPWD 未设定\n");
}
}
else
{
getcwd(prior, 100);
chdir(args[1]);
}
continue;
}
if(HT){
int fd=-1;
fd=open("/dev/null",O_WRONLY);
dup2(fd,0);
dup2(fd,1);
close(fd);
}
if(sep!=NULL)
{
int fd=-1;
switch(redir_status) {
case INPUT_REDIR:
fd=open(sep,O_RDONLY);
dup2(fd,0);
close(fd);
break;
case OUTPUT_REDIR:
fd=open(sep,O_WRONLY|O_TRUNC|O_CREAT,0666);
dup2(fd,1);
close(fd);
break;
case APPEND_REDIR:
fd=open(sep,O_WRONLY|O_APPEND|O_CREAT,0666);
dup2(fd,1);
close(fd);
break;
default:
printf("bug?\n");
break;
}
}
run_command(args);
// free(line);
// line=NULL;
dup2(x,0);
dup2(y,1);
}
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
if (strcmp(args[0], "exit") == 0) {
break;
}
if (strcmp(args[0], "cd") == 0)
{
if (args[1] && strcmp(args[1], "-") == 0)
{
if (prior)
{
char buf[100];
getcwd(buf, 100);
printf("%s\n", prior);
chdir(prior);
strcpy(prior, buf);
}
else
{
printf("bash: cd: OLDPWD 未设定\n");
}
}
else
{
getcwd(prior, 100);
chdir(args[1]);
}
continue;
}

如果命令是exit和cd,直接执行,不需要fork,这里实现cd -就是在cd前现将此时的位置存起来,下次cd - 时直接切回来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if(sep!=NULL)
{
int fd=-1;
switch(redir_status) {
case INPUT_REDIR:
fd=open(sep,O_RDONLY);
dup2(fd,0);
close(fd);
break;
case OUTPUT_REDIR:
fd=open(sep,O_WRONLY|O_TRUNC|O_CREAT,0666);
dup2(fd,1);
close(fd);
break;
case APPEND_REDIR:
fd=open(sep,O_WRONLY|O_APPEND|O_CREAT,0666);
dup2(fd,1);
close(fd);
break;
default:
printf("bug?\n");
break;
}
}

这里将重定向的工作做好

1
2
3
4
5
6
7
if(HT){
int fd=-1;
fd=open("/dev/null",O_WRONLY);
dup2(fd,0);
dup2(fd,1);
close(fd);
}

有后台运行就重入到dev/null

源码地址:https://github.com/mejomejo/mytask/blob/main/myshell/dyx.c

参考资料

  • man 手册.
  • MichaelKerrisk.Linux/UNIX系统编程手册[M].北京:人民邮电出版社.
  • W.RichardStevens.Stephen.UNIX环境高级编程[M].第3版.戚正伟,译.北京:人民邮电出版社.