【打造你自己的Shell:编写定制化命令行体验】(二):https://developer.aliyun.com/article/1425817
4.3.更改为多线程版本
我们来看一下让我们的子进程执行替换的工作
运行结果:
我们为什么要使用子进程进行替换工作呢?而让我们的父进程去阻塞等待呢?因为父进程能获取到子进程的退出信息,能知道替换的工作执行结果。为什么替换工作为什么没有影响我们的父进程呢?因为进程具有独立性,代码和数据在进行加载的时候,此时就进行了修改,就会发生写时拷贝,随后数据和代码父子进程各种拥有一份。那shell是如何运行一个指令的呢?
- 创建新进程: 一旦找到了可执行文件,Shell会创建一个新的进程来执行该文件。这个新进程是原始Shell进程的子进程,bash会waitpid等待子进程的退出信息。
- 加载可执行文件: 新的进程会加载你输入的命令对应的可执行文件到内存中,也就是进程程序替换。
- 执行命令: 子进程进程开始执行加载的可执行文件。这是实际的命令运行阶段。
- 命令完成:子进程执行完毕,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
是默认目标,它依赖于 program1
和 program2
这两个目标。当你运行 make
命令时,它会首先构建 program1
和 program2。
.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