小小小幸运 发表于 2024-6-29 10:26:45

Linux:进程控制(二.详细解说进程步伐替换)

上次讲了:Linux:进程地点空间、进程控制(一.进程创建、进程克制、进程等待)


1.进程步伐替换

   之前我们举行的步伐演示里,都只能运行自己的代码。那我们怎么样才能执行其他步伐的代码呢?(例如在步伐里利用ls之类的指令)就可以利用进程步伐替换,一开始我们先只看单进程的情况。后面在引入多进程的情况
1.1概念

进程步伐替换是指在运行过程中将一个进程的地点空间中的代码、数据和堆栈等内容完全替换为另一个步伐的代码、数据和堆栈的过程。这个过程通常是由操作系统提供的 exec 系列函数来实现的:


[*] 地点空间替换:进程的地点空间是指进程可以访问的内存范围。通过地点空间替换,进程可以在运行时动态地加载并执行不同的步伐,从而实现灵活的步伐执行和管理。
[*] exec 函数族:exec 函数族是一组系统调用,用于执行步伐替换操作。这些函数包括 execl, execv, execle, execve 等,它们允许以不同的方式通报参数给新步伐,并执行地点空间替换。
       我们要改变内存,那肯定是要调用系统调用接口的,这些函数会封装相应的接口
[*] 步伐入口点:新步伐的入口点是步伐中的起始执行位置,通常是 main 函数或其他指定的入口函数。替换完成后,控制权将转移到步伐入口点,开始执行新步伐的代码。
1.2原理



[*]当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新步伐替换
[*]替换完成后,控制权将转移到新步伐的入口点,开始执行新步伐的代码。
https://img-blog.csdnimg.cn/direct/0506078ad7df4eb1b09b933af5a6ce8e.png
1.3利用一个exec 系列函数

execl()函数

execl函数是Linux系统中用于执行新步伐的函数之一,它属于exec函数族的一部分。这个函数的作用是在当进步程的上下文中启动一个新的步伐,并替换当进步程的映像为新的步伐映像。调用execl函数后,当进步程将克制执行,并由新的步伐开始执行。
#include<unistd.h>
int execl(const char *path, const char *arg0, ... /* (char*) NULL */);
参数说明:


[*]path:要执行的步伐的路径。
[*]arg0:新步伐的参数列表的开始,通常这会是新步伐的名称(尽管这不是强制的,但它通常用于错误消息和步伐内部)。
[*]...:一个可变参数列表(参数的数量不固定),新步伐的参数列表,必须以NULL末端。
   execl函数会根据提供的路径path找到并执行相应的步伐,同时将arg0及厥后面的参数作为新步伐的下令行参数通报。注意,参数列表必须以NULL末端,这是告诉execl参数列表结束的标记。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
        printf("I'm a process, pid: %d\n", getpid());
        printf("execl begin...\n");

        int a=execl("/usr/bin/ls", "ls", "-a", "-l", NULL);

        printf("execl end...\n");
        return 0;
}
https://img-blog.csdnimg.cn/direct/830e68e5b90a45ebbd51e9797c9cad49.png
假如execl函数调用乐成,那么它实际上不会返回,因为当进步程的映像已经被新步伐替换。假如调用失败,它会返回-1,并设置全局变量errno以指示错误缘故原由。常见的错误缘故原由大概包括文件未找到、权限不敷等。
execl函数和其他exec函数一样,不会创建新的进程。它们只是在当进步程的上下文中启动另一个步伐。
因此,调用execl前后,进程的ID(PID)不会改变。同时,由于execl会替换整个进程映像,所以在调用execl之前,通常必要确保当进步程的所有打开的文件描述符、内存分配等都被适当地处置惩罚或释放,因为这些资源不会被新步伐继承。
结论与细节


[*] 步伐替换一旦乐成,exec后面的代码不在执行。因为被替换掉了,这也是什么代码没有输出execl end的缘故原由了
[*] exec函数调用乐成,那么它实际上不会有返回值;调用失败,它会返回-1
[*] exec函数不会创建新的进程。它们只是在当进步程的上下文中启动另一个步伐
[*] 创建一个进程。我们是先创建PCB、地点空间、页表等再先把步伐加载到内存
       先加载的话,页表都没办法映射的
[*] 步伐替换的本质就是加载 (可以看成一个加载器),有替换就是替换,没有就是步伐加载
       步伐替换的本质是步伐加载,因为在执行 exec 函数时,操作系统会加载新步伐的可执行文件,并将其代码、数据和堆栈等部分加载到进程的地点空间中。这个过程涉及将新步伐的内容从磁盘加载到内存中,为进程提供执行所需的资源。因此,虽然我们常说是“步伐替换”,但实际上更准确地说是将新步伐加载到内存中,替换掉原有的步伐,以实现进程的功能切换和更新。
[*] 步伐运行要加载到内存;为什么?冯诺依曼体系规定;如何加载的呢?就是步伐替换:步伐替换是操作系统的接口,所谓的把磁盘里的数据加载到内存就是把磁盘设备的数据拷贝到内存里。把数据从一个硬件搬到另一个硬件,只有操作系统能做
2.多进程时的步伐替换

我们可以创建一个子进程,由子进程来举行步伐替换,父进程来等待效果就可以。为什么? 父进程能得到子进程的执行效果
我们知道父进程与子进程映射到同一块代码,那么子进程举行步伐替换后,不是会覆盖吗,替换为什么不影响父进程?
   进程具有独立性,在举行步伐替换时要举行写时拷贝
写时拷贝的本质就是开辟新的空间
shell是如何运行起来一个指令的?
   首先创建子进程,shell会waitpid()等待进程效果,子进程会继承shell的代码,但是不影响。子进程举行步伐替换,替换为我们输入的指令
int main()
{
        pid_t id = fork();
        if (id == 0)
        {
                printf("I'm a process, pid: %d\n", getpid());
                printf("execl begin...\n");

                execl("/usr/bin/ls", "ls", "-a", "-l", NULL);

                printf("execl end...\n");
                exit(1);
        }

        pid_t rid = waitpid(id, NULL, 0);
        if (rid > 0)
        {
                printf("wait successfully\n");
        }
        return 0;
}
https://img-blog.csdnimg.cn/direct/49c07d41556e477c9b3909e0240deaa2.png
3.其他几个exec系列函数

https://img-blog.csdnimg.cn/direct/aa18814803d4475f944031bb987b6dd1.png

[*] execl:该函数允许通过提供可变数量的参数来执行指定的可执行文件。它的原型如下:
int execl(const char *path, const char *arg0, ... /*, (char *)0 */);
path 是要执行的可执行文件的路径,arg0 是第一个参数,后续参数都是通报给可执行文件的下令行参数,以 NULL 末端。
[*] execlp:该函数与 execl 雷同,但是它会在系统的环境变量 PATH 指定的目次中查找可执行文件。它的原型如下:
int execlp(const char *file, const char *arg0, ... /*, (char *)0 */);
file 是要执行的可执行文件的文件名,arg0 是第一个参数,后续参数都是通报给可执行文件的下令行参数,以 NULL 末端。
       相比于execl函数,execlp函数的第一个参数能直接写文件名,系统会PATH环境变量里去查找
    多的字母p:PATH环境变量
    int main()
{
        pid_t id = fork();
        if (id == 0)
        {
                printf("I'm a process, pid: %d\n", getpid());
                printf("execl begin...\n");

                execl("ls", "ls", "-a", "-l", NULL);

                printf("execl end...\n");
                exit(1);
        }

        pid_t rid = waitpid(id, NULL, 0);
        if (rid > 0)
        {
                printf("wait successfully\n");
        }
        return 0;
}
https://img-blog.csdnimg.cn/direct/a7eb72a66cc24265a8a88ecc77a89cb9.png
[*] execv:雷同于 execl,但是允许通报一个参数数组给被执行的步伐。它的原型如下:
int execv(const char *path, char *const argv[]);
path 是要执行的可执行文件的路径,argv 是一个以 NULL 末端的参数数组,此中每个元素都是一个字符串,表示下令行参数。
       相比于exec多个字母v:代表vector
    int main()
{
        pid_t id = fork();
        if (id == 0)
        {
                printf("I'm a process, pid: %d\n", getpid());
                printf("execl begin...\n");
                char* argv[] = { "ls","-a","-l",NULL};

                execv("/usr/bin/ls",argv);

                printf("execl end...\n");
                exit(1);
        }

        pid_t rid = waitpid(id, NULL, 0);
        if (rid > 0)
        {
                printf("wait successfully\n");
        }
        return 0;
}
https://img-blog.csdnimg.cn/direct/08fc170720b14733ab24cd5c05164cf6.png
[*] execvp:雷同于 execv,但是它会在系统的环境变量 PATH 指定的目次中查找可执行文件。它的原型如下:
int execvp(const char *file, char *const argv[]);
file 是要执行的可执行文件的文件名,argv 是一个以 NULL 末端的参数数组,此中每个元素都是一个字符串,表示下令行参数。
   既有字母p 又有v,结合上面那两种就行

[*] execle:函数与 execl 函数雷同,但允许在启动新步伐时通报额外的环境变量。它的原型如下:
int execle(const char *path, const char *arg, ..., char *const envp[]);
path 是要执行的可执行文件的路径,arg 是要通报给新步伐的下令行参数,后面的参数是额外的环境变量,以 NULL 末端。
   进程步伐替换不会替换环境变量的

[*] 想要子进程继承全部的环境变量,不消管,直接就能拿到
[*] 单纯新增环境变量,在父进程里利用putenv()函数,会影响子进程
putenv 是 C 语言中的一个库函数,它界说在 <stdlib.h> 头文件中。这个函数用于将字符串添加到环境变量中,或者修改已经存在的环境变量的值。
int putenv(const char *string);

[*]利用全新的环境变量,就利用execle()函数,那么替换后的代码切换后的环境变量就只是我们传入的表里的内容
也可以调用其他语言的步伐

code.c里:
int main()
{
        char* const env[] = {
                (char*)"first",
                (char*)"second",
                NULL };

        pid_t id = fork();
        if (id == 0)
        {
                printf("I'm a process, pid: %d\n", getpid());
                printf("execl begin...\n");

                execle("./mytest", "mytest", NULL, env)

                printf("execl end...\n");
                exit(1);
        }

        pid_t rid = waitpid(id, NULL, 0);
        if (rid > 0)
        {
                printf("wait successfully\n");
        }
        return 0;
}
test.cpp里:
#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
        for (int i = 0; environ; i++)
        {
                printf("env[%d]: %s\n", i, environ);
        }

        cout << "This is C++" << endl;
        return 0;
}
https://img-blog.csdnimg.cn/img_convert/779b48a1c78f7aa07260562f3195592e.png
当然我们也能传系统环境变量,但是没必要,如许的话直接默认就行
execle("./mytest", "mytest", NULL, environ)//传入这个全局变量
想要生成两个可执行文件的makefile

.PHONY:all
all:mycode mytest

mycode:code.c
        gcc -o $@ $^
mytest:test.cpp
        g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
        rm -f mycode mytest

[*].PHONY:声明一个或多个目标是伪目标(phony targets)通过声明伪目标,你可以确保 make 总是执行相应的下令,而不会因为同名的文件或目次的存在而跳过这些下令
[*]运行 make 下令时(没有指定详细目标),make 会首先查找 Makefile 中的第一个目标,并尝试构建它。在这个过程中,make 会查抄该目标的所有依赖项,并递归地处置惩罚这些依赖项,直到所有必要的依赖项都被构建或确认为是最新的
[*]当 make 工具被调用以构建某个目标时,它会查抄该目标的所有依赖项,并根据必要构建这些依赖项。然而,对于 clean 如许的伪目标,它并没有列出任何依赖项,因此其他目标的构建状态不会影响 clean 的执行
今天就到这里啦,感谢大家支持

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: Linux:进程控制(二.详细解说进程步伐替换)