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 命令,可以看到,形成了两个目标程序:

相关推荐
长流小哥5 分钟前
Linux网络编程实战:从字节序到UDP协议栈的深度解析与开发指南
linux·c语言·开发语言·网络·udp
极小狐39 分钟前
极狐GitLab 功能标志详解
linux·运维·服务器·elasticsearch·gitlab·极狐gitlab
jinan8861 小时前
加密软件的发展:从古典密码到量子安全
大数据·运维·服务器·网络·安全·web安全
雾原1 小时前
Red Hat Enterprise Linux (RHEL)系统部署
linux
您8131 小时前
二十、FTP云盘
linux·服务器·网络
用户3409704691151 小时前
ROS2-Jazzy编译功能包报错
linux
越学不动啦1 小时前
十、自动化函数+实战
运维·软件测试·自动化·测试
264玫瑰资源库2 小时前
2025年七星棋牌跨平台完整源码解析(200+地方子游戏+APP+H5+小程序支持,附服务器镜像导入思路)
服务器·游戏·小程序
程序员阿灿2 小时前
CentOS服务器能ping通却无法yum install:指定镜像源解决
linux·服务器·centos
丑过三八线2 小时前
在Linux下安装Gitlab
linux·运维·gitlab