【Linux】第十九站:进程替换

news2024/9/25 1:25:46

文章目录

  • 一、单进程版---最简单的程序替换
  • 二、进程替换的原理
  • 三、多进程的程序替换
    • 1.多进程的程序替换实例
    • 2.那么程序在替换时候有没有创建子进程呢
    • 3.再谈原理
    • 4.一个现象
    • 5.我们的CPU如何得知程序的入口地址?
  • 四、各个接口的介绍
    • 1.execl
    • 2.execlp
    • 3.execv
    • 4.execvp
    • 5.execle

一、单进程版—最简单的程序替换

在linux中存在这样的一批接口,exec系列的接口,我们可以用man手册去查看

man 3 exec

image-20231118152515461

我们先来看一下execl函数,它的第一个参数是路径,即某个程序的路径,第二个是该程序,后面的可变参数列表是该程序的选项,我们可以自己随意填写。但是最后一个一般是为NULL的,这是标准写法,但是有些编译器,即便不写NULL也是可以的

我们可以先简单的跑一下这个代码

#include<stdio.h>    
#include<unistd.h>    
#include<stdlib.h>    
int main()    
{    
    printf("before: I am a process, pid:%d,ppid:%d\n",getpid(),getppid());
    execl("/usr/bin/ls","ls","-a","-l",NULL);
    printf("after: I am a process, pid:%d,ppid:%d\n",getpid(),getppid());                                   
    return 0;                    
}   

运行结果为

image-20231118160059753

我们可以看到,这个进程将ls这个命令给跑起来了,而且最后after是没有被执行的。

甚至我们也可以这样做

#include<stdio.h>    
#include<unistd.h>    
#include<stdlib.h>    
int main()    
{    
    printf("before: I am a process, pid:%d,ppid:%d\n",getpid(),getppid());     
    execl("/usr/bin/top","top",NULL);                                                                
    printf("after: I am a process, pid:%d,ppid:%d\n",getpid(),getppid());    
    return 0;                                                   
}     

这样的话,最终会执行这个top命令的

image-20231118160334351

所以我们现在知道了使用这个execl后会发生什么事情了

将别人的程序给替换一下,然后去跑别人的程序。在替换之后,后面的程序都不会去跑了。

二、进程替换的原理

如下图所示

在程序一开始的时候,CPU调度这个进程,然后这个进程会将磁盘中的代码和数据加载到物理内存中,开始执行代码

image-20231118163116605

当我们前面的程序在执行execl时候

它里面由于用的是ls,所以ls直接将原来的代码和数据给替换下来

image-20231118163502480

也就是说,对于页表以左部分是不会变化的,只会将右侧部分的代码和数据给替换,然后将页表给稍微调整一下即可。

总之就是用自己原来的代码和数据替换为新的代码和数据,然后从新的代码和数据从0开始执行,重新开始执行。

这就是程序替换

三、多进程的程序替换

1.多进程的程序替换实例

我们用如下代码

#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)
    {
        printf("before: I am a process, pid:%d,ppid:%d\n",getpid(),getppid());
        sleep(5);
        execl("/usr/bin/ls","ls","-a","-l",NULL);
        printf("after: I am a process, pid:%d,ppid:%d\n",getpid(),getppid());
        exit(0);
    }
    pid_t ret = waitpid(id,NULL,0);
    if(ret > 0) 
    {
        printf("wait success,father pid : %d, ret id : %d\n",getpid(),ret);
    }
    sleep(5);
    return 0;
}

搭配上监控,我们来测试观察一下现象

image-20231118171230433

一开始,父子进程同时运行,但是父进程直接进入进程等待,等待子进程结束。对于子进程则是执行了一行代码后,进程替换。然后随之结束。

子进程执行进程替换的时候,需要替换代码和数据,但是并不会影响父进程,因为有写时拷贝,以及进程之间是要保持独立性的

注意:对于数据有写时拷贝我们可以理解,对于代码而言,也是存在写时拷贝的。所以代码也并不是那么的绝对不可写入的,主要看谁去修改。这里是由操作系统去修改的,当然是可以的。

image-20231118172148965


2.那么程序在替换时候有没有创建子进程呢

其实是没有的,还是在原来的进程上,从我们前面的运行结果可以看出来,进程等待完毕后,还是原来的子进程

所以这里只进行程序代码和数据的替换工作


3.再谈原理

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

image-20231118173516864


4.一个现象

我们可以注意到,exec系列函数之后的代码似乎没有被执行

所以说程序替换成功之后,exec*后续的代码不会被执行,如果替换失败,才可能执行后续代码,exec*函数,它只有在失败的时候,才会有返回值,如果成功是不会返回任何值的,因为也没办法返回。

所以其实我们上面的代码可以稍作修改

在这里我们可以让他退出的时候退出码为1,因为为0也没有什么意义,程序替换失败才会执行到这里。

image-20231118174536137


5.我们的CPU如何得知程序的入口地址?

Linux中形成的可执行程序,是有格式的,ELF,这个可执行的程序有它自己的表头。可执行程序入口地址就在表头。

所以说,在程序加载到内存的时候,一定会先将表头入口地址加载到内存中的,然后后面的在慢慢加载

当我们程序替换以后,它也有自己的表头,CPU可以直接读取到表头的入口地址,从而进行执行。

四、各个接口的介绍

我们可以用man手册查找到,其实,对于exec系列有很多个接口,一共有七个,但是man手册中的三号手册有6个

image-20231118180935126

还有一个是在2号手册中的。

image-20231118183450967

在exec系列的函数中,开头都是exec开头的

1.execl

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

这个l我们可以理解为list,即列表。

如下所示,像我们刚刚所用到的这个函数

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

list就相当于,在传参的时候,从第二个开始,一个一个的往里传参的,像链表一样,最后一个结点为空

像我们之前在命令行跑这个程序的时候,是这样的

image-20231118184332045

而现在,我们只需要将中间的空格改为逗号,最后加一个NULL,输入指令的时候是怎样的,最后就如何去传参。

对于第一个参数,我们要知道,执行一个程序的第一件事情一定是先找到这个程序。所以第一个参数就是用来找到这个程序的。

找到这个程序之后,接下来要怎么办???

找到这个程序以后,要做的就是如何执行这个程序,要不要涵盖选项,涵盖哪些?

所以最终就是命令行怎么写,我们就怎么传

2.execlp

如下所示,是第二个该系列的接口

 int execlp(const char *file, const char *arg, ...);

这个我们会发现,多了一个p

这个p其实就是PATH的意思,execlp会在默认的PATH环境变量中查找。

对于第一个参数,这里我们可以带上它的路径,也可以不带上路径,后面的参数都是和前面的函数一样的

如下是带上路径的

#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)
    {
        printf("before: I am a process, pid:%d,ppid:%d\n",getpid(),getppid());
        sleep(5);
        execlp("/usr/bin/ls","ls","-al",NULL);
        printf("after: I am a process, pid:%d,ppid:%d\n",getpid(),getppid());
        exit(1);
    }
    pid_t ret = waitpid(id,NULL,0);
    if(ret > 0) 
    {
        printf("wait success,father pid : %d, ret id : %d\n",getpid(),ret);
    }
    sleep(5);
    return 0;
}

运行结果为

image-20231119154631267

如果我们不带上路径

#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)
    {
        printf("before: I am a process, pid:%d,ppid:%d\n",getpid(),getppid());
        sleep(5);
       // execlp("/usr/bin/ls","ls","-al",NULL);
        execlp("ls","ls","-al",NULL);
        printf("after: I am a process, pid:%d,ppid:%d\n",getpid(),getppid());
        exit(1);
    }
    pid_t ret = waitpid(id,NULL,0);
    if(ret > 0) 
    {
        printf("wait success,father pid : %d, ret id : %d\n",getpid(),ret);
    }
    sleep(5);
    return 0;
}

运行结果为

image-20231119154747955

在这里所有的子进程都会继承父进程的环境变量。环境变量具有全局属性,所以最终可以解决路劲的问题。

不过在这里,我们可能会觉得这个函数的用法比较奇怪

 execlp("ls","ls","-al",NULL);

我们会注意到写了两个ls,很奇怪,其实这是合理的,第一个ls解决的是路径的问题,即去找到这条指令,后面是执行该指令的问题。

而要找到该指令,环境变量帮我们解决了一部分,剩下的就是在该路径下找到该指令到底有没有,要找到该指令的程序名

3.execv

其中这个v代表的就是vector。

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

这里没有带p,所以它就是需要路径去寻找。

像前面的两个都是以可变参数列表的形式传入的,现在这个使用的是字符串指针数组。

这个和前面不同之处就在于,将前面的改为了指针数组的形式。因为我们的指令最终也是要被解析为一个一个的字符串。所以这个操作直接就是将这些字符串整合为了一共字符串指针数组而已,没有什么太大的变化

image-20231119160312752

如下代码所示

#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)
    {
        char* const myargv[] = {"ls","-a","-l",NULL};
        printf("before: I am a process, pid:%d,ppid:%d\n",getpid(),getppid());
        sleep(5);
      //  execlp("/usr/bin/ls","ls","-al",NULL);
      //  execlp("ls","ls","-al",NULL);
        execv("/usr/bin/ls",myargv);
        printf("after: I am a process, pid:%d,ppid:%d\n",getpid(),getppid());
        exit(1);
    }
    pid_t ret = waitpid(id,NULL,0);
    if(ret > 0) 
    {
        printf("wait success,father pid : %d, ret id : %d\n",getpid(),ret);
    }
    sleep(5);
    return 0;
}

运行结果为

image-20231119161027701

我们知道ls是一个被编译好的程序,ls有它自己的main函数,ls也有它自己的命令行参数,而它的命令行参数就是在这个系统调用中传入的。以及前面的execl也是一样的,它这种链表的形式,也要最终变为一共指针数组,然后进行命令行传参

在linux,所有的进程都是被人的子进程,在命令行中,所有的进程都是bash的子进程

所以,所有的进程在启动的时候都是采用exec系列的函数启动执行的!

所以进程替换,在单进程中,是把在内存中开辟空间以后,然后把程序和代码加载到内存当中

所以exec系列函数承担的是一个加载器的效果!把可执行程序导入到内存中。而且由于它还能接收命令行参数,所以调用可执行程序的时候,就可以将这个argv传入给可程序程序

4.execvp

所以有了前面三个函数的基础,我们也就可以理解下面这个函数了

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

无非就是可以不用传入路径了,它可以直接在环境变量中找到对应的路径,然后我们只需要去找到这个可执行程序即可。

#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)
    {
        char* const myargv[] = {"ls","-a","-l",NULL};
        printf("before: I am a process, pid:%d,ppid:%d\n",getpid(),getppid());
        sleep(5);
      //  execlp("/usr/bin/ls","ls","-al",NULL);
      //  execlp("ls","ls","-al",NULL);
        //execv("/usr/bin/ls",myargv);
        execvp("ls",myargv);
        printf("after: I am a process, pid:%d,ppid:%d\n",getpid(),getppid());
        exit(1);
    }
    pid_t ret = waitpid(id,NULL,0);
    if(ret > 0) 
    {
        printf("wait success,father pid : %d, ret id : %d\n",getpid(),ret);
    }
    sleep(5);
    return 0;
}

运行结果如下

image-20231119163619064

5.execle

对于这个e,它代表的是env,即环境变量,换言之,它可以传入我们自己的环境变量

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

对于这个函数而言,前面的三个参数和execl是完全一样的,

如果exec*可以执行系统命令,能不能执行我们自己的命令呢?

当然是可以的

这里我们先试着使用makefile工具一次生成多个可执行程序

我们先来创建一个c++代码(注意,对于c++代码,我们可以有三种后缀,cc、cpp、cxx这三种后缀都是一样的可以的)

image-20231119180601520

然后我们将我们的Makefile文件改为这样

image-20231119181702578

.PHONY代表总是执行all这个依赖关系,然而all的依赖关系为这两个可执行程序,所以就会先去执行他们两个的依赖关系。all它本身就是一个伪依赖的,里面什么也不执行。这样的话,就可以执行我们的代码了

此时我们就可以一次生成两个可执行程序了

image-20231119181805847

然后我们试着让这个c语言程序去调用这个c++程序

image-20231119182239107

注意这里的两个参数,第一个代表的想要执行的可执行程序是谁,在哪里。第二个参数表示的是想怎么执行

运行结果为

image-20231119182328406

那么像我们C语言形成的可执行程序既然可以调用C++的可执行程序,那么可以调用那些脚本语言形成的可执行程序吗?

比如我们创建一个.sh为结尾的脚本语言文件

touch test.sh

然后我们打开它,注意脚本语言,第一行一般都是#!开头的,然后它后面跟的是解释器。

image-20231119182959300

然后这个解释器会对我们下面的文件边读取边执行,然后我们可以写一些脚本语言

image-20231119183221453

然后我们要执行的时候就是这样执行的

image-20231119183926077

此时就把刚刚的代码批量化的执行了

所以所谓的脚本语言就是利用这个命令行解释器,从对应的文件里,一行一行的读取然后一行一行的执行

对于这些脚本语言他们也有自己的语法,比如下面的

image-20231119184511908

它的运行结果为

image-20231119184541629

如果我们想要调用这个脚本语言的话,我们可以这样做,让bash这个可执行程序带上test.sh选项即可

image-20231119184839903

运行结果为

image-20231119184822499

我们也可以去执行一个python

image-20231119185514954

直接使用命令行的话,结果如下

image-20231119185541752

我们可以去用.c进程替换一下

image-20231119185727270

运行结果为

image-20231119185759133

总之无论如何,这里是可以跨语言调用的

那么为什么无论是可执行程序,还是脚本,为什么可以跨语言调用呢?

因为所有语言运行起来,本质都是进程,只要是进程,就可以被调用

我们可以在进一步的验证一下传入命令行参数的时候

image-20231119191827577

image-20231119192101848

最终运行结果为

image-20231119192120330

我们可以在试一下第三个命令行参数,环境变量

image-20231119192430322

我们直接运行结果为

image-20231119192504130

我们可以看到,即便我们没有传入环境变量,也会自动将环境变量给传入

那么这是为什么呢?

我们需要先知道,环境变量是什么时候传给进程的。

环境变量也是数据,当我们创建子进程的时候,环境变量就已经被子进程继承下去了!!!,所以像我们之前就可以用extern char** environ去指向环境变量表

我们还发现,在发生程序替换的时候,环境变量信息依然可以打印,所以环境变量信息不会被替换

所以如果想给子进程传递环境变量,那么应该如何传递呢?

这里的传递可以分为两种

  1. 新增环境变量
  2. 彻底替换

我们先来看新增环境变量

我们最简单,最粗暴的方式就是这样的,直接在bash上导入一个环境变量,然后这个环境变量由于不随着程序替换而消失,而是一路继承下去,所以最终的结果就是如下

image-20231119193729999

那么如果我们不想要这么粗暴呢?即只在这个子进程中有这个环境变量,在bash中是没有这个环境变量的

我们之前提过getenv函数获取一个环境变量(还有第三方的的变量,命令行参数两种),其实还有一个函数putenv用于添加一个环境变量

image-20231119193949859

image-20231119194244779

此时我们就可以看到我们的环境变量了

image-20231119194321395

而我们在自己的bash里面是看不到这个环境变量的

image-20231119194445549

所以这个环境变量是可以变得越来越多的

所以如果要新增一个环境变量,在对应的父进程的进程空间直接putenv即可

那么如果我们非要传入呢?

我们就需要用到exele函数了

image-20231119195538690

运行结果为

image-20231119195605849

所以这样就可以传入环境变量了

当然不过上面的都是利用了系统的环境变量,但是如果我们想要自定义环境变量的话,我们可以这样做

image-20231119200100318

最终运行结果为

image-20231119200118244

所以用这种方法当我们传递我们自定义的环境变量时候,采用的策略是覆盖,而不是追加

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1227946.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【机器学习13】生成对抗网络

1 GANs的基本思想和训练过程 生成器用于合成“假”样本&#xff0c; 判别器用于判断输入的样本是真实的还是合成的。 生成器从先验分布中采得随机信号&#xff0c;经过神经网络的变换&#xff0c; 得到模拟样本&#xff1b; 判别器既接收来自生成器的模拟样本&#xff0c; 也接…

基于一致性算法的微电网分布式控制MATLAB仿真模型

微❤关注“电气仔推送”获得资料&#xff08;专享优惠&#xff09; 本模型主要是基于一致性理论的自适应虚拟阻抗、二次电压补偿以及二次频率补偿&#xff0c;实现功率均分&#xff0c;保证电压以及频率稳定性。 一致性算法 分布式一致性控制主要分为两类&#xff1a;协调同…

Linux管道的工作过程

常用的匿名管道&#xff08;Anonymous Pipes&#xff09;&#xff0c;也即将多个命令串起来的竖线。管道的创建&#xff0c;需要通过下面这个系统调用。 int pipe(int fd[2]) 我们创建了一个管道 pipe&#xff0c;返回了两个文件描述符&#xff0c;这表示管道的两端&#xff…

『Spring Boot Actuator Spring Boot Admin』 实现应用监控管理

前言 本文将会使用 Spring Boot Actuator 组件实现应用监视和管理&#xff0c;同时结合 Spring Boot Admin 对 Actuator 中的信息进行界面化展示&#xff0c;监控应用的健康状况&#xff0c;提供实时警报功能 Spring Boot Actuator 简介 官方文档&#xff1a;Production-rea…

RocketMQ(三):集成SpringBoot

RocketMQ系列文章 RocketMQ(一)&#xff1a;基本概念和环境搭建 RocketMQ(二)&#xff1a;原生API快速入门 RocketMQ(三)&#xff1a;集成SpringBoot 目录 一、搭建环境二、不同类型消息1、同步消息2、异步消息3、单向消息4、延迟消息5、顺序消息6、带tag消息7、带key消息 一…

【IPC】 共享内存

1、概述 共享内存允许两个或者多个进程共享给定的存储区域。 共享内存的特点 1、 共享内存是进程间共享数据的一种最快的方法。 一个进程向共享的内存区域写入了数据&#xff0c;共享这个内存区域的所有进程就可以立刻看到 其中的内容。 2、使用共享内存要注意的是多个进程…

如何定位el-tree中的树节点当父元素滚动时如何定位子元素

使用到的方法 Element 接口的 scrollIntoView() 方法会滚动元素的父容器&#xff0c;使被调用 scrollIntoView() 的元素对用户可见。 参数 alignToTop可选 一个布尔值&#xff1a; 如果为 true&#xff0c;元素的顶端将和其所在滚动区的可视区域的顶端对齐。相应的 scrollIntoV…

C语言入门笔记—static、extern、define、指针、结构体

一、static static修饰局部变量的时候&#xff0c;局部变量出了作用域&#xff0c;不销毁。本质上&#xff0c;static修饰局部变量的时候&#xff0c;改变了变量的存储位置。详见下图&#xff0c;当a不被static修饰和被static修饰的时候。 C/C static关键字详解&#xff…

随机过程-张灏

文章目录 导论随机过程相关 学习视频来源&#xff1a;https://www.bilibili.com/video/BV18p4y1u7NP?p1&vd_source5e8340c2f2373cdec3870278aedd8ca4 将持续更新—— 第一次更新&#xff1a;2023-11-19 导论 教材&#xff1a;《随机过程及其应用》陆大絟.张颢 参考&…

CD36 ; + Lectin;

CD2 LIMP-2&#xff0c; LGP85 SR-BI&#xff0c; CD36&#xff1b; 清道夫受体蛋白CD36超家族的成员是 脂质代谢 和 先天免疫 的重要调节因子。它们识别正常和修饰的脂蛋白&#xff0c;以及与病原体相关的分子模式。 该家族由三个成员组成&#xff1a; SR-BI &am…

ZJU Beamer学习手册(二)

ZJU Beamer学习手册基于 Overleaf 的 ZJU Beamer模板 进行解读&#xff0c;本文则基于该模版进行进一步修改。 参考文献 首先在frame文件夹中增加reference.tex文件&#xff0c;文件内容如下。这段代码对参考文献的引用进行了预处理。 \usepackage[backendbiber]{biblatex} \…

异常语法详解

异常语法详解 一&#xff1a;异常的分类&#xff1a;二&#xff1a;异常的处理1:异常的抛出:throw2&#xff1a;异常的声明:throws3&#xff1a;try-catch捕获并处理异常 三&#xff1a;finally关键字四&#xff1a;自定义异常类&#xff1a; 一&#xff1a;异常的分类&#xf…

电路的基本原理

文章目录 一、算数逻辑单元(ALU)1、功能2、组成 二、电路基本知识1、逻辑运算2、复合逻辑 三、加法器实现1、一位加法器2、串行加法器3、并行加法器 一、算数逻辑单元(ALU) 1、功能 算术运算&#xff1a;加、减、乘、除等 逻辑运算&#xff1a;与、或、非、异或等 辅助功能&am…

对vb.net 打印条形码code39、code128A、code128C、code128Auto(picturebox和打印机)封装类一文的补充

在【精选】vb.net 打印条形码code39、code128A、code128C、code128Auto&#xff08;picturebox和打印机&#xff09;封装类_vb.net打印标签_WormJan的博客-CSDN博客 这篇文章中&#xff0c;没有对含有字母的编码进行处理。这里另开一篇帖子&#xff0c;处理这种情况。 在那篇文…

【C++入门】拷贝构造运算符重载

目录 1. 拷贝构造函数 1.1 概念 1.2 特征 1.3 常用场景 2. 赋值运算符重载 2.1 运算符重载 2.2 特征 2.3 赋值运算符 前言 拷贝构造和运算符重载是面向对象编程中至关重要的部分&#xff0c;它们C编程中的一个核心领域&#xff0c;本期我详细的介绍拷贝构造和运算符重载。 1. …

Js中clientX/Y、offsetX/Y和screenX/Y之间区别

Js中client、offset和screen的区别 前言图文解说实例代码解说 前言 本文主要讲解JavaScript中clientX、clientY、offsetX、offsetY、screenX、screenY之间的区别。 图文解说 在上图中&#xff0c;有三个框&#xff0c;第一个为屏幕&#xff0c;第二个为浏览器大小&#xff0c…

约数个数定理

首先在讲这个定理前&#xff0c;首先科普一下前置知识 约数&#xff1a; 何为约数&#xff0c;只要能整除n的整数就是n的约数&#xff0c;举个例子&#xff0c;3的约束是1和3因为1和3能整除3 质数&#xff1a; 除了这个数字本身和1以外没有其他因子的数字就叫质数&#xff…

AVL树和红黑树

AVL树和红黑树 一、AVL树1. 概念2. 原理AVL树节点的定义插入不违反AVL树性质违反AVL树性质左单旋右单旋左右双旋右左双旋总结 删除 3. 验证代码4. AVL树完整实现代码 二、红黑树1. 概念2. 性质3. 原理红黑树节点的定义默认约定插入情况一 &#xff08;u存在且为红&#xff09;情…

论文速览 Arxiv 2023 | DMV3D: 单阶段3D生成方法

注1:本文系“最新论文速览”系列之一,致力于简洁清晰地介绍、解读最新的顶会/顶刊论文 论文速览 Arxiv 2023 | DMV3D: DENOISING MULTI-VIEW DIFFUSION USING 3D LARGE RECONSTRUCTION MODEL 使用3D大重建模型来去噪多视图扩散 论文原文:https://arxiv.org/pdf/2311.09217.pdf…

【2017年数据结构真题】

请设计一个算法&#xff0c;将给定的表达式树&#xff08;二叉树&#xff09;转换成等价的中缀表达式&#xff08;通过括号反映次序&#xff09;&#xff0c;并输出。例如&#xff0c;当下列两棵表达式树作为算法的输入时&#xff1a; 输出的等价中缀表达式分别为(ab)(a(-d)) 和…