当前位置: 首页 > article >正文

Linux进程控制(四)之进程程序替换

文章目录

    • 进程程序替换
      • 单进程版程序替换
      • 替换原理
      • 多进程版程序替换
      • 替换函数
      • 函数解释
        • 小知识
      • 命名理解

进程程序替换

如果要让子进程执行与父进程完全不同的代码,就要进行进程程序替换。

单进程版程序替换

执行一个可执行文件

image-20250316212914342

makefile

mycommand:mycommand.c
    gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
    rm -rf mycommand    

mycommand.c

#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);//必须以NULL结尾                              
    printf("After,I am a process,pid:%d,ppid:%d\n",getpid(),getppid());
    return 0;
}

image-20250316215554319

现象:after没有执行,把ls -a -l执行了,每个pid也不一样了。

mycommand.c

#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);//必须以NULL结尾  
    execl("/usr/bin/top","top",NULL);//必须以NULL结尾                                      
    printf("After,I am a process,pid:%d,ppid:%d\n",getpid(),getppid());  
    return 0;                                               
}

image-20250316215936672

程序可以把系统里的命令封装起来,程序变成进程之后把命令跑起来。

替换原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),

子进程往往要调用一种exec函数以执行另一个程序。

当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。

调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。


代码运行起来就是我们系统当中的一个进程,

创建新进程总会创建一个进程的PCB(task_struct),地址空间,页表等,

要把代码、数据加载到内存里,通过页表映射内存,

然后CPU根据PCB找到虚拟地址、页表映射代码和数据并执行。

以ls为例,ls用execl加载进内存,

直接把ls的代码替换原来的代码,

ls的数据直接替换原来的数据。(没有创建新进程!)

(如果ls代码数据加载到内存了,小了就新增空间,大了就释放空间)

把页表的右侧的映射地址改变一下(收缩一下)。

新加载的程序从main函数重新运行(从0开始执行)。

–程序替换!

image-20250312144644539

多进程版程序替换

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.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);//必须以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 successfully,father pid:%d,ret:%d\n",getpid(),ret);              
    }
    sleep(5);
    return 0;
}  

image-20250317195303420

在子进程sleep(5)的时候,父进程阻塞等待子进程退出。

image-20250317103348714

子进程在调用execl时,不会影响父进程。

原因:发生了写时拷贝,进程之间是相互独立的。

没有调用execl的时候,子进程只指向父进程的代码和数据,

当子进程执行execl时,ls代码数据替换进来的时候,

发生写时拷贝,不管是代码还是数据都有写时拷贝。

代码不一定是不可被写入的,用户直接写,操作系统会拦截崩溃,

但是execl是让操作系统写。


程序替换没有创建新进程,只进行代码和数据的程序替换。

父子进程的pid一直都没有变。

子进程结束前是2254,结束后还是2254。

所以,task_struct,mm_struct是没有被释放或者重新建立的。(字段可能会稍微修改)

替换函数

其实有六种以exec开头的函数,统称exec函数:

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

函数解释

after及之后的代码没有被执行,

原因:after及后面代码是在execl程序替换之后的,

原来的代码会被替换,所以,after及后面代码被ls的代码替换了,

所以后续代码不会被执行。

程序替换成功之后,exec* 后续的代码不会被执行。

程序替换失败了,才可能执行后续代码。

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。

如果调用出错则返回-1

所以exec* 函数只有失败的返回值而没有成功的返回值。

小知识

CPU如何得知程序的入口地址?

Linux中形成的可执行程序,是由格式(ELF)的,可执行程序的表头,可执行程序的入口地址就在表头!!!

可执行程序的表包括:代码段,数据段,只读数据区等,这些段区的地址在表头。

加载可执行程序时,代码和数据可以先不加载,

但是一定要先加载表头,每个数据区的开始的数字(start)就是在表头中来的。

当我们替换了一个新进程,新进程也有表头,CPU就可以在表头读到对应的可执行程序的入口。

可执行程序在编译的时候,就产生了一个STARTART的地址(程序入口),

编写到表头中,加载到内存时,CPU可以获取。

表头一方面初始化我们的地址空间、页表等,另一方面告诉CPU代码的程序入口在哪里。

命名理解

image-20250317204830649

l(list) : 表示参数采用列表

v(vector) : 参数用数组

p(path) : 有p自动搜索环境变量PATH

e(env) : 表示自己维护环境变量

image-20250312144703544

#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[]);

execl:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
    pid_t id=fork();
    if(id==0)
    {
        printf("before,I am a process,pid:%d,ppid:%d\n",getpid(),getppid());
		execl("/usr/bin/ls","ls","-a","-l",NULL);//必须以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 successfully,father pid:%d,ret:%d\n",getpid(),ret);
    }                                                              
    return 0;                                                      
}

image-20250317212533917

传参的时候,从第二个参数开始,一个一个地传递,最后一个必须为NULL。

函数名带 ‘l’ 的,传参采用可变参数,而且传参时,一个一个地传。

从第二个参数开始,在命令行当中怎么写的,就依次怎么传递参数给程序,

空格改成逗号,最后加上NULL。

image-20250317210015346

要执行一个程序第一件事就是找到要执行的程序。

所有的exec* 第一个参数(const char *path) :决定如何找到该程序。

名字不带 ‘p’ 必须是全路径(绝对或者相对路径的方式找到要执行的程序)

第一个参数解决:在什么路径下找到该程序。

第二个参数解决:如何执行这个程序,要不要涵盖选项,涵盖哪些。

execlp:

PATH:execlp会在默认的PATH环境变量中查找。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
    pid_t id=fork();
    if(id==0)
    {
        printf("before,I am a process,pid:%d,ppid:%d\n",getpid(),getppid());
        //execlp("/usr/bin/ls","ls","-a","-l",NULL);
        execlp("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 successfully,father pid:%d,ret:%d\n",getpid(),ret);
    }                                                              
    return 0;                                                      
}

可以带路径也可以不带路径。

image-20250317212554232

函数名中带了 ‘p’ 就不用带路径了,execlp会自动在环境变量中查找,

所有子进程都会继承父进程的环境变量列表,

当前的进程所有的环境变量都是从bash里来的,

bash里面本来就有path,就会被所有的子进程继承。(环境变量具有全局属性)

image-20250317213114436

ls在usr/bin目录下,找到之后执行。

有两个ls是因为:

第一个参数为了 找到程序,不仅要告诉execlp路径(路径由path解决),

而且要告诉execlp要执行什么程序。

第一个ls代表要执行谁,第二个ls代表要怎么执行。


execv:

char *const argv[] -> 字符串指针数组

第一个参数:怎么找到该程序。

第二个参数:如何执行这个程序。

image-20250317214644413

就是把可变参数的传参形式变成了指针数组的形式。

中间const表示:

一旦写进去了,指向的地址不能改变。(指针本身不能改)

内容可以改,"-a"等可以改。(指针指向的内容可以修改)

  #include <stdio.h>
  #include <unistd.h>
  #include <stdlib.h>
  #include <sys/wait.h>
  #include <sys/types.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());
          execv("/usr/bin/ls",myargv);                           
          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 successfully,father pid:%d,ret:%d\n",getpid(),ret);
      }                                                    
      return 0;                                            
  }

image-20250317215242619

ls是一个程序,有main函数,main函数有命令行参数,命令行参数由execv系统调用,第二个参数传入的。

可变参数最终也要变成指针数组的形式,然后传入ls调用的main函数当中。

execv系统调用,系统获取的命令行参数,会把参数传递给ls的main函数。

在Linux当中,所有的进程都是别人的子进程,

在命令行当中,所有的进程都是bash的子进程。

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

程序替换在单进程当中,是把对应的可执行程序的代码和数据加载到内存当中,

为当前进程开辟空间等,然后把自己的代码数据加载进内存。

所以,exec*系列函数 - 起到了加载器的作用。(代码级别的加载器)

exec*函数把磁盘当中的可执行程序加载到内存中。

所以,exec*里会存在诸如内存申请、外设访问等动作。

exec把可执行程序导入到内存里,可以获得命令行参数,

所以execv就可以直接调用ls的main函数时,把argv参数传递给程序。

所有的函数都是压栈,在调用main函数之前先形成一个简单的栈帧结构,

把argv的地址push进去,构造一个main函数被调用的上下文,就可以把argv传入。

所以我们是可以把命令行参数传递给可执行程序的。


execvp:

  #include <stdio.h>
  #include <unistd.h>
  #include <stdlib.h>
  #include <sys/wait.h>
  #include <sys/types.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());
          execvp("ls",myargv);                           
          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 successfully,father pid:%d,ret:%d\n",getpid(),ret);
      }                                                    
      return 0;                                            
  }

image-20250317223505942


exec*能够执行系统命令,那么也能执行我们自己的命令

一个c语言程序调用一个C++程序,两个可执行代码。

#include <stdio.h>  
#include <unistd.h>  
#include <stdlib.h>  
#include <sys/wait.h>  
#include <sys/types.h>  
int main()  
{  
    pid_t id=fork();  
    if(id==0)  
    {
        printf("before,I am a process,pid:%d,ppid:%d\n",getpid(),getppid());  
        execl("./otherexe","otherexe",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 successfully,father pid:%d,ret:%d\n",getpid(),ret);
    }
    return 0;
} 

image-20250318134328091

第一个参数代表要执行的文件在哪,执行谁。

第二个参数代表怎么执行。

execl("./otherexe","otherexe",NULL);
execl("./otherexe","./otherexe",NULL);

以上两个都可以跑,结果一样。

命令行带"./"是因为要告诉bash可执行程序在哪。

但是execl的第一个参数已经说明了可执行程序的路径,所以第二个参数可以不用写"./"了。


用c语言调用其他的语言

c语言调用sh

test.sh

#!usr/bin/bash

function myfun()
{
    cnt=1
    while [ $cnt -le 10 ]
    do
        echo "Hello $cnt"
        let cnt++
    done
}
echo "Hello Linux!"
echo "Hello Linux!"
echo "Hello Linux!"

ls -a -l

myfun     

所有的脚本语言都以"#!"开头,后面跟着脚本语言对应的解释器。

脚本语言并不是脚本在跑,而是由解释器来解释式执行的。

image-20250318135937945

mycommand.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
    pid_t id=fork();
    if(id==0)
    {
        printf("before,I am a process,pid:%d,ppid:%d\n",getpid(),getppid());
        execl("/usr/bin/bash","bash","test.sh",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 successfully,father pid:%d,ret:%d\n",getpid(),ret);
    }                                                               
    return 0;                                                       
}                                                                   

在命令行上,要执行的可执行文件不是脚本文件,而是脚本文件的解释器。

image-20250318140323807


用c语言调用py

  1 #!/usr/bin/python3                                                              
  2                                                                                 
  3 print("Hello py!")      

image-20250318140720684

mycommand.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
    pid_t id=fork();
    if(id==0)
    {
        printf("before,I am a process,pid:%d,ppid:%d\n",getpid(),getppid());
        execl("/usr/bin/python3","python3","test.py",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 successfully,father pid:%d,ret:%d\n",getpid(),ret);
    }                                                           
    return 0;                                                   
}

py


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

所有语言运行起来,本质都是进程!

只要是进程就可以被调用。

基本上所有的语言都有execl等的接口。

补充:

C++文件名后缀包括: .cc .cpp .cxx

image-20250318131641351

image-20250318131736720

otherex.cc

#include <iostream>
using namespace std;
int main()
{
    cout<<"Hello C++ Linux!"<<endl;
    cout<<"Hello C++ Linux!"<<endl;
    cout<<"Hello C++ Linux!"<<endl;
    return 0;
}

image-20250318131817628

makefile

mycommand:mycommand.c
    gcc -o $@ $^ -std=c99
otherexe:otherexe.cpp
    g++ -o $@ $^ -std=c++11
.PHONY:clean                     
clean:                       
    rm -rf mycommand otherexe

image-20250318132144981

为什么只形成mycommand呢

在makefile中,自上往下的扫描,遇到的第一个文件就是目标文件,所以只执行目标文件的方法。

哪个目标文件在前就执行哪个依赖方法。

.PHONY:all
all:otherexe mycommand

mycommand:mycommand.c      
    gcc -o $@ $^ -std=c99  
otherexe:otherexe.cpp      
    g++ -o $@ $^ -std=c++11  
.PHONY:clean                 
clean:                       
    rm -rf mycommand otherexe

在makefile自顶向下扫描时,遇到的第一个目标文件是伪目标all

all又依赖于otherexe和mycommand,

所以就先形成otherexe,再形成mycommand

all没有依赖方法,所以关系推导完之后,就不执行了。

image-20250318133012474


execle:

我们可以在我们编写的代码里获取命令行参数和环境变量!

可以验证mycommand给otherexe传入命令行参数和环境变量

mycommand一个程序形成的环境变量如何导给另一个程序otherexe?

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h> 
int main()
  {
      pid_t id=fork();
      if(id==0)
      {
          printf("before,I am a process,pid:%d,ppid:%d\n",getpid(),getppid());
          char*const myargv[]={       
              "otherexe",
              "-a",
              "-b",
              "-c",
              NULL
          };
          execv("./otherexe",myargv);
          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 successfully,father pid:%d,ret:%d\n",getpid(),ret);
      }
      return 0;
  }

会把myargv作为参数传递给otherexe,otherexe就可以拿到对应的参数了。

  #include <iostream>
  using namespace std;
  int main(int argc,char *argv[])
  {
      cout<<argv[0]<<" begin running"<<endl;
      for(int i=0;argv[i];i++)
      {
          cout<<i<<" : "<<argv[i]<<endl;
      }
      cout<<argv[0]<<" stop running"<<endl;
      return 0;                           
  }     

image-20250319101028134

所以,exec所对应的参数就传入了。

  #include <iostream>
  using namespace std;
  int main(int argc,char *argv[],char*env[])
  {
      cout<<argv[0]<<" begin running"<<endl;
      cout<<"这是命令行参数:"<<endl;
      for(int i=0;argv[i];i++)
      {
          cout<<i<<" : "<<argv[i]<<endl;
      }
      cout<<"这是环境变量:"<<endl;
      for(int i=0;env[i];i++)
      {
          cout<<i<<" : "<<env[i]<<endl;
      }
      cout<<argv[0]<<" stop running"<<endl;
      return 0;
  }

image-20250319101532527

命令行参数和环境变量都有!

在默认情况下,尽管没有传环境变量,但是子进程自动获取(继承)环境变量。

环境变量也是数据,在地址空间里是有命令行参数和环境变量列表的,

创建子进程的时候,环境变量就已经被子进程继承下去了。

extern char**environ 这个第三方变量直接指向进程的环境变量信息。

这个变量已经被父进程初始化,指向自己的环境变量表了。

这个变量拷贝的时候,也被子进程继承下去了。

不通过传参方式,在程序地址空间里也可以获得环境变量和命令行参数。

因为子进程会继承父进程的地址空间、页表等,所以命令行参数和环境变量就可以被继承。

程序替换只替换了代码和数据,环境变量信息不会被替换!


想给子进程传递环境变量,如何传递?

1.新增环境变量

给父进程导入新的环境变量,就会被子进程继承下去。

在Shell里新增环境变量

image-20250319103853403

image-20250319104003883

环境变量信息不随着进程替换而被替换,只会随着系统一路的被子进程获取。

bash -> mycommand ->otherexe 环境变量具有全局属性。

但如果只想在mycommand父进程中新增环境变量导入传递给otherexe呢?

putenv 添加一个环境变量 添加到调用进程的上下文。

image-20250319104540716

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h> 
int main()
  {
      putenv("MY_ENV=6666666666666");
      pid_t id=fork();
      if(id==0)
      {
          printf("before,I am a process,pid:%d,ppid:%d\n",getpid(),getppid());           
          char*const myargv[]={
            "otherexe",
              "-a",
              "-b",
              "-c",
              NULL
          };
          execv("./otherexe",myargv);
          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 successfully,father pid:%d,ret:%d\n",getpid(),ret);
      }
      return 0;
}

image-20250319105047262

image-20250319105208178

putenv可以导入属于自己和自己的子进程的环境变量。

所以mycommand导入新环境变量与bash(父进程)没关系!

如果非得传

image-20250319105543484

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h> 
int main()
  {
      extern char** environ;
      pid_t id=fork();
      if(id==0)
      {
          printf("before,I am a process,pid:%d,ppid:%d\n",getpid(),getppid());           
          execle("./otherexe","otherexe","-a","-w",NULL,environ);
          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 successfully,father pid:%d,ret:%d\n",getpid(),ret);
      }
      return 0;
  }

image-20250319105927772

可以把环境变量交给子进程,那也可以把整型、字符串交给子进程。

可以通过子进程继承父进程的数据,并且不修改,这样就是共享的方式去传,

也可以通过环境变量去传。

2.彻底替换

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h> 
int main()
{
      pid_t id=fork();
      if(id==0)
      {          
			printf("before,I am a process,pid:%d,ppid:%d\n",getpid(),getppid());
          char *const myenv[]={
              "MYVAL=123666",
              "MYlll=567999",
              NULL
          };
          execle("./otherexe","otherexe","-a","-w",NULL,myenv);
          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 successfully,father pid:%d,ret:%d\n",getpid(),ret);
      }
      return 0;
  }

image-20250319110731196

当我们传入我们自己编写的环境变量时,采用的策略是覆盖,而不是追加。

如果不传入环境变量,子进程在被调用的时候,操作系统会把父进程的环境变量传给子进程。

如果有写相应的参数,操作系统就传对应的参数,如果没,就使用系统默认的。


事实上,只有execve是真正的系统调用,其它六个函数最终都调用 execve来完成程序替换。

所以execve在man手册 第2节–系统调用,

其它函数在man手册第3节–库函数。

区别:只是传参的不同。

image-20250319150732378

这些函数之间的关系如下图所示。

下图exec函数族 一个完整的例子:

image-20250312144752856


http://www.kler.cn/a/596576.html

相关文章:

  • 新能源汽车高压液体加热器总成技术解析及未来发展趋势
  • HashMap学习总结——JDK17
  • 介绍一个测试boostrap表格插件的好网站!
  • LVGL学习1
  • 【云上CPU玩转AIGC】——腾讯云高性能应用服务HAI已支持DeepSeek-R1模型预装环境和CPU算力
  • 基于linux平台的C语言入门教程(4)输入输出
  • SQL中的索引是什么
  • 建筑安全员考试:“实战演练” 关键词助力的答题提升策略
  • ARM架构薄记2——ARM学习架构抓手(以ARMv7为例子)
  • Linux小知识
  • 七桥问题与一笔画问题:图论的奠基石
  • Vue3(自定义指令directive详解)
  • 前端(vue)学习笔记(CLASS 5):自定义指令插槽路由
  • RK3588开发笔记-DDR4降频实战与系统稳定性优化
  • KnowGPT知识图谱整合
  • 深入理解 Spring 框架中的 AOP 技术
  • 2025年3月GESP八级真题解析
  • 收数据花式画图plt实战
  • 【CXX-Qt】2.3 类型
  • 网站蜜罐部署与攻击追踪方案