首页 > 系统相关 >Linux进程替换 && 自主shell程序

Linux进程替换 && 自主shell程序

时间:2024-06-04 12:30:52浏览次数:19  
标签:shell const 函数 char command && Linux 进程 cwd

        本篇将要讲解有关进程中最后一个知识点——进程替换,其中主要介绍有关进程替换的六个函数,直接从函数层面来理解进程替换(在使用函数的过程中,也会对进行替换进行解释)。本篇主要围绕如下的进程替换函数:

        以上的 exec* 函数就是 Linux 中的加载函数,可以将进程加载到内存中。

1. 进程替换函数

execl 函数

        我们将首先介绍 execl 函数,关于该函数的参数列表如下:

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

        如上,该函数是一个可变参数列表,第一个参数为路径(需要替换的进程的目录,应该去哪找到该进程),接下来的参数的类型都是 const char* ,需要传入的参数为进程的名字,进程需要带上的参数(也就是需要告诉函数,要求替换进程怎么执行),最后在结束的位置加上 NULL 即可。具体的使用如下:

        如上所示,当我们运行 execl 函数的时候,接下来会运行我们替换后的进程。但是需要注意的一点,我们在进程替换代码的后面也加入了一行打印代码,但是在运行之后并没有将其打印出来。这是为什么呢?

        这是因为我们进行了程序替换。在每个进程加载的时候,都会加载对应的页表,地址空间,在内存中开辟空间,以及建立各种的映射关系。当我们调用 execl 函数的时候,会在磁盘中找到对应的进程,然后使用该进程的代码和数据覆盖原有的代码和数据,但是并不会覆盖掉原进程的内核管理数据结构(其中个别的数据将会发生变化),所以进程替换的时候,并不会创建新进程(老进程壳子、新进程运行)。所以以上的代码执行完 execl 函数之后,不会在运行之后的代码了,因为以已经被覆盖了。

多进程运行程序

        通过以上函数可知,我们可以使用系统调用函数将进程进行替换,那么结合我们之前的 fork 创建子进程,我们就可以使用 fork 创建子进程,然后使用程序替换,让子进程运行我们想要运行的程序,这样就达到了多进程运行程序的效果,如下:

        如上所示,我们可以在子进程中进行程序替换,运行我们想要运行的程序。这样发生的原理又是什么呢?

        当我们进行程序替换的时候,会对子进程的代码和数据进行写时拷贝(因为和父进程共享的,不能直接覆盖原来的代码数据),从磁盘中写入我们想要运行进程的代码和数据。父进程在外可以进行阻塞等待和非阻塞等待,也就是可以做自己的事情,若还想要同时运行多个进程,可以创建多个子进程,然后进行替换。

execv 函数

        现在我们将开始讲解 execv 函数,关于该函数的参数形式如下:

int execv(const char *path, char *const argv[]);

        其中,我们需要传入的参数分别为:需要替换进程的地址(目录),第二个参数是一个指针数组,里面存的是我们要运行进程的方式,和以上的 execl 的形式一样,但是需要注意的是,指针数组的最后一个元素得是:NULL,使用如下:

        如上,我们使用 execv 函数成功替换了子进程。其实这些函数的使用都大同小异,我们只需要掌握其中一两个主要的用法,其余也就无师自通了。

execvp execlp 函数

        接下来我们将讲解 execvp 和 execlp 这两个函数,其函数参数形式如下:

int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);

        以上两个函数分别相对于 execv 函数和 execl 函数多了一个 p ,其实是多了一个环境变量,也就是我们不需要在第一个参数中传入路径了,直接传入我们想要执行的语句,后面的参数形式,根据以上 execv 和 execl 函数形式传参即可,如下:

        如上所示,传参形式也是和 execv execl 函数相差无几。

execvpe 函数

        现在我们来讲解我们将要讲解的最后一个函数:execvp 函数,如下:

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

        如上所示,其实和 execvp execl 函数相比,只多了一个 envp 参数,也就是我们的环境变量,在以下只演示一种函数的使用即可,另一种的使用方法大同小异,就不在演示,如下:

        如上所示,我们在我们的c语言程序中替换了一个C++程序,然后传入环境变量与参数,我们在C++程序中也在命令行中使用 argv 和 env 变量接收了命令行参数和环境变量。所以在 execvpe 函数中的 envp 变量可以将环境变量给替换掉,将程序中的环境变量替换为我们想要输入的环境变量(若我们想要传入bash中的环境变量,我们只需要传入environ进去即可)。

execve 系统调用

        我们先查看我们使用的进程替换所在的手册,如下:

        如上显示的为 3 好手册,是我们的 C语言中的库,说明这些进程替换函数都是来自于C语言,其中的底层调用的是系统调用:execve:

        其中,关于这些函数的关系如下:

 2. 自主shell -- code

        在我们在 Linux 中使用的命令行解释器(bash)就是一个 shell 程序,其中主要负责解释我们输入的命令,将其反馈给我们用户。比如我们常用的 ls pwd touch 指令,但其实这些指令都是进程,那么关于 bash 运行这些指令,其实在底层中就是使用进程替换函数实现的,现在我们将在下文中使用C语言实现一个简陋版的 shell 程序(主要依靠进程替换)。我们先给出我们所有的代码,然后在下文中解释:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>

#define SIZE 512 
#define NUM 32

char cwd[SIZE * 2];
char* MyArgv[NUM];
char tmp[SIZE * 2];
int lastcode = 0;

const char* GetUserName(){
    const char* name = getenv("USER");
    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;
}

void MakeCommandLine(){
    const char* name = GetUserName();
    const char* hostname = GetHostName();
    const char* cwd = GetCwd();
    if(strlen(cwd) != 1){
        cwd += strlen(cwd) - 1;
        while(*cwd != '/')
            cwd--;
        cwd++;
    }
    printf("[%s@%s %s]> ", name, hostname, cwd);
}

char* GetCommandFromStdin(){
    char* command = fgets(tmp, sizeof(tmp), stdin);
    if(command == NULL) exit(1);
    command[strlen(command) - 1] = '\0';
    return command;
}

void SplitComandToArgv(char* command){
    // 开始截取
    MyArgv[0] = strtok(command, " ");
    //MyArgv[0] = command;
    //char* tmp = strtok(command, " ");
    //tmp = '\0';
    int index = 1;
    while((MyArgv[index++] = strtok(NULL, " "))){

    }
    MyArgv[index] = NULL;
}

void ExecuteCommand(char* cmd){
    pid_t id = fork();
    if(id < 0) exit(1);
    else if(id == 0){
        execvp(MyArgv[0], MyArgv);
        exit(errno);
    }else{
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0){
            lastcode = WEXITSTATUS(status);
            if(lastcode != 0)
                printf("%s:%s:%d\n", MyArgv[0], strerror(lastcode), lastcode);
        }
    }
}

void ChangeDir(){
    char* path = MyArgv[1];
    if(strcmp(MyArgv[1], "-") == 0) path = getenv("HOME");
    chdir(path);
    // 更新环境变量
    getcwd(tmp, sizeof(tmp));
    snprintf(cwd, sizeof(cwd), "PWD=%s", tmp);
    putenv(cwd);
}

int BuildInCommand(){
    int yes = 0;
    if(strcmp(MyArgv[0], "cd") == 0){
        yes = 1;
        ChangeDir();
    }
    return yes;
}

    

int main(){
    // 先创建一个命令行输入
    int quit = 1;
    while(quit != 0){
        MakeCommandLine();    
         // 创建一个读入字符串
        char* command = GetCommandFromStdin();
        //printf("%s", command);
        // 将command指令截取到argv表格中
        
        // 检测是否存在重定向
        //CheckRedir(command);
       // printf("cmd: %s\n", command);
       // printf("file: %s\n", command + RedirPos);
        SplitComandToArgv(command);
       // int i = 0;
       // for(; MyArgv[i]; i++){
       //    printf("%s\n", MyArgv[i]);
       // }
        // 截取命令之后,就可以开始执行程序了
        int buildin = BuildInCommand();
        if(buildin) continue;
        ExecuteCommand(command);
    }
    return 0;
}

命令行读取命令

        首先我们需要先模拟出在命令行读取命令,其中还包括要显示出我们当前所在的目录,这就会涉及到我们的在之前文章中所提到的环境变量了(Linux环境变量-CSDN博客),我们需要从环境变量中找出当前所在的目录,以及当前的用户,实现如下:

const char* GetUserName(){
    const char* name = getenv("USER");
    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;
}

void MakeCommandLine(){
    const char* name = GetUserName();
    const char* hostname = GetHostName();
    const char* cwd = GetCwd();
    if(strlen(cwd) != 1){
        cwd += strlen(cwd) - 1;
        while(*cwd != '/')
            cwd--;
        cwd++;
    }
    printf("[%s@%s %s]> ", name, hostname, cwd);
}

        其中主要使用了能寻找到环境变量的返回值的系统调用函数 getenv。以上为函数为在命令行打印读取命令行信息的提示,接下来我们还需要读取我们的指令,其中的指令是以字符串的形式传入的,如下:

char* GetCommandFromStdin(){
    char* command = fgets(tmp, sizeof(tmp), stdin);
    if(command == NULL) exit(1);
    command[strlen(command) - 1] = '\0';
    return command;
}

        其中主要使用的函数为一个文件读取函数,一次性就可以读取一行。

截取命令行 / 进程替换

        我们进行环境变量之前,需要使用空格传入不同的穿,而我们刚刚的读取指令函数读取的是一整行的字符串,所以我们需要将其截取,以空格为间隙将其截取。如下:

void SplitComandToArgv(char* command){
    // 开始截取
    MyArgv[0] = strtok(command, " ");

    int index = 1;
    while((MyArgv[index++] = strtok(NULL, " "))){

    }
    MyArgv[index] = NULL;
}

        截取完命令之后,我们就可以进行进程替换了,进行进程替换,我们只需要使用使用 fork 创建出子进程,然后让父进程进行阻塞等待即可,如下:

void ExecuteCommand(char* cmd){
    pid_t id = fork();
    if(id < 0) exit(1);
    else if(id == 0){
        execvp(MyArgv[0], MyArgv);
        exit(errno);
    }else{
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0){
            lastcode = WEXITSTATUS(status);
            if(lastcode != 0)
                printf("%s:%s:%d\n", MyArgv[0], strerror(lastcode), lastcode);
        }
    }
}

        以上的进程替换虽然能替换大部分的指令,但是还是有部分内建命令不能替换,因为内建命令本质是 shell 程序中的一个函数,所以并不能算得上是进程替换,我们只能在函数中检测,然后运行,如下给出了 cd 内建命令的实现,关于其他的内建命令便可由读者自行完成,如下:

void ChangeDir(){
    char* path = MyArgv[1];
    if(strcmp(MyArgv[1], "-") == 0) path = getenv("HOME");
    chdir(path);
    // 更新环境变量
    getcwd(tmp, sizeof(tmp));
    snprintf(cwd, sizeof(cwd), "PWD=%s", tmp);
    putenv(cwd);
}

int BuildInCommand(){
    int yes = 0;
    if(strcmp(MyArgv[0], "cd") == 0){
        yes = 1;
        ChangeDir();
    }
    return yes;
}

测试

         测试结果如下:

标签:shell,const,函数,char,command,&&,Linux,进程,cwd
From: https://blog.csdn.net/m0_74830524/article/details/139010931

相关文章

  • 使用ansible自动化安装MySQL8的mysql-router+mysql-shell+mysql架构InnoDB ReplicaSet
    【说明】当前数据库MySQLCommunityServer8.4.0LTS版本已经发行,使用InnoDBReplicaSet架构自动化搭建 【自动化安装】使用ansible安装mysql-router+mysql-shell+mysqltreemysql8/mysql8/├──mysql_ms.yaml└──roles└──mysql_ms├──tasks......
  • Linux 修改文件和文件夹权限
    在Linux中,你可以使用chmod命令来修改文件和文件夹的权限。chmod命令用于更改文件和目录的访问权限,即控制谁可以读取、写入和执行文件。以下是在Linux中修改文件和文件夹权限的基本方法使用数字表示法修改权限使用数字表示法来设置文件或文件夹的权限。数字表示法使用三个......
  • 每天一个 Linux 命令(2):od
    功能简介od(OctalDump)命令用于将指定文件内容以八进制、十进制、十六进制、浮点格式或ASCII编码字符方式显示,通常用于显示或查看文件中不能直接显示在终端的字符。od命令系统默认的显示方式是八进制。常见的文件为文本文件和二进制文件。od命令主要用来查看保存在二进制文件中......
  • 在Linux中,如何在Linux中进行网络资源调度?
    在Linux中进行网络资源调度主要涉及控制网络带宽、管理网络流量以及优化网络性能。以下是一些关键步骤和工具用于进行网络资源调度:1.使用tc(TrafficControl)工具tc是Linux中用于网络资源调度的主要工具,它允许你创建和管理网络流量控制规则。查看现有规则:sudotcqdiscshow......
  • 在Linux中,如何在Linux中进行系统资源调度?
    在Linux中进行系统资源调度主要涉及对CPU、内存、磁盘I/O和网络等资源的分配和管理。Linux内核负责资源调度,但是管理员可以通过配置和调整来优化资源的使用。以下是一些关键步骤和策略:1.CPU调度CPU调度主要通过内核的调度器来管理,Linux提供了多种调度器,如CFS(完全公平调度器)等。......
  • 关于linux 系统inode快耗尽问题处理!
    一、inode是什么?要想理解inode,要从文件储存说起。文件储存在硬盘上,硬盘的最小存储单位叫做"扇区"(Sector)。每个扇区储存512字节(相当于0.5KB)。操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个"块"(block)。这种由多个扇区组成的"......
  • 在Linux中,如何在Linux中进行任务调度?
    在Linux中进行任务调度通常涉及使用cron守护进程和at命令。这些工具允许你安排任务在特定时间或在满足特定条件时自动执行。以下是使用这些工具进行任务调度的详细步骤:1.使用cron进行任务调度cron是Linux中用于任务调度的主要工具,它可以按照预定的时间表执行任务。编辑cronta......
  • 在Linux中,如何进行系统资源的隔离?
    在Linux中进行系统资源隔离的目的是确保不同的应用程序或用户之间的资源使用不会相互影响,从而提高系统的稳定性和安全性。以下是一些关键步骤和工具用于实现资源隔离:1.使用cgroups(ControlGroups)cgroups是一种Linux内核特性,用于限制、记录和隔离进程组使用的资源。安装cgrou......
  • 记一次“有手就行”的从SQL注入到文件上传Getshell的简单过程
    0x01前台SQL注入漏洞原理SQL注入漏洞的原理是应用程序没有对用户输入进行充分的验证和过滤,导致攻击者可以在输入框中插入恶意的SQL代码。当应用程序将用户输入的数据拼接到SQL查询语句中时,攻击者插入的恶意代码也会被执行,从而绕过身份验证和访问控制,直接访问或修改数据库......
  • Linux运维应知必会的LVS高可用负载均衡方案
    背景在业务量达到一定量的时候,往往单机的服务是会出现瓶颈的。此时最常见的方式就是通过负载均衡来进行横向扩展。其中我们最常用的软件就是Nginx。通过其反向代理的能力能够轻松实现负载均衡,当有服务出现异常,也能够自动剔除。但是负载均衡服务自身也可能出现故障,因此需要引......