(17)Linux的进程阻塞进程程序替换 exec 函数簇

前言:本章我们讲解它的 options 参数。在讲解之前我们需要理解进程阻塞,然后我们重点讲解二进程程序替换,这是本章的重点,然后介绍一个进程替换函数 execl,通过介绍这个函数来打开突破口,引入进程创建的知识点。最后,我们在学习进程创建的 exec 函数簇。

一、进程阻塞(Process Blocking)

1、继续讲解 waitpid

 我们先来简单回顾一下上一章的内容: 

#include <sys/types.h>
#include <sys/wait.h>pid_t waitpid(pid_t pid, int* status, int options);

 上一章介绍了 status 参数,知道了如何通过位操作来截 status 获取进程错误码与错误信号:

status&0x7F        // 获取错误信号
(status>>8)&0xFF   // 获取错误码

但是我们还是不建议这么做,因为直接用操作系统提供的宏就好了,我们可以通过 WIFEXITED 宏来检测子进程是否正常退出(检测进程退出时信号是否为 0),在用 WEXITSTAUS 宏还获取进程的退出码:

if (WIFEEXITED(status)) 
{printf("等待成功: exit code: %d\n", WIFEEXITED(status));
}

这些都是上一章讲解 status 参数的内容了,我们下面要讲的是 waitpid 另一个参数 options。 

options 为 0,则标识为 阻塞等待 

比如:如果子进程不退出,父进程在等,等的时候子进程是卡在那等的。

2、理解进程阻塞

思考:如何理解父进程进程阻塞? 

首先,进程状态我们说过:如果一个进程在系统层面上要等待某件事情发生,

但这件事情还没发生,那么当前进程的代码还没法向后运行,只能让该进程处于阻塞状态。

就是让父进程的 task_struct 状态由 R\rightarrow S,从运行队列投入到等待队列,等待子进程退出。

子进程退出的本质是条件就绪,如果子进程退出条件一旦就绪,操作系统会逆向地做上述工作。

将父进程的\textrm{pcb}从等待队列再搬回运行队列,并将状态S\rightarrow R,此时父进程就会继续运行。

 3、轮询检测(Polling)

所谓的阻塞,其实就是挂起。在上层表现来看,就是进程卡住了 

而非阻塞式等待是会做些自己的事,而不是傻等!

多次调用非阻塞接口,这个过程我们称之为 轮询检测 (Polling)。

我们上一章中讲解 waitpid 时,举的例子都是 阻塞式 的等待。

如果我们想 非阻塞式 的等,我们可以设置 options 选项为 WNOHANG (With No Hang)。

这个选项通过字面很好理解,就是等待的时候不要给我挂 (Hang) 住,其实就是非阻塞!

现在我们正式介绍一下 waitpid 的返回值:

  • 如果此时等待成功,返回值是子进程的退出码。
  • 如果你是非阻塞等待 (WNOHANG),等待的子进程没有退出,返回值为 0。

4、 基于非阻塞的轮询等待(waitpid)

 如果我们想把我们上一章节,演示 waitpid 使用方式的代码,改为非阻塞等待。

我们只需要将 waitpid 的 options 参数加上。

代码演示:基于非阻塞的轮询等待

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>int main(void)
{pid_t id = fork();if (id == 0) {// 子进程while (1) {printf("子进程:我的PID: %d,我的PPID: %d\n", getpid(), getppid());sleep(5);  // 先睡眠 5s,5s后退出break;}exit(233);}else if (id > 0) {// 父进程/* 基于非阻塞的轮询等待方案 */int status = 0;while (1) {pid_t ret = waitpid(-1, &status, WNOHANG);if (ret > 0) {          // 等待成功printf("等待成功,%d,退出信号: %d,退出码: %d\n", ret, status&0x7F, (status>>8)&0xFF);}else if (ret == 0) {    // 等待成功,但是子进程没有退出printf("父进程:子进程还没好,那我先做其他事情\n");sleep(1);}else {// 出错了,暂时不作处理}}}else {// 什么也不做}
}

说明:我们只需要将 waitpid 中的 options 参数带上 WHOHANG 就可以了。返回值 ret>0 就是等待成功,我们这里新增一个等于 0 的判断,作为 "等待成功但是子进程还没有退出" 的情况,因为等待的子进程没有退出,返回值为 0 。运行后,就会问子进程好没好,如果没有好父进程就可以做自己的事情了,这就是非阻塞式轮询等待。
运行结果如下:

我们可以让父进程在非阻塞等待时真正做点事 

代码演示:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <vector>typedef void (* handler_t)();  // 函数指针类型// 方法集
std::vector<handler_t> handlers;void func1() {printf("Hello,我是方法1\n");
}
void func2() {printf("Hello,我是方法2\n");
}void Load() {// 加载方法handlers.push_back(func1);handlers.push_back(func2);
}int main(void)
{pid_t id = fork();if (id == 0) {// 子进程while (1) {printf("子进程:我的PID: %d,我的PPID: %d\n", getpid(), getppid());sleep(3);}exit(233);}else if (id > 0) {// 父进程/* 基于非阻塞的轮训等待方案 */int status = 0;while (1) {pid_t ret = waitpid(-1, &status, WNOHANG);if (ret > 0) {          // 等待成功printf("等待成功,%d,退出信号: %d,退出码: %d\n", ret, status&0x7F, (status>>8)&0xFF);}else if (ret == 0) {    // 等待成功,但是子进程没有退出printf("父进程:子进程好了没?哦,还没,那我先做其他事情啦\n");if (handlers.empty()) {Load();}for (auto f : handlers) {f();  // 回调处理对应的任务}sleep(1);}else {// 出错了,暂时不作处理}}}else {// 什么也不做}
}

如果你想要你的程序直接父进程做更多的事情,把方法加到 Load 里就可以了。

写下 Makefile:

mytest:mytest.cppg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f mytest

运行结果如下:

二、 进程程序替换(Process Substitution)

1、 让子进程执行一个新的程序

我们之前做的所有代码演示,子进程执行的都是父进程的代码片段。

如果我们想让创建出来的子进程,执行全新的程序呢? 

之前我们通过写时拷贝,让子进程和父进程在数据上互相解耦,保证独立性。如果想让子进程和父进程彻底分开,让子进程彻彻底底地执行一个全新的程序,我们就需要 进程的程序替换

那为什么要让子进程执行新的程序呢?

我们一般在服务器设计的时候(Linux 编程)的时候,往往需要子进程干两件种类的事情:

  •     让子进程执行父进程的代码片段(服务器代码…)
  •     想让子进程执行磁盘中一个全新的程序(shell、想让客户端执行对应的程序、通过我们的进程执行其他人写的进程代码、C/C++ 程序调用别人写的 C/C++/Python/Shell/Php/Java...)

2、程序替换原理 

程序替换的原理:

  • 将磁盘中的内存,加载入内存结构。
  • 重新建立页表映射,设执行程序替换,就重新建立谁的映射(下图为子进程建立)。
  • 效果:让父进程和子进程彻底分离,并让子进程执行一个全新的程序!

程序替换的本质

本质上就是去替换一个进程pcb在内存中的对应的代码和数据(加载新程序到内存——>更新页表信息——>初始化虚拟地址空间),然后这个进程pcb重新开始执行这个新的程序

这个过程有没有创建新的进程呢?没有!根本就没有创建新的进程!

因为子进程的内核数据结构根本没变,只是重新建立了虚拟的物理地址之间的映射关系罢了。

 内核数据结构没有发生任何变化! 包括子进程的 \textrm{pid}\textrm{pid} 都不变,说明压根没有创建新进程。

 以可变参数列表的接收参数的 execl 接口

我们要调用接口,让操作系统去完成这个工作 —— 系统调用。

如何进行程序替换?我们先见见猪跑 —— 从 execl 这个接口讲,看看它怎么跑的。

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

如果我们想执行一个全新的程序,我们需要做几件事情:

  • 第一件事情:先找到这个程序在哪里。
  • 第二件事情:程序可能携带选项进行执行(也可以不携带)。

 简单来说就是:① 程序在哪?  ② 怎么执行?

所以,execl 这个接口就必须得把这两个功能都体现出来!

  •     它的第一个参数是 path,属于路径。
  •     参数  const char* arg, ... 中的 ... 表示可变参数,命令行怎么写(ls, -l, -a) 这个参数就怎么填。ls, -l, -a 最后必须以 NULL 结尾,表示 "如何执行程序的" 参数传递完毕。

代码演示:exec() 

#include <stdio.h>
#include <unistd.h>int main(void)
{printf("我是一个进程,我的PID是:%d\n", getpid());// ls -a -lexecl("/usr/bin/ls", "ls", "-l", "-a", NULL);  // 带选项printf("我执行完毕了,我的PID是:%d\n", getpid());return 0;
}

运行结果:

 运行最后就会执行ls命令execl("/usr/bin/ls","ls","-l","-a",NULL);

刚才是带选项的,现在我们再来演示一下不带选项的: 

 execl("/usr/bin/top", "top", NULL);  // 不带选项

运行结果如下:

这样我们的程序就直接能执行 top 命令了,除此之外,我们曾经学的大部分命令其实都可以通过 execl 执行起来。这就叫做 程序替换

printf("我执行完毕了,我的PID是:%d\n", getpid());

这句话为什么没有打印出来??

为什么我们最后的代码并没有被打印出来?

因为 一旦替换成功,是会将当前进程的代码和数据全部替换的!

所以自然后面的 printf 代码早就被替换了,这意味着该代码不复存在了,荡然无存!

因为在程序替换的时候,就已经把对应进程的代码和数据替换掉了!

而第一个 printf 执行了的原因自然是因为程序还没有执行替换,
所以,这里的程序替换函数用不用判断返回值?为什么?

int ret = execl(...);

一旦替换成功,还会执行返回语句吗?返回值有意义吗? 没有意义的!

 程序替换不用判断返回值!因为只要成功了,就不会有返回值。 而失败的时候,必然会继续向后执行。通过返回值最多能得到是什么原因导致替换失败的。只要执行了后面的代码,看都不用看,一定是替换失败了;只要有返回值,就一定是替换失败了。

我们来模拟一下失败的情况,我们来执行一个不存在的指令 :

execl("/usr/bin/lssssss", "ls", "-l", "-a", NULL);  // 带选项

execl 替换失败,就会继续向后执行。但是,一旦 execl 成功后就会跟着新程序的逻辑走,就不会再 return 了,再也不回来了,所以返回值加不加无所谓了。

3、引入进程创建 

以前我们的示例都是让子进程执行父进程的代码,我们今天想让子进程执行自己的程序。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>int main(void) 
{printf("我是父进程,我的PID是: %d\n", getpid());pid_t id = fork();if (id == 0) {/* child 我们想让子进程执行全新的程序 */printf("我是子进程,我的PID是:%d\n", getpid());execl("/usr/bin/ls", "ls", "-a", "-l", NULL);  /* 让子进程执行替换 */exit(1);   /* 只要执行了exit,就意味着 excel 系列函数失败了,终止子进程*/}/* 一定是父进程 */int status = 0;int ret = waitpid(id, &status, 0);if (ret == id) {/* 等待成功 */sleep(2);  printf("父进程等待成功!\n");}return( 0);
}

 运行结果:

成功执行代码,父进程也等待成功了。这里的子进程没有执行父进程的代码,而是执行了自己的程序。

子进程执行程序替换,会不会影响父进程呢?不会!因为进程具有独立性。

当程序替换的时候,我们可以理解成 —— 代码和数据都发生了写时拷贝,完成了父子分离。

三、exec 函数簇(Sheaf of functions exec)

1、以指针数组接收参数的 execv 接口

刚才我们学会了 execl 接口,我们下面开始学习更多的 exec 接口!它们都是用来替换的。

下面我们先来讲解一下和 execl 很近似的 execv:

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

path 参数和 execl 一样,关注的都是 "如何找到"

argv[] 参数关注的是 "如何执行",是个指针数组,放 char* 类型,指向一个个字符串。

大家在命令行上 $ ls -a -l ,在 execl 里我们是这么传的: "ls", "-a", "-l", NULL 。

所以 execv 和 execl 只有传参方式的区别,一个是可变参数列表 (l),一个是指针数组 (v)。

值得注意的是,在构建 argv[] 的时,结尾仍然是要加上 NULL!
代码:execv()

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>#define NUM 16int main()
{
pid_t id=fork();
if(id==0){//子进程//ls -a -lprintf("子进程开始运行,pid: %d\n",getpid());sleep(3);char* const _argv[NUM]={(char*)"ls",(char*)"-l",(char*)"-a",NULL};//execl("/usr/bin/ls","ls","-a","-l",NULL);execv("/usr/bin/ls",_argv);exit(1);}else{//父进程int status=0;printf("父进程开始运行,pid: %d\n",getpid());pid_t id=waitpid(-1,&status,0);//阻塞等待                                                                                                                      if(id>0){printf("wait success,exit code: %d\n",WEXITSTATUS(status));}             }               return 0;}

运行结果:

2、无需带路径就能直接执行的 execlp 接口(可变参数列表)

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

execlp,它的作用和 execv、execl 是一样的,它的作用也是执行一个新的程序。

仍然是需要两步:① 找到这个程序   ② 告诉我怎么执行

第一个参数 file 也是 "你想执行什么程序",第二个参数 arg 是 "如何去执行它"。

所以这一块的参数传递,和 execl 是一样的,唯一的区别是比 execl 多了一个 p

我们执行指令的时候,默认的搜索路径在环境变量 \textrm{PATH} 中,所以这个 p 的意思是环境变量。

这意味着:执行 execlp 时,会直接在环境变量中找,不用去输路径了,只要程序名即可。

execlp("ls", "ls", "-a", "-l", "NULL");   // 路径都不用,直接扔

 这里出现的两个 ls 含义是不一样的,不可以省略

代码演示:execlp()

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>#define NUM 16int main()
{
pid_t id=fork();
if(id==0){//子进程//ls -a -lprintf("子进程开始运行,pid: %d\n",getpid());sleep(3);char* const _argv[NUM]={(char*)"ls",(char*)"-l",(char*)"-a",NULL};//execl("/usr/bin/ls","ls","-a","-l",NULL);//execv("/usr/bin/ls",_argv);execlp("ls","ls","-l","-a",NULL);exit(1);}else{//父进程int status=0;printf("父进程开始运行,pid: %d\n",getpid());pid_t id=waitpid(-1,&status,0);//阻塞等待                                                                                                                      if(id>0){printf("wait success,exit code: %d\n",WEXITSTATUS(status));}             }               return 0;}

运行结果:

 3、无需带路径的 execvp 接口(指针数组)

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

execvp 也是带 p 的,执行 execvp 时,会直接在环境变量中找,只要程序名即可。

代码

   char* const _argv[NUM]={(char*)"ls",(char*)"-l",(char*)"-a",NULL};execvp("ls", _argv); 

运行结果:

 目前我们执行的程序,全部都是系统命令,如果我们要执行自己写的 C/C++ 程序呢?

4、利用 exec 调各种程序

假设有两个可执行程序:mycmd.c & exec.c,我们期望用 exec.c 调用 mycmp.c:

//mycmd.c
#include<stdio.h>  
#include<stdlib.h>  
#include<string.h>  int main(int argc,char *argv[])  
{  if(argc!=2){  printf("can not execute!\n");  exit(1);  }  if(strcmp(argv[1],"-a")==0){  printf("helllo a!\n");}else if(strcmp(argv[1],"-b")==0){printf("hello b!\n");}else{printf("default!\n");}return 0;                                                                                                                                                        }   

也就是 C 语言的可执行程序调用 C++ 的可执行程序,我们先来设计一下 Makefile。

我们需要在前面添加 .PHONY:all ,让伪目标 all 依赖 exec 和 mycmd。

如果不这样做,直接写,默认生成的是 mycmd,轮不到后面的 exec,属于 "先到先得"。

且 Makefile 默认也只能形成一个可执行程序,想要形成多个就需要用到 all 了。

 

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>#define NUM 16
const char *myfile="/home/amx/lesson3/mycmd";int main()
{
pid_t id=fork();
if(id==0){//子进程//ls -a -lprintf("子进程开始运行,pid: %d\n",getpid());sleep(3);char* const _argv[NUM]={(char*)"ls",(char*)"-l",(char*)"-a",NULL};execl(myfile,"mycmd","-a",NULL);//execv("/usr/bin/ls",_argv);//execlp("ls","ls","-l","-a",NULL);exit(1);}else{//父进程int status=0;printf("父进程开始运行,pid: %d\n",getpid());pid_t id=waitpid(-1,&status,0);//阻塞等待                                                                                                                      if(id>0){printf("wait success,exit code: %d\n",WEXITSTATUS(status));}             }               return 0;}

 

 运行结果:

这是用的是绝对路径,那我们使用相对路径可以吗?

结果:

依然可以!!!!

那么如何 执行其它语言的程序呢??

我们创建两个文件

准备工作:先给两个文件写一些内容

试试能否运行:

都没问题:

那么我们修改我们的exec.c文件

运行结果:

完美!!!

运行test.sh也是一样!!!

运行结果:

还有一种方法:

修改:exec.c

一样可以!!

5、添加环境变量给目标进程的 execle 接口

 

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

我们可以使用 execle 接口传递环境变量,相当于自己把环境变量导进去。

打开 mycmd 文件,我们加上几句环境变量:

 我们自己在exec.c中定义一个

传进去:

 我们来试一下:

 超级缝合怪 execvpe 接口

v - 数组,p - 文件名,e - 可自定义环境变量:

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

这也没什么好说的,execle、execve、execvpe 都是 "环境变量" 一伙的。

6、为什么会有这么多 exec 接口?

唯一的差别就是传参的方式不一样,有的带路径,有的不带路径,有的是列表传参,有的是数组传参,有的可带环境变量,有的不带环境变量。

因为要适配各种各样的应用场景,使用的场景不一样,有些人就喜欢列表传参,有些人喜欢数组传参。所以就配备了这么多接口,这就好比我们 C++ 函数重载的思想。
那为什么 execve 是单独的呢?

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

 它处于 man 2 号手册,execve 才属于是真正意义上的系统调用接口。

 总结一下它们的命名规律,通过这个来记忆对应接口的功能会好很多:

  • l (list) :表示参数采用列表形式
  • v (vector) :表示参数采用数组形式
  • p (path):有 p 自动收缩环境变量 PATH
  • e (env) :表示自己维护环境变量

感谢观看!!!

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

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

相关文章

openmediavault(OMV) (27)网络(2)adguardhome

简介 AdGuard Home 是一个开源的网络广告和隐私保护解决方案,它充当局域网中的 DNS 服务器,提供广告拦截、跟踪器阻止和家长控制等功能。它可以在个人电脑、树莓派或其他支持的硬件设备上运行。 AdGuard Home 的主要功能包括: 广告拦截:AdGuard Home 使用广泛维护的广告…

数据湖的概念

1.定义 不同的公司对数据湖有不同的描述&#xff1a; 维基百科&#xff1a;数据湖是一类存储数据自然/原始格式的系统或存储&#xff0c;通常是对象块或者文件。数据湖通常是企业中全量数据的单一存储。全量数据包括原始系统所产生的原始数据拷贝以及为了各类任务而产生的转换…

基于人工智能的数据库工具Chat2DB使用

文章目录 前言Chat2DB介绍Chat2DB地址下载安装 Chat2DB配置Chat2DB使用1、自然语言转sql2. SQL解释3. SQL优化4. SQL转换 写在最后 前言 随着人工智能的发展&#xff0c;各行各业都出现了不少基于AI的工具来提升工作效率。就连国内的各个大厂也都在基于大模型开发自己的产品线…

大公司里怎样开发和部署前端代码?

前端训练营&#xff1a;1v1私教&#xff0c;终身辅导计划&#xff0c;帮你拿到满意的 offer。 已帮助数百位同学拿到了中大厂 offer。欢迎来撩~~~~~~~~ Hello&#xff0c;大家好&#xff0c;我是 Sunday。 昨天的时候有同学问到前端部署相关的内容&#xff0c;正好在知乎中看到…

Flask入门教程

Flask入门教程 简介 Flask是由Armin ronacher于2010年用Python语言基于 Werkzeug 工具箱编写的轻量级Web开发框架。 特点 Flask只提供核心功能&#xff0c;其他几乎所有的功能都需要用到拓展&#xff0c;比如可以通过Flask-SQLAlchemy拓展对数据库进行操作等等。 核心 由…

基于双向LSTM模型完成文本分类任务

6.4.1 数据处理 IMDB电影评论数据集是一份关于电影评论的经典二分类数据集&#xff0e;IMDB 按照评分的高低筛选出了积极评论和消极评论&#xff0c;如果评分 ≥7≥7&#xff0c;则认为是积极评论&#xff1b;如果评分 ≤4≤4&#xff0c;则认为是消极评论&#xff0e;数据集包…

“ManageEngine荣获Gartner SIEM客户选择四连冠“

我们非常激动地宣布&#xff0c;ManageEngine已经连续第四次被认定为Gartner Peer Insights‘Voice of the Customer’&#xff1a;安全信息与事件管理&#xff08;SIEM&#xff09;中的客户选择。这不仅是对我们卓越SIEM解决方案承诺的肯定&#xff0c;也延续了ManageEngine在…

检测和缓解僵尸网络

僵尸网络源自“机器人网络”一词&#xff0c;是感染了恶意软件的网络或机器集群&#xff0c;允许黑客控制并发起一系列攻击。僵尸网络的强度完全取决于它所包含的受感染机器的数量。攻击者接管这些设备的操作&#xff0c;以使用僵尸网络命令和控制模型进行远程控制。 什么是僵…

K线+直线 现货黄金也可能变现

现货黄金行情怎么做&#xff0c;这是投资者需要思考的问题。幸运的是&#xff0c;现在市面上有很多书籍&#xff0c;是其他有经验、有想法的投资者们对其经验的总结和分享&#xff0c;此外网络上还有不同的文章和各种各样的视频介绍相关交易经验&#xff0c;这都是可以让我们借…

22款奔驰GLE450升级香氛负离子 车载香薰

相信大家都知道&#xff0c;奔驰自从研发出香氛负离子系统后&#xff0c;一直都受广大奔驰车主的追捧&#xff0c;香氛负离子不仅可以散发出清香淡雅的香气外&#xff0c;还可以对车内的空气进行过滤&#xff0c;使车内的有害气味通过负离子进行过滤&#xff0c;达到车内保持清…

前端根据URL地址实现下载(txt,图片,word,xlsx,ppt)

前端根据URL地址实现下载&#xff08;txt&#xff0c;图片&#xff0c;word&#xff0c;xlsx&#xff0c;ppt&#xff09; 一、对于txt,图片类的二、对于word&#xff0c;xlsx&#xff0c;ppt类的1.a标签可以实现下载2. window.open&#xff08;&#xff09; 一、对于txt,图片类…

aliexpress商品API(item_get-获得aliexpress商品详情):进行批量操作

使用AliExpress的店铺或分类API&#xff1a;这些API可以为你提供某个店铺或分类下的所有商品列表&#xff0c;然后你可以根据这个列表逐个查询商品详情。分批查询&#xff1a;你可以将商品ID分成多个批次&#xff0c;每次只查询一部分商品详情&#xff0c;这样既可以减少每次请…

记录一个常量定义导致的重复问题duplicate symbol ‘_kk‘ in:

原因&#xff1a; 在.h文件中定义了一个常量 如下 NSString *const kk "FASDF";interface CardCourseViewController : LBBaseViewControllerend将这句代码去掉即可 NSString *const kk "FASDF";![请添加图片描述](https://img-blog.csdnimg.cn/direct…

买工业用品就找震坤行,提供震坤行商品数据,数据分析的API接口

要接入API接口以采集电商平台上的商品数据&#xff0c;可以按照以下步骤进行&#xff1a; 1、找到可用的API接口&#xff1a;首先&#xff0c;需要找到支持查询商品信息的API接口。这些信息通常可以在电商平台的官方文档或开发者门户网站上找到。 2、注册并获取API密钥&#x…

什么是差值表达式

在Vue.js中&#xff0c;差值表达式是一种基本的数据绑定形式&#xff0c;用于将数据绑定到文档对象模型&#xff08;DOM&#xff09;上。差值表达式通常使用双大括号 {{ }} 来表示&#xff0c;这种语法非常直观。当Vue实例的数据发生变化时&#xff0c;差值表达式的内容也会相应…

CCNP课程实验-06-EIGRP-Trouble-Shooting

目录 实验条件网络拓朴 环境配置开始排错错误1&#xff1a;没有配置IP地址&#xff0c;IP地址宣告有误错误2&#xff1a;R3配置了与R1不同的K值报错了。错误3&#xff1a;R4上的AS号配置错&#xff0c;不是1234错误4&#xff1a;R2上配置的Key-chain的R4上配置的Key-chain不一致…

LCR 155. 将二叉搜索树转化为排序的双向链表

解题思路&#xff1a; 中序遍历法&#xff08;二叉搜索树在中序遍历时是从小到大排列的&#xff09;。 // 打印中序遍历 void dfs(Node root) {if(root null) return;dfs(root.left); // 左System.out.println(root.val); // 根dfs(root.right); // 右 }采用head作为返回&am…

跑步中位数

title: 跑步中位数 date: 2024-01-04 15:47:51 tags: 对顶堆 catefories: 算法进阶指南 题目大意 解题思路 动态维护中位数问题。可以建立两个二叉堆&#xff0c;一个大顶堆一个小顶堆&#xff0c;在依次读入整数序列的过程中&#xff0c;设当前序列长度为 M M M,我们始终保持…

中国5米分辨率坡度数据

中国5米分辨率坡度数据 坡度是地表单元陡缓的程度&#xff0c;通常把坡面的垂直高度和水平距离的比值称为坡度。坡度的表示方法有百分比法、度数法、密位法和分数法四种&#xff0c;其中以百分比法和度数法较为常用。 中国5米分辨率坡度数据集&#xff0c;利用5米分辨率DEM数据…

借还款记账表,借款还款记账软件

我们每个人都在为生活奔波&#xff0c;为事业打拼。但有时候&#xff0c;生活中的一些小事情&#xff0c;比如朋友间的借贷、还款&#xff0c;就可能让我们的生活变得有些混乱。为了解决这个问题&#xff0c;一个全新的借还款记账软件【晨曦记账本】横空出世&#xff0c;它不仅…