前言
这周操作系统的最后一个实验室写一个Shell,然而实验手册的pdf上只指明了你必须要完成什么函数,对于怎么写则是一点提示都没有。
所以本篇文章则立意于如何写出一个简单可用的shell,个人认为这个主题应该会分为上中下三篇文章来阐明从零开始的Shell编写过程
Shell做了什么
简单的说,shell是一个管理进程和运行程序的程序,所有常用的shell有三个基本功能
- 运行程序
- 管理输入输出
- 可编程
Shell是如何运行程序的
shell打印提示符,输入命令,然后就运行这个命令,随手再次打印提示符,如此反复。那么在这个过程中,背后到底有什么?
一个shell的主循环执行下面四步:
- 用户输入
- shell建立进程
- shell将程序从磁盘载入
- 程序在他的进程中运行直到结束
所以一个Shell的主循环可以写成
while(!end_of_input)
get command
execute command
wait for command to finish
所以要写一个shell ,就需要学会
- 运行程序
- 建立进程
- 等待exit
一个程序如何运行另一个程序
使用函数 int execvp(const char* file,char* const argv[]) ,这个函数会从PATH变量所指的目录中查找符合参数file的文件名,找到后便执行该文件,然后将第二个参数argv传递给执行的文件。
使用demo
main(){
char * arglist[3];
arglist[0] = "ls";
arglist[1] = "-l";
arglist[2] = 0;
execvp("ls",arglist);
}
第一个Shell Demo
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
/**
** prompting shell version 1
**
** Prompts for the command and its arguments.
** Builds the argument vector for the call to execvp.
** Uses execvp(), and never returns.
**/
main()
{
char *arglist[MAXARGS+1];
int numargs;
char argbuf[ARGLEN];
char *makestring();
numargs = 0;
while ( numargs < MAXARGS )
{
prompt( numargs );
if ( gets(argbuf) && *argbuf ) {
arglist[numargs++] = makestring(argbuf);
}
else
{
if ( numargs == 0 )
break;
arglist[numargs] = NULL ;
execute( arglist );
numargs = 0;
}
}
return 0;
}
execute( char *arglist[] )
{
execvp(arglist[0], arglist);
perror("execvp failed");
exit(1);
}
char *
makestring( char *buf )
{
char *cp, *malloc();
if ( cp = malloc( strlen(buf) + 1 ) ){
strcpy(cp, buf);
return cp;
}
fprintf(stderr,"out of memory\n");
exit(1);
}
建立新的进程
在学会了使用execvp函数以后,我们解决了第一个问题如何执行程序。 那么我们现在解决的第二个问题就是如何建立新的进程。 我们为何要建立新的进程? 假设我们已经为我们的程序写好了输入输出的接口,将参数传递给execvp函数以后,当函数执行完以后我们会发现我们的程序也结束了。 这显然不是我们想要的结果。
为何会这样
如果我们做一个实验,在使用demo的execvp函数以后加一个输出语句,重新编译执行以后你会发现我们新加的打印消息不见了。 那么这条程序去哪了呢?
这个原因都是因为execvp函数,exec系统调用从当前进程中把当前程序的机器指令清除,然后再空的进程中载入调用的程序代码,exec调整进程的内存让他适应新的程序对内存的要求。
解决办法
为了解决这个问题,我们的解决方法之一就是复制一个进程,这样就可以继续原来的程序了。这就是系统调用fork做的事情。
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构
- 复制原来的进程到新的进程
- 向运行进程集添加新的进程
- 将控制返回给两个进程
父进程如何等待子进程
当使用fork函数后,我们会得到两个进程,其中一个进程是我们原本的进程,他执行了会继续执行shell的程序,而另一个进程则是我们fork出的一个子进程,他会执行我们的execvp函数。
但是这就引出了新的问题。 当我们得到两个进程以后,这两个进程会并行的执行,如果你执行的程序耗时过久,你会发现在执行子进程的时候,主进程的提示打印符也会出现在屏幕中,并提示你输入下一条命令,这显然不是我们想要的。
为了解决这个问题,我们需要用的wait函数。 当进程调用wait函数以后,进程会立即阻塞自己并且自动分析是否当前进程的某个子进程已经退出,如果让他找到了这样一个变成僵尸的子进程,wait会收集这个进程的信息,并且把它彻底摧毁后返回,如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个这样的出现。
为了使用wait函数,首先我们需要知道进程是子进程还是父进程,这一点可以通过fork函数的返回值来判断。
第二个Shell Demo
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
/**
** prompting shell version 2
**
** Solves the `one-shot' problem of version 1
** Uses execvp(), but fork()s first so that the
** shell waits around to perform another command
** New problem: shell catches signals. Run vi, press ^c.
**/
main()
{
char *arglist[MAXARGS+1];
int numargs;
char argbuf[ARGLEN];
char *makestring();
numargs = 0;
while ( numargs < MAXARGS )
{
prompt( numargs );
if ( gets(argbuf) && *argbuf ) {
arglist[numargs++] = makestring(argbuf);
}
else
{
if ( numargs == 0 )
break;
arglist[numargs] = NULL ;
execute( arglist );
numargs = 0;
}
}
return 0;
}
execute( char *arglist[] )
/*
* use fork and execvp and wait to do it
*/
{
int pid,exitstatus;
pid = fork(); /* make new process */
switch( pid ){
case -1:
perror("fork failed");
exit(1);
case 0:
execvp(arglist[0], arglist);
perror("execvp failed");
exit(1);
default:
while( wait(&exitstatus) != pid )
;
printf("child exited with status %d,%d\n",
exitstatus>>8, exitstatus&0377);
}
}
char *
makestring( char *buf )
{
char *cp;
if ( cp = malloc( strlen(buf) + 1 ) )
{
strcpy(cp, buf);
return cp;
}
fprintf(stderr,"out of memory\n");
exit(1);
}
总结
到了这里,我们的shell demo已经可以基本的运行命令并且正常运行了,但是我们会发现一个新的问题,那么就是现在退出demo的唯一方法就是按ctrl-c键,那么如果在等待子进程结束时输入ctrl-c键会如何呢?
我们会发现子进程结束,但是shell也技术了,ctrl-c生成的SIGINT信号不但杀死了运行的子进程,而且也杀死了运行shell的进程,这是为什么?
键盘信号发给所有连接的进程
程序shell和tr都连接到终端,当按下中断键以后,ttr驱动会告诉内核向所有由这个终端控制的进程发送SIGINT信号,子进程死了,shell也死了,即使他还在等待子进程的结束。
那么如何才能让shell不被用户按下的中断或退出键杀死呢? 我将留到这一主题的中篇