寒假在做Linux Kernel Project这本书上的习题,第二章的练习是写一个简单的shell,看了一下要求觉得这个练习很有价值,涉及到很多Linux C Programming的知识,所以准备认真地做一下。
- 最终的目标如下:
- 命令解释执行
- 支持后台执行(&)
- 支持输入输出重定向(<, >, >>)
- 支持管道IPC
- 内建命令cd, pwd, exit等
可见写一个shell并不是一件简单的事,从简单的一步一步做起吧,手头有APUE,一边做一边查。
一个简单的命令解释器
命令解释执行是shell最基本的功能,实现的方法很简单:从标准输入流中读入命令,然后exec一下就行了。但是还有很多琐碎的地方需要处理:
1.命令行参数传递
首先需要将输入的命令字符串按空格打断(strsep实在是太方便了),然后将打断的字符串构建成一个char*数组,通过execv的第二个参数传递给程序。
注:man exec可以得到关于exec函数族的详细说明。需要说明的是execlp和execvp会在PATH环境变量中的目录搜索可执行程序,而其他的exec函数族函数不会,如果不使用这两个函数,则需要自己编写代码搜索PATH环境变量。
2.使用fork建立子进程
直接在当前进程里exec的话,exec执行的程序结束后,整个程序也就结束了,因为exec直接将原来的进程上下文替换。所以需要fork一个新进程来执行命令,而父进程阻塞直到子进程结束后继续执行,这个可以通过wait函数实现。
3.处理命令的返回值
大多数的shell在命令程序返回非零值(异常退出)会打印出其返回值。而子进程的返回值可以在父进程里通过wait函数的第一个参数得到。然后通过一组宏可以方便地确定子进程的返回状态。这部分内容在APUE里有详细说明(8.6节),下面代码里的pr_exit函数基本上就是从APUE上抄过来的。
4.检查各个函数的返回值
Linux C Programming的一个原则就是在所有可能fail的地方加入检查代码。绝大多数C库函数和Linux系统函数都以负数返回值表示出错,并且通过C库的全局变量errno可以获得错误号,从而得到错误原因,并输出到标准错误流。由于整个过程动作固定,就用一个CHKERR宏来完成了。
下面是源代码
CODE BELOW ARE UNDER GPLV3 LISENCE
/* By JackalDire, Jan 29 2010
* Tested on Linux Kernel 2.6.32, gcc 4.4.3 */
#include
#include
#include
#include
#include
#include
#define LINE_MAX 8192
#define ARG_MAX 1024
#define ARG_NR_MAX 32
#define CHKERR(ret, msg) if (ret < 0) {
fprintf(stderr, "ERROR : "%s", %sn",
msg, strerror(errno));
exit(-1);
}
char * args[ARG_NR_MAX + 1];
extern char ** environ;
char line[LINE_MAX + 1];
void parse_command(char * cmd)
{
char * res;
size_t cnt = 0;
/* tokenize the command string by space */
while ((res = strsep(&cmd, " ")) != NULL) {
printf("%sn", res);
args[cnt++] = strdup(res);
}
args[cnt] = NULL;
}
void pr_exit(const char * name, int status)
{
if (WIFEXITED(status)) // exit normally
return;
else if (WIFSIGNALED(status))
fprintf(stderr, "%s exit abnormally, signal %d caught%s.n",
name, WTERMSIG(status),
#ifdef WCOREDUMP
WCOREDUMP(status) ? " (core file generated)" : "");
#else
"");
#endif
else if (WIFSTOPPED(status))
fprintf(stderr, "child stopped, signal %d caught.",
WSTOPSIG(status));
}
int main(int argc, char * argv[])
{
char c;
size_t idx;
int r;
int status;
while (1) {
idx = 0;
bzero(line, LINE_MAX + 1);
c = fgetc(stdin);
while (c && c != 'n') {
line[idx++] = c;
c = fgetc(stdin);
}
parse_command(line);
r = fork();
CHKERR(r, "fork");
if (r == 0){
r = execvp(args[0], args);
//printf("ret : %dn", r);
CHKERR(r, args[0]);
} else {
wait(&status);
pr_exit(args[0], status);
}
}
return 0;
}
一个简单的命令解释器就这样完成了,下面的工作就是添加后台执行功能,休息一会^ ^
3 Responses to 自己动手写Linux Shell(一) —— 简单的命令解释器