自己动手写Linux Shell(一) —— 简单的命令解释器

晓之车 ~piano version~ — 梶浦由记

音频片段:需要 Adobe Flash Player(9 或以上版本)播放音频片段。 点击这里下载最新版本。您需要开启浏览器的 JavaScript 支持。

寒假在做Linux Kernel Project这本书上的习题,第二章的练习是写一个简单的shell,看了一下要求觉得这个练习很有价值,涉及到很多Linux C Programming的知识,所以准备认真地做一下。

    最终的目标如下:

  1. 命令解释执行
  2. 支持后台执行(&)
  3. 支持输入输出重定向(<, >, >>)
  4. 支持管道IPC
  5. 内建命令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

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
/* By JackalDire, Jan 29 2010 
 * Tested on Linux Kernel 2.6.32, gcc 4.4.3 */
#include <unistd.h>
#include <sys/wait.h>
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
 
#define LINE_MAX 8192
#define ARG_MAX 1024
#define ARG_NR_MAX 32
 
#define CHKERR(ret, msg) if (ret < 0) {\
        fprintf(stderr, "ERROR : \"%s\", %s\n", \
                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("%s\n", 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 : %d\n", r);
            CHKERR(r, args[0]);
        } else {
            wait(&status);
            pr_exit(args[0], status);
        }
    }
    return 0;
}

一个简单的命令解释器就这样完成了,下面的工作就是添加后台执行功能,休息一会^ ^

Related Post


  1. Iven Day 说:

    郁闷……怎么搜索了半天没找到你说的这本书呢……

    JackalDire 回复:

    Linux Kernel Project? 中文译名叫“Linux操作系统内核实习”
    http://www.douban.com/subject/1222518/
    书比较老了,用的还是2.2内核,反正也就是本习题册,也就无所谓了

Leave a Reply