天天看点

简单shell的编写

       在进行shell的编写之前,我们首先得了解到shell的运行原理,其次才能知道怎么编写shell,接下来我就简单的介绍shell怎么简单的编写吧!

shell执行过程:

1.读取用户从键盘输入的命令(调用read函数);

2.分析命令,以命令名为文件名,并将其他参数改造为系统调用execvp()参数处理所要求的格式;

3.终端进程(shell)调用fork()或者vfork()建立一个子进程(个人建议采用fork());

fork()与vfock()都是创建一个进程,那他们有什么区别呢?总结有以下三点区别: 

1.  fork  ():子进程拷贝父进程的数据段,代码段 (共享代码,数据私有)

    vfork ( ):子进程与父进程共享数据段 

2.  fork ()父子进程的执行次序不确定 

    vfork 保证子进程先运行,在调用exec 或exit 之前与父进程数据是共享的,在它调用exec或exit 之后父进程才可能被调度运行。 

3.  vfork ()保证子进程先运行,在她调用exec 或exit 之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

4.子进程依据文件名(命令名)到目录中查找有关文件,将他调入内存,并创建新的文本段,并根据写时拷贝的方式创建相应的数据段,堆栈段;

5当子进程完成处理或者出现异常后,通过调用exit()或者——exit()函数向父进程报告;

6.终端进程调用waitpid()函数等待子进程完成,并对子进程进行回收。

当普通用户成功登录,系统将执行一个称为shell的程序。正是shell进程提供了命令行提示符。作为默认值(TurboLinux系统默认的shell是BASH),对普通用户用“$”作提示符,对超级用户(root)用“#”作提示符。 

一旦出现了shell提示符,就可以键入命令名称及命令所需要的参数。shell将执行这些命令。如果一条命令花费了很长的时间来运行,或者在屏幕上产生了大量的输出,可以从键盘上按ctrl+c发出中断信号来中断它(在正常结束之前,中止它的执行)。 

当用户准备结束登录对话进程时,可以键入logout命令、exit命令或文件结束符(EOF)(按ctrl+d实现),结束登录。      

       在Linux中,有一些命令,例如cd是包含在shell内部的命令,还有一些命令,例如cp、mv或rm是存在于文件系统中某个目录下的单独的程序。对于用户而言,没必要关心一个命令是在shell内部还是在shell外部。 

shell对于命令的分析过程如下:

  1. 首先,检查用户输入的命令是否是内部命令,如果不是在检查是否是一个应用程序;
  2. shell在搜索路径或者环境变量中寻找这些应用程序;
  3. 如果键入命令不是一个内部命令并且没有在搜索路径中查找到可执行文件,那么将会显示一条错误信息;
  4. 如果能够成功找到可执行文件,那么该内部命令或者应用程序将会被分解为系统调用传给Linux内核,然后内核在完成相应的工作;

编写一个简单的shell:

1.调用了一个简单的read函数

简单shell的编写

       在这段小代码中,我们可以先清楚的看到我们首先调用了一个简单的read函数,现在我就先简单介绍一下read函数吧,(通过man查看函数)

简单shell的编写

read()会把参数fd所指的文件传送nbyte个字节到buf指针所指的内存中。若参数nbyte为0,则read()不会有作用并返回0。返回值为实际读取到的字节数,如果返回0,表示已到达文件尾或无可读取的数据。错误返回-1,并将根据不同的错误原因适当的设置错误码。

2.fork()函数创建子进程:

简单shell的编写

       fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。

       一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:

    1)在父进程中,fork返回新创建子进程的进程ID;

    2)在子进程中,fork返回0;

    3)如果出现错误,fork返回一个负值;

      在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

fork出错可能有两种原因:

    1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。

    2)系统内存不足,这时errno的值被设置为ENOMEM。

3.子进程调用execvp()函数,父进程调用waitpid()函数进行进程等待:

简单shell的编写

用函数fork创建子进程后,如果希望在当前子进程中运行新的程序,可以调用exec函数执行另一个程序.当进程调用exec函数时,该进程用户空间资源(正文、数据、堆和栈)完全由新程序替代,新程序则从main函数开始执行.因为调用exec函数并没有创建新的进程,所以前后的进程ID并没有改变,也即内核信息基本不做修改.

exec系列函数共有7函数可供使用,这些函数的区别在于:指示新程序的位置是使用路径还是文件名,如果是使用文件名,则在系统的PATH环境变量所描述的路径中搜索该程序;在使用参数时使用参数列表的方式还是使用argv[]数组的方式.

1.exec系列函数

函数定义:

#include <unistd.h>

int execl(const char *pathname, const char *arg0, ... );

int execv(const char *pathname, char *const argv[]);

int execle(const char *pathname, const char *arg0, ... );

int execve(const char *pathname, char *const argv[], char *const envp[]);

int execlp(const char *filename, const char *arg0, ... );

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

int fexecve(int fd, char *const argv[], char *const envp[]);

返回值:如果执行成功将不返回,否则返回-1,失败代码存储在errno中.

前4个函数取路径名作为参数,后两个是取文件名作为参数,最后一个是以一个文件描述符作为参数.

2.函数具体分析

当指定filename作为参数时:

1)如果filename中包含/,则将其视为路径名.

2)否则就按PATH环境变量,在它所指的各目录搜寻可执行文件.

2.1 execl()函数

int execl(const char *pathname, const char *arg0, ... );

execl()函数用来执行参数path字符串所指向的程序,第二个及以后的参数代表执行文件时传递的参数列表,最后一个参数必须是空指针以标志参数列表为空.

2.2 execle()函数

int execle(const char *pathname, const char *arg0, ... );

execle()函数用来执行参数path字符串所指向的程序,第二个及以后的参数代表执行文件时传递的参数列表,最后一个参数必须指向一个新的环境变量数组,即新执行程序的环境变量.

2.3 execlp()函数

int execlp(const char *filename, const char *arg0, ... );

execlp()函数会从PATH环境变量所指的目录中查找文件名为第一个参数指示的字符串,找到后执行该文件,第二个及以后的参数代表执行文件时传递的参数列表,最后一个参数必须是空指针.

2.4 execv()函数

int execv(const char *path, char *const argv[]);

execv()函数函数用来执行参数path字符串所指向的程序,第二个为数组指针维护的程序参数列表,该数组的最后一个成员必须是空指针.

2.5 execvp()函数

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

execvp()函数会从PATH环境变量所指的目录中查找文件名为第一个参数指示的字符串,找到后执行该文件,第二个及以后的参数代表执行文件时传递的参数列表,最后一个成员必须是空指针.

waitpid系统调用在Linux函数库中的原型是:

#include <sys/types.h>

#include <sys/wait.h>

pid_t waitpid(pid_t pid,int *status,int options)

参数status:

如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的(一个进程也可以被其他进程用信号结束,我们将在以后的文章中介绍),以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作,下面我们来学习一下其中最常用的两个:

1、WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值(请注意,虽然名字一样,这里的参数status并不同于wait唯一的参数---指向整数的指针status,而是那个指针所指向的整数,切记不要搞混了)

2、WEXITSTATUS(status) 当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status) 就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说, WIFEXITED返回0,这个值就毫无意义。

参数pid:从参数的名字pid和类型pid_t中就可以看出,这里需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。

         pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。

         pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。

         pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。

         pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

参数options:options提供了一些额外的选项来控制waitpid,目前在Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用,比如:

ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);

如果我们不想使用它们,也可以把options设为0,如:

ret=waitpid(-1,NULL,0);

如果使用了WNOHANG参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。

简单shell的编写

运行成果展示:

简单shell的编写

继续阅读