当前位置: 首页 > wzjs >正文

做网站有前景吗电子制作网站

做网站有前景吗,电子制作网站,博文阅读网站建设,同步显示一个wordpress🔑🔑博客主页:阿客不是客 🍓🍓系列专栏:深入代码世界,了解掌握 Linux 欢迎来到泊舟小课堂 😘博客制作不易欢迎各位👍点赞⭐收藏➕关注 ​ 一、进程替换 之前我们通过写…

🔑🔑博客主页:阿客不是客

🍓🍓系列专栏:深入代码世界,了解掌握 Linux

欢迎来到泊舟小课堂

😘博客制作不易欢迎各位👍点赞+⭐收藏+➕关注

一、进程替换 

之前我们通过写时拷贝,让子进程和父进程在数据上互相解耦,保证独立性。如果想让子进程和父进程彻底分开,让子进程彻彻底底地执行一个全新的程序,我们就需要 进程的程序替换

为什么要进行程序替换?因为我们想让我们的子进程执行一个全新的程序。

那为什么要让子进程执行新的程序呢?我们一般在服务器设计的时候(Linux 编程)的时候,往往需要子进程干两件种类的事情:

  • 让子进程执行父进程的代码片段(服务器代码…)
  • 想让子进程执行磁盘中一个全新的程序(shell、想让客户端执行对应的程序、通过我们的进程执行其他人写的进程代码、C/C++ 程序调用别人写的 C/C++/Python/Shell/Php/Java...)

1.1 进程替换原理

📃 程序替换的原理:

  • 将磁盘中的内存,加载入内存结构。
  • 重新建立页表映射,设执行程序替换,就重新建立谁的映射(下图为子进程建立)。
  • 效果:让父进程和子进程彻底分离,并让子进程执行一个全新的程序

这个过程有没有创建新的进程呢?没有!根本就没有创建新的进程!

因为子进程的内核数据结构根本没变,只是重新建立了虚拟的物理地址之间的映射关系罢了。内核数据结构没有发生任何变化! 包括子进程的 \textrm{pid}​ 都不变,说明压根没有创建新进程。

1.2 认识进程替换(execl 接口)

我们要调用接口,让操作系统去完成这个工作 —— 系统调用。如何进行程序替换?我们先见见猪跑 —— 从 execl 这个接口讲,看看它怎么跑的。

int execl(const char* path, const char& arg, ...);

如果我们想执行一个全新的程序,我们需要做几件事情:

(要执行一个全新的程序,以我们目前的认识,程序的本质就是磁盘上的文件)

  • 第一件事情:先找到这个程序在哪里。
  • 第二件事情:程序可能携带选项进行执行(也可以不携带)。

明确告诉 OS,我想怎么执行这个程序?要不要带选项。

\Rightarrow​ 简单来说就是:① 程序在哪?  ② 怎么执行?

所以,execl 这个接口就必须得把这两个功能都体现出来!

  • 它的第一个参数是 path,属于路径。
  • 参数  const char* arg, ... 中的 ... 表示可变参数,命令行怎么写(ls, -l, -a) 这个参数就怎么填。ls, -l, -a 最后必须以 NULL 结尾,表示 "如何执行程序的" 参数传递完毕。

💬 代码演示:exec():

#include<stdio.h>
#include<unistd.h>int main()
{printf("我是一个进程,我的PID是:%d\n", getpid());execl("/usr/bin/ls", "ls", "-l", "-a", NULL);printf("我执行完毕了,我的PID是:%d\n", getpid());return 0;
}

🚩 运行结果如下:

刚才是带选项的,现在我们再来演示一下不带选项的: 

#include<stdio.h>
#include<unistd.h>int main()
{printf("我是一个进程,我的PID是:%d\n", getpid());execl("/usr/bin/top", "top" , NULL);printf("我执行完毕了,我的PID是:%d\n", getpid());return 0;
}

🚩 运行结果如下:

这样我们的程序就直接能执行 top 命令了,除此之外,我们曾经学的大部分命令其实都可以通过 execl 执行起来。这就叫做 程序替换


不知道大家有没有发现问题?代码和输出结果有什么不对劲的地方?

printf("我执行完毕了,我的PID是:%d\n", getpid());

最后一句代码 ——  "我执行完毕了,我的PID是:%d" 似乎没有打印出来啊?

为什么我们最后的代码并没有被打印出来?

因为 一旦替换成功,是会将当前进程的代码和数据全部替换的!

所以自然后面的 printf 代码早就被替换了,这意味着该代码不复存在了,荡然无存!因为在程序替换的时候,就已经把对应进程的代码和数据替换掉了!而第一个 printf 执行了的原因自然是因为程序还没有执行替换。

这里的程序替换函数用不用判断返回值?为什么?

int ret = execl(...);

一旦替换成功,还会执行返回语句吗?返回值有意义吗? 没有意义的!

程序替换不用判断返回值!因为只要成功了,就不会有返回值,也不需要返回值。 而失败的时候,必然会继续向后执行。通过返回值最多能得到是什么原因导致替换失败的。只要执行了后面的代码,看都不用看,一定是替换失败了;只要有返回值,就一定是替换失败了。

我们来模拟一下失败的情况,我们来执行一个不存在的指令

💬 代码演示:

#include<stdio.h>
#include<unistd.h>int main()
{printf("我是一个进程,我的PID是:%d\n", getpid());execl("/usr/bin/Atop", "ls" , NULL);printf("我执行完毕了,我的PID是:%d, ret: %d\n", getpid(), ret);return 0;
}

🚩 运行结果如下:

💡 说明:execl 替换失败,就会继续向后执行。但是,一旦 execl 成功后就会跟着新程序的逻辑走,就不会再 return 了,再也不回来了,所以返回值加不加无所谓了。


诶……子进程执行程序替换,会不会影响父进程呢?不会!因为进程具有独立性

为什么?如何做到的?子进程是如何做到代码和数据做分离的呢?让子进程与父进程做相似的代码片段,子进程改了,父进程也不受影响。

我们在前几章,讲过数据层面发生写时拷贝的概念。我们说过:fork 之后父子是共享的,如果要替换新的程序我能理解把新的程序的代码加载到内存里,我的子进程新的代码程序出来之后发生数据的写时拷贝,生成新的数据段。

不是说代码是共享的吗?我们该如何去理解呢?当程序替换的时候,我们可以理解成 —— 代码和数据都发生了写时拷贝,完成了父子分离。

1.3 更多 exec 接口

1. execlp 接口:无需带路径就能直接执行(可变参数列表)

int execlp(const char* file, const char* arg, ...);

execlp,它的作用和 execl 是一样的,它的作用也是执行一个新的程序。

仍然是需要两步:① 找到这个程序   ② 告诉我怎么执行

所以这一块的参数传递,和 execl 是一样的:

  • 第一个参数为字符指针,指向要执行的可执行文件的名称
  • 后续的参数为传递给可执行文件的命令行参数列表。arg[0] 通常是程序的名称,虽然实际上你可以传递任何字符串作为arg[0],最后要以NULL结尾。

唯一的区别是比 execl 多了一个 p!我们执行指令的时候,默认的搜索路径在环境变量 \textrm{PATH}​ 中,所以这个 p 的意思是环境变量。

这意味着:执行 execlp 时,会直接在环境变量中找,不用去输路径了,只要程序名即可。而 execlp 可以不带路径,只说出你要执行哪一个程序即可,例如:子进程要执行一个ls命令,并带上 -al 参数:

execlp("ls", "ls", "-a", "-l", "NULL");

值得一提的是:这里出现的两个 ls 含义是不一样的,是不可以省略的。第一个参数是 "供系统去找你是谁的",后面的代表的是 "你想怎么去执行它" 。

💬 代码演示:

#include<stdio.h>
#include<unistd.h>
#include <stdlib.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if (id == 0) {// child printf("我是子进程,我的PID是:%d\n", getpid());execlp("ls", "ls", "-a", "-l", NULL);exit(10);  // 只要执行了exit,就意味着 excel 系列函数失败了,终止子进程}printf("我是父进程,我的PID是: %d\n", getpid());int status = 0;int ret = waitpid(id, &status, 0);// ret 接收的返回值不是exit的,而是子进程的pidif (ret == id){sleep(2);  printf("父进程等待成功!\n");}return 0;
}

🚩 运行结果如下:

2. execv 接口:以指针数组接收参数的

int execv(const char* path, char* const argv[]);
  • 第一个参数为要执行程序的路径
  • 第二个参数为一个指向字符指针数组的指针,数组中的每个元素都是一个指向字符串的指针,这些字符串就是命令行参数。最后要以NULL结尾。

大家在命令行上 $ ls -a -l ,在 execl 里我们是这么传的: "ls", "-a", "-l", NULL 。所以 execv 和execl 只有传参方式的区别,一个是可变参数列表 (l),一个是指针数组 (v)。值得注意的是,在构建 argv[] 的时,结尾仍然是要加上 NULL!

char* myargv[] = { "ls", "-l", "-a" NULL };
execv("/usr/bin/ls", myargv);

💬 代码演示:

#include<stdio.h>
#include<unistd.h>
#include <stdlib.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if (id == 0) {// child printf("我是子进程,我的PID是:%d\n", getpid());char* const argv_[] = {(char*)"-ls",(char*)"-l",(char*)"-a",NULL};execv("usr/bin/ls", argv_);exit(10);  // 只要执行了exit,就意味着 excel 系列函数失败了,终止子进程}printf("我是父进程,我的PID是: %d\n", getpid());int status = 0;int ret = waitpid(id, &status, 0);// ret 接收的返回值不是exit的,而是子进程的pidif (ret == id){sleep(2);  printf("父进程等待成功!\n");}return 0;
}

🚩 运行结果如下:

 3. execvp 接口:无需带路径(指针数组)

int execvp(const char* file, char* const argv[]);
  • 第一个参数为要执行程序的名称
  • 第二个参数是一个字符指针数组,为传递给可执行文件的命令行参数列表。arg[0] 通常是程序的名称,虽然实际上你可以传递任何字符串作为arg[0],最后要以NULL结尾。

看到这里,想必大家光看到这个接口的名字,就能猜到它是什么意思了。与 execlp 类似,execvp 也是带 p 的,执行 execvp 时,会直接在环境变量中中搜索可执行文件,只要程序名即可。

char* myargv[] = { "ls", "-l", "-a" NULL };
execvp("myargv[0]", myargv);

💬 代码演示:

#include<stdio.h>
#include<unistd.h>
#include <stdlib.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if (id == 0) {// child printf("我是子进程,我的PID是:%d\n", getpid());char* const argv_[] = {(char*)"-ls",(char*)"-l",(char*)"-a",NULL};execvp("argv_[0]", argv_);exit(10);  // 只要执行了exit,就意味着 excel 系列函数失败了,终止子进程}printf("我是父进程,我的PID是: %d\n", getpid());int status = 0;int ret = waitpid(id, &status, 0);// ret 接收的返回值不是exit的,而是子进程的pidif (ret == id){sleep(2);  printf("父进程等待成功!\n");}return 0;
}

🚩 运行结果如下:

4. 超级缝合怪 execvpe 接口

缝合怪罢了,v - 数组,p - 文件名,e - 可自定义环境变量:

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

 5. execle 接口:添加环境变量给目标进程

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

我们可以使用 execle 接口传递环境变量,相当于自己把环境变量导进去。

  • 第一个参数是要执行程序的路径
  • 第二个参数是一个字符指针数组,为传递给可执行文件的命令行参数列表。arg[0] 通常是程序的名称,虽然实际上你可以传递任何字符串作为arg[0],最后要以NULL结尾。
  • 最后一个参数是你自己设置的环境变量

创建 mycmd 文件,我们加上几句环境变量:

#include <iostream>
#include <stdlib.h>int main()
{std::cout << "PATH:" << getenv("PATH") << std::endl;std::cout << "--------------------------------------\n";std::cout << "MYPATH:" << getenv("MYPATH") << std::endl;std::cout << "--------------------------------------\n";std::cout << "hello c++" << std::endl;return 0;
}

其中,\textrm{PATH} 是自带的环境变量,\textrm{MYPATH} 是我们自己的环境变量:

💬 代码演示:

#include<stdio.h>
#include<unistd.h>
#include <stdlib.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if (id == 0) {// child printf("我是子进程,我的PID是:%d\n", getpid());char* const __env[] ={(char*)"MYPATH=123456789",NULL};execle("./mycmd", "mycmd", NULL, __env);  // 手动传递环境变量exit(10);  // 只要执行了exit,就意味着 excel 系列函数失败了,终止子进程}printf("我是父进程,我的PID是: %d\n", getpid());int status = 0;int ret = waitpid(id, &status, 0);// ret 接收的返回值不是exit的,而是子进程的pidif (ret == id){sleep(2);  printf("父进程等待成功!\n");}return 0;
}

🚩 运行结果如下:

为什么没有打印内容啊?

我们自己定义的 \textrm{MYPATH} 环境变量也没出来,我们先把 mycmd 的 \textrm{PATH} 注释掉,然后再运行:

#include <iostream>
#include <stdlib.h>int main()
{// std::cout << "PATH:" << getenv("PATH") << std::endl;// std::cout << "--------------------------------------\n";std::cout << "MYPATH:" << getenv("MYPATH") << std::endl;std::cout << "--------------------------------------\n";std::cout << "hello c++" << std::endl;return 0;
}

🚩 运行结果如下:

所以,为什么会出现这种情况?我自己不定义自己的环境变量时,\textrm{PATH} 可以获取得到,一旦我传入了一个自己定义的环境变量时,\textrm{PATH} 就打不出来了?!

execle 接口,这个 e 表示的是 添加环境变量给目标进程,如果是自己的变量,那么是覆盖式的

6. 为什么会有这么多 exec 接口?

为什么搞这么多接口,这些接口好像没有太大的差别啊。

唯一的差别就是传参的方式不一样,有的带路径,有的不带路径,有的是列表传参,有的是数组传参,有的可带环境变量,有的不带环境变量。

因为要适配各种各样的应用场景,使用的场景不一样,有些人就喜欢列表传参,有些人喜欢数组传参。所以就配备了这么多接口,这就好比我们 C++ 函数重载的思想。 

那为什么 execve 是单独的呢?

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

它处于 man 2 号手册,execve 才属于是真正意义上的系统调用接口。而刚才介绍的那些,实际上就是为了适配各种环境而封装的接口:

总结一下它们的命名规律,通过这个来记忆对应接口的功能会好很多:

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

二、实现简易 shell 脚本

shell就是一个命令解释器,它互动式地解释和执行用户输入的命令;当有命令要执行时,shell创建子进程让子进程去执行命令,而shell只需要等待子进程执行完退出即可。

具体步骤:

  1. 获取终端输入的命令
  2. 解析命令
  3. 创建子进程
  4. 对子进程进行程序替换
  5. 等待子进程执行完后退出

2.1 显示提示符和获取用户输入

 shell 本质就是个死循环,我们不关心获取这些属性的接口我们先从简单的入手,先来实现前两步,显示提示符 和 获取用户输入

我们需要按照终端的格式打印出命令提示符,那么就需要获取用户、主机和当前路径等信息,这就需要用到我们之前学习的获取环境变量的函数 getenv() :

💬 代码演示:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>// 用户名
const char *GetUserName()
{const char *name = getenv("USER");return name == NULL ? "None" : name;
}// 主机名
const char *GetHostName()
{const char *hostname = getenv("HOSTNAME");return hostname == NULL ? "None" : hostname;
}// 当前路径
const char *GetPwd()
{const char *pwd = getenv("PWD");return pwd == NULL ? "None" : pwd;
}int main()
{// 显示提示符printf("[%s@%s:%s]# ", GetUserName(), GetHostName(), GetPwd());return 0;
}

🚩 运行结果如下:


接下来是获取用户输入的内容:

我们用户输入一行本质上是一行完整的字符串,我们用 scanf 是以空格为分割符,而我们的指令是带空格的,我们利用 fgets 函数从键盘上获取,标准输入 stdin,获取到 C 风格的字符串,注意默认会添加 \0 。

但因为后续还会涉及到很多字符串的操作,我们将文件改成 C++ 的格式,同时也需要修改文件后缀为 .cc ,修改 makefile 中的文件名和 编译方式(gcc -> g++)。但因为一些接口适配性的问题,为了方便入门学习,我们这里不使用 string 类型和 getline 函数,依旧按照 C 语言风格来执行。

💬 代码演示:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>#define COMMAND_SIZE 1024// 用户名
const char *GetUserName()
{const char *name = getenv("USER");return name == NULL ? "None" : name;
}// 主机名
const char *GetHostName()
{const char *hostname = getenv("HOST");return hostname == NULL ? GetUserName() : hostname;
}// 当前路径
const char *GetPwd()
{const char *pwd = getenv("PWD");return pwd == NULL ? "None" : pwd;
}int main()
{// 显示提示符printf("[%s@%s:%s]# ", GetUserName(), GetHostName(), GetPwd());// 获取用户输入char commandline[COMMAND_SIZE];char *res = fgets(commandline, COMMAND_SIZE, stdin);printf("%s\n", commandline);return 0;
}

🚩 运行结果如下:

为什么这最后有一行空行?因为我们输入最后按了回车,commandline 里有一个 \n,我们把它替换成 \0 即可:

 commandline[strlen(commandline) - 1] = '\0';  // 消除 '\n'

🚩 运行结果如下:

至此,我们已经完成了提示用户输入,并且也获取到用户的输入了。 

但是目前的代码有些许不美观,我们换成其他的方式:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s:%s]#"// 用户名
const char *GetUserName()
{const char *name = getenv("USER");return name == NULL ? "None" : name;
}// 主机名
const char *GetHostName()
{const char *hostname = getenv("HOST");return hostname == NULL ? GetUserName() : hostname;
}// 当前路径
const char *GetPwd()
{const char *pwd = getenv("PWD");return pwd == NULL ? "None" : pwd;
}void MakeCommandLine(char cmd_prompt[], int size)
{snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());
}
// 输入命令行提示符
void PrintfCommandPrompt()
{char prompt[COMMAND_SIZE];// 将格式化内容(提示符行)输入进字符串promptMakeCommandLine(prompt, sizeof(prompt));printf("%s", prompt);fflush(stdout);
}//接收指令
bool GetCommandLine(char *out, int size)
{// ls -a -l => "ls -a -l\n"char *res = fgets(out, size, stdin);if(res == NULL)return false;out[strlen(out) - 1] = '\0';  // 消除 '\0'return strlen(out) == 0 ? false : true;
}int main()
{// 1.输出命令行提示符// printf("[%s@%s:%s]# ", GetUserName(), GetHostName(), GetPwd());PrintfCommandPrompt();// 2.获取用户输入的命令// char commandline[COMMAND_SIZE];// // ls -a -l => "ls -a -l\n"// char *res = fgets(commandline, COMMAND_SIZE, stdin);// commandline[strlen(commandline) - 1] = '\0';  // 消除 '\0'// printf("%s\n", commandline);char commandline[COMMAND_SIZE];if(GetCommandLine(commandline, sizeof(commandline))){printf("%s\n", commandline);}return 0;
}

其中值得注意的是,我们将命令提示符的内容使用 define 方便我们进行修改。

snprintf 是将格式化内容输入进字符串中,并给出字符串的起始地址和长度

🚩 运行结果如下:

2.2 将接收到的字符串拆开

我们系统读取指令肯定不是读取一个完整的字符串,而是拆分成一个个命令行参数,下面我们需要 将接收到的字符串拆开,比如:把 "ls -a -l" 拆成  "ls"  "-a"  "-l" 

我们可以使用 strtok 函数,将一个字符串按照特定的分隔符打散,将子串依次返回:

char* strtok(char* str, const char* delim);

💬 代码演示:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s:%s]#"// 下面是shell定义的全局数据
#define MAXARGC 128
char *g_argv[MAXARGC];
int g_argc = 0;// 用户名
const char *GetUserName()
{const char *name = getenv("USER");return name == NULL ? "None" : name;
}// 主机名
const char *GetHostName()
{const char *hostname = getenv("HOST");return hostname == NULL ? GetUserName() : hostname;
}// 当前路径
const char *GetPwd()
{const char *pwd = getenv("PWD");return pwd == NULL ? "None" : pwd;
}void MakeCommandLine(char cmd_prompt[], int size)
{snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());
}// 输入命令行提示符
void PrintfCommandPrompt()
{char prompt[COMMAND_SIZE];// 将格式化内容(提示符行)输入进字符串promptMakeCommandLine(prompt, sizeof(prompt));printf("%s", prompt);fflush(stdout);
}//接收指令
bool GetCommandLine(char *out, int size)
{// ls -a -l => "ls -a -l\n"char *res = fgets(out, size, stdin);if(res == NULL)return false;out[strlen(out) - 1] = '\0';  // 消除 '\0'return strlen(out) == 0 ? false : true;
}// 命令行拆分
bool CommandParse(char *commandline)
{g_argc = 0;g_argv[g_argc++] = strtok(commandline, " ");while(g_argv[g_argc++] = strtok(nullptr, " "));return true;
}
void PrintArgv()
{for(int i = 0; g_argv[i]; i++){printf("argv[%d]: %s\n", i, g_argv[i]);}
}int main()
{while(true){// 1.输出命令行提示符// printf("[%s@%s:%s]# ", GetUserName(), GetHostName(), GetPwd());PrintfCommandPrompt();// 2.获取用户输入的命令// char commandline[COMMAND_SIZE];// // ls -a -l => "ls -a -l\n"// char *res = fgets(commandline, COMMAND_SIZE, stdin);// commandline[strlen(commandline) - 1] = '\0';  // 消除 '\0'// printf("%s\n", commandline);char commandline[COMMAND_SIZE];if(GetCommandLine(commandline, sizeof(commandline))){printf("%s\n", commandline);}// 3.命令行分析  "ls -a -l" -> "ls" "-a" "-l"CommandParse(commandline);PrintArgv();}return 0;
}

🚩 运行结果如下:

2.3 通过创建进程和进程替换执行程序

我们命令存放在 g_argv 之中了,那要怎么执行呢?

我们命令可以看成一个个程序,所以为了不影响主进程,我们创建一个新的进程来执行命令,同时主进程进行进程等待,等待子进程的结束。

我们在子进程中要执行命令,自然需要使用进程替换,来把数组中的内容替换成进程,那么就需要用到带 v 的 exec 接口,因为程序名不带路径,所以需要从环境变量里面去找,要用到 execvp。

💬 代码演示:

// 4.执行命令
pid_t id = fork();
if(id == 0)
{// childexecvp(g_argv[0], g_argv);exit(1);
}
// father
pid_t rid = waitpid(id, nullptr, 0);
(void)rid;

🚩 运行结果如下:

2.4 让命令结果高亮

还有很多地方不完美,比如:在系统中使用 ls 命令,可执行程序是高亮的:

那么如何让我们的命令带颜色呢?

💬 代码演示:

// 命令行拆分
bool CommandParse(char *commandline)
{g_argc = 0;g_argv[g_argc++] = strtok(commandline, " ");if (strcmp(g_argv[0] , "ls") == 0)g_argv[g_argc++] = (char*)"--color=auto";while(g_argv[g_argc++] = strtok(nullptr, " "));g_argc--;return true;
}

🚩 运行结果如下:

2.5  内建命令:实现路径切换

目前还有一个问题,我们 cd.. 回退到上级目录时,我们的路径是不发生变化的:

真相:虽然系统中存在 cd 命令,但我们写的 shell 脚本中用的根本就不是这个 cd 命令。

当你在执行 cd 命令时,调用 execvp 执行的实际上是系统特定路径下的 cd,它只影响了子进程,如果我们直接 exec* 执行 cd,那么最多只是让子进程进行路径切换。但是请不要忘了:子进程是一运行就完毕的进程!运行完了你切换它的路径,毫无意义。所以,我们在 shell 中,更希望谁的路径发生变化呢?父进程!(shell 本身)

这部分由 shell 自己执行的命令,我们称之为 内建指令。

下面我们就来解决路径切换的问题,这一步应该需要在第四步之前,顶替执行命令成为第四步:

// 家目录路径
const char *GetHome()
{const char *home = getenv("HOME");return home == NULL ? "None" : home;
}// 检测并处理内建命令
bool CheckAndExecBuiltin()
{if(strcmp(g_argv[0], "cd") == 0){// g_argv 中只有一个参数 cdif(g_argc == 1){std::string home = GetHome();if(home.empty())return true;chdir(home.c_str());}else{std::string where = g_argv[1];chdir(where.c_str());}return true;}return false;
}int main()
{while(true){// ...// 4.检测并处理内建命令if(CheckAndExecBuiltin())continue;// ...}return 0;
}

🚩 运行结果如下:

似乎是成功执行了,但问题是命令行提示符这么为什么没变?

当一个进程的工作路径发生变化的时候,此时系统还存在一个环境变量 pwd。我们在系统中更改工作路径后,shell 会立刻更新环境变量 pwd,二者的值会保持一致,所以系统优化为使用命令 pwd 是查看的是当前工作路径,而不是去环境变量中查找 pwd。

而我们自己创建的 shell 并没有更新环境变量的功能,但是系统执行 pwd 命令还是按照原先查看当前工作路径,所以就能正常显示;而命令行提示符是从环境变量中获取的 pwd,所以依然保持不变。

那我们该怎么处理呢?我们使用一个系统调用:getcwd(),用来获取当前工作路径,并修改环境变量。

💬 代码演示:

char cwd[COMMAND_SIZE];// 当前路径
const char *GetPwd()
{// const char *pwd = getenv("PWD");const char *pwd = getcwd(cwd, sizeof(cwd));// 修改环境变量为当前路径if(pwd != NULL){snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);putenv(cwdenv);}return pwd == NULL ? "None" : pwd;
}

🚩 运行结果如下:

2.6 echo 命令

echo 同样也是内建命令,我们想打印环境变量,打印退出码等等,都需要 echo。

所以我们在执行程序时获取退出码 status,并将其保存在全局变量 lastcode 中,再在内建命令函数中实现 echo 函数。

// 最近一个程序的退出码
int lastcode = 0;bool Echo()
{if(g_argc == 2){// echo "hello"// echo $?// echo $PATHstd::string opt = g_argv[1];if(opt == "$?"){std::cout << lastcode << std::endl;lastcode = 0;}else if(opt[0] == '$'){std::string env_name = opt.substr(1);const char *env_value = getenv(env_name.c_str());if(env_value)std::cout << env_value << std::endl;}else if(opt[0] == '"'){std::string new_str(opt.begin() + 1, opt.end() - 1); // 去掉首尾双引号std::cout << new_str << std::endl;}}return true;
} // 检测并处理内建命令
bool CheckAndExecBuiltin()
{if(strcmp(g_argv[0], "cd") == 0){Cd();return true;}else if(strcmp(g_argv[0], "echo") == 0){Echo();return true;}return false;
}// 执行程序
int Execute()
{pid_t id = fork();if(id == 0){// childexecvp(g_argv[0], g_argv);exit(1);}int status = 0;// fatherpid_t rid = waitpid(id, &status, 0);if(rid > 0){lastcode = WEXITSTATUS(status);}(void)rid;return 0;
}int main()
{// ...return 0;
}

🚩 运行结果如下: 

2.7 再次理解环境变量

一个真正的 shell 还会存在一张环境变量表,启动的时候正常情况下是从系统配置文件中读取,但我们想模拟实现的时候,暂时做不到从配置文件中读取,所以我们的环境信息,从 父shell 中读取

获取环境变量,直接遍历环境变量列表就行:

// 环境变量表
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs = 0;// 创建环境变量表
void InitEnv()
{extern char** environ;memset(g_env, 0, sizeof(environ));g_envs = 0;//1. 获取环境变量for (int i = 0; environ[i]; i++){// 为每个环境变量分配内存并复制g_env[i] = new char[strlen(environ[i]) + 1]; // +1 用于 '\0' 结尾strcpy(g_env[i], environ[i]);g_envs++;}g_env[g_envs] = NULL;
}

获取完环境变量表之后,将其导入我的 shell:

//2. 导成环境变量
for(int i = 0; g_env[i]; i++)
{putenv(g_env[i]);
} 

为了证明这确实是我自己导入的环境变量表,我们在最后加入一个特殊的环境变量:

// ...g_env[g_envs++] = "HAHA=for_my_test";
g_env[g_envs] = NULL;// ...

🚩 运行结果如下: 

📚 环境变量的数据在进程的上下文中:

  1. 环境变量会被子进程继承下去,所以他会有全局属性。
  2. 当我们进行程序替换时, 当前进程的环境变量非但不会替换,而且是继承父进程的!
  • 如果你不传环境变量表,默认子进程全部都会自动继承。
  • 如果你 exel 函数簇带 e,就相当于你选择了自己传,就会覆盖式地把原本的环境变量弄没,然后你自己交给子进程。如果不带 e,那么环境变量就会自己被子进程继承。
  • 如果既不想覆盖系统,也不想新增,所以我们采用 putenv 的方式向父 Shell 获取导入新增一个它自己的环境变量,这样的话原始的环境变量还在,我们能在子进程的 shell 上下文上给它新增环境变量。

所以,如何理解环境变量具有全局属性?因为所有的环境变量会被当前进程之下的所有子进程默认继承下去。

如何在子进程 Shell 内部自己导入新增自己的环境变量?putenv,要注意的是,需要一个独立的空间,放置环境变量的数据被改写。

2.8 完整代码

我们为了美观,对应代码进行了一定程度的排版上的修改,功能无变化。

💬 代码演示:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s:%s]#"// 下面是shell定义的全局数据
// 1.命令行参数表
#define MAXARGC 128
char *g_argv[MAXARGC];
int g_argc = 0;// 2.环境变量表
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs = 0;// 测试使用
char cwd[COMMAND_SIZE];
char cwdenv[COMMAND_SIZE];// 最近一个程序的退出码
int lastcode = 0;// 用户名
const char *GetUserName()
{const char *name = getenv("USER");return name == NULL ? "None" : name;
}// 主机名
const char *GetHostName()
{const char *hostname = getenv("HOST");return hostname == NULL ? GetUserName() : hostname;
}// 当前路径
const char *GetPwd()
{// const char *pwd = getenv("PWD");const char *pwd = getcwd(cwd, sizeof(cwd));// 修改环境变量为当前路径if(pwd != NULL){snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);putenv(cwdenv);}return pwd == NULL ? "None" : pwd;
}// 家目录路径
const char *GetHome()
{const char *home = getenv("HOME");return home == NULL ? "None" : home;
}// 创建环境变量表
void InitEnv()
{extern char** environ;memset(g_env, 0, sizeof(environ));g_envs = 0;//1. 获取环境变量for (int i = 0; environ[i]; i++){// 为每个环境变量分配内存并复制g_env[i] = new char[strlen(environ[i]) + 1]; // +1 用于 '\0' 结尾strcpy(g_env[i], environ[i]);g_envs++;}g_env[g_envs++] = "HAHA=for_my_test";g_env[g_envs] = NULL;//2. 导成环境变量for(int i = 0; g_env[i]; i++){putenv(g_env[i]);}
}bool Cd()
{// g_argv 中只有一个参数: cdif(g_argc == 1){std::string home = GetHome();if(home.empty())return true;chdir(home.c_str());}else{std::string where = g_argv[1];chdir(where.c_str());}return true;
}void Echo()
{if(g_argc == 2){// echo "hello"// echo $?// echo $PATHstd::string opt = g_argv[1];if(opt == "$?"){std::cout << lastcode << std::endl;lastcode = 0;}else if(opt[0] == '$'){std::string env_name = opt.substr(1);const char *env_value = getenv(env_name.c_str());if(env_value)std::cout << env_value << std::endl;}else if(opt[0] == '"'){std::string new_str(opt.begin() + 1, opt.end() - 1); // 去掉首尾双引号std::cout << new_str << std::endl;}}
}// 测试:将格式化内容(提示符行)输入进字符串prompt
void MakeCommandLine(char cmd_prompt[], int size)
{snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());
}
// 1.输入命令行提示符
void PrintfCommandPrompt()
{char prompt[COMMAND_SIZE];// 将格式化内容(提示符行)输入进字符串promptMakeCommandLine(prompt, sizeof(prompt));printf("%s", prompt);fflush(stdout);
}// 2.接收指令
bool GetCommandLine(char *out, int size)
{// ls -a -l => "ls -a -l\n"char *res = fgets(out, size, stdin);if(res == NULL)return false;out[strlen(out) - 1] = '\0';  // 消除 '\0'return strlen(out) == 0 ? false : true;
}// 3.命令行拆分(导入命令行参数表)
bool CommandParse(char *commandline)
{g_argc = 0;g_argv[g_argc++] = strtok(commandline, " ");if (strcmp(g_argv[0] , "ls") == 0)g_argv[g_argc++] = (char*)"--color=auto";while(g_argv[g_argc++] = strtok(nullptr, " "));g_argc--;return g_argc > 0 ? true : false;
}// 测试:打印命令行参数 argv
void PrintArgv()
{for(int i = 0; g_argv[i]; i++){printf("argv[%d]: %s\n", i, g_argv[i]);}
}
// 4.检测并处理内建命令
bool CheckAndExecBuiltin()
{if(strcmp(g_argv[0], "cd") == 0){Cd();return true;}else if(strcmp(g_argv[0], "echo") == 0){Echo();return true;}return false;
}// 5.执行程序
int Execute()
{pid_t id = fork();if(id == 0){// childexecvp(g_argv[0], g_argv);exit(1);}int status = 0;// fatherpid_t rid = waitpid(id, &status, 0);if(rid > 0){lastcode = WEXITSTATUS(status);}(void)rid;return 0;
}int main()
{// shell启动的时候,从系统中获取环境变量// 我们自己的 shell 环境变量信息应该从父 shell 中获得InitEnv();while(true){// 1.输出命令行提示符PrintfCommandPrompt();// 2.获取用户输入的命令char commandline[COMMAND_SIZE];if(!GetCommandLine(commandline, sizeof(commandline)))continue;//printf("%s\n", commandline);// 3.命令行分析  "ls -a -l" -> "ls" "-a" "-l"if(!CommandParse(commandline))continue;// PrintArgv();// 4.检测并处理内建命令if(CheckAndExecBuiltin())continue;// 5.执行命令Execute();}return 0;
}
http://www.dtcms.com/wzjs/393583.html

相关文章:

  • 优秀b2c网站设计欣赏收录查询工具
  • 河南专业网站建设seo网站推广可以自己搞吗
  • 你建立的网站使用了那些营销方法搜索引擎营销流程是什么?
  • 黑马程序员前端培训费用优化网址
  • 重庆九龙坡营销型网站建设公司推荐解释seo网站推广
  • 湖南建筑信息网查询抖音优化是什么意思
  • 网站做的好的公司名称重庆关键词优化平台
  • 合肥做网站便宜mdyun企业网站建设方案模板
  • 宁夏公路建设管理局网站苏州seo排名公司
  • 北京东城网站建设公司淘宝seo具体优化方法
  • 成都网站建设龙兵网络域名服务器查询
  • 百度网站制作公司小企业广告投放平台
  • 男女做爰视频网站在线黑龙seo网站优化
  • 幼儿园手机网站模板十大放黄不登录不收费
  • 从哪个网站找钢做的微商正规网站优化哪个公司好
  • 怎么做有数据库的网站百度识图在线识别网页版
  • 怎样网站制作设计外国网站的浏览器
  • 沈阳营销型网站设计教程互联网项目推广
  • 做的好的购物网站外贸seo网站建设
  • 化妆品手机端网站模板搜索引擎优化的概念是什么
  • 为什么要进行电子商务网站规划男生最喜欢的浏览器推荐
  • 法学院网站建设建议网络营销的核心是什么
  • 网站建设论文答辩网站建成后应该如何推广
  • 建设高端网站公司的目的微信软文广告经典案例
  • 宿州网站推广外包网络推广营销
  • 邢台县教育局五库建设网站热搜关键词查询
  • 网页设计与制作教程电子教案完整seo排名关键词点击
  • 怎么做写真网站最吸引人的营销广告词
  • 建设网站北京市推广方案模板
  • 小区百货店网怎么做网站危机公关