【Linux】--- 进程间的通信
【Linux】--- 进程间的通信
- 一、进程间通信的介绍
- 1、进程间通信的概念
- 2、进程间通信的目的
- 3、 进程间通信的本质/前提
- 4、进程间通信的分类
- 二、管道
- 1、什么是管道
- 2、匿名管道
- (1)匿名管道的原理
- (2)pipe函数
- (3)匿名管道使用步骤
- (4)管道的特点
- (5)管道的四种特殊情况
- 3、命名管道
- (1)命名管道的原理
- (2)使用命令创建命名管道
- (3)创建一个命名管道
- (4)用命名管道实现serve&client通信
- (5)命名管道和匿名管道的区别
- (6)命令行当中的管道
- 三、system V进程间通信
- 1、system V ---- 共享内存
- (1)共享内存的基本原理
- (2)共享内存数据结构
- (3)共享内存的建立与释放
- (4)共享内存的创建
- (5)共享内存的释放
- (6)共享内存的关联(挂接)
- (7)共享内存的去关联
- (8)用共享内存实现serve&client通信
- (9)共享内存与管道进行对比
- 2、System V --- 信号量
- (1)信号量相关概念
- (2)信号量相关函数
- (3)进程互斥
- (4)system V IPC联系
一、进程间通信的介绍
1、进程间通信的概念
进程间通信简称IPC(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息。
2、进程间通信的目的
- 数据传输: 一个进程需要将它的数据发送给另一个进程。
- 资源共享: 多个进程之间共享同样的资源。
- 通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件,比如进程终止时需要通知其父进程。
- 进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
3、 进程间通信的本质/前提
进程间通信的前提:让不同的进程看到同一份资源。
由于各个运行进程之间具有独立性,这个独立性主要体现在数据层面,而代码逻辑层面可以私有也可以公有(例如父子进程),因此各个进程之间要实现通信是非常困难的。
各个进程之间若想实现通信,一定要借助第三方资源,这些进程就可以通过向这个第三方资源写入或是读取数据,进而实现进程之间的通信,这个第三方资源实际上就是操作系统提供的一段内存区域。
因此,进程间通信的本质(前提)就是,让不同的进程看到同一份资源(内存,文件内核缓冲等)。 由于这份资源可以由操作系统中的不同模块提供,因此出现了不同的进程间通信方式。
4、进程间通信的分类
管道
- 匿名管道
- 命名管道
System V IPC
- System V 共享内存
- System V 消息队列
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
二、管道
1、什么是管道
管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的数据流称为一个“管道”。
例如,统计我们当前使用云服务器上的登录用户个数。
其中,who命令和wc命令都是两个程序,当它们运行起来后就变成了两个进程,who进程通过标准输出将数据打到“管道”当中,wc进程再通过标准输入从“管道”当中读取数据,至此便完成了数据的传输,进而完成数据的进一步加工处理。
注明: who命令用于查看当前云服务器的登录用户(一行显示一个用户),wc -l用于统计当前的行数。
2、匿名管道
(1)匿名管道的原理
匿名管道用于进程间通信,且仅限于有血亲关系(父子)的进程之间的通信。
进程间通信的本质就是,让不同的进程看到同一份资源,使用匿名管道实现父子进程间通信的原理就是,让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。
注意:
- 这里父子进程看到的同一份文件资源是由操作系统来维护的,所以当父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝。
- 管道虽然用的是文件的方案,但操作系统一定不会把进程进行通信的数据刷新到磁盘当中,因为这样做有IO参与会降低效率,而且也没有必要。也就是说,这种文件是一批不会把数据写到磁盘当中的文件,换句话说,磁盘文件和内存文件不一定是一一对应的,有些文件只会在内存当中存在,而不会在磁盘当中存在。
(2)pipe函数
pipe函数用于创建匿名管道,pipe函数的函数原型如下:
int pipe(int pipefd[2]); // pipe函数的返回值就是:指向管道读端+写端的inode
pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符:
如何记忆:
(1)0就想象成人嘴巴张开 开始读的样子就是0:读端
(2)1就想象成人拿钢笔写的时候,钢笔就是1:写端
关于pipe函数的返回值:
pipe函数调用成功时返回0,调用失败时返回-1。
(3)匿名管道使用步骤
在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下:
1、父进程调用pipe函数创建管道。
2、父进程创建子进程。
3、父进程关闭写端,子进程关闭读端。
注意:
- 管道只能够进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。
- 从管道写端写入的数据会被内核缓冲区,直到从管道的读端被读取。
我们可以站在文件描述符的角度再来看看这三个步骤:
1、父进程调用pipe函数创建管道。
2、父进程创建子进程。
例如,在以下代码当中,子进程向匿名管道当中写入10行数据,父进程从匿名管道当中将数据读出。
//child->write, father->read
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{int fd[2] = { 0 };if (pipe(fd) < 0){ //使用pipe创建匿名管道perror("pipe");return 1;}pid_t id = fork(); //使用fork创建子进程if (id == 0){//childclose(fd[0]); //子进程关闭读端//子进程向管道写入数据const char* msg = "hello father, I am child...";int count = 10;while (count--){write(fd[1], msg, strlen(msg));sleep(1);}close(fd[1]); //子进程写入完毕,关闭文件exit(0);}//fatherclose(fd[1]); //父进程关闭写端//父进程从管道读取数据char buff[64];while (1){ssize_t s = read(fd[0], buff, sizeof(buff));if (s > 0){buff[s] = '\0';printf("child send to father:%s\n", buff);}else if (s == 0){printf("read file end\n");break;}else{printf("read error\n");break;}}close(fd[0]); //父进程读取完毕,关闭文件waitpid(id, NULL, 0);return 0;
}
运行结果如下:
(4)管道的特点
1、管道内部自带同步与互斥机制。
我们将一次只允许一个进程使用的资源,称为临界资源。管道在同一时刻只允许一个进程对其进行写入或是读取操作,因此管道也就是一种临界资源。
临界资源是需要被保护的,若是我们不对管道这种临界资源进行任何保护机制,那么就可能出现同一时刻有多个进程对同一管道进行操作的情况,进而导致同时读写、交叉读写以及读取到的数据不一致等问题。
为了避免这些问题,内核会对管道操作进行同步与互斥
- 同步: 两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行。比如,A任务的运行依赖于B任务产生的数据。
- 互斥: 一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。
实际上,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。对于管道的场景来说,互斥就是两个进程不可以同时对管道进行操作,它们会相互排斥,必须等一个进程操作完毕,另一个才能操作,而同步也是指这两个不能同时对管道进行操作,但这两个进程必须要按照某种次序来对管道进行操作。
也就是说,互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,而同步的任务之间则有明确的顺序关系。
2、管道的生命周期随进程。
管道本质上是通过文件进行通信的,也就是说管道依赖于文件系统,那么当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说管道的生命周期随进程。
3、管道提供的是流式服务。
对于进程A写入管道当中的数据,进程B每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据报服务:
- 流式服务: 数据没有明确的分割,不分一定的报文段。
- 数据报服务: 数据有明确的分割,拿数据按报文段拿。
4、管道是半双工通信的。
在数据通信中,数据在线路上的传送方式可以分为以下三种:
- 单工通信(Simplex Communication):单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
- 半双工通信(Half Duplex):半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输
- 全双工通信(Full Duplex):全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。
管道是半双工的,数据只能向一个方向流动,需要双方通信时,需要建立起两个管道。
(5)管道的四种特殊情况
在使用管道时,可能出现以下四种特殊情况:
- 写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被阻塞挂起,直到管道里面有数据后,读端进程才会被唤醒。
- 读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒
- 写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。
- 读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么操作系统会将写端进程杀掉。
其中前面两种情况就能够很好的说明,管道是自带同步与互斥机制的,读端进程和写端进程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。
读端进程读取数据的条件是管道里面有数据,写端进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒。
第三种情况也很好理解,读端进程已经将管道当中的所有数据都读取出来了,而且此后也不会有写端再进行写入了,那么此时读端进程也就可以执行该进程的其他逻辑了,而不会被挂起。
第四种情况也不难理解,既然管道当中的数据已经没有进程会读取了,那么写端进程的写入将没有意义,因此操作系统直接将写端进程杀掉。而此时子进程代码都还没跑完就被终止了,属于异常退出,那么子进程必然收到了某种信号。
我们可以通过以下代码看看情况四中,子进程退出时究竟是收到了什么信号。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{int fd[2] = { 0 };if (pipe(fd) < 0){ //使用pipe创建匿名管道perror("pipe");return 1;}pid_t id = fork(); //使用fork创建子进程if (id == 0){//childclose(fd[0]); //子进程关闭读端//子进程向管道写入数据const char* msg = "hello father, I am child...";int count = 10;while (count--){write(fd[1], msg, strlen(msg));sleep(1);}close(fd[1]); //子进程写入完毕,关闭文件exit(0);}//fatherclose(fd[1]); //父进程关闭写端close(fd[0]); //父进程直接关闭读端(导致子进程被操作系统杀掉)int status = 0;waitpid(id, &status, 0);printf("child get signal:%d\n", status & 0x7F); //打印子进程收到的信号return 0;
}
通过kill -l命令可以查看13对应的具体信号。
由此可知,当发生情况四时,操作系统向子进程发送的是SIGPIPE信号将子进程终止的。
3、命名管道
(1)命名管道的原理
匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间的通信,通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。
如果要实现两个毫不相关进程之间的通信,可以使用命名管道来做到。命名管道就是一种特殊类型的文件,两个进程通过(命名管道的文件名)打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。
注意:
- 普通文件是很难做到通信的,即便做到通信也无法解决一些安全问题。
- 命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。
(2)使用命令创建命名管道
我们可以使用mkfifo命令创建一个命名管道。
mkfifo fifo
可以看到,创建出来的文件的类型是p,代表该文件是命名管道文件。
使用这个命名管道文件,就能实现两个进程之间的通信了。我们在一个进程(进程A)中用shell脚本每秒向命名管道写入一个字符串,在另一个进程(进程B)当中用cat命令从命名管道当中进行读取。
现象就是当进程A启动后,进程B会每秒从命名管道中读取一个字符串打印到显示器上。这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。
之前我们说过,当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉,在这里就可以很好的得到验证:当我们终止掉读端进程后,因为写端执行的循环脚本是由命令行解释器bash执行的,所以此时bash就会被操作系统杀掉,我们的云服务器也就退出了。
(3)创建一个命名管道
在程序中创建命名管道使用mkfifo函数,mkfifo函数的函数原型如下:
这里是引用
int mkfifo(const char *pathname, mode_t mode);
mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。
- 若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。
- 若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。(注意当前路径的含义)
mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。
例如,将mode设置为0666,则命名管道文件创建出来的权限如下:
但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。
若想创建出来命名管道文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。
umask(0); //将文件默认掩码设置为0
mkfifo函数的返回值。
- 命名管道创建成功,返回0。
- 命名管道创建失败,返回-1。
创建命名管道示例:
用以下代码即可在当前路径下,创建出一个名为myfifo的命名管道
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>#define FILE_NAME "myfifo"int main()
{umask(0); //将文件默认掩码设置为0if (mkfifo(FILE_NAME, 0666) < 0){ //使用mkfifo创建命名管道文件perror("mkfifo");return 1;}//create success...return 0;
}
运行代码后,命名管道myfifo就在当前路径下被创建了。
(4)用命名管道实现serve&client通信
实现服务端(server)和客户端(client)之间的通信之前,我们需要先让服务端运行起来,我们需要让服务端运行后创建一个命名管道文件,然后再以读的方式打开该命名管道文件,之后服务端就可以从该命名管道当中读取客户端发来的通信信息了。
服务端(server)的代码如下:
#include "named_pipe.hpp"// server 只读 掌管着整个管道的生命周期
int main()
{named_pipe fifo(com_path,Creator);if(fifo.OpenForRead()){// 只要以读的方式打开成功,就一直读!while(true){string message;int n=fifo.ReadNamedPipe(&message);if(n>0){cout<<"Client say> "<< message <<endl;}}}return 0;
}
而对于客户端来说,因为服务端运行起来后命名管道文件就已经被创建了,所以客户端只需以写的方式打开该命名管道文件,之后客户端就可以将通信信息写入到命名管道文件当中,进而实现和服务端的通信。
客户端(client)的代码如下:
#include "named_pipe.hpp"// client是客户端 只管写入即可!
int main()
{named_pipe fifo(com_path,User);if(fifo.OpenForWrite()){while (true){cout<<"Please Enter:>";string message;getline(cin,message);fifo.WriteNamedPipe(message);}}return 0;
}
对于如何让客户端和服务端使用同一个命名管道文件,这里我们可以让客户端和服务端包含同一个头文件,该头文件当中提供这个共用的命名管道文件的文件名,这样客户端和服务端就可以通过这个文件名,打开同一个命名管道文件,进而进行通信了。
共用头文件(named_pipe.hpp)的代码如下:
#pragma once#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <cerrno>
#include <cstdio>
#include <string>
#include<unistd.h>
#include<fcntl.h>using namespace std;const string com_path = "./myfifo"; // 定义一个:全局路径
#define Defaultfd -1
#define Creator 1
#define User 2
#define Read O_RDONLY
#define Write O_WRONLY
#define BaseSize 4096// 创建一个命名管道的类
class named_pipe
{
private:bool OpenNamedPipe(int mode){_fd=open(_fifo_path.c_str(),mode);if(_fd<0)return false;return true;}
public:named_pipe(const string &path, int who) // 构造函数参数:1你要创建的路径? 2是谁创建的?: _fifo_path(path), _id(who), _fd(Defaultfd){if(_id==Creator)// 只有id等于creator,我才给你创建命名管道。{int ret=mkfifo(_fifo_path.c_str(),0666);if(ret!=0){perror("mkfifo fail");}cout<<"Creator create named_pipe"<<endl;}}bool OpenForRead(){return OpenNamedPipe(Read);}bool OpenForWrite(){return OpenNamedPipe(Write);}int ReadNamedPipe(string* out){char buffer[BaseSize];int n=read(_fd,buffer,sizeof(buffer));if(n>0){*out=string(buffer,n);}return n;}int WriteNamedPipe(string &in){return write(_fd,in.c_str(),in.size());}~named_pipe(){if(_id==Creator){int ret=unlink(_fifo_path.c_str());if(ret!=0){perror("unlink fail");}cout<<"Creator free named_pipe"<<endl;}if(_fd!=Defaultfd) close(_fd);}private:const string _fifo_path;int _id;int _fd;
};
代码编写完毕后,先将服务端进程运行起来,之后我们就能在客户端看到这个已经被创建的命名管道文件。
接着再将客户端也运行起来,此时我们从客户端写入的信息被客户端写入到命名管道当中,服务端再从命名管道当中将信息读取出来打印在服务端的显示器上,该现象说明服务端是能够通过命名管道获取到客户端发来的信息的,换句话说,此时这两个进程之间是能够通信的。
(5)命名管道和匿名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,由open函数打开
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于它们创建与打开的方式不同,一旦这些工作完成之后,它们具有相同的语义。
(6)命令行当中的管道
现有data.txt文件,文件当中的内容如下:
我们可以利用管道(“|”)同时使用cat命令和grep命令,进而实现文本过滤。
cat data.txt | grep dragon
那么在命令行当中的管道(“|”)到底是匿名管道还是命名管道呢?
由于匿名管道只能用于有亲缘关系的进程之间的通信,而命名管道可以用于两个毫不相关的进程之间的通信,因此我们可以先看看命令行当中用管道(“|”)连接起来的各个进程之间是否具有亲缘关系。
下面通过管道(“|”)连接了三个进程,通过ps命令查看这三个进程可以发现,这三个进程的PPID是相同的,也就是说它们是由同一个父进程创建的子进程。
而它们的父进程实际上就是命令行解释器,这里为bash。
也就是说,由管道(“|”)连接起来的各个进程是有亲缘关系的,它们之间互为兄弟进程。
现在我们已经知道了,若是两个进程之间采用的是命名管道,那么在磁盘上必须有一个对应的命名管道文件名,而实际上我们在使用命令的时候并不存在类似的命名管道文件名,
因此命令行上的管道实际上是匿名管道。
三、system V进程间通信
管道通信本质是基于文件的,也就是说操作系统并没有为此做过多的设计工作,而system V IPC是操作系统特地设计的一种通信方式。但是不管怎么样,它们的本质都是一样的,都是在想尽办法让不同的进程看到同一份由操作系统提供的资源。
system V IPC提供的通信方式有以下三种:
1、system V共享内存
2、system V消息队列
3、system V信号量
其中,system V共享内存 和 system V消息队列 是以传送数据为目的的,而system V信号量是为了保证进程间的同步与互斥而设计的,虽然system V信号量和通信好像没有直接关系,但属于通信范畴。
说明一下:
system V共享内存和system V消息队列就类似于手机,用于沟通信息;system V信号量就类似于下棋比赛时用的棋钟,用于保证两个棋手之间的同步与互斥。
1、system V ---- 共享内存
(1)共享内存的基本原理
共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做:共享内存。
注意:
这里所说的开辟物理空间、建立映射等操作都是调用系统接口完成的,也就是说这些动作都由操作系统来完成
(2)共享内存数据结构
根据先描述再组织,共享内存在底层,实际上就是一个链表一样的数据结构!
(3)共享内存的建立与释放
共享内存的建立大致包括以下两个过程:
- 在物理内存当中申请共享内存空间。
- 将申请到的共享内存挂接到地址空间,即建立映射关系。
共享内存的释放大致包括以下两个过程:
- 将共享内存与地址空间去关联,即取消映射关系。
- 释放共享内存空间,即将物理内存归还给系统。
(4)共享内存的创建
创建共享内存我们需要用shmget函数,shmget函数的函数原型如下:
int shmget(key_t key, size_t size, int shmflg);
shmget函数的参数说明:
- 第一个参数key,表示待创建共享内存在系统当中的唯一标识。
- 第二个参数size,表示待创建共享内存的大小
- 第三个参数shmflg,表示创建共享内存的方式
shmget函数的返回值说明:
- shmget调用成功,返回一个有效的共享内存标识符(用户层标识符)(int 类型的 shmid)
- shmget调用失败,返回-1。
注意: 我们把具有标定某种资源能力的东西叫做句柄,而这里shmget函数的返回值实际上就是共享内存的句柄,这个句柄可以在用户层标识共享内存,当共享内存被创建后,我们在后续使用共享内存的相关接口时,都是需要通过这个句柄对指定共享内存进行各种操作。
传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取
ftok函数的函数原型如下:
key_t ftok(const char *pathname, int proj_id);
ftok函数的作用就是,将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值,称为IPC键值,在使用shmget函数获取共享内存时,这个key值会被填充进维护共享内存的数据结构当中。需要注意的是,pathname所指定的文件必须存在且可存取。
注意:
- 使用ftok函数生成key值可能会产生冲突,此时可以对传入ftok函数的参数进行修改。
- 需要进行通信的各个进程,在使用ftok函数获取key值时,都需要采用同样的路径名和和整数标识符,进而生成同一种key值,然后才能找到同一个共享资源。
传入shmget函数的第三个参数shmflg,常用的组合方式有以下两种:
ipc选项:
单独使用ipcs命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:
- -q:列出消息队列相关信息。
- -m:列出共享内存相关信息。
- -s:列出信号量相关信息。
此时,根据ipcs命令的查看结果和我们的输出结果可以确认,共享内存已经创建成功了。
ipcs命令输出的每列信息的含义如下:
注意:key是在内核层面上保证共享内存唯一性的方式,而shmid是在用户层面上保证共享内存的唯一性,key和shmid之间的关系类似于fd和FILE*之间的的关系。
(5)共享内存的释放
通过上面创建共享内存的实验可以发现,当我们的进程运行完毕后,申请的共享内存依旧存在,并没有被操作系统释放。实际上,管道是生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放。
这说明,如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此),同时也说明了IPC资源是由内核提供并维护的。
此时我们若是要将创建的共享内存释放,有两个方法,一就是使用命令释放共享内存,二就是在进程通信完毕后调用释放共享内存的函数进行释放。
使用命令释放共享内存
我们可以使用ipcrm -m shmid命令释放指定id的共享内存资源。
注意: 指定删除时使用的是共享内存的用户层id,即列表当中的shmid。
使用程序释放共享内存资源
控制共享内存我们需要用shmctl函数,shmctl函数的函数原型如下:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
其中,作为shmctl函数的第二个参数传入的常用的选项有以下三个:
(6)共享内存的关联(挂接)
将共享内存连接到进程地址空间我们需要用shmat函数,shmat函数的函数原型如下:
void *shmat(int shmid, const void *shmaddr, int shmflg);
其中,作为shmat函数的第三个参数传入的常用的选项有以下三个:
(7)共享内存的去关联
取消共享内存与进程地址空间之间的关联我们需要用shmdt函数,shmdt函数的函数原型如下:
int shmdt(const void *shmaddr);
(8)用共享内存实现serve&client通信
在知道了共享内存的创建、关联、去关联以及释放后,现在可以尝试让两个进程通过共享内存进行通信了。在让两个进程进行通信之前,我们可以先测试一下这两个进程能否成功挂接到同一个共享内存上。
服务端负责创建共享内存,创建好后将共享内存和服务端进行关联,之后进入死循环,便于观察服务端是否挂接成功。
服务端代码如下:
#include"shm.hpp"
#include"namedPipe.hpp"
int main()
{Shm shm(gpathname,gproj_id,gCreator);char* addr=(char*)shm.Addr();named_pipe fifo(com_path,Creator);if(fifo.OpenForRead()){// 只要以读的方式打开成功,就一直读!while(true){string message;int n=fifo.ReadNamedPipe(&message);if(n>0){cout<<"Client say> "<< message <<endl;}}}return 0;
}
客户端:
#include "shm.hpp"
#include "namedPipe.hpp"int main()
{// 1\只要创建了Shm类的变量,就已经有了共享内存!Shm shm(gpathname, gproj_id, gUser);// 创建出来的共享内存,先清空!shm.Zero();// 获取共享内存的地址char *addr = (char *)shm.Addr();sleep(3);// 2. 打开管道named_pipe fifo(com_path, User);if (fifo.OpenForWrite()){while (true){cout << "Please Enter:>";string message;getline(cin, message);fifo.WriteNamedPipe(message);}}// 当成string// 无需手动释放,使用完了之后会自己释放!return 0;
}
头文件(shm.hpp)
#ifndef __SHM_HPP__
#define __SHM_HPP__#include <iostream>
#include <string>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>using namespace std;const int gCreator = 1;
const int gUser = 2;const string gpathname = "/home/yjl/lesson/2024-4-20-pipe/shm";
const int gproj_id = 0x66;
const int gShmSize = 4096;class Shm
{
private:// 获取key的函数key_t GetCommonKey(){key_t k = ftok(_pathname.c_str(), _proj_id);if (k < 0){perror("ftok");}return k;}// 获取shmid(shmid==shmget函数的返回值)的函数int GetShmHelper(key_t key, int size, int flag){int shmid = shmget(key, size, flag);if (shmid < 0){perror("shmget");}return shmid;}string RoleToString(int who){if (who == gCreator)return "Creator";else if (who == gUser)return "User";return "None";}// 挂接:void *AttachShm(){if (_addrshm != nullptr)DelShm(_addrshm);void *shmaddr = shmat(_shmid, nullptr, 0);if (shmaddr == nullptr){perror("shmat");}cout << "who->" << RoleToString(_who) << "attach the shm..." << endl;return shmaddr;}// 删除共享内存:shmdtvoid DelShm(void *shmaddr){if (shmaddr == nullptr)return;shmdt(shmaddr);cout << "who->" << RoleToString(_who) << "delect the shm..." << endl;}public:Shm(const string &pathname, int proj_id, int who): _pathname(pathname), _proj_id(proj_id), _who(who), _addrshm(nullptr){_key = GetCommonKey();if (who == gCreator)GetShmUseCreate();else if (who == gUser)GetShmUser();_addrshm = AttachShm(); // 把挂接函数也直接封装到:构造函数里面(极度封装:只要用Shm这个类成功创建出来变量,就直接挂接了!)cout << "shmid->" << _shmid << endl;cout << "_key->" << ToHex(_key) << endl;}~Shm(){if (_who == gCreator){int ret = shmctl(_shmid, IPC_RMID, nullptr);}cout << "shm remove done..." << endl;}// 将key转化为16进制的函数string ToHex(key_t key){char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", key);return buffer;}bool GetShmUseCreate(){if (_who == gCreator){_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | IPC_EXCL| 0666);sleep(2);if (_shmid >= 0)return true;cout << "shm create done..." << endl;}return false;}bool GetShmUser(){if (_who == gUser){_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | 0666);if (_shmid >= 0)return true;cout << "shm get done..." << endl;}return false;}void Zero() // 先对共享内存清空!{if (_addrshm){memset(_addrshm, 0, gShmSize);}}void *Addr(){return _addrshm;}private:key_t _key;int _shmid;string _pathname;int _proj_id;int _who;void *_addrshm;
};#endif
此时我们就可以让服务端和客户端进行通信了,这里以简单的发送字符串为例。
客户端不断向共享内存写入数据:
//客户端不断向共享内存写入数据
int i = 0;
while (1){mem[i] = 'A' + i;i++;mem[i] = '\0';sleep(1);
}
服务端不断读取共享内存当中的数据并输出:
//服务端不断读取共享内存当中的数据并输出
while (1){printf("client# %s\n", mem);sleep(1);
}
此时先运行服务端创建共享内存,当我们运行客户端时服务端就开始不断输出数据,说明服务端和客户端是能够正常通信的。
(9)共享内存与管道进行对比
当共享内存创建好后就不再需要调用系统接口进行通信了,而管道创建好后仍需要read、write等系统接口进行通信。实际上,共享内存是所有进程间通信方式中最快的一种通信方式。(因为他不需要数据间的拷贝)
我们先来看看管道通信:
从这张图可以看出,使用管道通信的方式,将一个文件从一个进程传输到另一个进程需要进行四次拷贝操作:
1、服务端将信息从输入文件复制到服务端的临时缓冲区中。
2、将服务端临时缓冲区的信息复制到管道中。
3、客户端将信息从管道复制到客户端的缓冲区中。
4、将客户端临时缓冲区的信息复制到输出文件中。
我们再来看看共享内存通信:
从这张图可以看出,使用共享内存进行通信,将一个文件从一个进程传输到另一个进程只需要进行两次拷贝操作:
- 从输入文件到共享内存。
- 从共享内存到输出文件。
所以共享内存是所有进程间通信方式中最快的一种通信方式,因为该通信方式需要进行的拷贝次数最少。
但是共享内存也是有缺点的,我们知道管道是自带同步与互斥机制的,但是共享内存并没有对数据提供任何的保护机制,包括同步与互斥。
2、System V — 信号量
(1)信号量相关概念
(2)信号量相关函数
信号量集的创建:
创建信号量集我们需要用semget函数,semget函数的函数原型如下:
int semget(key_t key, int nsems, int semflg);
信号量集的删除
删除信号量集我们需要用semctl函数,semctl函数的函数原型如下:
int semctl(int semid, int semnum, int cmd, ...);
信号量集的操作
对信号量集进行操作我们需要用semop函数,semop函数的函数原型如下:
int semop(int semid, struct sembuf *sops, unsigned nsops);
(3)进程互斥
进程间通信通过共享资源来实现,这虽然解决了通信的问题,但是也引入了新的问题,那就是通信进程间共用的临界资源,若是不对临界资源进行保护,就可能产生各个进程从临界资源获取的数据不一致等问题。
保护临界资源的本质是保护临界区,我们把进程代码中访问临界资源的代码称之为临界区,信号量就是用来保护临界区的,信号量分为二元信号量和多元信号量。
比如当前有一块大小为100字节的资源,我们若是一25字节为一份,那么该资源可以被分为4份,那么此时这块资源可以由4个信号量进行标识。
信号量本质是一个计数器,在二元信号量中,信号量的个数为1(相当于将临界资源看成一整块),二元信号量本质解决了临界资源的互斥问题,以下面的伪代码进行解释:
根据以上代码,当进程A申请访问共享内存资源时,如果此时sem为1(sem代表当前信号量个数),则进程A申请资源成功,此时需要将sem减减,然后进程A就可以对共享内存进行一系列操作,但是在进程A在访问共享内存时,若是进程B申请访问该共享内存资源,此时sem就为0了,那么这时进程B会被挂起,直到进程A访问共享内存结束后将sem加加,此时才会将进程B唤起,然后进程B再对该共享内存进行访问操作。
在这种情况下,无论什么时候都只会有一个进程在对同一份共享内存进行访问操作,也就解决了临界资源的互斥问题。
实际上,代码中计数器sem减减的操作就叫做P操作,而计数器加加的操作就叫做V操作,P操作就是申请信号量,而V操作就是释放信号量。
(4)system V IPC联系
通过对system V系列进程间通信的学习,可以发现共享内存、消息队列以及信号量,虽然它们内部的属性差别很大,但是维护它们的数据结构的第一个成员确实一样的,都是ipc_perm类型的成员变量。
这样设计的好处就是,在操作系统内可以定义一个struct ipc_perm类型的数组,此时每当我们申请一个IPC资源,就在该数组当中开辟一个这样的结构。
也就是说,在内核当中只需要将所有的IPC资源的ipc_perm成员组织成数组的样子,然后用切片的方式获取到该IPC资源的起始地址,然后就可以访问该IPC资源的每一个成员了。
相关文章:
【Linux】--- 进程间的通信
【Linux】--- 进程间的通信 一、进程间通信的介绍1、进程间通信的概念2、进程间通信的目的3、 进程间通信的本质/前提4、进程间通信的分类 二、管道1、什么是管道2、匿名管道(1)匿名管道的原理(2)pipe函数(3࿰…...
GlusterFS 深度洞察:从架构原理到案例实践的全面解读(上)
文章目录 一.GlusterFS简介二.GlusterFS原理架构三.适用场景四.Glusterfs与其他存储产品对比五.部署GlusterFS集群六. 使用heketi将glusterfs接入k8s作为后端存储 一.GlusterFS简介 GlusterFS是一个免费的开源分布式文件系统,具有无中心节点、堆栈式设计、全局统一…...
实现限制同一个账号最多只能在3个客户端(有电脑、手机等)登录(附关键源码)
如上图,我的百度网盘已登录设备列表,有一个手机,2个windows客户端。手机设备有型号、最后登录时间、IP等。windows客户端信息有最后登录时间、操作系统类型、IP地址等。这些具体是如何实现的?下面分别给出android APP中采集手机信…...
C#综合知识点面试集锦
在.NET Core 框架的面试中,可能会涉及基础概念、核心组件、依赖注入、性能优化等多方面的知识点,以下为你详细介绍: 基础概念 .NET Core 概述 定义与特点:解释 .NET Core 是一个跨平台、开源、模块化且高性能的通用开发框架,能在 Windows、Linux、macOS 等操作系统上运行…...
宝珀(Blancpain):机械制表的三项重大创新(中英双语)
宝珀(Blancpain):机械制表的创新先驱 本文灵感来源: 瑞士钟表业决定逆流而上,杀出一条生路,宝珀更是坚定地宣称“我们永远只做机械表”,它拒绝了石英技术。制表师们在提高腕表的技艺和品质、实…...
稠密架构和稀疏架构
稠密架构和稀疏架构 flyfish 稠密架构 参数使用方面:稠密架构中的大部分参数在每次计算时都会被使用。也就是说,对于输入的每一个样本,模型的所有或大部分参数都会参与到计算过程中。计算特点:计算密集,需要对大量的…...
SpringCloud - Gateway 网关
前言 该博客为Sentinel学习笔记,主要目的是为了帮助后期快速复习使用 学习视频:7小快速通关SpringCloud 辅助文档:SpringCloud快速通关 源码地址:cloud-demo 一、简介 官网:https://spring.io/projects/spring-clou…...
【如何掌握CSP-J 信奥赛中的排序算法】
要掌握CSP-J信奥赛中的排序算法,需要系统学习基础排序算法的原理、实现和应用场景。以下是分阶段的学习路径和建议: 一、必掌握的排序算法清单 CSP-J阶段需重点掌握以下算法(按考察频率排序): 冒泡排序(B…...
3. CSS中@scope
说说你对 CSS 中scope 的了解 <style>/* scope规则 */scope (#app) {.box {width: 100px;height: 100px;background-color: red;}} </style> <div id"app"><div class"box"></div> </div>CSS 中的scope 是一个相对较新…...
基于雷达和摄像头的无人机轨迹识别与激光照射控制研究
标题:基于雷达和摄像头的无人机轨迹识别与激光照射控制研究 内容:1.摘要 摘要:本文研究了基于雷达和摄像头的无人机轨迹识别与激光照射控制。通过对雷达和摄像头数据的融合处理,实现了对无人机轨迹的精确识别。同时,利用激光照射技术对无人机…...
Response 和 Request 介绍
怀旧网个人博客网站地址:怀旧网,博客详情:Response 和 Request 介绍 1、HttpServletResponse 1、简单分类 2、文件下载 通过Response下载文件数据 放一个文件到resources目录 编写下载文件Servlet文件 public class FileDownServlet exten…...
读 DeepSeek-R1 论文笔记
DeepSeek-R1:通过强化学习激发大语言模型的推理能力 DeepSeek-AI 摘要 我们推出第一代推理模型DeepSeek-R1-Zero和DeepSeek-R1。DeepSeek-R1-Zero作为无需监督微调(SFT)预训练阶段、直接通过大规模强化学习(RL)训练的基础模型,展现出卓越的推理能力。…...
【算法-动态规划】、魔法卷轴: 两次清零机会整个数组最大累加和
【算法-动态规划】、魔法卷轴: 两次清零机会整个数组最大累加和 文章目录 一、dp1.1 题意理解1.2 整体思路1.3 具体思路1.4 代码 二、多语言解法 一、dp 1.1 题意理解 nums 数组, 有正负0, 使用最多两次魔法卷轴, 希望使数组整体的累加和尽可能大. 求尽可能大的累加和 其实就…...
蓝桥杯C语言组:分治问题研究
蓝桥杯C语言组分治问题研究 摘要 本文针对蓝桥杯C语言组中的分治问题展开深入研究,详细介绍了分治算法的原理、实现方法及其在解决复杂问题中的应用。通过对经典例题的分析与代码实现,展示了分治算法在提高编程效率和解决实际问题中的重要作用ÿ…...
npm介绍(Node Package Manager)(JavaScript生态中最流行的包管理工具,主要用于Node.js项目的依赖管理)
文章目录 **核心功能****常用命令****关键文件****npm vs 其他工具****最佳实践**官方资源 npm(Node Package Manager)是 JavaScript 生态中最流行的包管理工具,主要用于 Node.js 项目的依赖管理。以下是核心要点: 核心功能 依赖管…...
小白零基础如何搭建CNN
1.卷积层 在PyTorch中针对卷积操作的对象和使用的场景不同,如有1维卷积、2维卷积、 3维卷积与转置卷积(可以简单理解为卷积操作的逆操作),但它们的使用方法比较相似,都可以从torch.nn模块中调用,需要调用的…...
【分布式架构理论3】分布式调用(1):负载均衡
文章目录 零、三种不同的负载均衡一、常见行业负载均衡方案1. 电商与互联网服务2. 金融与支付系统3. 云计算与分布式存储 二、负载均衡策略概述1. 无状态负载均衡(强调公平性)2. 有状态的负载均衡(强调正确性) 三、 总结 零、三种…...
QT 5.15.2 开发地图ArcGIS 100.15.6(ArcGIS Runtime SDK for Qt)
QT 5.15.2ArcGIS下载 Downloads | ArcGIS Runtime API for Qt | Esri Developer ArcGIS安装(略)参考 Display a map | ArcGIS Maps SDK for Qt | Esri Developer QT新建工程 步骤1 步骤2 步骤3 步骤4(选择Topographic不需要KEY) 步骤5&a…...
细读 React | React Router 路由切换原理
2022 北京冬奥会开幕式 此前一直在疑惑,明明 pushState()、replaceState() 不触发 popstate 事件,可为什么 React Router 还能挂载对应路由的组件呢? 翻了一下 history.js 源码,终于知道原因了。 源码 假设项目路由设计如下&#…...
kubernetes学习-Helm 包管理器(十二)
一、Helm解释 Helm:Kubernetes 的软件包管理器 Helm 被誉为查找、分享及使用 Kubernetes 软件组件的最佳途径。作为 Kubernetes 包的管理工具,Helm 专注于管理名为 chart 的软件包。以下是 Helm 所具备的核心功能: 创建新 chart࿱…...
PbootCMS最新代码注入漏洞(CNVD-2025-01710、CVE-2024-12789)
PbootCMS是一套高效、简洁、 强悍的可免费商用的CMS源码,使用PHPMySQL开发,能够满足各类企业网站开发建设的需要。 国家信息安全漏洞共享平台于2025-01-14公布该程序存在代码注入漏洞。 漏洞编号:CNVD-2025-01710、CVE-2024-12789 影响产品…...
网络安全与AI:数字经济发展双引擎
在2025年年初,一场科技攻防战引发了全球关注。国产人工智能DeepSeek的爆火,伴随着大规模的网络攻击事件,将网络安全的重要性推上了风口浪尖。 在此背景下,我们计划探讨网络安全与人工智能如何为数字经济发展提供强大动力。网络安…...
【DeepSeek × Postman】请求回复
新建一个集合 在 Postman 中创建一个测试集合 DeepSeek API Test,并创建一个关联的测试环境 DeepSeek API Env,同时定义两个变量 base_url 和 api_key 的步骤如下: 1. 创建测试集合 DeepSeek API Test 打开 Postman。点击左侧导航栏中的 Co…...
如何将网站提交百度收录完整SEO教程
百度收录是中文网站获取流量的重要渠道。本文以我的网站,www.mnxz.fun(当然现在没啥流量) 为例,详细讲解从提交收录到自动化维护的全流程。 一、百度收录提交方法 1. 验证网站所有权 1、登录百度搜索资源平台 2、选择「用户中心…...
【unity实战】实现摄像机跟随效果
考虑到每个人基础可能不一样,且并不是所有人都有同时做2D、3D开发的需求,所以我把 【零基础入门unity游戏开发】 分为成了C#篇、unity通用篇、unity3D篇、unity2D篇。 【C#篇】:主要讲解C#的基础语法,包括变量、数据类型、运算符、流程控制、面向对象等,适合没有编程基础的…...
使用Hexo部署NexT主体网站
一.使用git提交文件 参考: 从零开始搭建个人博客(超详细) - 知乎 致谢! 第一种:本地没有 git 仓库 直接将远程仓库 clone 到本地;将文件添加并 commit 到本地仓库;将本地仓库的内容push到远程仓…...
使用 AlexNet 实现图片分类 | PyTorch 深度学习实战
前一篇文章,CNN 卷积神经网络处理图片任务 | PyTorch 深度学习实战 本系列文章 GitHub Repo: https://github.com/hailiang-wang/pytorch-get-started 本篇文章内容来自于 强化学习必修课:引领人工智能新时代【梗直哥瞿炜】 使用 AlexNet 实现图片分类…...
ES6 Proxy 用法总结以及 Object.defineProperty用法区别
Proxy 是 ES6 引入的一种强大的拦截机制,用于定义对象的基本操作(如读取、赋值、删除等)的自定义行为。相较于 Object.defineProperty,Proxy 提供了更灵活、全面的拦截能力。 1. Proxy 语法 const proxy new Proxy(target, hand…...
初次体验Tauri和Sycamore (2)
原创作者:庄晓立(LIIGO) 原创时间:2025年2月8日(首次发布时间) 原创链接:https://blog.csdn.net/liigo/article/details/145520637 版权所有,转载请注明出处。 关键词:Sy…...
Qt - 地图相关 —— 2、Qt调用百度在线地图功能示例全集,包含线路规划、地铁线路查询等(附源码)
效果:由于录制软件导致exe显示不正常,实际运行没有任何问题。 作者其他相关文章链接: Qt - 地图相关 —— 1、加载百度在线地图(附源码)...
ffmpeg基本用法
一、用法 ffmpeg [options] [[infile options] -i infile]... {[outfile options] outfile}... 说明: global options:全局选项,应用于整个 FFmpeg 进程,它们通常不受输入或输出部分的限制。 infile options:输入选…...
redis底层数据结构——链表
文章目录 定义内部实现总结 定义 链表提供了高效的节点重排能力,以及顺序性的节点访间方式,并且可以通过增删节点来灵活地调整链表的长度。 作为一种常用数据结构,链表内置在很多高级的编程语言里面,因为Redis使用的C语言并没有…...
Repo命令使用
repo 命令与 git 类似,但它主要用于管理多个 Git 仓库的操作。以下是等效的 repo 命令: 1. 获取新仓库代码 克隆仓库 repo init -u <manifest_url> -b <branch_name> repo sync repo init:初始化 repo,指定远程清单…...
React 高级教程
使用 React 高级组件(HOC)实现的完整项目示例,包含权限控制、数据加载状态处理、性能优化等常见高级功能。创建一个简单的博客系统: // 项目结构: src/ |-- components/ | |-- ArticleList.jsx | |-- Article.jsx | |-- Header.jsx | |-- LoginForm.jsx | |-- U…...
Linux: ASoC 声卡硬件参数的设置过程简析
文章目录 1. 前言2. ASoC 声卡设备硬件参数2.1 将 DAI、Machine 平台的硬件参数添加到声卡2.2 打开 PCM 流时将声卡硬件参数配置到 PCM 流2.3 应用程序对 PCM 流参数进行修改调整 1. 前言 限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失&am…...
网络基础知识与配置
目录 网络基础知识 (一)网络的概念 (二)网络协议 (三)网络拓扑结构 (四)IP地址和子网掩码 显示和配置网络接口 (一)在Windows系统中 (二&a…...
【STM32】ADC|多通道ADC采集
本次实现的是ADC实现数字信号与模拟信号的转化,数字信号时不连续的,模拟信号是连续的。 1.ADC转化的原理 模拟-数字转换技术使用的是逐次逼近法,使用二分比较的方法来确定电压值 当单片机对应的参考电压为3.3v时,0~ 3.3v(模拟信…...
centos 7 关于引用stdatomic.h的问题
问题:/tmp/tmp4usxmdso/main.c:6:23: fatal error: stdatomic.h: No such file or directory #include <stdatomic.h> 解决步骤: 1.这个错误是因为缺少C编译器的标准原子操作头文件 stdatomic.h。在Linux系统中,我们需要安装开发工具…...
用语言模型探索语音风格空间:无需情感标签的情 感TTS
用语言模型探索语音风格空间:无需情感标签的情感TTS 原文:Exploring speech style spaces with language models: Emotional TTS without emotion labels 今天我们要说的是 一种无需情感标签的情感TTS。提出了一个基于FastSpeech2的E-TTS框架࿰…...
将Excel中的图片保存下载并导出
目录 效果演示 注意事项 核心代码 有需要将excel中的图片解析出来保存到本地的小伙子们看过来!!! 效果演示 注意事项 仅支持xlsx格式:此方法适用于Office 2007及以上版本的.xlsx文件,旧版.xls格式无法使用。 图片名…...
2.11日学习总结
题目一 : AC代码 #include <stdio.h> #include <stdlib.h>// 定义长整型 typedef long long ll;// 定义求最大值和最小值的宏函数 #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define MIN(a, b) ((a) < (b) ? (a) : (b))// 定义数组和变量 ll…...
安川伺服控制器MP系列优势特点及行业应用
在工业自动化领域,运动控制器的性能直接决定了设备的精度、效率和可靠性。作为全球领先的运动控制品牌,安川电机伺服控制器凭借其卓越的技术优势和广泛的应用场景,正在为智能制造注入强劲动力! MP3100:主板型运动控制…...
【腾讯地图】录入经纬度功能 - 支持地图选点
目录 效果展示代码引入地图服务地址弹框中输入框 - 支持手动输入经纬度/地图选点按钮地图选点弹框组件 当前文章 - 地图功能与 https://blog.csdn.net/m0_53562074/article/details/143677335 功能类似 效果展示 代码 引入地图服务地址 public/index.html <!-- 互联网地图…...
Mybatis快速入门与核心知识总结
Mybatis 1. 实体类(Entity Class)1.1 实体类的定义1.2 简化编写1.2.1 Data1.2.2 AllArgsConstructor1.2.3 NoArgsConstructor 2. 创建 Mapper 接口2.1 Param2.2 #{} 占位符2.3 SQL 预编译 3. 配置 MyBatis XML 映射文件(可选)3.1 …...
RK3568平台开发系列讲解(调试篇)网卡队列均衡负载
🚀返回专栏总目录 文章目录 一、RPS 的介绍1. RPS 的工作原理2. RPS 配置3. 启用和调优 RPS4. RPS 优势二、下行测试iperf测试沉淀、分享、成长,让自己和他人都能有所收获!😄 RPS(Receive Packet Steering) 是一种用于提高网络接收性能的技术,通常用于多核处理器系统中…...
Matlab机械手碰撞检测应用
本文包含三个部分: Matlab碰撞检测的实现URDF文件的制作机械手STL文件添加夹爪 一.Matlab碰撞检测的实现 首先上代码 %% 检测在结构环境中机器人是否与物体之间发生碰撞情况,如何避免? % https://www.mathworks.com/help/robotics/ug/che…...
【前端】几种常见的跨域解决方案代理的概念
几种常见的跨域解决方案&代理的概念 一、常见的跨域解决方案1. 服务端配置CORS(Cross-Origin Resource Sharing):2. Nginx代理3. Vue CLI配置代理:4 .uni-app在manifest.json中配置代理来解决:5. 使用WebSocket通讯…...
服务器有多少线程?发起一个请求调用第三方服务,是新增加一个请求吗?如果服务器线程使用完了怎么办?
目录 1. 服务器有多少线程? (1)服务器类型 (2)配置参数 (3)硬件资源 2. 发起一个请求调用第三方服务,是新增加一个线程吗? (1)同步调用 (2)异步调用 (3)HTTP 客户端 3. 如果服务器线程使用完了怎么办? (1)请求被拒绝 (2)性能下降 (3)解决方案…...
【Spring AI】基于SpringAI+Vue3+ElementPlus的QA系统实现一
整理不易,请不要吝啬你的赞和收藏。 1. 前言 这是 SpringAI 系列的第二篇文章,这篇文章将介绍如何基于 RAG 技术,使用 SpringAI Vue3 ElementPlus 实现一个 Q&A 系统。本文使用 deepseek 的 DeepSeek-V3 作为聊天模型,使用…...
前端快速生成接口方法
大家好,我是苏麟,今天聊一下OpenApi。 官网 : umijs/openapi - npm 安装命令 npm i --save-dev umijs/openapi 在根目录(项目目录下)创建文件 openapi.config.js import { generateService } from umijs/openapi// 自…...