本文参考自《Unix/Linux编程实践教程》, 这是一本讲解unix系统编程的书,注重实践,理解难度不大,推荐大家阅读,敲完本书后,对于理解unix系统如何运作会有更深的视角,回过头再学习别的 Linux相关的东西时,感受非常不一样,这是一本可以提高“内功”的书。自己加了些很菜的解释,以便其他小白理解,大牛直接飘过吧,错误之处希望指正。
shell是一个管理进程和运行程序的程序,用来人和机器交互
常用的shell如sh,bash,zsh,csh,ksh等都有三个主要功能:
1. 运行程序
date, ls, who都是用C写的实用程序, shell负责将它们装入内存运行, 因此shell可以看成一个程序启动器
2. 管理输入输出
利用重定向符号<, >,管道符号 | , 可以告诉shell将输入输出定向到文件或其他进程,也可以从文件定向到标准输入输出。尤其是管道,感觉非常酷!通过组合那些基本命令,实现很多功能
3. 可编程
即带有变量和控制。其实变量是缓冲思想的在最小处的一个应用,先暂存到一个地方,一会儿再用。控制即if, while啥的,控制执行过程。有了变量和控制,单独执行的那些程序便可以放到一个文件中,即所谓的脚本,这样就能一次运行多个命令,也可以保存供以后使 用。其他脚本语言也是类似的原理。
本篇先讲解shell如何运行程序,写一个不带变量和控制的shell,老子曰:“千里之行,始于足下”。 shell的工作看起来是这样的:开一个终端后,打印提示符,一般就是那个"$"或"#", 愚蠢的人类输入命令,命令执行完了,又出现提示符,无尽的循环......直到退出终端,比如输入exit,这是通过命令退出;或提示符后按ctrl + d,这产生一个文件结束符;或图形终端模拟器中鼠标点了窗口的关闭,这是由窗口管理器处理。其实这三个都是用来结束那个无尽的循环,退出shell自己 的。
shell的主体是这样的:
while(!end_of_input) { 等待人类输入命令; 执行命令; 等待命令结束; }
那个end_of_input由前面提到的三种退出方法产生。有一个情形是这样的,在shell里再运行一个shell,然后在shell里运行的 shell那个shell里再运行一个shell,然后在......你可以买个俄罗斯套娃玩了 :P .一般的程序都是干完自己的活就退出了(命令行界面下常用的程序都是这样的,但图形界面程序为了交互大都需要人类自己去关闭),但因为shell是运行其 他程序的程序,因此它的退出需要另外干预。
为了写一个shell,要知道:
1. 在程序中运行一个程序(相当于创建一个进程);
2. 等待程序中那个新程序的退出
关于进程:运行中的程序。或者说就是在内存中的程序和一些设置,比如状态、时间、进程号等,ps -x命令的输出中,每一行就是一个进程的信息。top命令可以查看实时的进程信息。我们小白初学编程时,写的都是些单进程的程序,一下子到底,比如打印个"hello"。但要把程序执行两遍,只能你再输入一遍,让它再执行一遍,而这可以让程序自己完成,那就是用多进程。这个思路可以用C语言中函数调用来类比。你可以把所有要做的事写道main里,有重复的工作时,一般是建立一个子函数,然后多次调用,而不是复制代码。
execvp调用: execvp(program,arglist). program为调用的程序名,arglist为参数列表,用它来从程序中运行程序,它会利用环境变量查找program,就是ls,who之类。
fork调用:fork(). 创建新进程,它干的活就是把原来运行的程序复制一份,这样,内存中就有了两个一样的程序。这两个程序不再叫程序了,就叫他们进程吧。fork原始意思就是分叉,一条道变成两条道,分道扬镳之后,就走自己的路了。
wait调用:wait(&status). 等待子进程结束。等待分为阻塞和非阻塞,比如要喝一壶茶这个进程。你就是shell。先创建一个烧水的进程,你可以选择阻塞,就是i蹲在旁边看着壶冒热气,也可以非阻塞,水开了壶会有鸣叫,这就属于信号了,另外壶也可以把它的状态存进status里。shell是最初的父进程,它一般执行一个程序是都是阻塞的,不过你看不到,因为机器太快。而后台进程就是非阻塞的,就是命令后边加个"&".
下面开工!
1.只能运行一个程序的shell
有一组系统调用exec完成“在程序中运行另一个程序”的工作,具体怎么完成的细节先不深究,那又属于另一个编程层次了,这里只是为了写个小shell,只会用这调用就行了,就当成是调用自己的main程序之外的一个函数吧。
这里用到的是execvp.下面是只能运行一个程序的“残疾”shell的代码,因为这货运行完你输入的第一个程序后自己也退出了.
/* egg_sh.c * 你认为是先有蛋呢还是鸡呢,这个连鸡和蛋自己都不知道的问题困扰了愚蠢的人类很长时间,姑且认为先有蛋吧,此残疾shell被命名为egg_sh * by the way, 使用大写字母开头分隔程序名是很丑陋的,比如EggSh, 真正的程序员用"_"分隔程序名 */ #include <stdio.h> #include <signal.h> #include <string.h> #define MAXARGS 20 /* 参数的最大个数 */ #define ARGLEN 100 /* 参数缓冲区长度 */ char * makestring(char *buf); int execute(char *arglist[]); int main() { char *arglist[MAXARGS+1]; /* 参数数组 */ int numargs = 0; /* 参数数组索引 */ char argbuf[ARGLEN]; /* 存放读入内容的缓冲区 */ while( numargs < MAXARGS ) { printf("arg[%d]? ", numargs); /* 打印提示符 */ if( fgets(argbuf, ARGLEN, stdin) && *argbuf != ‘\n‘ ) arglist[numargs++] = makestring(argbuf); else{ if( numargs > 0 ){ arglist[numargs] = NULL; execute(arglist); numargs = 0; } } } return 0; } int execute(char *arglist[]) { execvp(arglist[0], arglist); /* 此处即开始执行程序中的程序, arglist[0]为新程序的名称,arglist为参数列表 */ perror("execvp failed"); exit(1); } char *makestring(char * buf) /* * 去掉每个参数最后位置的换行,改成‘\0‘,即C语言的字符串结束符 * 并为每个参数分配内存,以便存放它们 */ { char *cp; buf[strlen(buf)-1] = ‘\0‘; /* 将‘\n‘改为‘\0‘ */ cp = malloc(strlen(buf)+1); if( cp == NULL ){ fprintf(stderr, "no memory\n"); /* 从开始学编程到现在,内存不足这个情况我从来没碰到过=_=! */ exit(1); } strcpy(cp, buf); /* 把参数缓冲区里的内容复制到刚分配的地方 */ return cp; /* 返回参数所在位置的指针 */ }
wc -l egg_sh.c 查看一下,才60多行代码,没错,一个可以成为shell的程序就这么点,只是现在还是个“蛋”。编译运行大概是这样的:
[email protected]? ./a.out arg[0]? ls arg[1]? -l arg[2]? -a arg[3]? 总用量 32 drwxrwxrwt 4 root root 4096 7月 29 12:11 . drwxr-xr-x 23 root root 4096 7月 10 02:39 .. -rwxr-xr-x 1 hotea hotea 6251 7月 29 12:05 a.out -rw-r--r-- 1 hotea hotea 1788 7月 29 12:05 egg_sh.c drwxrwxrwt 2 root root 4096 7月 29 08:36 .ICE-unix -r--r--r-- 1 root root 11 7月 29 2014 .X0-lock drwxrwxrwt 2 root root 4096 7月 29 2014 .X11-unix [email protected]?
你可以用它运行别的程序试试,空行回车表示命令输入结束。egg_sh退出的原因是execvp用ls的程序覆盖了egg_sh的程序,结束后egg_sh就没了。要想像真正的shell那样运行完一个程序后继续等待命令,就需要把execvp放在新进程里执行,ls所在的进程退出不会影响egg_sh的进程
2.可以运行多个程序的shell
之前的蛋shell只用了exec,所以只能执行一个程序,现在加上fork调用,可以运行多个程序,把exec放到fork之后的叉路上,它退出了,shell也不会退出。fork执行后,由于分身为两个,为了区分,子进程中fork返回0, 父进程中fork返回子进程的pid。
这样一来执行流程是这样的:
1.提示符 -> 2.取得命令 -> 3.建立新进程 -> 4.父进程 等待..................... 得到子进程状态 -> 回到提示符
| |
子进程 -> exec运行新程序 -> 结束退出 -> 退出状态
只需更改execute函数, 这个能运行多个程序的shell已经可以完成最基本的工作了,只是用起来还是不舒服,像蛋shell那样得一次一行输入内容
int execute(char *arglist[]) /* 使用fork()和execvp(), 用wait()等待子进程 */ { int pid,exitstatus; /* 子进程的进程号和退出状态 */ pid = fork(); /* 创建子进程 */ switch( pid ){ case -1: perror("fork failed"); exit(1); case 0: execvp(arglist[0], arglist); /* 执行在shell中输入的程序 */ perror("execvp failed"); exit(1); default: while(wait(&exitstatus) != pid) ; printf("child exited with status %d, %d\n",exitstatus>>8, exitstatus&0377); /* 退出信息 */ } }
fork之后,上面这段代码在父子进程中是一样的,不过由于pid不同,才导致执行的部分不同,如果fork不出错的话,子进程会执行case 0后面部分,因为它的pid为0,这样由于调用了exit,子进程也就退出了;父进程执行default后部分,得到子进程的退出状态信息,这信息保存在exitstatus中,可以用,也可以扔掉,这里把它打印出来了,exitstatus>>8是退出值,后面和0377按位与得到信号的号,我们先不用这些。
执行情况类似下面这样
[email protected]? ./a.out arg[0]? ls arg[1]? a.out big_egg_sh.c egg_sh.c child exited with status 0, 0 arg[0]? ps arg[1]? PID TTY TIME CMD 3708 pts/0 00:00:00 bash 5266 pts/0 00:00:00 a.out 5268 pts/0 00:00:00 ps child exited with status 0, 0 arg[0]? 按ctrl+D arg[0]? arg[0]? exit arg[1]? execvp failed: No such file or directory child exited with status 1, 0 arg[0]? ^C [email protected]?
运行多个程序可以了,但^D不管用了,exit也不好使了,原因简单解释一下,子进程调用execvp(exit,NULL),这里把exit当成了新程序,而我们可以用type exit产看exit是shell内嵌的,也就是在环境变量PATH里是找不到的,像ls,who这些多在/bin,/usr/bin这些目录,可以找到,而cd,exit这些内嵌命令,它就会提示no such file or directory. 另外,要退出这个big_egg_sh, 只能通过ctrl+C信号杀死他了,而我们系统用的shell用ctrl+C是杀不死的,而要用ctrl+D退出。为了使big_egg_sh不被^C杀死,可以在其main函数中加入这一句,表示忽略^C产生的信号
signal(SIGINT,SIG_IGN)
至此,一个相当粗糙的shell算是完成了,但这终究是个蛋而已,下一篇让我们把这蛋进化成chicken!(source code at
git)
学习理解shell的好办法--编写自己的shell 之一