zoukankan      html  css  js  c++  java
  • 60行C代码实现一个shell

    此文转载自:https://blog.csdn.net/juS3Ve/article/details/100840019

    继 300来行代码带你实现一个能跑的最小Linux文件系统 之后,我们来看看如何60行C代码实现一个shell!

    在实现它之前,先看看这样做的意义。

    美是有目共睹的。Unix之美,稍微体会,便能得到。

    1969年,Unix初始,没有fork,没有exec,没有pipe,没有 “一切皆文件” ,但是那时它已经是Unix了。它简单,可塑。

    Melvin Conway在1963年的论文中叙述fork思想时就解释说并行路径要用结果来交互,也就是在汇合的join点来同步结果。这个同步点所得到的,就是一个并行进程的 输出 。

    在此之外,Unix还有另一个原则,就是 组合小程序!

    Unix把一系列功能单一的小程序组合成一个复杂的逻辑,这个原则有以下优势:

    • 每一个小程序都很容易编写。

    • 每一个小程序可以分别完成。

    • 每一个小程序可以分别迭代修复。

    • 多个小程序可以自由组合。

    这是典型的模块化思想,小到统筹佐餐烧饭,大到组成生命的嘌呤嘧啶,都不自觉地和这种模块化思想相契机,原来这就是真理。 程序尽量小,只做一件事并且做好它。

    Unix程序在自身的逻辑之外对外暴露的只有输入和输出。那么 用输出连接另一个程序输入 就是一种好的方法。所谓Conway的join点对于Unix进程指的就是输出。

    对外暴露的越少,程序越内聚。这是一种范式,类似RISC处理器也是抽象出仅有的load和store来和内存交互。

    简单来讲,Unix程序通过输入和输出来彼此连接。下面是一幅来自Wiki的图示:640?wx_fmt=png

    Unix的另一个原则,即著名的 “一切皆文件!” 连接输出和输入的那个管道在Unix中被实现为Pipe,显然,它也是文件,一个FIFO文件。

    说实话,协作几个小程序形成一个大逻辑的思想还是来自于Convey,在Convey的论文里,他称为 协程, Pile可以说是直接实现了 Convey协程 之间的交互。有关这段历史,请看:

    用Pipe连接作为输出和输入连接Unix进程可以做成什么事情呢?让我们去感受一个再熟悉不过的实例,即数学式子:

    640?wx_fmt=png

    我们把运算符加号,乘号,除号(暂不考虑括号,稍后解释为什么)这些看作是程序(事实上它们也真的是),那么类似数字3,5,7,6就是这些程序的输入了,这个式子最终需要一个输出,获得这个输出的过程如下:

    1. 数字3,5是加号程序的输入,3+5执行,它获得输出8.

    2. 第1步中的输出8连同数字7作为乘号程序的输入,8 × 7执行,获得输出56.

    3. 第2步中的输出56连同数字6作为除号的输入,…

    这个数学式子的求值过程和pipe连接的Unix程序组合获得最终结果的过程完全一致。

    如果你相信数学可以描述整个世界,那么Pipe连同Unix程序同样是描述这个世界的语言 。

    在数学领域,程序 就是所有的运算符,加号,减号,乘号,除号,乘方,开方,求和,积分,求导…它们无一例外, 只做一件事。

    在Unix看来也同样。它做的事情和下面的应该差不多,而且更多:640?wx_fmt=png

    // plus.c
    #include <stdio.h>
    int main(int argc, char **argv)
    {
    	int a, b;
    
    	a = atoi(argv[1]);
    	b = atoi(argv[2]);
    
    	a = a + b;
    	printf("%d
    ", a);
    }
    

    同样,我们可以写出除法,直到偏导的程序。然后我们通过pipe就能将它们组合成任意的数学式子。

    现在谈谈Unix组合程序的具体写法,如果我们要化简薛定谔方程,我们应该如何用Unix命令写出与上述式子等价的组合程序命令行呢?我们无法像数学家手写那样随意使用括号,显然,计算机并不认识它。我们能够使用的只有两个符号:

    1. 代表具体Unix小程序的命令。

    2. Pipe符号"|"。

    换句话说,我们需要写出一个 链式组合表达式。 这时就要用到前缀表达式了。

    数学式子里的括号,其实它无关紧要,括号只是给人看的,它规定一些运算的优先级顺序,这叫 中缀表达式 ,一个中缀表达式可以轻松被转换为 前缀表达式,后缀表达式 ,从而消除括号。事实上,Unix的Pipe最初也面临过这样的问题,到底是中缀好呢,还是前/后缀好呢?

    我们现在使用的Unix/Linux命令,以cp举例:

    cp $in $out
    

    这是一个典型的前缀表达式,但是当pipe的发明者McIlroy最初引入pipe试图组合各个程序时,最初上面的命令行被建议成:

    $in cp $out
    

    就像我们的(3 + 5) × 8 一样。但是这非常不适合计算机处理的风格,计算机不得不首先扫描解析这个式子,试图:

    1. 理解 “括号括起来的要优先处理” 这句复杂的话;

    2. 区分哪些是输入,哪些是操作符…

    对于式子(3 + 5) × 8 的求值,计算机更适合用一种在简单规则下非常直接的方式去 顺序执行 求解,这就是前缀表达式的优势。

    × 8 +  35就是(3 + 5) × 8 的前缀表达式,可以看到,没有了括号。对于pipe组合程序而言,同样适用于这个原则。于是前缀命令成了pipe组合命令的首选,现如今,我们可以用:

    pro1 $stdin|pro2|pro3|pro4|...|proX $stdout
    

    轻松组合成任意复杂的逻辑。

    Pipe协同组合程序的Unix原则是一个创举,程序就是一个加工过滤器,它把一系列的输入经过自己的程序逻辑生成了一系列的输出,该输出又可以作为其它程序的输入。

    在Unix/Linux中,各种shell本身就实现了这样的功能,但是为了彻底理解这种处理方式的本质,只能自己写一个才行。来写一个微小的shell吧。

    再次看上面提到的Unix Pipe的处理序列:

    pro1 $stdin|pro2|pro3|pro4|...|proX $stdout
    

    如果让一个shell处理以上组合命令,要想代码量少,典型方案就是递归,然后用Pipe把这些递归调用过程给串起来,基本逻辑如下:

    int exec_cmd(CMD *cmd, PIPE pipe)
    {
        // 持续解析命令行,以pipe符号|分割每一个命令
        while (cmd->next) {
            PIPE pp = pipe_create();
            if (fork() > 0) {
                // 父进程递归解析下一个
                exec_cmd(cmd->next, pp);
                return 0;
            }
            // 子进程执行
            dup_in_out(pp);
            exec(cmd->cmdline);
        }
        if (fork() > 0) {
            wait_all_child();
            return 0;
        } else {
            dup_in_out(pp);
            exec(cmd->cmdline);
        }
    }
    

    按照上面的思路实现出来,大概60行左右代码就可以:

    // tinysh.c
    // gcc tinysh.c -o tinysh
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    #define CMD_BUF_LEN	512
    char cmd[CMD_BUF_LEN] = {0};
    
    void fork_and_exec(char *cmd, int pin, int pout)
    {
        if (fork() == 0) {
            if (pin != -1) {
                dup2 (pin, 0);
                close(pin);
            }
            if (pout != -1) {
                dup2 (pout, 1);
                close(pout);
            }
            system(cmd);
            exit(0);
        }
    	if (pin != -1)
    		close(pin);
    	if (pout != -1)
    		close(pout);
    }
    
    int execute_cmd(char *cmd, int in)
    {
    	int status;
    	char *p = cmd;
    	int pipefd[2];
    
    	while (*p) {
    		switch (*p) {
    		case '|':
    			*p++ = 0;
    			pipe(pipefd);
    			fork_and_exec(cmd, in, pipefd[1]);
    			execute_cmd(p, pipefd[0]);
    			return 0;
    		default:
    			p++;
    		}
    	}
    	fork_and_exec(cmd, in, -1);
    	while(waitpid(-1, &status, WNOHANG) != -1);
    	return 0;
    }
    
    int main(int argc, char **argv)
    {
    	while (1) {
    		printf("tiny sh>>");
    		gets(cmd);
    		if (!strcmp(cmd, "q")) {
    			exit(0);
    		} else {
    			execute_cmd(cmd, -1);
    		}
    	}
    	return 0;
    }
    

    下面是执行tinysh的结果:

    [root@10 test]# ls -l
    总用量 28
    -rw-r--r-- 1 root root    0 9月   1 05:39 a
    -rwxr-xr-x 1 root root 9000 9月   1 05:38 a.out
    -rw-r--r-- 1 root root    0 9月   1 05:39 b
    -rw-r--r-- 1 root root    0 9月   1 05:39 c
    -rw-r--r-- 1 root root    0 9月   1 05:39 d
    -rw-r--r-- 1 root root    0 9月   1 05:39 e
    -rwxr-xr-x 1 root root 9000 9月   1 05:38 tinysh
    -rw-r--r-- 1 root root 1167 9月   1 05:38 tinysh.c
    [root@10 test]# ./tinysh
    tiny sh>>ls -l |wc -l
    9
    tiny sh>>cat /etc/inittab |grep init
    # inittab is no longer used when using systemd.
    tiny sh>>cat /etc/inittab |grep init|wc -l
    1
    tiny sh>>q
    [root@10 test]#
    

    递归解析的过程中fork/exec,一气呵成,这就是一个最简单shell实现。它可完成组合程序的执行并给出结果。

    这个tiny shell命令解析器的逻辑可以表示如下:640?wx_fmt=png

    640?wx_fmt=png

    的计算,我需要写表示四则混合运算符的Unix程序,首先看加号运算符程序,将上文中plus.c改成从标准输入读取加数即可:

    // plus.c
    // gcc plus.c -o plus
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(int argc, char **argv)
    {
    	float a, b;
    
    	a = atof(argv[1]);
    	scanf("%f", &b);
    
    	b = b + a;
    	printf("%f
    ", b);
    }
    

    再看减法运算符程序代码:

    // sub.c
    // gcc sub.c -o sub
    #include <stdio.h>
    #include <stdio.h>
    
    int main(int argc, char **argv)
    {
    	float a, b;
    
    	a = atof(argv[1]);
    	scanf("%f", &b);
    
    	b = b - a;
    	printf("%f
    ", b);
    }
    

    接下来是乘法和除法的代码:

    // times.c
    // gcc times.c -o times
    #include <stdio.h>
    #include <stdio.h>
    
    int main(int argc, char **argv)
    {
    	float a, b;
    
    	a = atof(argv[1]);
    	scanf("%f", &b);
    
    	b = b*a;
    	printf("%f
    ", b);
    }
    
    // div.c
    // gcc div.c -o div
    #include <stdio.h>
    #include <stdio.h>
    
    int main(int argc, char **argv)
    {
    	int a, b;
    
    	a = atof(argv[1]);
    	scanf("%d", &b);
    
    	b = b/a;
    	printf("%d
    ", b);
    }
    

    可以看到,这些都是非常简单的程序,但是任意组合它们便可以实现任意四则运算,我们看看640?wx_fmt=png这个如何组合。

    首先在标准的Linux bash中我们试一下:

    [root@10 test]# ./plus 5|./times 7|./sub 20|./div 6
    3
    6.000000
    [root@10 test]#
    

    计算结果显然是正确的。现在我在自己实现的tinysh中去做类似的事情:

    [root@10 test]# ./tinysh
    tiny sh>>./plus 5|./times 7|./sub 20|./div 6
    3
    6.000000
    tiny sh>>q
    [root@10 test]#
    

    可以看到,tinysh的行为和标准Linux bash的行为是一致的。

    简单吧,简单!无聊吧,无聊!Pipe连接了若干小程序,每一个小程序只做一件事。

    如果我们的系统中没有任何shell程序,比如我们没有bash,我们只有tinysh,加上以上这4个程序,一共5个程序,就可以完成任意算式的四则混合运算。

    现在我们用以上的组合Unix程序的方法试试计算下面的式子:

    640?wx_fmt=png

    根号怎么办?

    按照非Unix的编程风格,就要在程序里写函数计算开根号,但是用Unix的风格,则只需要再加个开根号的程序即可:

    // sqrt.c
    // gcc sqrt.c -lm -o sqrt
    #include <stdio.h>
    #include <stdlib.h>
    #include <math.h>
    
    int main(int argc, char *argv[])
    {
    	float b;
    
    	scanf("%f", &b);
    
    	b = sqrt(b);
    	printf("%f
    ", b);
    }
    

    有了这个开根号的程序,结合已经有的四则运算程序,让我们的tinysh用pipe将它们串起来,就成了。好了,现在让我们计算上面的式子:

    ./tinysh
    tiny sh>>./sqrt |./plus 3|./div 2
    9
    3.000000
    tiny sh>>q
    

    本文该结束了,后面要写的应该就是关于经典Unix IPC的内容了,是的,自从Pipe之后,Unix便开启了IPC,System V开始称为标准并持续引领着未来,但这是另一篇文章的话题了。

    最后,来自Unix初创者之一Dennis M. Ritchie关于Unix的满满回忆,非常感人:640?wx_fmt=pngThe Evolution of the Unix Time-sharing System :


    浙江温州皮鞋湿,下雨进水不会胖!

    (完)

    Linux阅码场原创精华文章汇总

    更多精彩,尽在"Linux阅码场",扫描下方二维码关注

    640?wx_fmt=png

    感谢您的耐心阅读,请随手转发一下或者点个“在看”吧~

       

    更多内容详见微信公众号:Python测试和开发

    Python测试和开发

  • 相关阅读:
    HDU2027 统计元音 一点点哈希思想
    湖南工业大学第一届ACM竞赛 数字游戏 字符串处理
    湖南工业大学第一届ACM竞赛 我素故我在 DFS
    HDU3293sort
    HDU2082 找单词 母函数
    HDU1018 Big Number 斯特林公式
    湖南工业大学第一届ACM竞赛 分糖果 位操作
    UVA 357 Let Me Count The Ways
    UVA 147 Dollars
    UVA 348 Optimal Array Multiplication Sequence
  • 原文地址:https://www.cnblogs.com/phyger/p/14344751.html
Copyright © 2011-2022 走看看