【Linux】 Linux 进程控制

参考博客:https://blog.csdn.net/sjsjnsjnn/article/details/125581083

一、进程创建

1.1 fork()函数

  • 在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
  • 进程调用fork,当控制转移到内核中的fork代码后,内核做:
    1. 分配新的内存块和内核数据结构给子进程
    2. 将父进程部分数据结构内容拷贝至子进程
    3. 添加子进程到系统进程列表当中
    4. fork返回,开始由调度器调度

1.2 fork函数的返回值

返回值:

  1. 给父进程返回子进程的PID;
  2. 给子进程返回0;
  3. 子进程创建失败会返回-1;
void test1(){int ret = fork();std::cout << "pid = " << getpid() << ", fork()  = " << ret << std::endl;
}
  • 参照运行结果可以看出,给父进程返回的值为子进程的pid,而子进程返回的是0
    在这里插入图片描述

创建子进程之后,也就是调用fork()函数之后,新创建的进程才开始执行之后的操作,如下图所示

在这里插入图片描述

fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。

1.3 写时拷贝

  • 通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:

在这里插入图片描述

  • 在修改内容之前,父子进程的数据和代码都是共享的
  • 当任意一方试图写入时,操作系统会识别到缺页中断
  • 所谓的缺页中断:是指计算机在执行程序的过程中,当出现异常情况或特殊请求时,计算机停止现行程序的运行,转向对这些异常情况或特殊请求的处理,处理结束后再返回现行程序的间断处,继续执行原程序。
  • 那么,操作系统重新分配一块空间,将旧空间的数据拷贝下来,此时操作系统也会重新映射页表。

1.4 fork函数常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec类函数。

1.5 fork函数调用失败的原因

  • 系统中有太多的进程,导致内存严重不足,无法加载数据
  • 实际用户的进程数超过了限制

二、进程终止

2.1 进程的退出场景以及退出码

进程一旦退出,就会存在以下三种情况:

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果不正确
  3. 代码异常终止
  • 这三种情况,作为用户怎样才能知道某个进程是以什么样的形式退出的呢?那么就有了退出码的概念。
  • Linux 系统中,程序可以在执行终止后传递值给其父进程,这个值被称为退出码。
  • 用户就可以通过相应的退出码,对进程退出状态做以判断
  • 例如,我们的main函数,每次都会写上 return 0; 其实这就是进程的退出码。

我们可以通过 echo $? 来获取最近一次进程退出时的退出码。

echo $?
  • 上次进程退出是正常退出,因此结果为0
  • 对于每个指令,对应的都是一个个进程,我们输错指令也会有错误的进程返回值,比如下面输入lsss,返回值为127

在这里插入图片描述

2.2 查询返回值的含义

可以通过strerror()函数来查看对应返回值的含义

void test2(){for(int i = 0;i<140;++i){std::cout << "error[" << i << "] = " << strerror(i) << std::endl;}
}

实际上只有133以内的才是有含义的,打印的结果如下:

在这里插入图片描述

在这里插入图片描述

三、进程常见的退出方法

3.1 return退出

  • 刚刚我们已经介绍过main函数是通过return退出进程,需要注意以其他函数(非main函数)return进行区分,非main函数的return是函数返回,而main函数的return是进程退出。

3.2 exit( )退出

  • exit可以在程序的任何位置退出,exit退出会刷新缓冲区,和return一样
void test3(){std::cout << "test3 exit(100)" <<  std::endl;sleep(1);exit(100);
}
  • 查看进程的返回值,eixt(100)退出的返回值就为100

在这里插入图片描述

3.3 _exit退出

  • 除了上面两种方法来退出进程,我们还可以使用_exit函数来使进程退出。
  • _exit也是可以在代码中的任何位置终止进程,但是_exit函数终止进程时,是强制终止,不会进行进程的后续收尾工作,如:刷新缓冲区

exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:

  1. 执行用户通过 atexit或on_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入
  3. 调用_exit

3.4 return、exit 和 _exit 的区别

  1. _exit()执行后会立即返回给内核,而exit()要先执行一些清除操作,然后将控制权交给内核。
  2. 调用_exit()函数时,其会关闭进程所有的文件描述符,清理内存,以及其他一些内核清理函数,但不会刷新流(stdin 、stdout、stderr)。exit()函数是在_exit()函数上的一个封装,它会调用_exit,并在调用之前先刷新流。
  3. return是一种更常见的退出进程方法。执行return(num)等同于执行exit(num),因为调用main的运行时函数会将main的返回值当做 exit的参数。

在这里插入图片描述

3.5 异常退出

  • 以上是正常退出的情况,和进程的退出码有关;
  • 对于进程的异常退出,就是程序执行了一半后由于地址访问错误、主动终止进程(比如ctrl+c或者kill,也有对应的错误码,这个错误码实际上是包含了进程的终止信号,下面会讲解

四、进程等待

4.1 进程等待的必要性

  1. 子进程退出,父进程如果不获取到子进程的退出信息,就可能造成 僵尸进程 的问题,进而造成内存泄漏。
  2. 进程一旦变成僵尸状态,所谓的 kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  3. 父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  4. 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

4.2 进程等待方法

4.2.1 wait方法

函数原型以及所需头文件

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
  • 返回值:等待成功则返回等待进程的PID,等待失败,返回-1;

  • 参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

  • 下面的代码演示了创建一个子进程,子进程执行对应的任务,然后自动退出,父进程等待子进程结束,并且回收子进程的内存

void test4(){pid_t id = fork();if(id == 0){for(int i =0 ;i< 5 ; ++i){std::cout << "child id = " << getpid() << ", i = " << i << std::endl;sleep(1);}exit(0);}else{sleep(10);std::cout << "father wait begin..." << std::endl;pid_t cur = wait(NULL);if(cur > 0){std::cout << "father wait: "<< cur << " sucess" << std::endl;}else{std::cout << "father wait failed!" << std::endl;}sleep(10);}
}
  • 使用下面的shell脚本来监听对应的进程状态
while :; do ps axj | head -1 && ps axj | grep mytest | grep -v grep; sleep 1; echo "**********************"; done
  • 可以发现,在运行结束后,子进程退出,然后被父进程回收了,最后父进程也退出了

在这里插入图片描述

运行结果

在这里插入图片描述

4.2.2 waitpid方法

函数原型以及所需头文件

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

返回值:

  • 当正常返回的时候waitpid返回收集到的子进程的进程ID;
  • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:

  • pid:
    Pid=-1,等待任一个子进程。与wait等效。
    Pid>0.等待其进程ID与pid相等的子进程。

  • status:
    WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
    WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

  • options:
    WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的PID。

下面的代码为等待任意一个子进程后,回收子进程,这里options指定为0,将会阻塞在此

void test5()
{pid_t id = fork();if (id == 0){for (int i = 0; i < 5; ++i){std::cout << "child id = " << getpid() << ", i = " << i << std::endl;sleep(1);}exit(100);}sleep(10);std::cout << "father wait begin..." << std::endl;// pid_t cur = wait(NULL);int status = 0;pid_t cur = waitpid(-1, &status, 0); // 等待任意一个子进程if (cur > 0){std::cout << "father wait: " << cur << " sucess, WIFEXITED(status) = " << WIFEXITED(status)<<",WEXITSTATUS(status) = "<< WEXITSTATUS(status) <<  std::endl;}else{std::cout << "father wait failed!" << std::endl;}sleep(10);
}
  • 运行结果如下,通过WIFEXITED宏和WEXITSTATUS可以查看子进程是否正常退出以及退出码是多少

在这里插入图片描述

4.2.3 获取子进程status

4.2.3.1 什么是status

int status:它是一种输出型的参数*
所谓获取子进程的status,就是获取子进程退出时的退出信息;
首先,在子进程中分别用exit(0)和exit(10)来中断子进程,父进程获取status值,判断进程的退出状态。

4.2.3.2 status的构成
  • status是由32个比特位构成的一个整数,目前阶段我们只使用低16个位来表示进程退出的结果
  • 如下图所示,就是status低16位的表示图;
status exit_code = (status >> 8) & 0xFF; //退出码
status exit_code = status7 & 0x7F;       //退出信号

在这里插入图片描述

  • 进程正常退出有两种,与退出码有关,异常退出与信号有关
  • 所以这里我们就需要获取到两组信息:退出码与信号
  • 如果没有收到信号,就表明我们所执行的代码是正常跑完的,然后在判断进程的退出码,究竟是何原因使进程结束的
  • 反之则是异常退出,也就不需要关心退出码了
void test6()
{pid_t id = fork();if (id == 0){for (int i = 0; i < 5; ++i){std::cout << "child id = " << getpid() << ", i = " << i << std::endl;sleep(1);}exit(100);}sleep(10);std::cout << "father wait begin..." << std::endl;// pid_t cur = wait(NULL);int status = 0;pid_t cur = waitpid(-1, &status, 0); // 等待任意一个子进程if (cur > 0){std::cout << "father wait: " << cur << " sucess, status = " << status << std::endl;std::cout << "exit_code = " << ((status >> 8)& 0xff) << ", exit_signal = " <<(status & 0x7f) << std::endl;}else{std::cout << "father wait failed!" << std::endl;}sleep(10);
}
  • 正常退出,这里退出码为100,终止信号可以忽略

在这里插入图片描述

  • 在进程运行时使用kill -9命令终止进程,可以发现终止信号为9

在这里插入图片描述

  • 查询shell指令,发现9对应的是SIGKILL
kill -l

在这里插入图片描述

  • 这里可以通过WIFEXITED宏和WEXITSTATUS宏查看是否是正常退出,以及正常退出的返回值
pid_t cur = waitpid(-1, &status, 0); // 等待任意一个子进程if (cur > 0)
{std::cout << "father wait: " << cur << " sucess, WIFEXITED(status) = " << WIFEXITED(status)<<",WEXITSTATUS(status) = "<< WEXITSTATUS(status) <<  std::endl;
}
else
{std::cout << "father wait failed!" << std::endl;
}
4.2.3.3 阻塞等待与非阻塞等待
  • 这里我们所讲的阻塞等待和非阻塞等待,其实就是waitpid函数的第三个参数,我们之前并未提及,直接给的是0,这种是默认行为,阻塞等待;阻塞等待:父进程一直在等待子进程,什么事都不干,直到子进程正常退出。

  • 如果设置为WNOHANG,表示的是非阻塞等待方式。非阻塞等待:父进程的PCB由运行队列转变为等待队列,直达子进程结束,操作系统获取到子进程退出的信号时,再将父进程从等待队列中调度到运行队列,由父进程去获取子进程的退出码以及退出信号。

通过判断返回值来查看是否子进程已经结束了,如果没有结束就继续干父进程自己的任务,否则就回收子进程

void test7()
{pid_t id = fork();if (id == 0){for (int i = 0; i < 10; ++i){std::cout << "child id = " << getpid() << ", i = " << i << std::endl;sleep(1);}exit(100);}std::cout << "father wait begin..." << std::endl;while (true){int status = 0;pid_t cur = waitpid(-1, &status,WNOHANG); // WNOHANG = 1,非阻塞if (cur > 0){std::cout << "father wait: " << cur << " sucess, status = " << status << std::endl;std::cout << "exit_code = " << ((status >> 8) & 0xff) << ", exit_signal = " << (status & 0x7f) << std::endl;break;}else if(cur == 0){std::cout << "do father process things" << std::endl;sleep(1);}else{std::cout << "father wait failed!" << std::endl;break;}}
}

运行结果如下

在这里插入图片描述

五、进程程序替换

5.1 替换原理

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

在这里插入图片描述

  • 从上图可以看出,进程程序替换前后,进程本身并没有发生任何变化,只是所执行的代码发什么改变。
  • 如果子进程进行程序替换,不会影响父进程的代码和数据吗?首先进程是具有独立性的,虽然子进程共享父进程的代码和数据,但是由于进行了函数替换,发生了代码和数据的修改,此时就会进行写时拷贝。所有子进程进行程序替换时,并不会影响父进程的代码和数据。

5.2 替换函数

有六种以exec开头的函数,统称exec函数: 他们所需的头文件均为 #include <unistd.h>

execl函数

int execl(const char *path, const char *arg, ...);
// path --- 可执行程序的路径
// arg --- 可变参数列表,表示你要如何执行这个程序,并以NULL结尾
// 例如:
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);

execlp函数

int execlp(const char *file, const char *arg, ...);
// file --- 可执行程序的名字
// arg --- 可变参数列表,表示你要如何执行这个程序,并以NULL结尾
// 例如:
execlp("ls", "ls", "-a", "-l", NULL);

execle函数

int execle(const char *path, const char *arg, ..., char * const envp[]);
// path --- 可执行程序的路径
// arg ---  可变参数列表,表示你要如何执行这个程序,并以NULL结尾
// envp --- 自己维护的环境变量// 例如:
char* envp[] = { "Myval=12345", NULL };
execle("./myexe", "myexe", NULL, Myval);

execv函数

int execv(const char *path, char *const argv[]);
// path --- 你要执行程序的路径
// argv --- 指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾// 例如:
char* argv[] = { "ls", "-a", "-l", NULL };
execv("/usr/bin/ls", argv);

execvp函数

int execvp(const char *file, char *const argv[]);
// file --- 你要执行程序的名字
// argv --- 指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾// 例如:
char* argv[] = { "ls", "-a", "-l", NULL };
execvp("ls", argv);

execve函数

int execvpe(const char *file, char *const argv[], char *const envp[]);
// file --- 你要执行程序的路径
// argv --- 指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾
// envp --- 自己维护的环境变量//例如:
char* argv[] = { "mycmd", NULL };
char* envp[] = { "Myval=12345", NULL };
execve("./myexe", argv, envp);

函数解释

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值。也就是说,exec系列函数只要返回了,就意味着调用失败。
函数名参数格式是否带路径是否使用当前环境变量
execl列表不是
execlp列表
execle列表不是不是,须自己装环境变量
execv数组不是
execvp数组
execve数组不是不是,须自己装环境变量
void test8(){char *argv[] = {"ls","-a","-l",NULL}; execl("/usr/bin/ls","ls","-a","-l",NULL); //可变参,NULL结尾execv("/usr/bin/ls",argv); //字符串数组形式execlp("ls","ls","-a","-l"); //文件名+可变参execvp("ls",argv); //字符串数组形式//可执行文件路径char* argv_[] = {"./process-test","-test2",NULL};char* env_[] = {NULL};execvpe("./process-test",argv_,env_);}

在这里插入图片描述

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

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

事实上,只有execve才是真正的系统调用,其它五个函数最终都是调用的execve,所以execve在man手册的第2节,而其它五个函数在man手册的第3节,也就是说其他五个函数实际上是对系统调用execve进行了封装,以满足不同用户的不同调用场景的。

下图为exec系列函数族之间的关系:

在这里插入图片描述

更多资料:https://github.com/0voice

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

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

相关文章

【大模型】MCP是啥?它和点菜、做菜、端菜有啥关系?

什么是 Model Context Protocol (MCP)? Model Context Protocol(模型上下文协议),通俗来说,就是一套用来管理、传递和维护对话或交互中上下文信息的规则和格式标准。 换句话说,MCP定义了模型在处理用户输入和生成回答时,如何理解、保留和传递上下文信息的协议,确保对…

机器学习的数学基础:决策树

决策树 文章目录 决策树决策树的基本思想划分选择信息增益增益率基尼指数 减枝处理回归问题对连续值的处理对缺失值的处理 决策树的基本思想 决策树是基于树结构来进行决策的&#xff0c;通过对问题的判断与决策&#xff0c;得到最终决策。 一般的&#xff0c;决策树包括一个…

基于若依前后分离版-用户密码错误锁定

sys_config配置参数 user.password.maxRetryCount&#xff1a;最大错误次数 user.password.lockTime&#xff1a;锁定时长 //SysLoginController//登录 PostMapping("/login") public AjaxResult login(RequestBody LoginBody loginBody) {AjaxResult ajax AjaxR…

Java线程安全集合类

Java线程安全集合类全面解析 目录 并发集合概述List线程安全实现Set线程安全实现Map线程安全实现Queue线程安全实现总结 并发集合概述 Java提供了多种线程安全的集合类&#xff0c;主要分为两大类&#xff1a; 传统同步集合&#xff1a;通过synchronized关键字实现线程安全…

汇川变频器MD600S-4T-5R5为什么要搭配GRJ9000S-10-T滤波器?

一、变频器的工作原理与电磁干扰 汇川MD600S-4T-5R5变频器是一款紧凑型高性能变频器&#xff0c;适用于三相380V-480V电网&#xff0c;额定电流5.5A&#xff0c;支持矢量控制和多种编码器接口&#xff0c;适用于需要高精度速度和转矩控制的场景&#xff0c;如机器人、电梯、纺…

数学运算在 OpenCV 中的核心作用与视觉效果演示

在计算机视觉中&#xff0c;图像不仅仅是我们肉眼所见的内容&#xff0c;它其实是由数值矩阵组成的“数据”。而在 OpenCV&#xff08;Open Source Computer Vision Library&#xff09;中&#xff0c;正是数学运算赋予了图像处理无限的可能——从基本的滤波、增强到复杂的特征…

【快速预览经典深度学习模型:CNN、RNN、LSTM、Transformer、ViT全解析!】

&#x1f680;快速预览经典深度学习模型&#xff1a;CNN、RNN、LSTM、Transformer、ViT全解析&#xff01; &#x1f4cc;你是否还在被深度学习模型名词搞混&#xff1f;本文带你用最短时间掌握五大经典模型的核心概念和应用场景&#xff0c;助你打通NLP与CV的任督二脉&#xf…

springboot mysql/mariadb迁移成oceanbase

前言&#xff1a;项目架构为 springbootmybatis-plusmysql 1.部署oceanbase服务 2.springboot项目引入oceanbase依赖&#xff08;即ob驱动&#xff09; ps&#xff1a;删除原有的mysql/mariadb依赖 <dependency> <groupId>com.oceanbase</groupId> …

电网“逆流”怎么办?如何实现分布式光伏发电全部自发自用?

2024年10月9日&#xff0c;国家能源局综合司发布了《分布式光伏发电开发建设管理办法&#xff08;征求意见稿&#xff09;》&#xff0c;意见稿规定了户用分布式光伏、一般工商业分布式光伏以及大型工商业分布式光伏的发电上网模式&#xff0c;当选择全部自发自用模式时&#x…

C语言之编译器集合

C语言有多种不同的编译器&#xff0c;以下是常见的编译工具及其特点&#xff1a; 一、主流C语言编译器 GCC&#xff08;GNU Compiler Collection&#xff09; 特点&#xff1a;开源、跨平台&#xff0c;支持多种语言&#xff08;C、C、Fortran 等&#xff09;。 使用场景&…

负载均衡将https请求转发后端http服务报错:The plain HTTP request was sent to HTTPS port

https请求报错&#xff1a;The plain HTTP request was sent to HTTPS port 示例背景描述&#xff1a; www.test.com:11001服务需要对互联网使用https提供服务后端java服务不支持https请求&#xff0c;且后端程序无法修改&#xff0c;仅支持http请求 问题描述&#xff1a; 因…

(3)Playwright自动化-3-离线搭建playwright环境

1.简介 如果是在公司局域网办公&#xff0c;或者公司为了安全对网络管控比较严格这种情况下如何搭建环境&#xff0c;我们简单来看看 &#xff08;第一种情况及解决办法&#xff1a;带要搭建环境的电脑到有网的地方在线安装即可。 &#xff08;第二种情况及解决办法&#xf…

【Fiddler抓取手机数据包】

Fiddler抓取手机数据包的配置方法 确保电脑和手机在同一局域网 电脑和手机需连接同一Wi-Fi网络。可通过电脑命令行输入ipconfig查看电脑的本地IP地址&#xff08;IPv4地址&#xff09;&#xff0c;手机需能ping通该IP。 配置Fiddler允许远程连接 打开Fiddler&#xff0c;进入…

PublishSubject、ReplaySubject、BehaviorSubject、AsyncSubject的区别

python容易编辑&#xff0c;因此用pyrx代替rxjava3做演示会比较快捷。 pyrx安装命令&#xff1a; pip install rx 一、Subject&#xff08;相当于 RxJava 的 PublishSubject&#xff09; PublishSubject PublishSubject 将对观察者发送订阅后产生的元素&#xff0c;而在订阅前…

BLE中心与外围设备MTU协商过程详解

一、MTU基础概念​​ 1. ​​MTU定义​​ ​​最大传输单元&#xff08;MTU&#xff09;​​ 指单次数据传输中允许的最大字节数&#xff0c;包含协议头部&#xff08;3字节&#xff09;和有效载荷&#xff08;最多517字节&#xff09;。BLE默认MTU为​​23字节​​&a…

【华为云Astro-服务编排】服务编排使用全攻略

目录 概述 为什么使用服务编排 服务编排基本能力 拖拉拽式编排流程 逻辑处理 对象处理 服务单元组合脚本、原生服务、BO、第三方服务 服务编排与模块间调用关系 脚本 对象 标准页面 BPM API接口 BO 连接器 如何创建服务编排 创建服务编排 如何开发服务编排 服…

centos实现SSH远程登录

1. 生成SSH密钥对 首先&#xff0c;你需要在客户端机器上生成一个SSH密钥对。打开终端&#xff0c;执行以下命令 ssh-keygen 或ssh-keygen -t rsa -b 2048&#xff08;效果相同&#xff09; 按照提示操作&#xff0c;可以按回车键接受默认的文件名&#xff08;通常是~/.ssh/id_…

定制开发开源AI智能名片S2B2C商城小程序在无界零售中的应用与行业智能升级示范研究

摘要&#xff1a;本文聚焦无界零售背景下京东从零售产品提供者向零售基础设施提供者的转变&#xff0c;探讨定制开发开源AI智能名片S2B2C商城小程序在这一转变中的应用。通过分析该小程序在商业运营成本降低、效率提升、用户体验优化等方面的作用&#xff0c;以及其与京东AI和冯…

ZooKeeper 安装教程(Windows + Linux 双平台)

ZooKeeper 安装教程(Windows + Linux 双平台) Zookeeper 和 Kafka 版本与 JDK 要求 一、安装前准备 系统要求 Java 环境(JDK17+)开放端口:2181(客户端),2888(集群通信),3888(选举)安装 Java Linux(Ubuntu/CentOS) # Ubuntu

【Git系列】如何同步原始仓库的更新到你的fork仓库?

&#x1f389;&#x1f389;&#x1f389;欢迎来到我们的博客&#xff01;无论您是第一次访问&#xff0c;还是我们的老朋友&#xff0c;我们都由衷地感谢您的到来。无论您是来寻找灵感、获取知识&#xff0c;还是单纯地享受阅读的乐趣&#xff0c;我们都希望您能在这里找到属于…