从0开始自己用C语言写个shell__01_整体的框架以及fork和exec族函数的理解

时间:2021-01-08 05:26:15

最近才忙完了一个操作系统的作业,让我们用C语言实现一个Shell。总的来说,其实就是让我们 对系统调用有比较深的了解。

首先 介绍一下我的Shell 所实现的功能。
1.运行可执行程序 即输入某个 标志符号 使得其能在我的Shell中运行,并且不退出当前shell。
2.获得某个程序的中某个字符串的个数(其实就是调用了/bin/里面的grep)

3.使用管道,实现两个子进程之间的联系,当然不能连shell自己都退出了。。

4.定向输出到某个个文本文件中。

但是在这之前,我想先谈谈我对fork()这个函数的理解。

首先我们把最开始的程序叫做F,然后 我们开始运行这个 程序,让我们一条条指令运行!当我们的运行到fork的时候,我们OS将整个程序 复制出几乎完全一样的一个程序(子程序)!注意在此之前的命令已经执行完了,所有的数据空间中的数据都将被复制一份,供子程序使用(包括fork()这个函数也会被复制一份。)。注意是复制一份,并不是让子程序共用父程序的数据空间!(再直白一点就是同一个变量,你在子程序里面调用改变了他,但是当你在父程序里面打印出它时,数值仍然是改变前的)。

前面提到的父子程序,难免会让人产生很多疑惑!有人肯定会问:“你不是说我的父程序 和子程序是完全一样的吗?那我fork出一个子程序有什么用呢?反正都是干的同一件事" 或者问:“那我们怎么区分父子进程呢?”。

我逐个解释。

首先第一 前面我提过,fork也会被复制到子进程中,那么可想而之,两个fork会返回两次,注意不是一个fork返回两次(只是看起来像而已),这就是fork神奇的地方。 fork的调用不需要任何参数,

而他的返回值 是一个整数。

当这个整数大于0时,表示着,当前进程为父程序,并且这个返回的整数表示它的子进程的ID(第几个进程)。
当返回值小于0的时候,表示没能成功创建出子进程。

当等于0的时候表示着,我们的当前进程是子进程。

网上大佬都说 把整个过程看成链表即可,返回值指向下一个进程。

所以人们往往就运用这个特性来设计父程序干啥,程序干啥。这样每次判断一下当前是在父程序还是子程序 就可做不同的事情。也就是说两个进程完全可以干不同的事情。

需要注意的是系统调用函数头文件<unistd.h>

可能又有老哥会问“那执行的时候先执行谁啊?”

当然是“同时”执行,由于concurrency 的存在,中文貌似翻译是并发,这就是指CPU能在极高速度的运转下,不停切换处理对象,让我们觉得貌似他俩同时执行,其实每次cpu都只能对某一个进程进行处理。

至于父子进程谁先执行,我觉得应该问 他俩到底谁先执行完。 这个就难判断了,他们需要执行的内容不一样,出结果的时间当然就不一样,最后谁先结束我们不得而知。

所以往往,我们会在父进程中加入wait 或者waitpid函数,来让父进程来等待子进程。这样能做确保我们Shell的功能实现了以后(当前在父进程),不会立即打断子进程的输出。(这个稍后解释)

实际我们操作fork的时候往往需要搭配exec族的系统调用来配合使用,我们往往希望我们的子程序能运行一些自己的内容或者说是另一个程序。
所以接下里 我想稍微花点时间一个个解释这几个函数。

exec函数一共有六个,其中execve为内核级系统调用,其他(execl,execle,execlp,execv,execvp)都是调用execve的库函数

也就是说最终我们调用的其实还是execve而它的库函数,仅仅是传递参数的方法不同。

我先解释execve的参数传递要求

函数定义:

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

第一个参数: filename 字符串所代表的文件路径。(这个文件路径包括了 此用户的环境变量中的 PATH路径,和当前程序所在目录。即在这两种路径中寻找符合filename的文件,然后执行它)。

第二个参数:是利用指针数组来传递给执行文件。

第三个参数:传递给执行文件的新环境变量数组。

我还想在这多啰嗦一句 这个环境变量的意思。注意这里我指的全是Linux 环境下的环境变量。

所谓环境变量,其实是一个很大的集合,它包含了一个或者多个应用程序所将使用到的信息。

那一个程序所将用到哪些信息呢?

常见的环境变量

PATH:决定了shell将到哪些目录中寻找命令或程序

HOME:当前用户主目录

MAIL:是指当前用户的邮件存放目录。

SHELL:是指当前用户用的是哪种Shell。

HISTSIZE:是指保存历史命令记录的条数

LOGNAME:是指当前用户的登录名。

HOSTNAME:是指主机的名称,许多应用程序如果要用到主机名的话,通常是从这个环境变量中来取得的。

LANG/LANGUGE:是和语言相关的环境变量,使用多种语言的用户可以修改此环境变量

https://blog.csdn.net/l494926429/article/details/52816334 详细的介绍可以看这里。

这里我们用到了 环境变量 中的PATH。

现在我们先看看,如果我们想运行程序 需要在哪些地方寻找目标文件。

从0开始自己用C语言写个shell__01_整体的框架以及fork和exec族函数的理解

用 echo $PATH 可以查看这些路径,这里有很多路径,但是我们主要关注/usr/bin这个路径,接下来我们再看看这里面有啥。

从0开始自己用C语言写个shell__01_整体的框架以及fork和exec族函数的理解 可以看到都是些exec 文件,换句话说每当我们在终端里使用ls cd 等命令时起时调用的都是这里面程序,注意这是系统关键文件,不可以随便乱动哦!bin目录下都是二进制可执行文件。/bin目录放置的是最基本的一些命令的可执行文件,比如cp、mv、mkdir、chmod、chown等等;/usr下面也有一个bin目录:/usr/bin,它里面的文件也是一些命令的可执行文件;如果是用户自己安装的软件,软件的主程序文件就会在/usr/local/bin这个目录里面(或者是用户自己指定的安装目录,比如/usr/local/apache/bin)。

https://zhidao.baidu.com/question/1707850194618091740.html  转自这里。

花了点时间解释了环境变量,现在让我继续解释一下其他类型的exec族函数。

int execlp(const char * file,const char * arg,....) 这个函数可以百度到其中的解释。

execlp()会从PATH 环境变量所指的目录中查找符合参数file的文件名,找到后便执行该文件,然后将第二个以后的参数当做该文件的argv[0]、argv[1]……,注意参数的个数有设计者决定。你调用的那个程序需要多少参数你就传几个。最后一个参数必须用空指针(NULL)作结束。如果用常数0来表示一个空指针,则必须将它强制转换为一个字符指针,否则它将解释为整形参数,如果一个整形数的长度与char * 的长度不同,那么exec函数就将出错。如果函数调用成功,进程自己的执行代码就会变成加载程序的代码,execlp()后边的代码也就不会执行了. 转自百度百科

int execvp(const char * path,const char * arg,....,char *const envp[]);

这个参数起时没有特别大的不同,只不过我们的第二个参数,从一个个给目标程序传递参数,变成了一次性传递一组字符串指针,也就是一次性把所有参数传完。

举个栗子。(注意这里传递的参数的结尾同样也需要NULL)

char *args[] = {"./hello","hello" ,NULL};

execvp("./hello",args);

其实 其他的exec族函数基本用法都差不多。大家可以自行百度。

这里我就先暂时总结到这,下一章节,我将大面积解释Shell 功能的具体实现。