【Linux】进程间通信(管道通信、共享内存通信)

一.什么是进程间通信

进程间通信这五个字很好理解,就是进程和进程之间通信

那么为什么要有进程间通信呢?

1.数据传输:一个进程需要将它的数据发送给另一个进程

2.资源共享:多个进程之间共享同样的资源

3.通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件

4.进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。


在这里再举一个例子,例如在网络中,我们通过聊天软件和远处的别人进行聊天就是一种进程间的通信,是你在这边的进程和他在那边的进程之间的通信。 


那么进程间通信又有那些呢?

我们大致可以分为三种管道System V进程间通信POSIX进程间通信

1.管道           :是基于文件系统的进程间通信,又分为匿名管道和命名管道。

2.System V  :聚焦在本地的通信。

3.POSIX       :让通信可以跨主机。

但是在这篇博客中,只讲前两种,第三种会在后面与网络有关的博客中讲到。

那么我们应该如何理解通信的本质呢?

1.操作系统需要直接或者间接给通信双方进程提供“内存空间”。

2.要通信的进程,必须看到一份公共的资源。 

二.管道进程间通信

在Linux中对于管道的使用,大家可能也有过了解,但是管道到底是什么,以及它是如何实现的呢?

管道又可以分为匿名管道命名管道,我们先来看看匿名管道。 

又由于管道是基于文件系统的,这里会涉及到文件操作基础IO的内容,对于文件操作和基础IO的内容,我之前写过一篇博客来详细讲过,在这里就不再详细讲,有需要的可以到下面这个博客中看:

【Linux】基础IO-CSDN博客

匿名管道 

首先管道是基于文件系统的,那么在文件系统中,如果一个进程打开了一个文件,那么操作系统就会为这个文件创建一个文件结构体struct file变量,当我们对文件写入的时候,就是进程向这个struct file进行写入,同时我们也可以从这个struct file中读取。

其中这个struct file就可以用来做管道

如下图所示。

但是现在我们只有一个进程,还做不到进程间通信,所以我需要创建一个子进程来继续完成下面的工作。

这个是时候管道的基本雏形就已经出来,但是这个图还有一些问题,我们稍后慢慢讲。

pipe函数

对于struct file来说,通常都是我们来打开一个文件时才会有的,但是现在的管道操作不需要打开文件,所以这个struct file又该怎么创建呢?

操作系统给我们提供了一个pipe的函数,这个函数可以给我们的进程创建一个特殊的struct file这个struct file不属于磁盘中的任何文件,而是操作系统创建的一个临时的struct file,其目的就是为了给我们提供管道的操作。

同时这个pipe系统调用会默认打开读和写的操作。 


返回值:

如果成功创建管道,则返回0,如果创建失败,则返回-1,并设置errno来表示错误。

pipe系统调用如上图所示,其中参数为一个大小为2的数组,当调用这个函数后,操作系统就会自动的为当前这个进程创建一个管道,同分别以读和写的形式来打开管道,其中,pipefd[0]为读取操作的文件描述符,pipefd[1]为写入操作的文件描述符。 

调用这个函数后,效果如下图所示:


然后我们在进行创建一个子进程,创建完成后如下图所示:


 此时我们的父子进程都能对管道进行读写操作,但是这不是我们所希望的,因为管道应该是单向的,而不是双向的,而且此时的这种情况有一点的不安全,所以,我们需要关闭一些文件描述符,在下面的代码演示中,我会进行一个子进程往管道写入,而父进程往管道读取的操作,所以接下来还要关闭一些文件描述符,关闭后如下图所示。

使用例子 

这样我们就能达到一个进程间通信的效果了,完整示例代码如下: 

#include<iostream>#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>#include<cassert>
#include<cstdlib>
#include<cstdio>
#include<cstring>using namespace std;int main()
{int fds[2] = {0,0};int ret = pipe(fds);//其中fds[0]为读,fds[1]为写pid_t id = fork();//创建子进程assert(id >= 0);//我想做的是,父进程读取管道,子进程写入管道//子进程if(id == 0){//子进程关闭读取管道close(fds[0]);char buffer[1024];//缓冲区int cnt = 0;//计数器const char * str = "hello linux";while(1){char buffer[1024];//缓冲区sprintf(buffer,"我是父进程,我在往管道写入数据:%s:%d",str,cnt++);//将内容写入缓冲区中write(fds[1],buffer,strlen(buffer));//将缓冲区中的数据写入到管道中sleep(1);}close(fds[1]);exit(0);}//父进程//父进程关闭写入管道close(fds[1]);char buffer[1024];//缓冲区int cnt = 5;while(cnt--){int sz = read(fds[0],buffer,sizeof(buffer)-1);//sz为返回读到几个字节buffer[sz] = '\0';//按C语言的标准给末尾添加\0cout<<buffer<<endl;sleep(1);}close(fds[0]);int status = 0;//存储子进程的退出信息waitpid(id,&status,0);//阻塞等待子进程cout<<(status&0x7F)<<endl;  return 0;
}

管道的读写特征

1.读慢,写快(当父进程读取的很慢,而子进程写入的很快,管道会充斥着大量的数据)

2.读快,写慢(当子进程写入很慢,而父进程读取很快,则父进程会进行阻塞等待)

3.写关闭,读到0(当写入端关闭的时候,读取端会读到末尾)

4.读关闭,操作系统会终止终端,并且给写入的进程发送信号,终止终端。

管道的特征

1. 管道是生命周期进程。(当该进程结束后,管道也会相应的删除)

2.管道可以用来进行具有血缘关系的进程之间进行通信,常用于父子进程通信。

3.管道是面向字节流的。

4.管道是半双工通信。(即单向通信)

5.管道是互斥与同步机制的。(对共享资源进行保护的方案)

匿名管道一般都是使用与有血缘关系进程之间的通信,例如上面的代码中,是父子进程之间的通信,那么如果想在没有血缘关系的进程之间通信,又应该怎么做呢?

我们可以使用命名管道。

命名管道

看完了匿名管道,我们再来看一下命名管道。

命名管道匿名管道最大的区别就是有名和没名,在匿名管道中,我们创建了一个没有名字的管道,也无法在通过路径来找到,而命名管道是有名的,我们可以通过路径来找到它,所有它就可以在没有血缘关系的进程之间进行通信。


那么命名管道又是如何做到在不同的进程间进行通信的呢?

命名管道的操作,会在磁盘中,创建一个真实存在的文件,然后需要通信的两个进程,都要打开这个文件,其中一个进程往这个管道文件里写入另一个进程往这个管道文件里读取,这样就可以进行进程间通信了。

但是当有一个进程往管道文件里面进行写入的时候,并不会真真正正的往文件里写入数据而是往这个文件的struct file中写入,而struct file中的数据是不会写入到文件中的,因为这个过程涉及到IO,会非常浪费性能。

如下图所示。

mkfifo函数

知道了命名管道的原理后,我们就可以来进行操作了。

首先,使用命名管道的前提是,在磁盘中,有这个管道文件,这个管道文件的创建,操作系统为我们提供了一个系统接口来完成,mkfifo系统调用

下面是这个系统调用的手册说明

mkfifo这个系统调用有两个参数:pathname,mode

pathname:你创建这个管道文件的路径

mode:你创建这个管道文件的权限,例如0666,就是这个管道文件对所有人都可读可写可执行。

这个系统调用的功能就是在磁盘中创建一个管道文件。

使用效果如下。

会创建一个管道文件。

返回值:

mkfifo如果成功创建了管道文件,会返回0,如果出现错误,则会返回-1,并且设置errno来表示错误。

unlink函数

能创建管道文件,那么也要能删除管道文件,unlink就是用来删除管道文件的。

unlink的参数只有一个:管道文件的文件名

使用后会删除该管道文件。 

返回值:

如果删除成功,则返回0,删除失败,返回-1,并且适当的设置errno来表示错误 

使用例子 

对于命名管道的操作的前置条件已经说完了,接下来就可以来尝试使用一下了。

在下面我会写一个模拟单向聊天的小程序,就是一个进程给另一个进程发送消息。 


在这个程序中,我会进行模块化编程,整个程序有三个文件server.cc、client.cc、common.hpp

common.hpp

在这个hpp文件中,我定义了两个函数

CreatPipe() :创建管道文件

RemotePipe():删除管道文件

同时定义了个宏,就是这个管道文件的名字

#pragma once#include <cerrno>
#include <cstring>
#include <cassert>#include <iostream>#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>#define NAMED_PIPE "named_pipe"    //管道文件名//创建命名管道
bool CreatPipe()
{umask(0);int ret = mkfifo(NAMED_PIPE,0666);//创建命名管道if(ret == 0)return true;else if(ret == -1){std::cout<<"errno: "<<errno<<" "<<strerror(errno)<<std::endl;//如果创建失败,输出错误信息return false;}
}//删除命名管道
bool RemotePipe()
{int ret = unlink(NAMED_PIPE);//删除管道文件if(ret == 0)return true;else if(ret == -1){std::cout<<"errno: "<<errno<<" "<<strerror(errno)<<std::endl;//如果删除失败,输出错误信息return false;}
}
client.cc 

在这个client.cc文件中,就是一个发送消息的进程。 

将我们需要发送的信息写入到管道文件中。

#include"common.hpp"int main()
{int fd = open(NAMED_PIPE,O_WRONLY);//以写的形式打开管道文件assert(fd != -1);char buffer[1024];//缓冲区while(true){std::cout<<"# please input:";//输入提示符fgets(buffer,sizeof(buffer)-1,stdin);//将你要发送的消息读取到buffer缓冲区中buffer[strlen(buffer)-1] = '\0';//去除末尾的 \nssize_t ret = write(fd,buffer,sizeof(buffer)-1);//将buffer中的内容写入到管道文件中if(ret == -1){std::cout<<"errno: "<<errno<<" "<<strerror(errno)<<std::endl;}}close(fd);//关闭管道文件return 0;
}
server.cc 

在这个sercer.cc中,也是一个进程,在这个进程中,我创建了管道文件,并且从管道文件中读取内容,再将其输出。

#include"common.hpp"int main()
{CreatPipe();//创建命名管道int fd = open(NAMED_PIPE,O_RDONLY);//以读的形式打开管道文件assert(fd != -1);char buffer[1024];while(true){ssize_t ret = read(fd,buffer,sizeof(buffer)-1);//从管道文件中读取内容buffer[ret] = '\0';if(ret > 0)//说明读取成功{std::cout<<"client -> server #"<<buffer<<std::endl;}else if(ret == 0)//说明管道文件已关闭,退出循环{std::cout<<"client quit, me too"<<std::endl;break;}}close(fd);RemotePipe();//删除命名管道return 0;
}

 写到这里,这个小程序就完成了,接下来将两个.cc文件进行编译,再一起运行起来。

如下图所示。 

三.system V进程间通信 

讲完了管道类型的进程间通信,接下来到system V进程间通信了。

system V进程间通信是聚焦在本地的进程间通信,也可以说是和管道差不多,在同一台机器上进行通信。

system V进程间通信有三种:共享内存消息队列信号量

但在这里,只讲解共享内存方法的通信。 

共享内存 

什么是共享内存?

不同的进程看到同一块内存空间,就叫做共享内存。 

在C/C++的内存分布中,大家都知道有堆区和栈区,同时在堆区和栈区之间还有一块区域叫做共享内存。这块共享内存就是给我们的进程是实现进程间通信的,如下图所示。

通过共享内存的方式,可以让不同的进程看到同一份资源而实现进程间通信。

那么如何通过共享内存来实现进程间通信呢?我们接着往下讲解。

要想实现system V的进程间通信,我们要先来学习一些函数。 

shmget函数

shmget函数是用来创建一块共享内存的。我们来看看手册。

通过手册,我们可以看到,shmget有三个参数:key、size、shmflg

这个函数的参数稍微有点复杂,我们一个一个来说。 

key:

key值是指能唯一标识一块共享内存的标志,key值的内容是什么不重要,重要的是它能唯一标识一块共享内存,至于key值如何获取,我在下面会讲到。

size:

size是指我们创建的共享内存的大小,一般这个大小我们通常设置为4kb的整数倍

shmflg:

这个参数是一组标志位,通常这个标志位的参数只有两个用的最多:IPC_CREATIPC_EXCL

其中,IPC_EXCL不能单独使用,一般都是结合IPC_CREAT来一起使用的。

标志位的用法两种

①IPC_CREAT:如果共享内存不存在,则创建它,如果存在则获取它。

什么意思呢?

进程间通信通常涉及到两个进程,这两个进程需要有一个进程创建这块共享内存,而另一个进程则需要获取这块共享内存

②IPC_CREAT | IPC_EXCL:如果共享内存不存在,则创建它,如果存在,则出错返回,这样传参的意义是确保我们创建的共享内存是全新的

返回值:

如果创建共享内存成功,则返回一个非负整数,即共享内存的标识符,否则返回-1并设置errno来表示错误。 

ftok函数 

ftok函数的作用就是来生成我们的key值。我们来看看手册。 

ftok有两个参数,其中返回值就是我们所需要的key值。

pathname:我们在Linux中一个所存在的路径名

proj_jd     :  一个我们想任意给的数字id

ftok这个函数,会通过我们所给的参数,自动生成出一个key值出来,注意相同的pathname和proj_id会生成出相同的key值。

返回值:

如果成功,则返回key值,否则返回-1,并设置errno来表示错误。 

创建共享内存 

当把上面的两个函数学会后,就可以创建我们的共享内存了,我们来操作一下。 

在实现这里的功能的时候,我们可以适当的对shmget函数和ftok函数进行一个封装,以便我们使用。如下面的代码所示。 

#include <cstring>#include <iostream>#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>#define PATHNAME "."  //当前路径
#define PROJ_ID  1    //proj_id随便给,在这里给1
#define SIZE     4096 //共享内存大小//获取key值
key_t GetKey()
{key_t key = ftok(PATHNAME,PROJ_ID);//获取key值if(key == -1){std::cerr<<"获取key值失败: "<<strerror(errno)<<std::endl;exit(1);}return key;
}//创建共享内存
int CreatShm(key_t key)
{int id = shmget(key,SIZE,IPC_CREAT | IPC_EXCL | 0666);//创建共享内存,其中0666是指这块共享内存的权限if(id == -1){std::cerr<<"创建共享内存失败: "<<strerror(errno)<<std::endl;exit(2);}return id;
}int main()
{key_t key = GetKey();printf("0x%x\n",key);//将key值输出,进行观察CreatShm(key);return 0;
}

在上面的代码中,我写了两个函数,一个是获取key值,一个是创建共享内存。

当这段代码运行起来后,可以成功的创建共享内存了。 


那么我们怎么确定这块共享内存存在呢?

通过ipcs -m命令,可以查看我们在系统中的共享内存。如下图所示。 


通过实践,我们发现,当test程序第一次运行的时候是能成功的,但是第二次及以上的时候再运行时,会失败,如下图所示。

同时,我们可以观察到,输出的key值是唯一的,因为我们给的参数没有变。

但是为什么后面再运行程序时,会创建共享内存失败呢?

那是因为我们第一次运行的时候,共享内存就已经被创建出来了,当这个程序结束后,共享内存是不会被主动删除的,需要我们通过命令或者代码来删除。 


首先我们来介绍一下通过命令的方式来删除。

我们可以通过使用        ipcrm -m [id]        的方式来删除,如下图所示。

获取共享内存 

当一个进程可以创建共享内存后,那么另一个进程就要获取共享内存,同样的,获取共享内存也是用到shmget函数,我们来实现一下。

//获取共享内存
int GetShm(key_t key)
{int id = shmget(key,SIZE,IPC_CREAT);if(id == -1){std::cerr<<"获取共享内存失败: "<<strerror(errno)<<std::endl;exit(6);}return id;
}

 shmctl函数

学习完了命令行方式的删除共享内存,接下来再来看一下通过函数的方式来删除共享内存

先来看一下函数的手册。 

这个函数有三个参数:

shmid :共享内存的id。

cmd    :这个cmd也是一个标志位,通过标志位可以实现不同的功能,但是在这先讲删除共享内存的功能,当需要删除共享内存的时候,传参传IPC_RMID即可。

buf      :在删除操作中,这个参数一般为nullptr即可。

返回值:

如果删除成功,则返回0,否则返回-1,并设置errno来表示错误。 

删除共享内存 

知道了shmctl函数后,接下来就可以来实现删除共享内存的操作了。代码如下。 

//删除共享内存
void DeleteShm(int shmid)
{int ret = shmctl(shmid,IPC_RMID,nullptr);if(ret == -1){std::cerr<<"删除共享内存失败: "<<std::endl;exit(3);}
}

shmat函数 

当创建共享内存和删除共享内存都搞完后,接下来还需要做的工作是连接共享内存

创建共享内存是不够的,还需要将进程和共享内存连接起来,连接共享内存,需要使用shmat函数,我们先来看一下手册。 

shmat一共有三个参数,其中我们一般只关心第一个。

shmid:我们需要连接共享内存的id。

shmaddr:我们需要连接到那个地址空间中,这个参数绝大多数情况下是不需要传参的,给一个nullptr即可。

shmflg:这个参数与我们的读写权限有关,一般也不需要管,设置成0即可。

返回值:

如果连接成功,则返回这段共享内存的起始地址,否则返回-1,并设置errno来表示错误。

连接共享内存 

知道了shmat函数后,接下来就可以实现出连接共享内存的函数了,如下面代码所示。 

//连接共享内存
void* AttachShm(int shmid)
{void* ret = shmat(shmid,nullptr,0);if((long long)ret == -1){std::cerr<<"连接共享内存失败: "<<strerror(errno)<<std::endl;exit(4);}return ret;
}

 shmdt函数

共享内存连接成功后就可以开始使用了,使用完成后,进程还要与共享内存断开连接,这里就要用到shmdt函数。我们先来看一下这个函数的手册。 

这个函数的参数只有一个,就是共享内存的起始地址。 

返回值:

如果断开连接成功,则返回0,否则返回-1,并设置errno来表示错误。 

 断开共享内存

知道了shmdt函数后,接下就可以完成断开共享内存的函数了。如下面代码所示。  

//断开连接共享内存
int DisattchShm(void* start)
{int ret = shmdt(start);if(ret == -1){std::cerr<<"断开共享内存失败: "<<strerror(errno)<<std::endl;exit(5);}return ret;
}

使用例子 

现在,创建共享内存,获取共享内存,连接共享内存,断开共享内存,删除共享内存的函数我们都已经实现了,接下来就可以来真真正正的实现进程间通信了,在这里例子中,我会模拟客户端给服务器端发送消息的案例。


同时在这里我会使用模块化编程,其中有三个文件:client.cc、server.cc、common.hpp

client.cc用来模拟客户端,server.cc模拟服务器端,common.hpp来实现函数定义。

 common.hpp

在这个文件中,我实现了获取key值,创建共享内存,获取共享内存,连接共享内存,断开共享内存,删除共享内存的功能函数。 

#include <cstring>
#include <cstdlib>
#include <cstdio>#include <iostream>#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>#define PATHNAME "."
#define PROJ_ID  1
#define SIZE     4096//获取key值
key_t GetKey()
{key_t id = ftok(PATHNAME,PROJ_ID);if(id == -1){std::cerr<<"生成key值失败: "<<strerror(errno)<<std::endl;exit(1);}return id;
}//创建共享内存
int CreatShm(key_t key)
{int ret = shmget(key,SIZE,IPC_CREAT | IPC_EXCL | 0666);if(ret == -1){std::cerr<<"创建共享内存失败: "<<strerror(errno)<<std::endl;exit(2);}return ret;
}//获取共享内存
int GetShm(key_t key)
{int ret = shmget(key,SIZE,IPC_CREAT);if(ret == -1){std::cerr<<"获取共享内存失败: "<<strerror(errno)<<std::endl;exit(3);}return ret;
}//连接共享内存
void* AttachShm(int shmid)
{void* ret = shmat(shmid,nullptr,0);if((long long)ret == -1){std::cerr<<"连接共享内存失败: "<<strerror(errno)<<std::endl;exit(4);}return ret;
}//断开连接共享内存
void DisattchShm(void* start)
{int ret = shmdt(start);if(ret == -1){std::cerr<<"断开共享内存失败: "<<strerror(errno)<<std::endl;exit(5);}
}//删除共享内存
void DeleteShm(int shmid)
{int ret = shmctl(shmid,IPC_RMID,nullptr);if(ret == -1){std::cerr<<"删除共享内存失败: "<<strerror(errno)<<std::endl;exit(6);}
}
 server.cc

在这个源文件中,我通过服务器端来创建共享内存和删除共享内存,同时,每隔1秒中就将共享内存中的内容进行输出。 

#include "common.hpp"int main()
{//1.创建共享内存key_t key = GetKey();int id = CreatShm(key);//2.连接共享内存void* start = AttachShm(id);//3.使用int count = 10;while(count--){printf("client say: ");printf("%s\n",start);sleep(1);}//4.断开连接共享内存DisattchShm(start);//5.删除共享内存DeleteShm(id);return 0;
}
client.cc 

在这个源文件中,我不断的往共享内存中写入数据。 

#include "common.hpp"int main()
{//1.获取共享内存key_t key = GetKey();int id = GetShm(key);//1.连接共享内存void* start = AttachShm(id);//2.使用char message[1024] = "我正在给你发消息";int cnt = 0;int count = 10;while(count--){snprintf((char*)start,SIZE,"%s:%d",message,cnt++);sleep(1);}//3.断开连接共享内存DisattchShm(start);return 0;
}

至此,我们的程序就已经写完了,运行效果如下。

共享内存属性的获取

如果我们想要查找我们的共享内存的具体属性时,又该如何操作呢?

通过查看shmctl手册,我们可以看到手册中提到了struct shmid_ds和struct ipc_perm两个结构体中的信息,这些信息是操作系统暴露给我们以便于去查看的信息,那么这些信息怎么获取呢,使用shmctl函数即可。

获取共享内存信息代码如下:

        shmctl(id,IPC_STAT,&ds);printf("共享内存的大小:%d,最后一次连接时间:%d,key值:%d",ds.shm_segsz,ds.shm_atime,ds.shm_pe

四.管道和共享内存的区别 

管道实现的进程间通信和共享内存实现的进程间通信又有什么区别呢? 

共享内存的优点

共享内存是所有进程间通信方法中,速度最快的一个,它能大大的减少数据的拷贝次数。

共享内存的缺点

不能进行同步和互斥的操作,对数据没有任何保护。

对于共享内存的优点,这很好理解,但是缺点呢,该怎么理解?

这个缺点,我们通过现象来理解,将上面使用例子的代码中的client.cc源文件中,

将sleep(1),改为sleep(5),就能看到现象。

当server.cc对共享内存的数据进行输出的时候,它不像管道一样,会将共享内存中的内存清空,而是一直输出。 

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

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

相关文章

遗传算法与深度学习实战——生命模拟与进化论

遗传算法与深度学习实战——生命模拟与进化论 0. 前言1. 模拟进化1.1 代码实现1.2 代码改进 2. 达尔文进化论3. 自然选择和适者生存3.1 适者生存3.2 进化计算中的生物学 小结系列链接 0. 前言 生命模拟通过计算机模拟生物体的基本特征、遗传机制、环境互动等&#xff0c;试图模…

WPF 依赖属性 IsHitTestVisible

IsHitlTestVisible 仅影响本身的元素&#xff08;含内部包含的子元素&#xff09;&#xff0c;不影响父元素效果&#xff0c;且事件会传递到父元素。 Eg&#xff1a; 如父元素有click事件&#xff0c; 子元素设置了IsHitTestVisiblefalse&#xff0c; 当鼠标单击这个子元素时&…

Android 埋点信息分析——内存篇

源码基于&#xff1a;Android U 0. 前言 在前一篇《Android statsd 埋点简析》一文中简单剖析了Android 埋点采集、传输的框架&#xff0c;本文在其基础对埋点信息进行解析&#xff0c;来看下Android 中埋下的内存信息有哪些。 1. 通过代码剖析google 埋点内容 1.1 PROCESS_M…

BootStrap前端面试常见问题

在前端面试中&#xff0c;关于Bootstrap的问题通常围绕其基本概念、使用方式、特性以及实际应用等方面展开。以下是一些常见的问题及其详细解答&#xff1a; 1. Bootstrap是哪家公司研发的&#xff1f; 回答&#xff1a;Bootstrap是由Twitter的Mark Otto和Jacob Thornton合作…

脊髓损伤小伙伴的活力重启秘籍! 让我们一起动起来,拥抱不一样的精彩生活✨

Hey小伙伴们~&#x1f44b; 今天咱们来聊聊一个超级重要又温暖的话题——脊髓损伤后的锻炼大法来啦&#xff01;&#x1f389; 记住&#xff0c;无论遇到什么挑战&#xff0c;我们都要像打不死的小强一样&#xff0c;活力满满地面对每一天&#xff01;&#x1f4aa; 首先&#…

2024实验班选拔考试(热身赛)

比赛传送门 邀请码&#xff1a;2024wksyb A. 简单的数列问题 签到&#xff0c;记得开long long。 #include<bits/stdc.h> #define rep(i,a,b) for (int ia;i<b;i) #define per(i,a,b) for (int ia;i>b;--i) #define se second #define fi first #define endl …

【C#语音文字互转】.NET的TTS文本转语音合成

官方文档给出环境为Visual Studio 2017及以上&#xff1b;C#SDK为.NET4.8及以上 本文章环境介绍&#xff1a; Visual Studio 2022&#xff1b;C#SDK为.NET6.0 语音转文字请移步&#xff1a;【C#语音文字互转】C#语音转文字&#xff08;方法一&#xff09; 一. 启动 Visual Stud…

【OceanBase系列】—— OceanBase应急三板斧

作者&#xff1a; 花名&#xff1a;洪波&#xff0c; OceanBase 数据库解决方案架构师 目前随着OceanBase数据库越来越流行&#xff0c;社区已经有很多用户在生产环境使用了OceanBase&#xff0c;也有不少用户的核心业务用到了OceanBase数据库&#xff0c;在使用OceanBase数据库…

新址·新征程|美创科技北京中心喜迎乔迁

7月30日&#xff0c;北京暴雨倾城 连绵大雨和隆隆雷声 却像是在为一场新征程洗礼 这一天&#xff0c;我们迎来了重要的时刻 ——美创科技北京中心搬新家啦&#xff01; 新址&#xff1a;北京市海淀区庚坊国际大厦6层 喜迎新址&#xff0c;一场简单但喜气盈盈、温馨十足的乔…

【Python学习手册(第四版)】学习笔记16-函数基础

个人总结难免疏漏&#xff0c;请多包涵。更多内容请查看原文。本文以及学习笔记系列仅用于个人学习、研究交流。 本文主要介绍Python中函数的基本概念&#xff0c;作用域以及参数传递&#xff0c;函数语法以及def和return语句的操作&#xff0c;函数调用表达式的行为&#xff…

Delphi5实现DLL的编写、调用

效果图 显式跟隐式调用差不多的&#xff0c;就重新画了窗体&#xff0c;画的有点粗糙。 DLL文件 DLL文件是一种包含了可执行代码的库文件&#xff0c;但它不能独立运行&#xff0c;必须由其他程序&#xff08;如EXE文件&#xff09;显式或隐式地加载并调用。DLL文件通常用于实…

全国地铁路线及站点SHP数据

数据是GIS的血液&#xff01; 我们在《126M全球手机基站SHP数据分享》一文中&#xff0c;为你分享过全球手机基站分布数据。 现在再为你分享全国地铁轻轨路线与站点SHP数据&#xff0c;你可以在文末查看该数据的领取方法。 全球地铁路线及站点数据 截至2023年12月31日&…

LAVIS在Mac,M1PRO芯片下的安装实战

LAVIS在Mac,M1PRO芯片下的安装实战 契机 ⚙ 本地想装个图片理解的大模型&#xff0c;看了下blip2感觉比较合适&#xff0c;macos安装的时候有点坑需要注意下&#xff0c;但是最终也无法使用mps加速&#xff0c;比较蛋疼。这里记录下安装步骤。 安装 LAVIS/projects/blip2 a…

【研发日记】Matlab/Simulink技能解锁(十二)——Stateflow中的两种状态机嵌套对比

文章目录 前言 项目背景 两级状态机 函数状态机 分析和应用 总结 参考资料 前言 见《【研发日记】Matlab/Simulink技能解锁(七)——两种复数移相算法》 见《【研发日记】Matlab/Simulink技能解锁(八)——分布式仿真》 见《【研发日记】Matlab/Simulink技能解锁(九)——基…

数据结构(其四)--特殊矩阵的存储

目录 11.特殊矩阵的压缩存储 &#xff08;1&#xff09;.一维数组的储存结构 &#xff08;2&#xff09;.二维数组的存储结构 &#xff08;3&#xff09;.普通矩阵的存储 &#xff08;4&#xff09;.特殊矩阵的压缩存储 i.对称矩阵 ii.三角矩阵 iii.三对角矩阵 iiii.稀疏矩…

Java多商户新零售超市外卖商品系统

解锁新零售奥秘&#xff0c;多商户外卖超市商品系统大揭秘&#xff01; &#x1f31f; 开篇&#xff1a;新零售时代的浪潮 在这个日新月异的数字化时代&#xff0c;新零售已悄然成为商业变革的新风口。想象一下&#xff0c;足不出户就能逛遍全城商家&#xff0c;心仪商品一键…

力扣——238.移动零

题目 思路 利用双指针&#xff0c;先找到第一个为0的地方指向&#xff0c;指针2指向下一个&#xff0c;指针1之前是已经处理好的数据&#xff0c;指针2进行遍历&#xff0c;遇到非零则与指针1数据交换&#xff0c;然后指针1。 代码 class Solution { public:void moveZeroes(…

离心机转子适配器容量转换器的作用

离心机转子是离心机的核心部件&#xff0c;离心机中的所有系统都配置为保证转子在一定条件下安全运行。转子不仅直接影响分离效果&#xff0c;而且也是离心机技术中的主要承力部件&#xff0c;对离心机的安全性极为重要。 简而言之&#xff0c;离心机可分为两部分&#xff1a;…

Java Web——第二天

什么是JavaScript? JavaScript(简称:JS) 是一门跨平台、面向对象的脚本语言。是用来控制网页行为的&#xff0c;它能使网页可交互 JavaScript和Java是完全不同的语言&#xff0c;不论是概念还是设计。但是基础语法类似 JavaScript在1995年由 Brendan Eich 发明&#xff0c;…

【MySQL】索引概念解析

1.什么是索引&#xff1f; MySQL中的索引是一种数据结构&#xff0c;用于帮助MySQL数据库管理系统快速查询数据。索引的主要目的是提高数据检索的速度&#xff0c;减少数据库系统需要扫描的数据量。 优点&#xff1a; 索引可以极大的提高数据检索效率&#xff0c;降低数据库…