Linux:进程替换

目录

进程程序替换

替换原理

进程替换相关函数

环境变量与进程替换函数

命令行解释器(my_xshell)


进程程序替换

上一篇进程控制讲到,父进程创建子进程就是为了让子进程去做一些另外的事情,但是不管怎么说,子进程的部分代码也还是父进程的一部分,那么想要子进程去执行一个新的程序呢?也就是去执行一个与父进程毫无相关的程序,一个全新的代码和访问全新的数据,那么如何进行的呢?也就是我们现在要讲的进程程序替换!所以现在我们可以理清思路回答以下问题:

1.为什么要有程序替换?

创建子进程的目的一般是这两个: 执行父进程的部分代码,完成特定功能。执行其它新的程序。------> 需要进行「进程替换」,用新程序的代码和数据替换父进程的代码和数据,让子进程执行。

2.在程序替换中OS有没有创建新的进程?

没有。进程的程序替换,不改变内核相关的数据结构,只修改部分的页表数据,将新程序的代码和数据加载带内存,重新构建映射关系,和父进程彻底脱离。

3.OS是如何做到创新构建映射关系的呢?

操作系统可以对父进程的全部代码和数据进行写入,子进程会自动触发写时拷贝,开辟新的空间,再把磁盘中第三方程序的代码和数据写入到其中,子进程页表重新建立映射关系。**最终结果是:**父进程指向自己的代码和数据,而子进程指向第三方程序的代码和数据。

那么现在又有问题了,子进程指向第三方程序的代码和数据?是如何实现的?代码中也没有呀!那么我们接下分析它是如何做的。

替换原理

一直说父子进程,这是两个进程,那么我们先看单进程进行程序替换是如何进行的。首先要调用一个函数execl( ) 。该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

单进程演示:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    printf("execl begin:\n");
    execl("/usr/bin/ls", "ls", "-a", "-l", "-n", NULL);
    printf("execl end:\n");
    return 0;
}

运行结果:

我们首先发现,execl begin之后,打印的内容和我们在解释器输入指令ls -a -l -n 打印的内容一样,可以很好的证明了,这个单进程通过调用execl函数,帮我们执行了ls -a -l -n这条指令。

我们又发现,程序中有两个printf函数,但是只打印了一个?这又是为什么呢?

我们可以退出打印了第一个printf函数之后,在单进程中就发生了进程替换,去执行另外的程序了,第二个printf没有执行的原因是执行到进程替换函数的时候,如果成功,整个进程的代码和数据都会被替换为所需替换的目标代码和数据,这样在后续执行的时候都会使用这份新的代码和数据,因此不会调用后续出现的代码。

多进程演示:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // child
        printf("pid:%d,begin to exec!\n",getpid());
        sleep(3);
        execl("/usr/bin/ls","ls","-a","-l",NULL);
        printf("pid:%d,end to exec!\n",getpid());
    }
    else 
    {
        // father
        printf("wait child\n");
        pid_t rid = waitpid(-1,NULL,0);
        if(rid > 0)
        {
            printf("wait success\n");
        }
    
    }
    return 0;
}

运行结果:


和单进程相差并不是很大,只是多进程替换中增加了父进程对子进程的等待和回收的部分功能。

那在多进程下应该如何理解进程替换呢?用下面图示的过程来演示:

子进程原先和父进程共用代码和数据,但是子进程发生改变,就出发了写时拷贝, 构建新的映射关系,页表也就指向新的代码和数据。从这里的进程替换中可以发掘出一些东西,替换的是进程,而不是代码,所以这里可以替换的内容有很多,甚至可以是Java写的程序运行起来的进程等等,看下面的实验:

myprocess.c:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // child
        printf("pid:%d,begin to exec!\n",getpid());
        sleep(3);
        execl("./cpptest","./cpptest",NULL);
        //execl("/usr/bin/ls","ls","-a","-l",NULL);
        printf("pid:%d,end to exec!\n",getpid());
    }
    else 
    {
        // father
        printf("wait child\n");
        pid_t rid = waitpid(-1,NULL,0);
        if(rid > 0)
        {
            printf("wait success\n");
        }
    
    }
    return 0;
}

cpptest.cc:

cpp 复制代码
#include <iostream>

int main()
{
    std::cout<<"this is a cpp program"<<std::endl;
    return 0;
}

makefile:

cpp 复制代码
.PHONY:all
all:myprocess cpptest

cpptest:cpptest.cc 
	g++ -o $@ $^

myprocess:myprocess.c
	gcc -o $@ $^
.PHONY:clean
clean:
	rm -rf myprocess cpptest

运行结果:

此两个程序分析,运行的是myprocess.c但是调用execl程序后,它帮我们执行了./cpptest进而运行了一个cc文件,也就是进程替换了,并且替换的还是其它进程。这也就解释了在不同的公司中是可以存在分块进行构建模块功能的,最后都可以通过进程的形式链接起来。

从某种意义来说,进程的替换已经可以被看成是一种系统调用了,站在系统的视角看内存中的所谓进程,实际上是一样的,系统高于一切,它可以对进程进行调度和分配。

进程替换相关函数

系统调用 execve 函数,功能:执行文件名 filename 指向的程序,文件名必须是一个二进制的 exe 可执行文件。

cpp 复制代码
#include <unistd.h>
 
int execve(const char *filename, char *const argv[], char *const envp[]);

有6种exec 系列的库函数,统称为 exec 函数,功能:执行文件。

cpp 复制代码
#include <unistd.h>
 
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

只有 execve 是真正的系统调用,其它 6 个函数都是库函数,最终都是调用的 execve,execve 在 man 手册的第 2 节,其它函数在 man 手册第 3 节。

  • l (list):表示参数采用列表(可变参数列表)
  • v (vector):参数采用数组
  • p (path):自动在环境变量 PATH 中搜索可执行程序(不需要带可执行程序的路径)
  • e (env):可以传入默认的或者自定义的环境变量给目标可执行程序

环境变量与进程替换函数

细心的我们发现,上面程序execl中,都是传了环境变量路径的, 那么当进行进程替换的过程中,对于环境变量的角度来讲,是以什么样的情况进行的传递呢?我们在环境变量中讲过的,直接得出结论是:子进程对应的环境变量,是可以直接从父进程来的。

对这个结论进行验证:

1.execl函数,需要找到命令所在的文件目录,使用方法如下:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // child
        // 进行进程替换
        execl("/usr/bin/ls", "ls", "-a", "-l", "-d", NULL);
    }
    else 
    {
        // parent
        // 对子进程回收
        pid_t rid = waitpid(-1, NULL, 0);
        if(rid > 0)
        {
            printf("wait success\n");
        }
    }
    return 0;
}

2.execlp函数:会到系统默认的路径下寻找命令:

cpp 复制代码
execlp("ls", "ls", "-a", "-l", "-d", NULL);

3.execle函数:用一个程序调用另外一个程序,但环境变量是自己的环境变量,不是系统的,通过获取环境变量查看。

如何在进程中添加一个环境变量?用到的是putenv函数:

cpp 复制代码
void *putenv(char *name)

程序演示:

cpp 复制代码
//cpptest.c

#include <iostream>

int main(int argc, char* argv[], char* env[])
{
    // 输出命令行参数
    for(int i = 0; argv[i]; i++)
    {
        std::cout << i << "->" << argv[i] << std::endl;
    }
    std::cout << "##############" << std::endl;
    
    // 输出环境变量
    for(int i = 0; env[i]; i++)
    {
        std::cout << i << "->" << env[i] << std::endl;
    }
    return 0;
}
cpp 复制代码
//myprocess.c

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    // 在程序中新增环境变量
    char* myenv = "MYVAL1 = 11111111";
    putenv(myenv);
    pid_t id = fork();
    if(id == 0)
    {
        // child
        // 进行进程替换
        execl("./cpptest", "cpptest", NULL);
    }
    else 
    {
        // parent
        // 对子进程回收
        pid_t rid = waitpid(-1, NULL, 0);
        if(rid > 0)
        {
            printf("wait success\n");
        }
    }
}

从中看出,在子进程中是出现了新增的这个环境变量的,由此可以基本验证,在父进程中添加的环境变量会继承到子进程中。那么父进程的父进程是谁呢?答案是bash,那么是不是在bash中添加的环境变量也会继承到子进程中?

程序演示(对上面程序进行修改):

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char* argv[], char* env[])
{
    // 输出环境变量
    for(int i = 0; env[i]; i++)
    {
        printf("%d -> %s\n", i, env[i]);
    }
    // 在程序中新增环境变量
    char* myenv = {
        "MYVAL1 = 11111111",
        "MYVAL2 = 22222222",
        NULL
    };
    putenv(myenv);
    pid_t id = fork();
    if(id == 0)
    {
        // child
        // 进行进程替换
        execl("./cpptest", "cpptest", NULL);
    }
    else 
    {
        // parent
        // 对子进程回收
        pid_t rid = waitpid(-1, NULL, 0);
        if(rid > 0)
        {
            printf("wait success\n");
        }
    }
}

运行结果:

由此可以得出这样的一条线索化的示意图:

环境变量的传递方式:

前面的例子证明,子进程的环境变量是由父进程传递的,而execle函数就是一个显示传递环境变量的函数,它的第三个参数是envp[],实际上就是环境变量。

程序演示:

cpp 复制代码
//myprocess.cc

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char* argv[], char* env[])
{
    // 在程序中新增环境变量
    char* const myenv[] = {
        "MYVAL1 = 11111111",
        "MYVAL2 = 22222222",
        NULL
    };
    pid_t id = fork();
    if(id == 0)
    {
        // child
        // 进行进程替换
        execle("./cpptest", "cpptest", NULL, myenv);
    }
    else 
    {
        // parent
        // 对子进程回收
        pid_t rid = waitpid(-1, NULL, 0);
        if(rid > 0)
        {
            printf("wait success\n");
        }
    }
    return 0;
}
cpp 复制代码
//cpptest.cc
#include <iostream>

int main(int argc, char* argv[], char* env[])
{
    // 输出命令行参数
    for(int i = 0; argv[i]; i++)
    {
        std::cout << i << "->" << argv[i] << std::endl;
    }
    std::cout << "##############" << std::endl;
    
    // 输出环境变量
    for(int i = 0; env[i]; i++)
    {
        std::cout << i << "->" << env[i] << std::endl;
    }
    return 0;
}

运行结果:

从中看出,通过这个函数可以把环境变量进行显示传递给子进程,并且是一种覆盖式传递

到此,有关进程替换的基本逻辑已经结束,那进程替换可以做什么实际的东西呢?比如我们用的xshell,我们可以自主实现一个简易版的xshell。

命令行解释器(my_xshell)

在前面的认知中,命令行解释器,也就是bash,可以把用户在命令行中敲的命令转换成命令再输出,而实际上,这是一个逻辑很简单的过程:

bash程序相当于是一个一直在后台运行的程序,而当用户敲了一些命令行后,bash创建子进程,就将这些命令行转换为一个字符串数组,采用进程替换的方式就可以把要找的命令和选项替换到前台,那依据这个原理,其实我们自己也能实现一个命令行解释器:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define NUM 1024
#define SIZE 64
#define SEP " "

char cwd[1024];
char enval[1024];
int lastcode = 0;

const char *getUsername()
{
    const char *name = getenv("USER");
    if(name) return name;
    else return "none";
}

const char *getHostname()
{
    const char *hostname = getenv("HOSTNAME");
    if(hostname) return hostname;
    else return "none";
}

const char *getCwd()
{
    const char *cwd = getenv("PWD");
    if(cwd) return cwd;
    else return "none";
}

int getUserCommand(char *command, int num)
{
    printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());
    char *r = fgets(command, num, stdin);
    if(r == NULL) return -1;
    command[strlen(command) - 1] = '\0';
    return strlen(command);
}

void commandSplit(char *in, char *out[])
{
    int argc = 0;
    out[argc++] = strtok(in, SEP);
    while(out[argc++] = strtok(NULL, SEP));
}

int execute(char *argv[])
{
    pid_t id = fork();
    if(id < 0) 
    {
        return -1;
    }
    else if(id == 0)
    {
        execvp(argv[0], argv);
        exit(1);
    }
    else
    {
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0)
        {
            lastcode = WEXITSTATUS(status);
        }
    }
    return 0;
}

void cd(const char *path)
{
    chdir(path);
    char tmp[1024];
    getcwd(tmp, sizeof(tmp));
    sprintf(cwd, "PWD=%s", tmp);
    putenv(cwd);
}

int doBuildin(char *argv[])
{
    if(strcmp(argv[0], "cd") == 0)
    {
        char *path = NULL;
        if(argv[1] == NULL) path = ".";
        else path = argv[1];
        cd(path);
        return 1;
    }
    else if(strcmp(argv[0], "export") == 0)
    {
        if(argv[1] == NULL) return 1;
        strcpy(enval, argv[1]);
        putenv(enval); // ???
        return 1;
    }
    else if(strcmp(argv[0], "echo") == 0)
    {
        char *val = argv[1] + 1;
        if(strcmp(val, "?") == 0)
        {
            printf("%d\n", lastcode);
            lastcode = 0;
        }
        else
        {
            printf("%s\n", getenv(val));
        }
        return 1;
    }
    else if(0)
    {}

    return 0;
}

int main()
{
    while(1)
    {
        char usercommand[NUM];
        char *argv[SIZE];
        // 1. 打印提示符&&获取用户命令字符串获取成功
        int n = getUserCommand(usercommand, sizeof(usercommand));
        if(n <= 0) continue;
        // 2. 分割字符串
        // "ls -a -l" -> "ls" "-a" "-l"
        commandSplit(usercommand, argv);
        // 3. check build-in command
        n = doBuildin(argv);
        if(n) continue;
        // 4. 执行对应的命令
        execute(argv);
    }
}

【补充】

一次性编译两个目标程序的makefile文件编写:

cpp 复制代码
.PHONY:all                    #定义为目标
all:myprocess cpptest         #依赖项,all依赖于myprocess cpptest 这两个目标程序

cpptest:cpptest.cc            #依赖关系,形成目标程序
	g++ -o $@ $^
myprocess:myprocess.cc
	gcc -o $@ $^
.PHONY:clean                  #定义为目标,clean总是可以被执行的
clean:                        #依赖项为空
	rm -rf myprocess cpptest  #依赖方法

执行 make 命令,可以看到,形成了两个目标程序:

相关推荐
黎明晓月11 分钟前
‌CentOS 7.9 安装 Docker 步骤
linux·docker·centos
菜鸟xy..20 分钟前
winhex软件简单讲解,虚拟磁盘分区介绍
linux·运维·服务器
网硕互联的小客服23 分钟前
如何排查服务器内存泄漏问题
linux·运维·服务器·安全·ssh
驰驰的老爸26 分钟前
elk单机版安装
运维·jenkins
Evoxt 益沃斯32 分钟前
How to enable Qemu Guest Agent for Virtual Machines
linux·运维·服务器·qemu
钟离墨笺1 小时前
【Linux】【网络】UDP打洞-->不同子网下的客户端和服务器通信(未成功版)
linux·服务器·网络
llkk星期五1 小时前
ubuntu 22.04附加驱动安装NVIDIA显卡驱动重启后无WiFi蓝牙等问题
linux·ubuntu
CVer儿1 小时前
ubuntu挂载固态硬盘
linux·运维·ubuntu
music&movie1 小时前
Win11安装VMware和Ubuntu并使用ssh访问部署模型
linux·ubuntu·ssh
J.Pei1 小时前
Linux(ubuntu)环境下部署The Fuck项目的方法(保姆级教程)
linux·ubuntu