一步一步实现自己的shell程序(一)---《Unix环境高级编程》读书笔记

代码见github

引言

用过Unix/Linux系统的人都知道,Unix系统中很多命令和程序都是在终端中运行,这个终端就是shell。不同的Unix都有不同的默认shell程序,包括sh、csh、bash等等不同的shell程序。那么什么是shell,shell究竟做了什么,如何实现简单的shell呢?

基础知识

程序与进程

进程实际上就是运行中的程序,而程序就是存储在文件中的机器指令,每个进程就是程序的一个实例,逐条执行程序文件中的CPU指令。
进程有很多属性,包括进程ID,父进程ID等等,内容太多现在介绍也很难记住,我们在使用时再介绍也不迟。

shell的基本功能

首先,shell本身就是一个程序,但是他是用来管理进程和运行进程的程序。主要有三个功能:

  • 运行程序
  • 可以编程
  • 管理输入和输出

首先是运行程序,ls和date实际上都是C语言编写的普通程序,shell将他们载入内存并运行,之后将结果输出到屏幕。因此,shell很大的作用就是运行程序。

 wutao@wutao-XXXX:~$ ls
Desktop    Downloads         git    Pictures  Templates
Documents  examples.desktop  Music  Public    Videos
wutao@wutao-XXXX:~$ date
20170311日 星期六 15:36:12 CST

其次是管理输入和输出,shell可以使用< > 和|将输入和输出重定向。如下代码所示,我们就能将程序ls的输出重定向到文件file中(默认为屏幕终端)。

wutao@wutao-XXXX:~$ ls -l > file
wutao@wutao-XXXX:~$ cat file
total 48
drwxr-xr-x 2 wutao wutao 4096 311  2017 Desktop
drwxr-xr-x 2 wutao wutao 4096 311  2017 Documents
drwxr-xr-x 2 wutao wutao 4096 311 15:05 Downloads
-rw-r--r-- 1 wutao wutao 8980 311 14:28 examples.desktop
-rw-rw-r-- 1 wutao wutao    0 311 15:36 file
drwxrwxr-x 3 wutao wutao 4096 311 15:00 git
drwxr-xr-x 2 wutao wutao 4096 311  2017 Music
drwxr-xr-x 2 wutao wutao 4096 311  2017 Pictures
drwxr-xr-x 2 wutao wutao 4096 311  2017 Public
drwxr-xr-x 2 wutao wutao 4096 311  2017 Templates
drwxr-xr-x 2 wutao wutao 4096 311  2017 Videos

同理<可以将输入从默认的键盘输入重定向为文件输入,|则用于进程之间管道通信。
最后是可编程,也就是说shell命令中可以带有变量,循环等流程控制语句。如下图,我们将变量NAME赋值(等号两边不能有空格),然后用 file NAME
变量的值,如果包含则echo(反射程序)输出hello,最后finish条件语句。

wutao@wutao-XXXX:~$ NAME=wutao
wutao@wutao-XXXX:~$ if grep $NAME file;then echo hello;fi;
drwxr-xr-x 2 wutao wutao 4096 311  2017 Desktop
drwxr-xr-x 2 wutao wutao 4096 311  2017 Documents
drwxr-xr-x 2 wutao wutao 4096 311 15:05 Downloads
-rw-r--r-- 1 wutao wutao 8980 311 14:28 examples.desktop
-rw-rw-r-- 1 wutao wutao    0 311 15:44 file
drwxrwxr-x 3 wutao wutao 4096 311 15:00 git
drwxr-xr-x 2 wutao wutao 4096 311  2017 Music
drwxr-xr-x 2 wutao wutao 4096 311  2017 Pictures
drwxr-xr-x 2 wutao wutao 4096 311  2017 Public
drwxr-xr-x 2 wutao wutao 4096 311  2017 Templates
drwxr-xr-x 2 wutao wutao 4096 311  2017 Videos
hello

shell如何运行程序

从我们可以看到的东西出发,我们来研究shell程序到底做了什么?我们打开一个终端,shell会打印一些提示字符,比如上面代码显示的wutao@wutao-XXXX:~$,主要包括了用户名和用户路径(~)。然后我们键入命令,shell程序执行命令直到执行的程序终止,之后shell再次打印提示符。
我们可以把shell程序的主循环分为以下几步:
1. 用户键入命令
2. shell启动新进程执行程序
3. shell等待程序执行完毕
4. 程序结束,shell完成一次主循环

因此,我们只要理解以上几步的原理,我们就能写出简单的shell程序了。主要知识就是:

  • 如何在一个程序中新建进程执行另一个程序
  • 如何等待程序的结束

一个程序如何执行另一个程序

熟悉操作系统的话,对exec()系统调用一定不会陌生,exec()函数簇包括7个和执行程序有关的函数,主要区别在于执行是按文件名还是路径名,传递参数的方式等。我们这里只用其中的execvp(),函数原型如下:

int execvp(const char *filename,char *const argv[]);

函数参数为一个文件名和一个参数列表。比如说,如果要在一个程序中运行ls程序,我们就可以调用函数execvp(“ls”,arglist),函数会在环境变量中搜索ls程序,并将命令参数arglist传递给ls程序。这里我写了一个示范代码如下所示:

#include<stdlib.h>
#include<unistd.h>
#include<stdio.h>
int main(){

        char *arglist[3] = {"ls","-l",0};
        printf("exec ls\n");
        execvp("ls",arglist);
        printf("ls has done\n");
}

编译运行程序,结果输出如下(需要注意的是,第一个printf函数字符串一定要有换行符,否则printf会将字符存放在缓存区,只有调用刷新缓存区函数fflush或者缓存区满才会输出):

wutao@wutao-XXXX:~/git/shell$ ./a.out
exec ls
total 16
-rwxrwxr-x 1 wutao wutao 8872 311 16:32 a.out
-rw-rw-r-- 1 wutao wutao  175 311 16:33 test_exec.c

我们发现,函数只打印了第一个printf的字符串,而没有打印第二个字符串。原因就在于execvp()函数。

execvp函数做了什么

前面说到,execvp能够执行一个新程序,但是需要注意的是,程序执行完新的程序没有能力返回到原先的程序,原因在于execvp函数将新程序载入当前进程,并替换了当前进程的代码和数据。也就是说新程序开始后,原程序就自动被清除了。

让原程序也能够存活

我们的shell程序是一个无止境的循环,不能够执行一次命令就终止了,因此我们必须想办法让原程序执行命令后还能够继续等待命令的完成。所以我们要新建一个进程,让命令在这个新建的进程中调用execvp函数,这样我们的shell函数就不会被清除了。这里我们使用的系统调用就是fork()函数。

fork函数

fork函数也是Unix的系统调用,调用fork函数能够复制调用该函数的进程(写时复制,只有产生写操作时会对将要写的部分进行复制)。复制产生新的进程后,我们再调用execvp函数,这样就能既保存shell程序和又能执行新程序。
那么我们如何判断哪个进程执行新程序呢?实际上fork的返回值就能判断进程是子进程还是父进程。

fork的返回值

fork函数很特殊的地方在于一次调用会产生两个返回值,父进程返回值为子进程的ID,子进程返回值为0。原因在于,父进程有可能有很多子进程,因此必须在调用子进程时获取子进程ID,而子进程只有一个父进程,可以通过调用getppid函数得到父进程ID。了解了父子进程返回值的不同,就能够通过判断返回值来决定进程是执行新程序还是等待子程序结束。

wait函数

如果只使用fork和execvp函数,shell程序不会等待子程序结束,而会自顾自地继续主循环,为了等待子进程的结束,我们要使用系统的wait函数。

Unix系统中,当一个进程终止时,内核会向父进程发送SIGCHLD信号。系统默认是忽略SIGCHLD信号的,但是通过调用wait函数(还有waitpid函数),可以获取子进程的结束状态:

pid_t wait(int *statloc);
pid_t waitpid(pid_t pid,int *statloc,int options);

调用wait或者waitpid函数后:

  • 如果所有子进程都还在运行,则阻塞
  • 如果一个子进程已经终止,正等待父进程获取其终止状态,则获取子进程终止状态并立即返回
  • 如果没有子进程则立即出错返回

而waitpid和wait函数的区别则在于waitpid可以选择需要等待的子进程,也可以通过调整选项使得调用者不阻塞,statloc 保存子进程终止状态,如果不关心终止状态则设置statloc为NULL。

至此我们理解了execvp函数 fork函数以及wait函数,可以利用这三个函数写出简单的shell程序了。strtok函数可将字符串根据delimiter分割。

#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<sys/wait.h>
#define MAXARGS 20
#define ARGLEN 80

int main(){
        while(1){
                printf(">");
                char argbuf[ARGLEN * MAXARGS];
                char *arglist[MAXARGS + 1];
                int index = 0;
                fgets(argbuf,ARGLEN,stdin);
                arglist[index++] = strtok(argbuf," \n");
                while((arglist[index++] = strtok(NULL," \n")));
                arglist[index-1] = 0;
                int newpid;
                if((newpid =fork()) == -1)
                        perror("fork");

                //child code
                else if (newpid == 0)
                        execvp(*arglist,arglist);

                //parent code
                else
                {
                        int wait_rv;
                        wait_rv = wait(NULL);
                        printf("done waiting for %d.returned:%d\n",newpid,wait_rv);
                }

}}

小结

本文主要介绍了shell程序的主要功能,利用了execvp函数,fork函数和wait函数简单实现了在shell程序中执行其他程序的功能。

猜你喜欢

转载自blog.csdn.net/wutao1530663/article/details/61417599