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

这篇具有很好参考价值的文章主要介绍了【Linux】进程程序替换 && 做一个简易的shell。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

目录

文章目录

前言

进程程序替换

替换原理

先看代码和现象

替换函数

第一个execl():

第二个execv():

第三个execvp():

第四个execvpe():

环境变量

第五个execlp():

第六个execle():

函数解释

命名理解

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

方法一:

方法二:

做一个简易的shell

总结



前言

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


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

进程程序替换

替换原理

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

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

【Linux】进程程序替换 && 做一个简易的shell,Linux,linux,运维,服务器

先看代码和现象

#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;
}

【Linux】进程程序替换 && 做一个简易的shell,Linux,linux,运维,服务器

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

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

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

【Linux】进程程序替换 && 做一个简易的shell,Linux,linux,运维,服务器

父进程创建一个子进程,子进程继承父进程的代码和数据,子进程刚开始的时候,用的是和父进程一样的代码和数据;但是当子进程中用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系列函数族之间的关系:

【Linux】进程程序替换 && 做一个简易的shell,Linux,linux,运维,服务器

在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程序并等待那个进程结束。

【Linux】进程程序替换 && 做一个简易的shell,Linux,linux,运维,服务器

然后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)

总结

好了,本篇博客到这里就结束了,如果有更好的观点,请及时留言,我会认真观看并学习。
不积硅步,无以至千里;不积小流,无以成江海。文章来源地址https://www.toymoban.com/news/detail-847329.html

到了这里,关于【Linux】进程程序替换 && 做一个简易的shell的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 进程程序替换+简易版shell实现

    什么是进程程序替换? 指在一个正在运行的进程中,将原来的程序替换成新的程序的过程。 eg:如果我们想让fork创建出来的子进程执行全新的任务,此时就需要进程程序替换 为什么要进程程序替换呢? 我们一般在服务器设计的时候(linux编程的时候)往往需要子进程干两种

    2024年02月05日
    浏览(37)
  • 【Linux】教你用进程替换制作一个简单的Shell解释器

    本章的代码可以访问这里获取。 由于程序代码是一体的,本章在分开讲解各部分的实现时,代码可能有些跳跃,建议在讲解各部分实现后看一下源代码方便理解程序。 我们想要制作一个简单的 Shell 解释器,需要先观察Shell是怎么运行的,根据 Shell 的运行状态我们再去进行模

    2024年02月02日
    浏览(64)
  • 【Linux】Linux进程控制 --- 进程创建、终止、等待、替换、shell派生子进程的理解…

    柴犬: 你好啊,屏幕前的大帅哥or大美女,和我一起享受美好的今天叭😃😃😃 1. 在调用fork函数之后, 当执行的程序代码转移到内核中的fork代码后 ,内核需要分配 新的内存块 和 内核数据结构 给子进程, 内核数据结构包括PCB、mm_struct和页表,然后构建起映射关系 ,同时

    2024年01月16日
    浏览(55)
  • 【Linux初阶】进程替换的应用 - 简易命令行解释器的实现

    🌟hello,各位读者大大们你们好呀🌟 🍭🍭系列专栏:【Linux初阶】 ✒️✒️本篇内容:使用代码手段实现一个简易的命令行解释器,其中功能包括:打印输出提示符、获取用户输入、字符串切割、执行命令、ls指令下拥有颜色提示、cd、echo; 🚢🚢作者简介:计算机海洋的

    2024年02月07日
    浏览(64)
  • 【linux】进程替换的应用|shell解释器的实现

    当我们学过了进程替换之后,本篇文章可以根据进程替换的知识带你自主实现一个shell命令行 实现步骤 1.显示命令行提示 2.读取输入指令以及对应选项 3.分割第二步的指令以及选项到命令行参数表中 4.处理内建命令 5.进程替换 我们通过观察bash的命令行提示发现他是由三部分

    2024年04月26日
    浏览(50)
  • Linux进程控制【进程程序替换】

    ✨个人主页: Yohifo 🎉所属专栏: Linux学习之旅 🎊每篇一句: 图片来源 🎃操作环境: CentOS 7.6 阿里云远程服务器 Good judgment comes from experience, and a lot of that comes from bad judgment. 好的判断力来自经验,其中很多来自糟糕的判断力。 子进程 在被创建后,共享的是 父进程 的代码

    2024年01月17日
    浏览(60)
  • [Linux 进程控制(二)] 进程程序替换

    首先,我们要认识到,我们之前fork()所创建的子进程,执行的代码,都是父进程的一部分(用if-else分流或者执行同样的代码)! 如果我们想让子进程执行新的程序呢? 执行全新的代码和访问全新的数据,不再和父进程有瓜葛,这种技术就叫做程序替换 ,下面我们就来学习一

    2024年03月14日
    浏览(47)
  • 【Linux】Linux进程控制及程序替换

    🍎 作者: 阿润菜菜 📖 专栏: Linux系统编程 在linux中fork是一个很重要的函数,它可以已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。 fork函数返回两个值,一个是子进程的进程号(pid),另一个是0。 父进程可以通过pid来区分自己和子进程,子进程可

    2024年02月02日
    浏览(43)
  • Linux :进程的程序替换

    目录 一、什么是程序替换 1.1程序替换的原理 1.2更改为多进程版本 二、各种exe接口 2.2execlp  ​编辑 2.2execv 2.3execle、execve、execvpe 用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种

    2024年04月10日
    浏览(33)
  • 【Linux】进程的程序替换

    目标:为了让子进程帮父进程执行特定的任务 具体做法:1. 让子进程执行父进程的一部分代码 红框中的代码实际上是父进程的代码,在没有执行fork之前代码就有了,在没有创建子进程之前,父进程的代码加载到内存了,子进程被创建出来是没有独立的代码,这个代码是父进

    2024年01月17日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包