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

寒假在做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

/* 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;
}

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

This entry was posted in 技术学习 and tagged , . Bookmark the permalink.

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

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据