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

news2024/11/27 13:40:29

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

目录

文章目录

前言

进程程序替换

替换原理

先看代码和现象

替换函数

第一个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 mypragma

testexec : testexec.c
    gcc - o $@ $ ^
mypragma:mypragma.cc
     g++ - 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]# pwd
   PID 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");
    // 成功:返回环境变量的内容(字符串)  失败:返回NULL
    if (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)
{
    // 从键盘中输入命令放入指定的缓存区中,缓存区大小为n
    char* s = fgets(command, n, stdin);
    if (s == NULL) return -1;
    // 假设我们输入的字符串abcd,我们按回车就相当于换行(\n),
    // 此时字符串为abcd\n,五个字符,5-1下标为4,我们将下标为4的赋值为\0
    command[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)
    {
        // child
        execvp(gArgv[0], gArgv);
        exit(errno);
    }
    else
    {
        // fahter
        int 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的赋值为\0
        usercommand[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.coloradmin.cn/o/1558430.html

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

相关文章

android WMS服务

android WMS服务 WMS的定义 窗口的分类 WMS的启动 WindowManager Activity、Window、DecorView、ViewRootImpl 之间的关系 WindowToken WMS的定义 WMS是WindowManagerService的简称&#xff0c;它是android系统的核心服务之一&#xff0c;它在android的显示功能中扮演着…

YOLOv9改进策略 :卷积魔改 | 感受野注意力卷积运算(RFAConv)

💡💡💡本文改进内容:感受野注意力卷积运算(RFAConv),解决卷积块注意力模块(CBAM)和协调注意力模块(CA)只关注空间特征,不能完全解决卷积核参数共享的问题 💡💡💡使用方法:代替YOLOv9中的卷积,使得更加关注感受野注意力,提升性能 💡💡💡RFAConv…

vue3:通过【自定义指令】实现自定义的不同样式的tooltip

一、效果展示 vue3自定义不同样式的tooltip 二、实现思路 1.ts文件 在ts文件中创建一个全局容器 import一个容器组件&#xff0c;用于存放自定义的各式组件 创建一个指令并获取到指令传递的数据&#xff0c;并为容器组件传值 2.容器组件 用于存放自定义Tooltip样式的组件…

最新2024年增强现实(AR)营销指南(完整版)

AR营销是新的最好的东西&#xff0c;就像元宇宙和VR营销一样。利用AR技术开展营销活动可以带来广泛的利润优势。更不用说&#xff0c;客户也喜欢AR营销&#xff01; 如果企业使用AR&#xff0c;71%的买家会更多地购物。40%的购物者准备在他们可以在AR定制的产品上花更多的钱。…

详解Java线程的状态

一、观察线程的所有状态 线程的状态是⼀个枚举类型 Thread.State public class ThreadState {public static void main(String[] args) {for (Thread.State state : Thread.State.values()) {System.out.println(state);}} } NEW: 安排了⼯作, 还未开始⾏动 RUNNABLE: 可⼯…

JavaSE day16笔记 - string

第十六天课堂笔记 学习任务 Comparable接口★★★★ 接口 : 功能的封装 > 一组操作规范 一个抽象方法 -> 某一个功能的封装多个抽象方法 -> 一组操作规范 接口与抽象类的区别 1本质不同 接口是功能的封装 , 具有什么功能 > 对象能干什么抽象类是事物本质的抽象 &…

MYSQL——索引概念索引结构

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

《深入理解计算机系统》学习(9):链接和执行

目录 一、链接1.1 编译器驱动程序1.2 链接任务 二、目标文件2.1 目标文件三种形式2.2 可重定位目标文件 三、符号3.1 符号表3.2 符号解析3.3 链接器解析多重定义的全局符号 四、重定位4.1 重定位条目4.2 重定位符号引用 五、可执行目标文件5.1 可执行文件结构5.2 加载可执行目标…

设置asp.net core WebApi函数请求参数可空的两种方式

以下面定义的asp.net core WebApi函数为例&#xff0c;客户端发送申请时&#xff0c;默认三个参数均为必填项&#xff0c;不填会报错&#xff0c;如下图所示&#xff1a; [HttpGet] public string GetSpecifyValue(string param1,string param2,string param3) {return $"…

C++格式化输入和输出

格式化输入与输出 除了条件状态外&#xff0c;每个iostream对象还维护一个格式状态来控制IO如何格式化的细节。 格式状态控制格式化的某些方面&#xff0c;如整型值是几进制、浮点值的精度、一个输出元素的宽度等。 标准库定义了一组操纵符来修改流的格式状态。 一个操纵符…

【进程IO】详细讲解文件描述符fd

文章目录 前言什么叫文件描述符FILE与fd的关系 再次理解文件为什么要有文件的方法列表呢&#xff1f; 进程和struct file的关系再次理解open操作 前言 C语言的关于文件操作的各种函数实际上是对系统调用的封装。那么从进程的角度看&#xff0c;一个文件到底是如何被描述的呢&a…

Postwoman 安装

Postwoman作为Postman的女朋友&#xff0c;具有免费开源、轻量级、快速且美观等特性&#xff0c;是一款非常好用的API调试工具。能帮助程序员节省时间&#xff0c;提升工作效率。 Github地址&#xff1a;GitHub - hoppscotch/hoppscotch: &#x1f47d; Open source API devel…

Qt/QML编程之路:画线及倒车影响(48)

前言: 倒车影像中有一个属性比较实用,那就是倒车线,这条线很明显会在视频图像上叠加显示,或者说在视频上面一个图层画的线。这里有一个画线的Qt示例,用于在一个scene上画一个对角线: #include "mainwindow.h" #include <QApplication> #include <QtW…

ES6 学习(一)-- 基础知识

文章目录 1. 初识 ES62. let 声明变量3. const 声明常量4. 解构赋值 1. 初识 ES6 ECMAScript6.0(以下简称ES6)是JavaScript语言的下一代标准&#xff0c;已经在2015年6月正式发布了。它的目标&#xff0c;是使得」JavaScript语言可以用来编写复杂的大型应用程序&#xff0c;成为…

关系网络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…

FLASH的读取与写入

FLASH的写入 结合HAL库所给参数&#xff1a; 查阅具体使用芯片的参考手册。 就不在详细解释&#xff0c;英文自行翻译。具体代码如下&#xff1a; /*FLASH写入程序*/ void WriteFlashTest(uint32_t L, uint32_t addr, uint32_t *Data,int Page) {int i0;/* 1/4解锁FLASH*/HAL…

【Anaconda】Linux下Anaconda安装和虚拟环境配置

Linux下Anaconda安装和虚拟环境配置 一、安装anaconda二、conda虚拟环境管理三、jupyter相关启动部署四、遇到问题 下面介绍整体流程&#xff0c;遇到问题优先看“遇到问题章节”&#xff01; 一、安装anaconda 1.下载anaconda安装包 &#xff08;1&#xff09;可以选择在官网…

文件名目录名或卷标语法不正确:数据恢复策略与预防措施

一、文件名目录名或卷标语法不正确的现象 在日常使用电脑或移动设备时&#xff0c;我们经常会遇到“文件名目录名或卷标语法不正确”的错误提示。这种错误通常发生在尝试访问、修改或删除文件、目录或卷标时&#xff0c;系统会提示无法完成操作&#xff0c;因为文件名、目录名…

JavaScript高级 —— 学习(二)

目录 一、深入对象 &#xff08;一&#xff09;创建对象三种方式 1.利用对象字面量创建 2.利用 new Object() 创建 3.利用构造函数创建 &#xff08;二&#xff09;利用构造函数创建对象 1.构造函数介绍 2.约定 3.实例化执行过程 &#xff08;三&#xff09;实例成员…

动态规划之子序列(一)

300.最长递增子序列 给你一个整数数组 nums &#xff0c;找到其中最长严格递增子序列的长度。 子序列是由数组派生而来的序列&#xff0c;删除&#xff08;或不删除&#xff09;数组中的元素而不改变其余元素的顺序。例如&#xff0c;[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序…