【Linux】进程程序替换 做一个简易的shell

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

目录

文章目录

前言

进程程序替换

替换原理

先看代码和现象

替换函数

第一个execl():

第二个execv():

第三个execvp():

第四个execvpe():

环境变量

第五个execlp():

第六个execle():

函数解释

命名理解

在Makefile中形成两个可执行程序

方法一:

方法二:

做一个简易的shell

总结



前言

世上有两种耀眼的光芒,一种是正在升起的太阳,一种是正在努力学习编程的你!一个爱学编程的人。各位看官,我衷心的希望这篇博客能对你们有所帮助,同时也希望各位看官能对我的文章给与点评,希望我们能够携手共同促进进步,在编程的道路上越走越远!


提示:以下是本篇文章正文内容,下面案例可供参考

进程程序替换

替换原理

用fork()创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec系列的函数以执行另一个程序。当进程调用一种exec系列的函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的pid并未改变。

其实是操作系统将磁盘设备里的代码和数据加载到内存设备上了,也就是说exec系列的函数是系统调用接口或者exec系列的函数底层由系统调用。

先看代码和现象

#include <stdio.h>
#include <unistd.h>int main()
{printf("testexec ... begin!\n");execl("/usr/bin/ls", "ls", "-l", "-a", NULL);printf("testexec ... end!\n");return 0;
}

  • 用exec系列的函数执行起来新的程序。
  • exec系列的函数,执行完毕之后,后续的代码不见了,因为被替换了。
  • execl函数的返回值可以不用关心,只要替换成功,就不会向后继续运行;只要继续运行了,一定是替换失败了!

fork()创建子进程,让子进程自己去替换。

创建子进程,让子进程完成任务:1、让子进程执行父进程代码的一部分;2、让子进程执行一个全新的程序。

父进程创建一个子进程,子进程继承父进程的代码和数据,子进程刚开始的时候,用的是和父进程一样的代码和数据;但是当子进程中用exec系列函数执行新的进程时,会让新进程的代码和数据替换原来的代码和数据,不过因为各个进程之间都有独立性,所以,OS发生写实拷贝,将代码和数据复制一份,放入新申请的空间内,重新建立映射关系。

替换函数

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

#include <unistd.h>

第一个execl():

int execl(const char *path, const char *arg, ...);
  • 第一个参数:path:我们要执行的程序,需要带路径(怎么找到程序,你得告诉我)
  • 第二个参数:可变参数列表(命令行中怎么执行,你就怎么传参),并以NULL结尾

举一个例子:

execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);

第二个execv():

 int execv(const char *path, char *const argv[]);
  • 第一个参数:path:我们要执行的程序,需要带路径(怎么找到程序,你得告诉我)
  • 第二个参数:命令行参数列表(指针数组,数组当中的内容表示你要如何执行这个程序),指针数组要以NULL结尾

举一个例子:

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

第三个execvp():

int execvp(const char *file, char *const argv[]);
  • 第一个参数:用户可以不传要执行的文件的路径(但是文件名要传),直接告诉exec系列的函数,我要执行谁就行(注:查找这个程序,系统会自动在环境变量PATH中进行查找)。
  • 第二个参数:命令行参数列表(指针数组,数组当中的内容表示你要如何执行这个程序),指针数组要以NULL结尾。

举一个例子:

char *const argv[] = 
{(char*)"ls",(char*)"-a",(char*)"-b",NULL
};execvp("ls", argv);

第四个execvpe():

int execvpe(const char *file, char *const argv[], char *const envp[]);
  • 第一个参数:用户可以不传要执行的文件的路径(但是文件名要传),直接告诉exec系列的函数,我要执行谁就行(注:查找这个程序,系统会自动在环境变量PATH中进行查找)。
  • 第二个参数:命令行参数列表(指针数组,数组当中的内容表示你要如何执行这个程序),指针数组要以NULL结尾。
  • 第三个参数:环境变量表。
环境变量
  • 1、用老的环境变量给子进程,environ;
char *const argv[] = 
{(char*)"mypragma",(char*)"-a",(char*)"-b",NULL
};extern char**environ;execvpe("./mypragma", argv, environ);
  • 2、自定义环境变量:整体替换所有的环境变量
char *const argv[] = 
{(char*)"mypragma",(char*)"-a",(char*)"-b",NULL
};char *const envp[] =
{(char*)"HAHA=111111",(char*)"HEHE=222222",NULL
};execvpe("./mypragma", argv, environ);
  • 3、把老的环境变量稍加修改,给子进程
putenv("HHHH=111111111111111111");
// 将HHHH变量添加到当前进程的环境变量表里// 我的父进程main()本身就有一批环境变量!!!, 从bash来char *const argv[] = 
{(char*)"mypragma",(char*)"-a",(char*)"-b",NULL
};execvpe("./mypragma", argv, environ);

第五个execlp():

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

第六个execle():

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

函数解释

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

命名理解

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

  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量
函数名参数格式是否带路径是否使用当前环境变量
execl列表
execlp列表
execle列表否,需自己组装环境变量
execv数组
execvp数组
execve数组否,需自己组装环境变量

2号手册是系统调用接口 。
我们说的exec系列的函数不是2号手册(系统调用),而是三号手册。
exec系列的函数实际上是在C语言层面上做了一个简单的封装。

int execve(const char* path, char* const argv[], char* const envp[]); 
//2号手册

事实上,只有execve才是真正的系统调用,其它五个函数最终都是调用的execve,所以execve在man手册的第2节,而其它五个函数在man手册的第3节,也就是说其他五个函数实际上是对系统调用execve进行了封装,以满足不同用户的不同调用场景的。

下图为exec系列函数族之间的关系:

在Makefile中形成两个可执行程序

方法一:

Makefile在形成的时候,默认从上到下匹配时,只会默认形成第一个目标文件所对应的可执行程序,所以,我们要把Makefile文件中的两个程序倒一下,才能形成第二个可执行程序。

方法二:

那我们想要在Makefile中一次性形成两个可执行程序该怎么办呢?
我们可以定义一个尾目标,尾目标后面跟着两个可执行程序的名字。尾目标有依赖关系,不写依赖方法。

.PHONY:all
all : testexec mypragmatestexec : testexec.cgcc - o $@ $ ^
mypragma:mypragma.ccg++ - o $@ $ ^ -std = c++11
.PHONY:clean
clean :rm - f testexec mypragma

所有的脚本语言都要有一个对应的解释器(python、bash等)。解释器本身使用C/C++写的。
解释器就相当于一个可执行程序。
python3 test.py   // test.py:命令行参数,它就是一个文件;  
解释器会将命令行参数传进来,就知道解释器要解释那个文件了,在解释器的代码中将文件test.py打开,然后会一行一行解释。

做一个简易的shell

考虑下面这个与shell典型的互动:

[root@localhost epoll]# ls
client.cpp  readme.md  server.cpp  utility.h
[root@localhost epoll]# pwdPID TTY          TIME CMD
3451 pts / 0    00:00 : 00 bash
3514 pts / 0    00 : 00 : 00 pwd

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

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

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

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

命令行(命令行解释器、bash、父进程)本质上就是一个输出的字符串: [root@localhost epoll]# root:用户 localhost:主机名 epoll:路径
 

#define _CRT_SECURE_NO_WARNINGS 1#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>#define SIZE 512  // 在缓冲区定义一个命令行字符串
#define ZERO '\0'
#define SEP " "   // 定义分隔符为空格,分隔符为空格字符串 ----> strtok
#define NUM 32    // 定义指针数组(命令行参数标)当前有几个元素:命令 + 选项
// 写宏函数时,如果有代码块,一般建议放入do{ .... }while(0)里
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)// 环境变量本身就需要我们单独维护的
// 为了方便,我就直接定义环境变量,环境变量是cwd:当前的工作路径(缓冲区)
char cwd[SIZE * 2];
char* gArgv[NUM];
int lastcode = 0;// 退出码// 子进程创建失败,死去了
void Die()
{exit(1);
}// 返回用户家目录
const char* GetHome()
{const char* home = getenv("HOME");if (home == NULL) return "/";return home;
}// 获取用户名
const char* GetUserName()
{// getenv():根据环境变量名,得到环境变量的内容const char* name = getenv("USER");// 成功:返回环境变量的内容(字符串)  失败:返回NULLif (name == NULL) return "None";return name;
}// 获取当前的主机名
const char* GetHostName()
{const char* hostname = getenv("HOSTNAME");if (hostname == NULL) return "None";return hostname;
}
// 临时 获取当前的工作路径(有坑的)
const char* GetCwd()
{const char* cwd = getenv("PWD");if (cwd == NULL) return "None";return cwd;
}// 做一个命令行
// commandline : output输出型参数,我们像通过commandline把我们的命令行字符串获取出来
void MakeCommandLineAndPrint()
{char line[SIZE];// 定义一个命令行字符串的缓冲区const char* username = GetUserName();const char* hostname = GetHostName();const char* cwd = GetCwd();// 我们想要获取当前路径的最后一块路径SkipPath(cwd);// 更安全的进行把指定参数按照特定格式写入到指定长度的缓冲区当中snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, strlen(cwd) == 1 ? "/" : cwd + 1);// cwd+1:最后一个/的下一个位置// 第三个参数特殊处理了一下,当只剩最后一个"/"时,打印出来printf("%s", line);fflush(stdout);// 把标准输出显示一下命令行
}// 获取用户命令
int GetUserCommand(char command[], size_t n)
{// 从键盘中输入命令放入指定的缓存区中,缓存区大小为nchar* s = fgets(command, n, stdin);if (s == NULL) return -1;// 假设我们输入的字符串abcd,我们按回车就相当于换行(\n),// 此时字符串为abcd\n,五个字符,5-1下标为4,我们将下标为4的赋值为\0command[strlen(command) - 1] = ZERO;return strlen(command);// 获得命令有几个字符
}void SplitCommand(char command[], size_t n)
{(void)n;// "ls -a -l -n" -> "ls" "-a" "-l" "-n"gArgv[0] = strtok(command, SEP);int index = 1;while ((gArgv[index++] = strtok(NULL, SEP))); // done, 故意写成=,表示先赋值,在判断. 分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL, 并且while判断结束
}void ExecuteCommand()
{pid_t id = fork();if (id < 0) Die();else if (id == 0){// childexecvp(gArgv[0], gArgv);exit(errno);}else{// fahterint status = 0;pid_t rid = waitpid(id, &status, 0);if (rid > 0){lastcode = WEXITSTATUS(status);if (lastcode != 0) printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);}}
}void Cd()
{const char* path = gArgv[1];// 返回用户家目录if (path == NULL) path = GetHome();// path 一定存在// 切换一个进程的路径,进程的当前路径chdir(path);// 如果当前所在的路径发生变化了,一定要对环境变量更新,否则命令行解释器上的路径不会发生变化// 环境变量本来就是让父进程bash来维护的:导环境变量// 刷新环境变量char temp[SIZE * 2];// temp:临时的缓冲区getcwd(temp, sizeof(temp));// 重新获取绝对路径// 我们要导环境变量,就得把路径给刷新一下// 更安全的进行把指定参数按照特定格式写入到指定长度的缓冲区当中snprintf(cwd, sizeof(cwd), "PWD=%s", temp);// 我们每一次要刷新PWD环境变量时,我们都要采用绝对路径// putenv语意:存在就更新,不存在就设置putenv(cwd); // OK
}int CheckBuildin()
{int yes = 0;// 假设当前命令不是内建命令// 用户输入的命令const char* enter_cmd = gArgv[0];if (strcmp(enter_cmd, "cd") == 0){yes = 1;Cd();}else if (strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0){yes = 1;printf("%d\n", lastcode);lastcode = 0;}return yes;
}int main()
{int quit = 0;// 让命令行一直执行下去while (!quit){// 1. 我们需要自己输出一个命令行MakeCommandLineAndPrint();// 2. 获取用户命令字符串char usercommand[SIZE];// 定义一个用户命令字符串usercommand的缓冲区int n = GetUserCommand(usercommand, sizeof(usercommand));if (n <= 0) return 1;// 这里也不会存在越界的问题// 假设我们输入的字符串abcd,我们按回车就相当于换行(\n),此时字符串为abcd\n,五个字符,5-1下标为4,我们将下标为4的赋值为\0usercommand[strlen(usercommand) - 1] = ZERO;// 3. 命令行字符串分割. SplitCommand(usercommand, sizeof(usercommand));// 4. 检测命令是否是内建命令n = CheckBuildin();if (n) continue;// 5. 执行命令ExecuteCommand();}return 0;
}

当时我们讲的故事:命令行解释器就是bash(王婆),王婆给别人做命令行解释,把命令交给操作系统,通过程序替换的方式交给操作系统。

#include <stdio.h>
// 按行获取char *fgets(char *s,int size,FILE *stream);
// 按行进行从特定的文件流当中获取指定的内容,指定的内容放在s指向的缓冲区,缓冲区的大小是size。
// 成功:返回值是s指向缓冲区的起始地址  失败:返回NULL

每个进程都会记录当前所处的路径,父进程和子进程都分别有属于自己的路径。
我们今天实现的shell,执行任何命令,都是要执行fork()创建子进程的,所以,当我们在shell中执行 cd .. 命令的时候,是让子进程去执行去了,子进程把自己的路径切换了,但和当前的bash没有关系。
命令行是属于父进程bash的,所以, cd .. 这样的命令应该让父进程去执行。
要让父进程执行的命令,我们叫做内建命令

// 切换一个进程的路径 man chdir 系统调用
int chdir(const char *path)
// man getcwd :获取一下当前的工作目录;所以不管修改的是绝对路径,还是相对路径,重新获取一下,就是绝对路径
char *getcwd(char *buf,size_t size)

总结

好了,本篇博客到这里就结束了,如果有更好的观点,请及时留言,我会认真观看并学习。
不积硅步,无以至千里;不积小流,无以成江海。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/783032.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

编程语言|C语言——C语言操作符的详细解释

这篇文章主要详细介绍了C语言的操作符&#xff0c;文中通过示例代码介绍的非常详细&#xff0c;对大家的学习或者工作具有一定的参考学习价值&#xff0c;需要的朋友们下面随着小编来一起学习学习吧 一、基础 1.1 算数操作符 - * / % - * / 这些操作符是我们…

QT初识(1)

QT初识 桌面开发什么是QT下载QT安装好之后的工具AssisantDesignerQT Creator 创建一个简单的项目 我们今天来认识一下QT。 桌面开发 在了解QT&#xff0c;我们得了解一下桌面开发&#xff1a; 桌面开发指的是编写和构建在个人计算机或其他桌面操作系统&#xff08;如Windows、…

关系网络c++

题目&#xff1a; 代码&#xff1a; #include<bits/stdc.h>using namespace std;int n,x,y;struct node{int num;//编号 int t;//步数 node(){}node(int sum,int tt){numsum;ttt;} }; int mp[101][101];//图 bool flag[101];//标记 queue<node> q; void bfs() {q…

【Docker】Windows中打包dockerfile镜像导入到Linux

【Docker】Windows中打包dockerfile镜像导入到Linux 大家好 我是寸铁&#x1f44a; 总结了一篇【Docker】Windows中打包dockerfile镜像导入到Linux✨ 喜欢的小伙伴可以点点关注 &#x1f49d; 前言 今天遇到一个新需求&#xff0c;如何将Windows中打包好的dockerfile镜像给迁移…

Autodesk Maya 2025---智能建模与动画创新,重塑创意工作流程

Autodesk Maya 2025是一款顶尖的三维动画软件&#xff0c;广泛应用于影视广告、角色动画、电影特技等领域。新版本在功能上进行了全面升级&#xff0c;新增了对Apple芯片的支持&#xff0c;建模、绑定和角色动画等方面的功能也更加出色。 在功能特色方面&#xff0c;Maya 2025…

equals()和hashcode()的区别【大白话Java面试题】

equals()和hashcode()的区别 大白话 1.equals()&#xff1a;反应的是对象或变量具体的值&#xff0c;及两个对象包含的具体的值&#xff08;可能是对象的引用&#xff0c;也可能是值类型的值&#xff09; 2.hashcode():计算两个对象的哈希值&#xff0c;并返回哈希码&#xff…

逆向分析之antibot

现在太卷了&#xff0c;没资源&#xff0c;很难接到好活&#xff0c;今天群里看到个单子&#xff0c;分析了下能做&#xff0c;结果忙活了一小会&#xff0c;幸好问了下&#xff0c;人家同时有多个人再做&#xff0c;直接就拒绝再继续了。就这次忘了收定金了&#xff0c;所以原…

使用python实现i茅台自动预约

使用python实现i茅台自动预约[仅限于学习,不可商用] 运行: 直接运行 imtApi.py 打包:切换到imt脚本目录,执行打包命令: pyinstaller --onefile imtApi.py这个应用程序可以帮助你进行茅台自动化配置。以下是一些使用说明: 平台注册账号(可用i茅台)不用登录,你可以进行…

Linux的VirtualBox中USB设备无法选择USB3.0怎么办?

在VirtualBox中&#xff0c;如果遇到USB设备无法选择 USB 3.0 的问题&#xff0c;可以尝试按照以下步骤来解决&#xff1a; 确保VirtualBox版本支持USB 3.0&#xff1a;首先&#xff0c;你需要确认你的VirtualBox版本是否支持USB 3.0。一些较旧的版本可能不支持&#xff0c;因此…

一篇搞定AVL树+旋转【附图详解旋转思想】

&#x1f389;个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名乐于分享在学习道路上收获的大二在校生 &#x1f648;个人主页&#x1f389;&#xff1a;GOTXX &#x1f43c;个人WeChat&#xff1a;ILXOXVJE &#x1f43c;本文由GOTXX原创&#xff0c;首发CSDN&…

【Effective Web】页面优化

页面优化 页面渲染流程 JavaScript 》 Style 》 Layout 》 Paint 》 Composite 首先js做了一些逻辑&#xff0c;触发了样式变化&#xff0c;style计算好这些变化后&#xff0c;把影响的dom元素进行重新布局&#xff08;layout&#xff09;,再画到画布中&#xff08;Paint&am…

半导体工艺技术

完整内容点击&#xff1a;【半导体工艺技术】

将jupyter notebook文件导出为pdf(简单有效)

1.打开jupyter notebook笔记&#xff1a; 2.点击file->print Preview 3.在新打开的页面右键打印 4.另存为PDF 5.保存即可 6.pdf效果 &#xff08;可能有少部分图片显示不了&#xff09; 网上也有其他方法&#xff0c;比如将其转换为.tex再转为PDF等&#xff0c;但个人觉…

ubuntu 中安装docker

1 资源地址 进入ubuntu官网下载Ubuntu23.04的版本的镜像 2 安装ubuntu 这里选择再Vmware上安装Ubuntu23.04.6 创建一个虚拟机&#xff0c;下一步下一步 注意虚拟机配置网络桥接&#xff0c;CD/DVD选择本地的镜像地址 开启此虚拟机&#xff0c;下一步下一步等待镜像安装。 3…

数据可视化-ECharts Html项目实战(8)

在之前的文章中&#xff0c;我们学习了如何设置散点图涟漪效果与仪表盘动态指针效果。想了解的朋友可以查看这篇文章。同时&#xff0c;希望我的文章能帮助到你&#xff0c;如果觉得我的文章写的不错&#xff0c;请留下你宝贵的点赞&#xff0c;谢谢 今天的文章&#xff0c;会…

【c++】类和对象(六)深入了解隐式类型转换

&#x1f525;个人主页&#xff1a;Quitecoder &#x1f525;专栏&#xff1a;c笔记仓 朋友们大家好&#xff0c;本篇文章我们来到初始化列表&#xff0c;隐式类型转换以及explicit的内容 目录 1.初始化列表1.1构造函数体赋值1.2初始化列表1.2.1隐式类型转换与复制初始化 1.3e…

python基础——文件操作【文件编码、文件的打开与关闭操作、文件读写操作】

&#x1f4dd;前言&#xff1a; 这篇文章主要讲解一下python中对于文件的基础操作&#xff1a; 1&#xff0c;文件编码 2&#xff0c;文件的打开与关闭操作 3&#xff0c;文件读写操作 &#x1f3ac;个人简介&#xff1a;努力学习ing &#x1f4cb;个人专栏&#xff1a;C语言入…

04 | Swoole 源码分析之 epoll 多路复用模块

首发原文链接&#xff1a;Swoole 源码分析之 epoll 多路复用模块 大家好&#xff0c;我是码农先森。 引言 在传统的IO模型中&#xff0c;每个IO操作都需要创建一个单独的线程或进程来处理&#xff0c;这样的操作会导致系统资源的大量消耗和管理开销。 而IO多路复用技术通过…

OceanBase OBCA 数据库认证专员考证视频

培训概述 OceanBase 认证是 OceanBase 官方推出的唯一人才能力认证体系&#xff0c;代表了阿里巴巴及蚂蚁集团官方对考生关于 OceanBase 技术能力的认可&#xff0c;旨在帮助考生更好地学习 OceanBase 数据库产品&#xff0c;早日融入 OceanBase 技术生态体系&#xff0c;通过由…

MYSQL——索引概念索引结构

索引 索引是帮助数据库高效获取数据的排好序的数据结构。 有无索引时&#xff0c;查询的区别 主要区别在于查询速度和系统资源的消耗。 查询速度&#xff1a; 在没有索引的情况下&#xff0c;数据库需要对表中的所有记录进行扫描&#xff0c;以找到符合查询条件的记录&#…