学习理解shell的好办法--编写自己的shell 之一

本文参考自《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 之一

时间: 2024-10-12 04:37:36

学习理解shell的好办法--编写自己的shell 之一的相关文章

学习理解shell的好办法--编写自己的shell 之二

shell脚本的最简单形式就是一串命令的罗列,shell充当解释器,一条条挨个执行,直到最后一个或遇到退出命令.但这只能做很简单的事情,只是省区了每次都要敲一边命令的时间,要想完成更负责的功能,还要加上这些东西: 1.控制 前面的条件满足了,然后干什么:不满足,干什么. 2.变量 c=a+b, 用一种形式代表另一种形式,就是变量.因为形式不同了,就能用一种不变的表示另一种变化的.比如"编程语言"就可以当一个变量,可以赋值为"C语言","Perl语言&quo

编写简单的Shell脚本

在一些复杂的Linux维护工作中,大量重复性的输入和操作不但费时费力,而且容易出错,而编写一个恰到好处的shell脚本程序,可以批量处理.自动化的完成一系列维护任务,大大减轻了管理员的负担 shell脚本基础 1. 编写第一个shell脚本 Linux中的shell脚本是一个特殊的应用程序,介于操作系统内核和用户之间,充当了一个命令解释器的角色,负责接收用户的操作指令并进行解释,将需要执行的操作传递给内核执行,并输出执行结果 常见的shell解释器 可通过/etc/shells文件了解当前系统所

bb_black_学习笔记——(3)点亮LED之shell命令

这次改变一下笔记的结构,直接上图.点亮LED永远是学习MCU,ARM,FPGA的入门例程,可以说是经典例程.这里笔者也从点亮LED开始开始ARM学习之旅. 接下来就开始点亮LED之旅,在开始之前需要向读者说明:本文作者也是一个ARM初学者,学习过程中也是在网上查找各种资料,最后总结于此.可以确定实验的过程中有好多地方笔者自己也是没有搞明白的,所以请读者原谅不能写的太透彻. 第一步:要点亮LED,首先需要了解GPIO的硬件位置,这样才能去操作相应的IO口,实现点灯目的 1.连接好bb-black之

Android中的context的学习理解

Android中Context的学习理解Context是一个抽象基类,通过它getResuources.getAssets and start 其他组件(Activity,Service,broadCast,getSystemService),可以这样理解:Context提供了一个运行环境for App, then app 可以访问资源,才能完成与其他组件,服务的交互,Context定义了一套基本的功能接口or一套规范 //todo

第十天:shell编程基础与编写Makefile

一:shell编程基础 shell定义:shell是一个作为用户与linux系统间接口的程序.它允许用户向操作系统输入需要执行的命令.shell有很多中,linux系统中shell为bash. shell编程可以看作是一堆命令的集合.和windows中的bat程序类似的脚本程序.为解释性语言. 第一个shell程序是判断两个数字的大小. 1 #!/bin/bash 2 3 num1=10 4 num2=9 5 6 if test $num1 -gt $num2 7 then 8 echo $nu

Hadoop基础学习(一)分析、编写并运行WordCount词频统计程序

前面已经在我的Ubuntu单机上面搭建好了伪分布模式的HBase环境,其中包括了Hadoop的运行环境. 详见我的这篇博文:http://blog.csdn.net/jiyiqinlovexx/article/details/29208703 我的目的主要是学习HBase,下一步打算学习的是将HBase作为Hadoop作业的输入和输出. 但是好像以前在南大上学时学习的Hadoop都忘记得差不多了,所以找到以前上课做的几个实验:wordCount,PageRank以及InversedIndex.

学习理解 makefile

学习理解 makefile 模拟个应用的例子: 有个工程包括头文件 001.h.002.h.003.h.004.h.005.h.006.h.007.h 共7个:程序文件 001.c.002.c.003.c.004.c.005.c.006.c.007.c.008.c.009.c.010.c 共10个文件.看着头大吧,先不关心具体内容. 现在来编译该工程.如下: # cd example/ # gcc 001.c 002.c 003.c 004.c 005.c 006.c 007.c 008.c 0

理解TCP/IP网络栈&amp;编写网络应用(下)

1.摘要 这是<翻译:理解TCP/IP网络栈&编写网络应用>的下篇,文章中会通过讲解TCP的代码实现帮助大家理解发送.接收数据的流程,也描述了一些网卡.驱动等网络栈底层的原理. 原文地址:原文地址 原作者:Hyeongyeop Kim 2.数据结构 以下是一些关键数据结构.我们了解一下这些数据结构再开始查看代码. 2.1.sk_buff_structure 首先,sk_buff结构或skb结构代表一个数据包.图6展现了sk_buff中的一些结构.随着功能变得更强大,它们也变得更复杂了.

小鸟初学Shell编程(二)编写简单的Shell脚本

Shell脚本 编写Python.PHP脚本通常需要掌握语言的函数,那么Shell脚本则不需要,只需要掌握Linux命令就可以编写Shell脚本,因为Shell脚本就是由多个Linux命令组成,通过将多个Linux命令组合保存成一个脚本文件,可直接给其他人使用. 组合命令 进入一个目录,查看目录的文件,这个过程分别需要执行两条命令,分别是cd 和ls. 分开执行两个命令的形式如下: [[email protected] usr]# cd /usr/ [[email protected] usr]