【打造你自己的Shell:编写定制化命令行体验】(三)

简介: 【打造你自己的Shell:编写定制化命令行体验】

【打造你自己的Shell:编写定制化命令行体验】(二):https://developer.aliyun.com/article/1425817


4.3.更改为多线程版本


我们来看一下让我们的子进程执行替换的工作


运行结果:


我们为什么要使用子进程进行替换工作呢?而让我们的父进程去阻塞等待呢?因为父进程能获取到子进程的退出信息,能知道替换的工作执行结果。为什么替换工作为什么没有影响我们的父进程呢?因为进程具有独立性,代码和数据在进行加载的时候,此时就进行了修改,就会发生写时拷贝,随后数据和代码父子进程各种拥有一份。那shell是如何运行一个指令的呢?


  1. 创建新进程: 一旦找到了可执行文件,Shell会创建一个新的进程来执行该文件。这个新进程是原始Shell进程的子进程,bash会waitpid等待子进程的退出信息。
  2. 加载可执行文件: 新的进程会加载你输入的命令对应的可执行文件到内存中,也就是进程程序替换。
  3. 执行命令: 子进程进程开始执行加载的可执行文件。这是实际的命令运行阶段。
  4. 命令完成:子进程执行完毕,bash进程获取子进程的退出信息,随后子进程退出销毁。


4.4.学习各种exec的接口


4.4.1替换函数


其实有七种以exec开头的函数,统称exec函数:

#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
// 系统调用
int execve(const char *filename, char *const argv[], char *const envp[]);


4.4.2.函数解释


  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值。


4.4.3.命名理解


这些函数原型看起来很容易混,但只要掌握了规律就很好记。


  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量


exec调用举例如下:

#include <unistd.h>
int main()
{
  char* const argv[] = { "ps", "-ef", NULL };
  char* const envp[] = { "PATH=/bin:/usr/bin", "TERM=console", NULL };
    execl("/bin/ps", "ps", "-ef", NULL);
    // 带p的,可以使用环境变量PATH,无需写全路径
  execlp("ps", "ps", "-ef", NULL);
    // 这里的参数不重复,第一个参数意思是你想执行谁?
    // 第二个参数是你想怎么执行?
    // 带e的,需要自己组装环境变量
  execle("ps", "ps", "-ef", NULL, envp);
  execv("/bin/ps", argv);
  // 带p的,可以使用环境变量PATH,无需写全路径
  execvp("ps", argv);
    // 带e的,带p的
    execvpe("ps", argv, envp);
  // 带e的,需要自己组装环境变量
  execve("/bin/ps", argv, envp);
  exit(0);
}


题外话:exec*可以执行系统的指令(程序),那可以执行我们自己的程序吗?当然可以


文件扩展名 .cc 通常表示一个C++源代码文件。在编程领域,C++是一种通用的、面向对象的编程语言,而 .cc 扩展名通常用于标识C++源代码文件。其他常见的C++源代码文件扩展名还包括 .cpp 和 .cxx。随后我们创建一个.cc的C++源代码文件


随后编译可以形成我们的可执行文件,然后我们运行一下。


所以今天我们就要在makefile里面一次形成两个可执行程序了,那怎么做呢?我们想使C程序把C++程序调起来。我们首先就要修改一下我们的makefile文件。

mytest:mytest.cc
    g++ -o $@ $^ std=c++11
myprocess:myprocess.c
    gcc -o $@ $^ 
.PHONY=clean
clean:
    rm -f myprocess mytest


然后我们make编译一下


只形成了一个可执行程序文件,而且是c++可执行程序,我们试着把make里面c语言提到最前面

myprocess:myprocess.c
    gcc -o $@ $^ 
mytest:mytest.cc
    g++ -o $@ $^ std=c++11
.PHONY=clean
clean:
    rm -f myprocess mytest


然后我们make编译一下,看看结果


此时依然之形成了一个可执行程序文件,只不过此时是c语言的可执行文件。为什么呢?因为makefile在从上到下扫描的时候,只会形成一个可执行程序。根据上面的结论,我们可以得出那个可执行程序放在最前面,那个可执行就会被执行,而后面的那个就不会执行。那如果我们想要通过makefile形成多个可执行程序呢?在Makefile中,如果你想生成多个可执行程序,可以在Makefile中定义多个目标(target)。每个目标对应一个可执行程序。以下是一个简单的例子:

.PHONY:all # 伪目标,总是被执行的
all: program1 program2 # all依赖于program1 program2
                       # 就要形成program1 program2
# 没有依赖方法
program1: source1.c
    gcc -o program1 source1.c
program2: source2.c
    gcc -o program2 source2.c
.PHONY=clean
clean:
    rm -f program1 program2

在这个例子中,all 是默认目标,它依赖于 program1program2 这两个目标。当你运行 make 命令时,它会首先构建 program1program2。

.PHONY:all
all:myprocess mytest
myprocess:myprocess.c
    gcc -o $@ $^ 
mytest:mytest.cc
    g++ -o $@ $^ std=c++11
.PHONY=clean
clean:
    rm -f myprocess mytest


此时我们再make编译一下我们的文件。


此时就形成了两个可执行程序了,接下里我们就使用C程序把C++程序调起来,直接看代码


上面我们才传入参数的时候带入了选项,我们想观察一下C++程序有没有接收到C语言程序传入的命令行参数,我们来修改C++程序代码


然后我们来make编译运行一下


这样我们就用了C语言代码调用了C++的代码。除了能调用C++代码,还能调用其他语言的代码吗?我们来试一下shell脚本,它的后缀名是.sh

#!/user/bin/bash
echo "hello shell"
echo "hello shell"
echo "hello shell"
echo "hello shell"
echo "hello shell"


然后我们再来运行一下shell脚本。


然后我们再来修改一下C语言代码,使其能调用我们的shell脚本

excel("/usr/bin/bash","bash","test.sh",NULL);


然后我们再运行一下


所以exec*可以执行系统的指令(程序),也可以执行我们自己写的程序(无论什么语言,只要能在Linux下运行即可)。但是为什么呢?因为所有的语言运行之后,都是进程,都拥有自己的代码和数据,而进程替换只有有代码和数据就可以替换!对于exce来说,只有数据和代码,没有语言的区别,它只看进程。我们上面的C++代码可以获取程序的命令行参数,那现在也就可以获取环境变量了,它的环境变量就可以从C语言程序中获取来,所有我们就可以研究带'e'的程序替换。直接看代码


然后我们运行就发现此时同为子进程的C++程序就能打印出父进程所有的环境变量。但是此时我们的C语言代码是没有传入环境参数的,说明没传它就默认能拿到环境变量,怎么做到的呢?如果我们导入一个其他的环境变量呢?它能拿到吗?我们导入一个环境变量导给bash。./myprocess的时候会创建一个子进程,myprocess就是bash的子进程,此时myprocess就能拿到父进程导入的环境变量,而我们的mprocess还创建了一个子进程,子进程完成的任务是进程替工作,但是它是子进程,它也会继承父进程的环境变量。


那为什么呢?因为命令行参数和环境变量都在进程地址空间,而创建的这个子进程会继承父进程的进程地址空间的,所以我们的子进程可以直接拿到父进程的环境变量,所以Linux系统默认可以通过地址空间继承的方式,让所有的子进程拿到环境变量。所以进程替换,不会替换环境变量数据。


1.如果我们想让子进程继承全部的环境变量,直接就能拿到。

2.如果我们想单纯的新增呢?putebv


putenv函数是一个C标准库中的函数,用于设置环境变量。putenv谁调用这个函数,就把这个环境变量导入到谁的进程地址空间里。


此时putenv导入到myprocess程序里,所以bash进程不会拿到这个环境变量,而C++程序作为myprocess的子进程,会拿到这个环境变量,我们来看一下运行结果。


3.如果我们想设置全新的环境变量(覆盖方式)呢?使用exce带'e'的接口


运行结果:


事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册第2节,其它函数在 man手册第3节。这些函数之间的关系如下图所示。 下图exec函数族 一个完整的例子:


五、自定义shell编写


用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左 向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。


然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。 所以要写一个shell,需要循环以下过程:


  • 1. 获取命令行
  • 2. 解析命令行
  • 3. 建立一个子进程(fork)
  • 4. 替换子进程(execvp)
  • 5. 父进程等待子进程退出(wait)


根据这些思路,和我们前面的学的技术,就可以自己来实现一个shell了。


我们每次使用滴时候都会看到[xyc@xyc-alicloud myshell][@],它是一个提示符包括[用户名@主机名 所处路径],然后再输入我们的指令,我们的指令其实是一个命令行字符串,所以首先我们就要输出提示符并获取用户输入的命令字符串"ls -a -l"。要获取我们的提示符的相关信息我们都可以调用我们的系统调用,它这里面都可以获取这些信息,比如我们的getcwd获取所处路径


但是现在我们不想使用系统调用呢?我们其实还可以使用环境变量,环境变量里面包含了提示符的信息,并且环境变量能随着用户和工作目录的改变而改变,这样就可以让我们的shell功能更加高效。


我们可以通过getenv获取这些环境变量。


然后我们来看代码

#include <stdio.h>
#include <stdlib.h>
const char* info[3];//全局数组
void getInfo()
{
  char* user = getenv("USER");
  if(user) info[0] = user;
  else info[0] = "None";
  char* hostname = getenv("HOSTNAME");
  if(hostname) info[1] = hostname;
  else info[1] = "None";
  char* pwd = getenv("PWD");
  if(pwd) info[2] = pwd;
  else info[2] = "None";
}
int main()
{
  //输出提示符并获取用户输入的命令字符串"ls -a -l"
  getInfo();
  printf("[%s@%s %s]$",info[0],info[1],info[2]);
  return 0;
}


运行结果:


然后我们再来获取用户的输入,先来使用我们的scanf函数,看看能不能使用?这里要注意我们自己写的shell删除键是ctrl + Backspace。


运行结果:


此时我们发现我们只能获取【ls】,剩余的字符不能获取,这是因为scanf默认输入的时候以空格或者换行为分隔符,当读取到空格或者换行的时候,就会认为当前的输入已经完成,所以这里我们就不使用scanf了,使用fgets函数。


运行结果:


【打造你自己的Shell:编写定制化命令行体验】(四):https://developer.aliyun.com/article/1425828

相关文章
|
5月前
|
Shell
【打造你自己的Shell:编写定制化命令行体验】(四)
【打造你自己的Shell:编写定制化命令行体验】
|
3月前
|
Java Shell Linux
【Linux】手把手教你做一个简易shell(命令行解释器)
【Linux】手把手教你做一个简易shell(命令行解释器)
42 0
|
4月前
|
监控 Unix Shell
探秘GNU/Linux Shell:命令行的魔法世界
探秘GNU/Linux Shell:命令行的魔法世界
|
5月前
|
存储 Unix Shell
【打造你自己的Shell:编写定制化命令行体验】(二)
【打造你自己的Shell:编写定制化命令行体验】
|
5月前
|
Shell Linux
【linux课设】自主实现shell命令行解释器
【linux课设】自主实现shell命令行解释器
|
5月前
|
Shell
【shell】shell命令行放在变量中执行以及变量的常用方法
【shell】shell命令行放在变量中执行以及变量的常用方法
|
5月前
|
存储 Shell Linux
【Shell 命令集合 系统设置 】Linux 将参数作为命令行输入 eval命令 使用指南
【Shell 命令集合 系统设置 】Linux 将参数作为命令行输入 eval命令 使用指南
77 0
|
5月前
|
缓存 Shell Linux
【打造你自己的Shell:编写定制化命令行体验】(一)
【打造你自己的Shell:编写定制化命令行体验】
|
5月前
|
Shell Linux
Linux之简单的Shell命令行解释器
Linux之简单的Shell命令行解释器
73 0