【Linux取经路】进程通信之匿名管道

在这里插入图片描述

文章目录

  • 一、进程间通信介绍
    • 1.1 进程间通信是什么?
    • 1.2 进程间通信的目的
    • 1.3 进程通信该如何实现
  • 二、管道
    • 2.1 匿名管道
      • 2.1.1 站在文件描述符角度深入理解管道
      • 2.1.2 接口使用
      • 2.1.3 PIPE_BUFFER 和 Pipe capacity
      • 2.1.4 管道中的四种情况
      • 2.1.5 管道特征总结
    • 2.2 匿名管道使用场景
      • 2.2.1 命令行中的管道
      • 2.2.2 基于管道的简易进程池
  • 三、结语

一、进程间通信介绍

1.1 进程间通信是什么?

是两个或多个进程实现数据层面的交互,因为进程之间是存在独立性的,所以进程通信的成本比较高。

1.2 进程间通信的目的

  • 数据传输:一个进程需要将它的数据发送给另外一个进程。

  • 资源共享:多个进程之间共享同样的资源。

  • 通知事件:一个进程需要向另一个或者一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时需要通知父进程)。

  • 进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望及时知道它的状态改变。

1.3 进程通信该如何实现

进程通信的本质是:必须让不同的进程看到同一份“资源”。这个“资源”就是一种特定形式的内存空间。为了不破坏进程的独立性,这个资源是由操作系统提供的。进程访问这个空间进行通信,本质就是访问操作系统。进程代表的就是用户,因为一个进程本质上是从一个 .c 源代码进化而成的,而源代码是程序员写的,操作系统是不相信用户的,这意味着程序员在代码中不能直接去访问操作系统提供的资源,必须通过系统调用去创建、使用、释放这个资源。在操作系统内部可能会存在多组进程之间都需要通信,因此资源可能会有多份,操作系统需要通过“先描述,再组织”的形式将多份资源管理起来,一般操作系统会有一个独立的通信模块(IPC通信模块),隶属于文件系统。 进程间通信有 system Vposix 两个标准,前者主要是针对本机内部通信,后者是针对网络通信。在这两个标准出来之前也就是操作系统还没有通信模块的时候,进程之间也可以通过基于文件级别的通信方式,管道进行通信。

  • 管道:匿名地址 pipe、命名管道。

  • System V IPC:System V 消息队列、System V 共享内存、System V 信号量。

  • POSIX IPC:消息队列、共享内存、信号量、互斥量、条件变量、读写锁。

二、管道

管道是 Unix 中最古老的进程将通信的形式,我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

2.1 匿名管道

2.1.1 站在文件描述符角度深入理解管道

在这里插入图片描述
管道本质上是一种内存级文件,它不用往磁盘上进行刷新。上图是以创建子进程为基础,来演示管道的原理。首先父进程以读写方式分两次打开一个文件,分两次的原因是为了获得两个 struct file 对象,这样对一个文件就有两个读写指针,让读写操作使用各自独立的指针,这样读写之间就不会相互影响。读写指针记录了当前文件读取或写入的位置,一个 struct file 中只有一个读写指针,在向文件写入(或读取)的时候,读写指针会发生移动,然后再去读取(写入),此时读写指针已经不再最初的位置,无法将刚写入的内容读取上来,因此这里需要分两次以不同的方式打开同一个文件。接着创建子进程,子进程会继承父进程中打开的文件,也就是继承父进程的文件描述符表,此时父子进程就会共享同一个文件资源,子进程可以通过4号文件描述符向文件中进行写入,父进程就可以通过3号文件描述符从文件中进程读取,此时父子进程就实现了数据传输,也就是通信。一般为了避免误操作,根据需要只会将读写其中的一个文件描述符保留,另外一个关闭,上图中的虚线就表示,在开始通信之前,将不需要的文件描述符进行关闭。通过上面的描述可以发现,这种通信模式只能是单向的,所以我们就把它叫做管道。如果要实现双向通信,可以创建两个管道。

小Tips:父进程可能创建多个子进程,暂且把它们成为“兄弟进程”,兄弟进程之间也可以采用上述的方式进行管道通信。此外,子进程可能还会继续创建子进程,暂且把它叫做“孙子进程”,孙子进程和爷爷进程、父进程、叔叔进程之间都可以采用上述的方式进行管道通信。

结论:上面这种管道通信方式,只适用于具备血缘关系的进程之间进行通信

2.1.2 接口使用

可以用 pipe 系统调用来创建一个管道,下面是函数声明:

int pipe(int pipefd[2]);

参数:一个有两个元素的整形数组,输出型参数,将两个文件描述符数字返回给用户使用,其中 pipefd[0]
中存的是读对应的文件描述符,pipefd[1] 中存的是写对应的文件描述符下标。

返回值:管道创建成功,0被返回;创建失败,-1被返回,错误码被设置。

#include <unistd.h>
#include <iostream>using namespace std;#define N 2int main()
{int pipefd[N] = {0};int ret = pipe(pipefd);if(ret == -1){perror("pipe");return errno;}cout << "pipefd[0]: " << pipefd[0] << ", pipefd[1]: " << pipefd[1] << endl;return 0;
}

在这里插入图片描述
上面代码执行的工作是创建管道,接下来需要创建子进程进行通信。

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cstdlib>
#include <cstdio>
#include <iostream>
#include <string>
#include <string.h>using namespace std;#define N 2
#define NUM 1024void Writer(int wfd)
{string str = "Hello, I am child";pid_t self_id = getpid();int num = 0;char buffer[NUM];while (true){// 构建发送字符串buffer[0] = 0; // 字符串清空,只是为了提醒阅读代码的人,我把这个数组当做字符串了snprintf(buffer, sizeof(buffer), "%s-%d-%d", str.c_str(), self_id, num);write(wfd, buffer, strlen(buffer));sleep(1);num++;}
}void Reader(int rfd)
{char buffer[NUM];while(true){ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);if(n > 0){buffer[n] = 0;}cout << "father("<< getpid() << ")  get a message: " << buffer << endl;}
}int main()
{int pipefd[N] = {0};int ret = pipe(pipefd);if (ret == -1){perror("pipe");return errno;}// cout << "pipefd[0]: " << pipefd[0] << ", pipefd[1]: " << pipefd[1] << endl;// child -> W, father -> Rpid_t id = fork();if (id > 0){// 父进程close(pipefd[1]);// IPC codeReader(pipefd[0]);pid_t rid = waitpid(id, nullptr, 0); // 不关心退出嘛码和状态,阻塞式等待if(rid < 0){// 等待失败perror("waitpid");return errno;}close(pipefd[0]);}else if (id == 0){// 子进程close(pipefd[0]);// IPC codeWriter(pipefd[1]);close(pipefd[1]);exit(0);}else{perror("fork");return errno;}return 0;
}

在这里插入图片描述
代码中只让子进程进行了 sleep,父进程并没有进行 sleep,但是通过运行结果可以发现,父进程并没有一直去管道中读取并进行打印,而是在子进程向管道中写入后才去读取打印。并且,子进程一定是在父进程读取结束后才进行写入的,没有出现父进程只读取了一半,然后子进程就进行写入。这里可以得出一个结论:父子进程是会进行协同的,会有同步与互斥,以保证管道文件的数据安全。read 读到管道的结尾,write 就会从管道的开始处进行写入。

小Tips:整个通信过程,数据一共发生了两次拷贝,第一次是将数据从子进程的 buffer 中拷贝到管道中(管道文件的页缓冲区中),第二次拷贝是将文件页缓冲区中的内容拷贝到父进程的 buffer 中。

在这里插入图片描述
从上面的运行结果可以得出一个结论:管道是面向字节流的,即无论写端写入了多少次,对于读端来说,只要管道中有数据,有多少就读多少,前提是读端的缓冲区足够大。虽然写端每次都是按照一个字符串一个字符串写入进管道,但是对读端来说,管道里面就是一个个的字符,把这些字符按照某种格式还原成一个一个的字符串这是用户来做的事情,管道不管。

2.1.3 PIPE_BUFFER 和 Pipe capacity

PIPE_BUFFER 是内核管道缓冲区的容量,这个值可以通过 ulimit -a 来查看:

在这里插入图片描述
在这里插入图片描述
如果写入的大小小于 PIPE_BUFFER,也就是小于 512bytes * 8 = 4096bytes = 4kb,那么写入操作就是原子的,也就是要写入的数据应该被连续的写入到管道。

Pipe capacity,表示管道容量的大小,由 PIPE_BUFFER 和缓冲条数的数量来共同决定其大小,缓冲条目的数量与 Linux 内核的版本有关,我这里的数量是16。

在这里插入图片描述

2.1.4 管道中的四种情况

管道中情况总结:读写端正常,管道如果为空,读端就要堵塞;读写端正常,管道如果被写满,写端就要被阻塞;读端正常,写端关闭,读端就会读到0,表示读到了管道(文件)结尾,不会被阻塞;写端正常,读端关闭,操作系统会通过 13 号信号把正在写入的进程 kill 掉。

在这里插入图片描述

2.1.5 管道特征总结

  • 具有血缘关系的进程进行进程间通信。

  • 管道只能单向通信。

  • 父子进程是会协同的,进行同步与互斥,以保证管道文件中数据的安全。

  • 管道是面向字节流的。

  • 管道是基于文件的,而文件的生命周期是随进程的,进程如果退出了,管道也会被自动关闭掉。

2.2 匿名管道使用场景

2.2.1 命令行中的管道

命令行中的 | 底层就是通过 pipe 来创建管道的。它的实现原理是:bash 对输入的指令做分析,统计出指令中 | 的个数,创建出对应数量的管道,然后通过 fork 创建出一批子进程,然后进行重定向工作,将管道左边进程的输出重定向到管道文件中,将管道右边进程的输入重定向到管道文件中,然后通过程序替换去执行指令。程序替换不会影响预先设置好的重定向。

2.2.2 基于管道的简易进程池

进程池就是把一个一个的进程当做资源,提前准备好,需要的时候直接分配,无需再去调用 fork 创建子进程。系统调用是有成本的,当程序执行到系统调用的时候,首先使用类似 int 80H 的软中断指令,在通过系统调用进入内核态的时候,需要保存用户程序的上下文数据,再由用户栈切入内核栈,进入内核态。在内核态返回用户态的时候,需要恢复用户程序的上下文。实际上的操作会更复杂。因此,为了减少用户态和内核态之间的切换次数,以提高系统效率,池化技术应运而生,池化技术就是一次多申请一些系统资源,在用户(程序)需要的时候,直接从池子里面分配即可。

#include <unistd.h>
#include <vector>
#include <cstdio>
#include <cerrno>
#include <string>
#include <iostream>
#include <cstdlib>
#include <ctime>
#include "Task.hpp"
#include <sys/types.h>
#include <sys/wait.h>const int processnum = 5;class pipeline
{
public:pipeline(pid_t rprocessid, int wfd, std::string processname): _rprocessid(rprocessid),_wfd(wfd),_processname(processname){}public:pid_t _rprocessid;        // 读取端的进程 idint _wfd;                 // 写入端的文件描述符std::string _processname; // 子进程的名字
};void slaver()
{while (true){int cmdcode = 0;int n = read(0, &cmdcode, sizeof(int));if (n == sizeof(int)){// 执行 cmdcod 对应的任务std::cout << "child-" << getpid() << "-say@ get a cmdcode: " << cmdcode << std::endl;if(cmdcode >=0 && cmdcode < tasks.size()){tasks[cmdcode]();}}else if(n == 0) break;}
}void InitProcessPool(std::vector<pipeline> *pipelines)
{for (int i = 0; i < processnum; i++){// 创建管道int pipefd[2] = {0};int ret = pipe(pipefd);if (ret == -1){perror("pipe");return;}// 创建子进程pid_t id = fork();if (id == 0){// childclose(pipefd[1]);// codedup2(pipefd[0], 0);slaver();exit(0);}else if (id > 0){// fatherclose(pipefd[0]);std::string processname = "process-" + std::to_string(i);pipelines->push_back(pipeline(id, pipefd[1], processname));}else{perror("fork");return;}}
}void Debug(const std::vector<pipeline> pipelines)
{for (auto &c : pipelines){std::cout << c._processname << "-" << c._rprocessid << "-" << c._wfd << std::endl;}sleep(1000);
}void ctrlSlaverRandom(const std::vector<pipeline> &pipelines) // 随机选择子进程
{for(int i = 1; i <= 100; i++){// 1. 选择任务int cmdcode = rand() % tasks.size();// 2. 选择进程int processpos = rand() % processnum;// 3. 发送任务std::cout << "father say: " << "cmdcode-"  << cmdcode << ", already sendto: " << pipelines[processpos]._rprocessid << " processname: " << pipelines[processpos]._processname << std::endl;write(pipelines[processpos]._wfd, &cmdcode, sizeof(cmdcode));sleep(1);}
}void ctrlSlaverPoll(const std::vector<pipeline> &pipelines) // 子进程轮询
{int which = 0;int cnt = 5;while(cnt--){// 1. 选择任务int cmdcode = rand() % tasks.size();// 2. 选择进程----轮询方式// 3. 发送任务std::cout << "father say: " << "cmdcode-"  << cmdcode << ", already sendto: " << pipelines[which]._rprocessid << " processname: " << pipelines[which]._processname << std::endl;write(pipelines[which]._wfd, &cmdcode, sizeof(cmdcode));which++;which %= pipelines.size();sleep(1);}
}void QuitProcess(const std::vector<pipeline> &pipelines)
{for(const auto &c : pipelines) close(c._wfd); // 写端关闭,读端正常会读到0sleep(5);for(const auto &c : pipelines) waitpid(c._rprocessid, NULL, 0);sleep(5);
}int main()
{srand((unsigned int)time(NULL));std::vector<pipeline> pipelines;LoadTask(&tasks);// 1、 初始化InitProcessPool(&pipelines);// test// Debug(pipelines);// 2、开始控制子进程----给子进程布置任务ctrlSlaverPoll(pipelines);// 3、 结束QuitProcess(pipelines);return 0;
}

在这里插入图片描述
上面代码中存在一个小问题,因为,父进程是需要向管道中进行写入的,所以,父进程对创建出管道的读端始终没有关闭,每次只把写端进行关闭,而新创建的子进程会继承父进程中的所有文件描述符,以父进程和子进程 A 之间的管道为例,这就导致,后创建的子进程继承了之前管道的写端描述符,这样其实是有问题的。问题一是后创建的子进程可以向之前的管道中进行写入。问题二是,在结束的时候如果处理不当程序会出现卡住的现象,上面说过,写端关闭,读端就会读到0,可以通过判断,让子进程结束终止任务,如果忽略了上图展示的 Bug,父进程在 close(4) 之后就立刻去调用 waitpid 等待子进程A,此时因为实际上,在其它的子进程中也有指向该管道的写端,而只是在父进程中调用 close(4),把父进程中的写端关闭了,所以子进程A并不会读到0,而是读写端都正常,管道为空,子进程A会阻塞等待。下面这样的代码就是错误的。

void QuitProcess(const std::vector<pipeline> &pipelines)
{for(const auto &c : pipelines) {close(c._wfd); // 写端关闭,读端正常会读到0waitpid(c._rprocessid, NULL, 0);}
}

正确的写法有以下几种:

void QuitProcess(const std::vector<pipeline> &pipelines)
{for(const auto &c : pipelines) close(c._wfd); // 写端关闭,读端正常会读到0sleep(5);for(const auto &c : pipelines) waitpid(c._rprocessid, NULL, 0);
}

这种写法正确的原因是,最后一个子进程和父进程之间的管道,就只有父进程中的一个读端,通过 for 循环将父进程中所有的读端都关闭,虽然前面的子进程并不会退出,但是最后一个子进程一定会退出处于僵尸状态,最后一个进程退出,它里面的文件描述符就会全部关闭,这就回间接导致指向倒数第二个管道的所有读端也被关闭了,这样倒数第二个子进程就会退出,以此类推,最终所有的子进程都会退出处于僵尸状态。然后再去通过 for 循环去回收所有的子进程,此时就能回收成功。

void QuitProcess(const std::vector<pipeline> &pipelines)
{for(int i = pipelines.size()-1; i >= 0; i--){close(pipelines[i]._wfd);waitpid(pipelines[i]._rprocessid, NULL, 0);}
}

上面这样写也是对的,倒着去关闭父进程中的读端,然后立即回收。除了上面这两种方法外,还可以在子进程最开始的时候,将继承下来的无用的文件描述符进行关闭,因此需要定一个 oldfd 数组,记录父进程每次创建出管道的写端。

#include <unistd.h>
#include <vector>
#include <cstdio>
#include <cerrno>
#include <string>
#include <iostream>
#include <cstdlib>
#include <ctime>
#include "Task.hpp"
#include <sys/types.h>
#include <sys/wait.h>const int processnum = 5;class pipeline
{
public:pipeline(pid_t rprocessid, int wfd, std::string processname): _rprocessid(rprocessid),_wfd(wfd),_processname(processname){}public:pid_t _rprocessid;        // 读取端的进程 idint _wfd;                 // 写入端的文件描述符std::string _processname; // 子进程的名字
};void slaver()
{while (true){int cmdcode = 0;int n = read(0, &cmdcode, sizeof(int));if (n == sizeof(int)){// 执行 cmdcod 对应的任务std::cout << "child-" << getpid() << "-say@ get a cmdcode: " << cmdcode << std::endl;if(cmdcode >=0 && cmdcode < tasks.size()){tasks[cmdcode]();}}else if(n == 0) break;}
}void InitProcessPool(std::vector<pipeline> *pipelines)
{std::vector<int> oldfd;for (int i = 0; i < processnum; i++){// 创建管道int pipefd[2] = {0};int ret = pipe(pipefd);if (ret == -1){perror("pipe");return;}// 创建子进程pid_t id = fork();if (id == 0){sleep(10);for(auto c : oldfd) close(c);// childclose(pipefd[1]);// codedup2(pipefd[0], 0);slaver();exit(0);}else if (id > 0){// fatherclose(pipefd[0]);std::string processname = "process-" + std::to_string(i);pipelines->push_back(pipeline(id, pipefd[1], processname));oldfd.push_back(pipefd[1]);}else{perror("fork");return;}}
}void Debug(const std::vector<pipeline> pipelines)
{for (auto &c : pipelines){std::cout << c._processname << "-" << c._rprocessid << "-" << c._wfd << std::endl;}sleep(1000);
}void ctrlSlaverRandom(const std::vector<pipeline> &pipelines) // 随机选择子进程
{for(int i = 1; i <= 100; i++){// 1. 选择任务int cmdcode = rand() % tasks.size();// 2. 选择进程int processpos = rand() % processnum;// 3. 发送任务std::cout << "father say: " << "cmdcode-"  << cmdcode << ", already sendto: " << pipelines[processpos]._rprocessid << " processname: " << pipelines[processpos]._processname << std::endl;write(pipelines[processpos]._wfd, &cmdcode, sizeof(cmdcode));sleep(1);}
}void ctrlSlaverPoll(const std::vector<pipeline> &pipelines) // 子进程轮询
{int which = 0;int cnt = 10;while(cnt--){// 1. 选择任务int cmdcode = rand() % tasks.size();// 2. 选择进程----轮询方式// 3. 发送任务std::cout << "father say: " << "cmdcode-"  << cmdcode << ", already sendto: " << pipelines[which]._rprocessid << " processname: " << pipelines[which]._processname << std::endl;write(pipelines[which]._wfd, &cmdcode, sizeof(cmdcode));which++;which %= pipelines.size();sleep(1);}
}// void QuitProcess(const std::vector<pipeline> &pipelines)
// {
//     for(const auto &c : pipelines) close(c._wfd); // 写端关闭,读端正常会读到0
//     sleep(5);
//     for(const auto &c : pipelines) waitpid(c._rprocessid, NULL, 0);
// }// void QuitProcess(const std::vector<pipeline> &pipelines)
// {
//     for(const auto &c : pipelines) 
//     {
//         close(c._wfd); // 写端关闭,读端正常会读到0
//         waitpid(c._rprocessid, NULL, 0);
//     }//     // sleep(5);
// }// void QuitProcess(const std::vector<pipeline> &pipelines)
// {
//     for(int i = pipelines.size()-1; i >= 0; i--)
//     {
//         close(pipelines[i]._wfd);
//         sleep(2);
//         waitpid(pipelines[i]._rprocessid, NULL, 0);
//     }
// }void QuitProcess(const std::vector<pipeline> &pipelines)
{for(const auto &c : pipelines) {close(c._wfd); // 写端关闭,读端正常会读到0waitpid(c._rprocessid, NULL, 0);}
}int main()
{srand((unsigned int)time(NULL));std::vector<pipeline> pipelines;LoadTask(&tasks);// 1、 初始化InitProcessPool(&pipelines);// test// Debug(pipelines);// 2、开始控制子进程----给子进程布置任务ctrlSlaverPoll(pipelines);// 3、 结束QuitProcess(pipelines);return 0;
}

三、结语

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!

在这里插入图片描述

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

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

相关文章

华为、小米、魅族都开始造车了!中国还有多少手机厂家要下场造车?2024如果创业适合干什么?2024最适合创业的细分行业

要说现在中国最火、声量最高的创业是什么&#xff1f;那一定是造车&#xff01; 小米这样的手机公司在造车、创维这样的电视家电品牌在造车、甚至就连五粮液这样的白酒品牌也在造车&#xff01;至于其他的还有什么做地产的恒大、做电动车的新日等等&#xff0c;数不数胜。而在手…

如何进行事务处理

1、问题背景 在数据库存储系统中&#xff0c;事务处理是一种保证多个数据库操作作为单个逻辑单元执行的技术。事务处理可以确保数据的一致性、完整性和隔离性。 在使用 Google Cloud Datastore 时&#xff0c;可以使用 datastore.transaction() 函数来进行事务处理。datastor…

DBeaver连接数据库

前言 DBeaver是一款免费和开源的数据库管理工具&#xff0c;为开发人员和数据库管理员提供了一个通用的数据库解决方案。以下是DBeaver的主要特点和功能&#xff1a; 跨平台支持&#xff1a;DBeaver支持Windows、Linux、macOS等主流操作系统&#xff0c;用户可以在不同的平台…

MATLAB公式推导和导出Latex格式的方法

最近在推机械臂正逆运动学公式&#xff0c;那个旋转矩阵乘起来是真滴多&#xff0c;手算算的脑浆疼。突然想起来MATLAB还有符号计算这个功能&#xff0c;于是翻了翻手册&#xff0c; 找到了这个利用MATLAB帮助计算公式并且直接导出Latex格式的办法。 先定义符号变量&#xff0…

我的职场加速器-高效工作从可道云teamOS开始

不论是作为管理者&#xff0c;还是普通打工人&#xff0c;在日常工作中&#xff0c;我们都需要一款既能够保障数据安全&#xff0c;又能够提升团队协作效率的平台。 近期&#xff0c;我体验了可道云teamOS&#xff0c;这款企业级云协作平台给我留下了深刻的印象。 一、极简风…

基于springboot+mybatis+vue的项目实战之前端

步骤&#xff1a; 1、项目准备&#xff1a;新建项目&#xff0c;并删除自带demo程序&#xff0c;修改application.properties. 2、使用Apifox准备好json数据的mock地址 3、编写基于vue的静态页面 4、运行 整个的目录结构如下&#xff1a; 0、项目准备 新建项目&#xff0…

力扣【旋转函数】python

如果直接用暴力的话&#xff0c;只能过4个样例好像&#xff0c;超时 因此得用递推公式 F1F0前n-1个数-(n-1)*第n个数 F0sum(nums)-n*第n个数 nlen(nums) ans[]#定义一个存最大值值的列表 ss sum(nums) dm 0 for j in range(n):dm j * nums[j] ans.append(dm) print(dm) n…

Google IO 2024有哪些看点呢?

有了 24 小时前 OpenAI 用 GPT-4o 带来的炸场之后&#xff0c;今年的 Google I/O 还未开始&#xff0c;似乎就被架在了一个相当尴尬的地位&#xff0c;即使每个人都知道 Google 将发布足够多的新 AI 内容&#xff0c;但有了 GPT-4o 的珠玉在前&#xff0c;即使是 Google 也不得…

应用软件安全保证措施方案书

系统安全保证措施方案—word原件 软件全套资料进主页获取或者本文末个人名片直接获取。

“网络安全新纪元:等保2.0的详细解读与实践”

网络安全等级保护基本要求》&#xff08;等保2.0&#xff09;于2019年6月发布&#xff0c;是我国网络安全等级保护制度的一项重要标准。等保2.0主要针对关键信息基础设施的网络安全保护&#xff0c;对数据安全和个人信息保护提出了更高的要求。本文将对等保2.0进行详细解读&…

微信小程序如何变现

微信小程序有多种变现方式&#xff0c;以下是一些主要的方法&#xff1a; 广告变现&#xff1a;在小程序中嵌入广告&#xff0c;通过点击、曝光等手段获取收益。这是一种非常普遍的变现方式&#xff0c;尤其适合流量较大、用户活跃度较高的小程序。 电商变现&#xff1a;通过…

嵌入式科普(16)c语言函数参数的传递方式

目录 一、概述 二、C函数参数 2.1 一张图讲清 2.2 按数据类型分类&#xff1a; 2.2.1 基本数据类型参数&#xff1a; 2.2.2 数组参数&#xff1a; 2.2.3 结构体参数&#xff1a; 2.2.4 指针参数&#xff1a; 2.2.5 函数指针参数&#xff1a; 2.3 按传递方式分类&…

分布式搜索-elaticsearch基础 概念

什么是elaticsearch: 倒排索引&#xff1a;就是将要查询的内容分成一个个词条&#xff0c;在将词条文档id存入&#xff0c;词条是唯一的。 文档词条总结: mysql和Elasticsearch概念对比: 架构: 基本概念总结:

一.使用MySQL Workbench连接mysql数据库

一.使用MySQL Workbench连接数据库 MySQL Workbench这是MySQL官方主推的数据库可视化工具。 它提供了管理MySQL数据库,设计模型,进行SQL开发和管理的功能。主要功能包括: 数据库设计:可以通过直观的GUI创建EER模型,生成数据库结构。 SQL开发:提供代码编辑器,可以方便编写和执行…

【微服务】spring aop实现接口参数变更前后对比和日志记录

目录 一、前言 二、spring aop概述 2.1 什么是spring aop 2.2 spring aop特点 2.3 spring aop应用场景 三、spring aop处理通用日志场景 3.1 系统日志类型 3.2 微服务场景下通用日志记录解决方案 3.2.1 手动记录 3.2.2 异步队列es 3.2.3 使用过滤器或拦截器 3.2.4 使…

如何使用AspectJ做切面,打印jar包中方法的执行日记

最近在工作中遇到一个redis缓存中的hash key莫名其妙被删除的问题&#xff0c;我们用了J2Cache&#xff0c;二级缓存用的是redis。hash key莫名其妙被删除又没有日志&#xff0c;就想到做一个切面在调用redis删除hash key的方法的时候&#xff0c;打印日志&#xff0c;并且把调…

MES系统追溯管理功能,迅速定位问题源头

一、MES系统概述 MES系统是一种实现车间生产智能化、信息化的管理系统&#xff0c;通过对生产现场的数据进行实时采集、处理和分析&#xff0c;为企业管理者提供准确、及时的生产信息。MES系统主要包括生产订单管理、物料追溯、质量管理、设备管理、物料管理、人员管理等功能模…

品鉴中的食物搭配:如何创造美味的红酒与食物组合

品鉴云仓酒庄雷盛红酒时&#xff0c;食物搭配是一个不可忽视的环节。通过巧妙的搭配&#xff0c;红酒与食物可以相互衬托&#xff0c;呈现出更加美妙的风味。下面就让我们一起探讨如何创造美味的红酒与食物组合。 首先&#xff0c;了解红酒与食物的搭配原则是关键。一般来说&a…

解决wangEditor使用keep-alive缓存后,调用editor.cmd.do()失败

前提&#xff1a;wangeditor版本&#xff1a;4.7.11 vue版本&#xff1a;vue2 问题&#xff1a;在使用wangeditor富文本编辑器时&#xff0c;需求需要通过点击一个按钮&#xff0c;手动插入定义好的内容&#xff0c;所以使用了 editor.cmd.do(insertHTML, ....) 方法新增…

ChatGPT未来可能应用于iPhone?

苹果接即将与OpenAI达成协议 ChatGPT未来应用于iPhone 前言 就在5月11日&#xff0c;苹果公司正与OpenAI进行深入讨论&#xff0c;计划在其最新的iOS操作系统中整合OpenAI的先进技术。这一举措是苹果公司在为其产品线融入更先进的人工智能功能所做努力的一部分。 目前情况双方…