0


【Linux】剧幕中的灵魂更迭:探索Shell下的程序替换

🎬 个人主页:谁在夜里看海.****

📖 个人专栏:《C++系列》《Linux系列》《算法系列》

⛰️ 一念既出,万山无阻



📖一、进程程序替换

上一篇博客我们讲到了进程的诞生过程:父进程调用fork创建子进程,子进程执行父进程相同的程序。但是很多时候我们希望子进程执行另一个程序,此时就要用到exec函数调用,子进程中调用exec函数之后,该程序就会被调用的程序代替,这就是程序替换

1.替换的演示

  1. #include<stdio.h>
  2. #include<unistd.h>
  3. int main()
  4. {
  5. int a = 0;
  6. a++;
  7. execl("/usr/bin/pwd", "pwd", NULL);
  8. printf("%d\n", a++);
  9. }

此时程序执行结果:

我们可以看到,原先的程序执行结果应该是打印变量a,但是被替换成了pwd指令(指令本身也是一个可执行程序),这就是程序替换的过程:当进程调用exec函数时,该进程的代码和数据完全被新程序替换,从新程序的启动例程开始执行。

❓替换与执行流
  1. int main()
  2. {
  3. int a = 0;
  4. printf("Before: %d\n",a++);
  5. execl("/usr/bin/pwd", "pwd", NULL);
  6. printf("After: %d\n", a++);
  7. }

不对呢,不是说程序替换之后原来的代码和数据都会被替换吗,那为什么这里还会显示原程序的打印信息呢?下面进行分析:

✅虽然进程调用exec函数后会发生程序替换,原程序的代码和数据会被覆盖,但在调用

  1. exec

函数之前,执行流还是要经过原来的步骤的,上述代码中,在调用execl之前,执行流先执行printf函数代码,由于以“\n”结尾,输出缓冲区的数据会被刷新到终端,所以我们能看到“Before: 0”:

修改一下代码,结尾不加“\n”, 此时数据会被保留在输出缓冲区当中,后面又因为发生程序替换,缓冲区的内容被清除了,所以最终终端不会显示"Before: 0"内容:

  1. int main()
  2. {
  3. int a = 0;
  4. printf("Before: %d",a++);
  5. execl("/usr/bin/pwd", "pwd", NULL);
  6. printf("After: %d\n", a++);
  7. }

❓程序替换≠进程替换

程序替换会改变进程的执行内容,但它不会改变进程的进程ID,也就是说,进程还是原来的进程,程序替换并不是进程替换,且看下面示例:

先写一个可执行程序test2,源代码为:

  1. #include<stdio.h>
  2. #include<unistd.h>
  3. int main()
  4. {
  5. // 打印当前pid,ppid
  6. printf("After: pid = %d, ppid = %d\n",getpid(),getppid());
  7. }

另一个可执行程序test源代码为:

  1. #include<stdio.h>
  2. #include<unistd.h>
  3. int main()
  4. {
  5. // \n结尾直接打印当前内容
  6. printf("Before: pid = %d, ppid = %d\n",getpid(),getppid());
  7. // 程序替换成test2
  8. execl("/home/ywh/linux_gitee/test_excel/test2", "test2", NULL);
  9. }

test执行结果:

我们可以看到,程序替换前后都是同一个进程,结论:exec并不创建新进程。

2.替换的原理

📚 系统调用exec
  1. exec

系列函数(如

  1. execl

,

  1. execv

,

  1. execve

等)是用来将当前进程的内存空间、程序代码段、数据段等替换成一个新的程序。该系统调用不会创建新进程,而是直接用新程序替换当前进程的内容。

具体来说,

  1. exec

调用会:

①:清空当前进程的代码段、数据段、堆栈等。

②:加载并执行新程序的代码段、数据段、堆栈等。

③:保留当前进程的进程 ID (PID)、父进程标识符 (PPID)、文件描述符等。

📚 进程控制块 (PCB)

操作系统通过 进程控制块 (PCB) 来管理进程,每个进程都有一个独立的 PCB,包含了进程的各种状态信息,比如进程的 PID、父进程 ID、程序计数器、堆栈指针等。

当调用

  1. exec

时,进程的 PCB 中的状态信息并没有被改变,操作系统只会根据

  1. exec

调用的参数加载新的程序内容(代码段、数据段等),并且更新程序计数器和堆栈指针等信息。

📚 内存管理

操作系统中的内存管理模块负责为进程分配内存。当进程调用

  1. exec

时,操作系统会:

①:释放原进程的内存(代码段、数据段、堆栈)。

②:加载新程序的内存:从磁盘(例如 ELF 文件或其他可执行文件)中加载新的程序到内存,包括新的代码段、数据段等。

③:更新堆栈和堆的布局,准备新程序的运行环境。

3. 替换的函数

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

  1. #include <unistd.h>
  2. int execl(const char *path, const char *arg, ...);
  3. int execlp(const char *file, const char *arg, ...);
  4. int execle(const char *path, const char *arg, ...,char *const envp[]);
  5. int execv(const char *path, char *const argv[]);
  6. int execvp(const char *file, char *const argv[]);

为了便于理解,我们可以把exec后面出现的 **l、p、e、v **看作exec的四个选项,下面我们依次介绍这些选项:

📚 execl

**l(list) : 参数采用列表 **

path:表示要执行的程序路径;

arg:表示程序本身的参数,第一个是程序本身的名称,后续为程序的参数(传递系统指令时,参数就是指令的选项),必须以NULL结尾。

示例:

  1. execl("/bin/ls", "ls", "-l", (char *)NULL);
📚 execv

v(vector) : 参数用数组

path:表示要执行的程序路径;

argv:参数列表,程序的参数以数组的形式传递,数组内部也必须以NULL结尾。

示例:

  1. execv("/bin/ls", (char *[]){"ls", "-l", NULL});
📚 execp

p(path) : 自动搜索环境变量PATH

它可以通过环境变量

  1. PATH

来查找可执行文件,而不需要提供绝对路径。

示例:

  1. execlp("ls", "ls", "-l", (char *)NULL);
📚 exece

**e(env) : 表示自己维护环境变量 **

  1. execle

允许显式地传递一个 环境变量数组,而不是继承当前进程的环境变量。通过

  1. execle

,你可以自定义新进程的环境变量。

示例:

  1. char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
  2. execle("ps", "ps", "-ef", NULL, envp);
🚩本质

事实上,只有execve才是真正的系统调用,而其他四个函数最后都会调用execve:

📖二、命令行解释器shell

我们在linux学习过程中离不开shell,shell是命令行解释工具,是用户与内核之间的工具,提供了一个接口,通过它,我们可以执行命令、启动程序等与操作系统进行交互。shell解析用户输入的命令,返回执行结果。

❓shell的本质是什么呢?

1.shell的本质

✅shell本质其实是一个进程:

当我们启动一个终端或打开一个命令行窗口的时候,相当于启动了一个shell进程(也叫bash进程),这个进程会等待用户输入的命令,并将命令通过系统调用传递给内核,内核执行相应的操作后,返回给shell。

shell的工作原理就是循环以下操作

1️⃣获取命令行 --> 2️⃣解析命令行 --> 3️⃣fork创建子进程

--> 4️⃣execve替换子进程 --> 5️⃣wait等待子进程退出 ->1️⃣

根据这些思路,我们可以模拟实现一个shell:

2.shell的模拟实现

实现一个简化版的shell,需要执行以下功能:

① 获取当前工作目录、用户名、主机名。

② 解析用户输入的命令行并执行命令。

③ 内置支持一些常见命令,如

  1. cd

  1. echo

  1. export

等。

④ 创建子进程来执行普通命令,并支持基本的命令分割和管道处理。

📚头文件
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <assert.h>
  5. #include <unistd.h>
  6. #include <sys/types.h>
  7. #include <sys/wait.h>

这些头文件提供了标准输入输出、字符串处理、系统调用等功能。unistd包含与进程相关的函数(如fork,exit)

📚宏定义
  1. #define LEFT "["
  2. #define RIGHT "]"
  3. #define LABLE "#"
  4. #define DELIM " \t"
  5. #define LINE_SIZE 1024
  6. #define ARGC_SIZE 32
  7. #define EXIT_CODE 44

LEFT、RIGHT、LABLE:用于命令行提示符的格式化;

DELIM:用于命令行字符串的分隔符;

LINE_SIZE、ARGC_SIZE:定义了命令行和参数的缓冲区大小;

EXIT_CODE:用于子进程异常退出的返回值。

📚全局变量
  1. int lastcode = 0;
  2. int quit = 0;
  3. extern char **environ;
  4. char commandline[LINE_SIZE];
  5. char *argv[ARGC_SIZE];
  6. char pwd[LINE_SIZE];
  7. char myenv[LINE_SIZE];

lastcode:保存上一个命令的退出码;

quit:用于控制shell是否退出;

commandline:存储用户输入的命令行字符串;

argv:存储解析后的命令和参数;

pwd:保存当前工作目录;

myenv:存储自定义的环境变量。

📚获取信息
  1. const char *getusername() {
  2. return getenv("USER");
  3. }
  4. const char *gethostname() {
  5. return getenv("HOSTNAME");
  6. }
  7. void getpwd() {
  8. getcwd(pwd, sizeof(pwd));
  9. }

getusername:获取用户名

gethostname:获取主机名

getpwd:获取当前工作目录

📚交互式命令行输入
  1. void interact(char *cline, int size) {
  2. getpwd();
  3. printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), gethostname(), pwd);
  4. char *s = fgets(cline, size, stdin);
  5. assert(s);
  6. (void)s;
  7. cline[strlen(cline)-1] = '\0';
  8. }

interact函数显示格式化的提示符,并等待用户输入命令。输入命令存储在cline中;输入的命令符末行换行符替换成终止符 '\0'。

📚字符串分割
  1. int splitstring(char cline[], char *_argv[]) {
  2. int i = 0;
  3. argv[i++] = strtok(cline, DELIM);
  4. while(_argv[i++] = strtok(NULL, DELIM));
  5. return i - 1;
  6. }

splitstring函数使用strtok将输入的命令行字符串按空格和制表符分割成多个命令或参数,存储在指针数组argv中。

📚内置命令
  1. int buildCommand(char *_argv[], int _argc) {
  2. if(_argc == 2 && strcmp(_argv[0], "cd") == 0) {
  3. chdir(argv[1]);
  4. getpwd();
  5. sprintf(getenv("PWD"), "%s", pwd);
  6. return 1;
  7. }
  8. else if(_argc == 2 && strcmp(_argv[0], "export") == 0) {
  9. strcpy(myenv, _argv[1]);
  10. putenv(myenv);
  11. return 1;
  12. }
  13. else if(_argc == 2 && strcmp(_argv[0], "echo") == 0) {
  14. if(strcmp(_argv[1], "$?") == 0) {
  15. printf("%d\n", lastcode);
  16. lastcode=0;
  17. }
  18. else if(*_argv[1] == '$') {
  19. char *val = getenv(_argv[1]+1);
  20. if(val) printf("%s\n", val);
  21. }
  22. else {
  23. printf("%s\n", _argv[1]);
  24. }
  25. return 1;
  26. }
  27. if(strcmp(_argv[0], "ls") == 0) {
  28. _argv[_argc++] = "--color";
  29. _argv[_argc] = NULL;
  30. }
  31. return 0;
  32. }

提供了几个内置命令:

cd:改变当前目录

export:设置一个新的环境变量

enho:打印变量值或退出码

📚普通命令

队友普通命令的执行,需要调用exec程序替换成目标命令的程序:

  1. void NormalExcute(char *_argv[]) {
  2. pid_t id = fork();
  3. if(id < 0) {
  4. perror("fork");
  5. return;
  6. }
  7. else if(id == 0) {
  8. execvp(_argv[0], _argv);
  9. exit(EXIT_CODE);
  10. }
  11. else {
  12. int status = 0;
  13. pid_t rid = waitpid(id, &status, 0);
  14. if(rid == id) {
  15. lastcode = WEXITSTATUS(status);
  16. }
  17. }
  18. }

NormalExcute使用fork创建子进程,子进程调用execvp,替换当前程序,父进程等待子进程结束。

📚main函数
  1. int main() {
  2. while(!quit) {
  3. interact(commandline, sizeof(commandline));
  4. int argc = splitstring(commandline, argv);
  5. if(argc == 0) continue;
  6. int n = buildCommand(argv, argc);
  7. if(!n) NormalExcute(argv);
  8. }
  9. return 0;
  10. }

main函数进入循环,不断接收用户输入的命令并解析执行。

如果命令是内置命令,则在当前进程中执行;如果是普通命令,通过程序替换在子进程中执行。


以上就是【剧幕中的灵魂更迭:探索Shell下的程序替换】的全部内容,欢迎指正~

码文不易,还请多多关注支持,这是我持续创作的最大动力!

标签: linux 运维 服务器

本文转载自: https://blog.csdn.net/dhgiuyawhiudwqha/article/details/144005648
版权归原作者 谁在夜里看海. 所有, 如有侵权,请联系我们删除。

“【Linux】剧幕中的灵魂更迭:探索Shell下的程序替换”的评论:

还没有评论