【Linux】进程间通信详解

news2025/1/13 15:48:57

 环境:centos7.6,腾讯云服务器
Linux文章都放在了专栏:【Linux】欢迎支持订阅


进程间通信介绍

什么是进程间通信?

进程间通信(Interprocess communication,简称IPC)就是让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。

通信的目的

数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

通信的本质

  • 由于进程具有独立性,所以这也就增加了通信的成本。
  • 让两个不同的进程实现通信,前提条件就是让两个进程看到同一份资源。因此,任何通信的手段都是先让不同的进程看到同一份资源,然后一方写入,一方读取,实现通信。

通信的发展与分类

管道 :匿名管道、命名管道
System V进程间通信: System V共享内存、System V消息队列、System V信号量
POSIX进程间通信:消息队列 、共享内存 、信号量 、互斥量 、条件变量 、读写锁

本次章节讲着重讲解管道通信以及System V共享内存,其余有兴趣自行了解即可。

管道通信

管道概念

管道是unix中最古老的进程间通信的方式,把一个程序的输出直接连接到另一个程序的输入。既然是通信,那么一定遵循通信的原理,即:使不同进程看到同一份资源,一方写入一方读取。如下所示:

在这里,whoami与cat运行时为两个不同的进程。whoami用来查看当前用户,whoami进程通过stdout,讲数据打印到“管道”。然后cat进程通过stdin,进行从管道中读取,并对stdout重定向,输出到文件log.txt中。

匿名管道通信

通信原理

匿名管道通信的本质也是使不同进程看到同一份资源,匿名管道用于具有血缘关系的进程进行通信,一般用于父子进程。在学习之前,我们要知道,父进程fork后,子进程继承父进程的相关数据结构,不会继承父进程曾经打开的文件对象。如下所示:

 而匿名管道的原理,便是根据这种父子继承的特点,使父子进程看到同一份“被打开的文件”,从而完成通信的前提条件,只不过这个“同一份被打开的文件”,是由OS来维护,只存在于内存。属于内存级别的文件。如下所示:

 pipe函数创建匿名管道

#include<unistd.h>

int pipe(int pipefd[2]);

参数pipefd[2]为一个输出型参数,是一个数组,pipefd[0] :表示管道的读端、pipefd[1]:表示管道的写端。调用成功返回0,失败返回-1.

通信步骤

  • 1、父进程调用pipe函数创建管道 
  • 2、fork创建子进程
  • 3、父关闭写,子关闭读。实现子进程写入,父进程读取

 代码示例

子进程向父进程写入“hello  i am your child !”;

#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<wait.h>
#include<cstring>
using namespace std;

int main()
{
    int pipefd[2]={0};
    //1、创建管道文件
    int n=pipe(pipefd);
    if(n == -1)
    {
        cout<<"mkpipe false"<<endl;
        return 1;
    }
    //2、创建子进程
    pid_t id=fork();
    if(id == 0)
    {
        //child
        //3、子进程关闭读,父进程关闭写
        close(pipefd[0]);//pipefd[0] 为读,pipefd[1] 为写
        
        //4、子进程写入数据、父进程读取数据
        const char* str="hello i am your child!";
        int wn=write(pipefd[1],str,strlen(str));
        if(wn<0)
        {
            cout<<"child write false"<<endl;
            return 2;
        }

    }

    //father
    //3、子进程关闭读,父进程关闭写
    close(pipefd[1]);
    //4.从读端读取数据
    char buffer[30];
    int rn=read(pipefd[0],buffer,sizeof(buffer)-1);
    if(rn >0)
    {
        buffer[rn]='\0';
        cout<<"我是父进程,收到子进程的信息:"<<buffer<<endl;
    }
    waitpid(id,NULL,0);
    return 0;
}

管道通信四大现象

  • 1、如果读端将数据读取完毕后,写端不进行写入,那么读端将会一直等待,直到写端写入数据

现象如下:

 

  • 2、如果写端将管道写满了,那么就不能继续写入数据了,除非读端将管道数据读取后,才能继续写。

  •  3、将写端关闭,那么读端读完管道中的数据后就会读到文件结尾,也就是说,此时read函数会返回0

 

  • 4、将读端关闭,写端进行写入,但是此时的写入就毫无意义,而OS不会为了维护无意义的写入,此时OS会发送13号信号,终止写端进程。

 

管道通信特点

  1. 管道的本质就是文件,所以管道的生命周期随进程,因为fd文件描述符的生命周期 随进程
  2. 管道通信是一种单向通信,如果要实现双向,则需要借助两个管道
  3. 匿名管道通信通常用来进行有血缘关系的进程,常用于父子进程。(因为我们用pipe函数创建管道,我们不知道该管道文件的名称,所以叫匿名管道通信)
  4. 道通信中,数据写入的次数与读取的次数不一定严格匹配。(管道自带同步与互斥机制,同步与互斥,将放在后面章节讲解)

命名管道通信

命名管道通信原理

实际上与匿名管道原理相同,创建一个管道文件,然后让不同的进程分别以读和写的方式打开,然后实现通信。不同的是,上方所讲的“匿名管道”只能用来具有“血缘”关系的进程通信,而本次所讲的命名管道,既可以实现“血缘进程”,也可以实现互不相关的进程进行通信

命名管道的创建

第一种方式:命令行指令:mkfifo filename创建名称为filename的管道文件

如下图所示,创建了一个名为pipe的管道文件

既然创建了该管道文件,我们也可以在命令行输入指令,利用管道进行通信,如下所示: 我让两个互不相关的进程,一个进程每秒循环往管道进行打印数据,另一个cat进程则进行读取数据。 

 当然,这种命名管道也遵循管道通信的四大现象,比如假如我将读端关闭,那么此时写端进程就成了无意义的写入,OS不会进行维护,就会就发送13号信号,终止写端进程。如下所示:

第二种方式:系统调用函数:mkfifo

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

其中,参数pathname表示命名管道文件的名称(默认在当前路径下创建),mode表示文件权限。(注意:文件实际的权限受权限掩码umask的影响,在前面权限相关章节已做讲解。umask()函数可以设置权限掩码)。

管道文件创建成功,返回0,失败返回-1.

如下所示

 如果比较细心的话,我们会发现,该管道文件的大小为0,事实上,进程间通信中往管道中写入数据,都是在内存中完成的,OS并不会把数据刷新到磁盘文件中,因为这样做并没有意义,所以即使我们不断地往管道文件中写入数据,我们查看磁盘下该文件的大小时,始终为0。

命名管道的删除

第一种方式:直接在命令行rm指令删除即可

第二种方式:系统调用函数:unlink()函数

#include <unistd.h>
int unlink(const char *pathname);

命名管道打开规则

如果当前打开操作是为读而打开 FIFO
O_NONBLOCK disable :阻塞直到有相应进程为写而打开该 FIFO
O_NONBLOCK enable :立刻返回成功
如果当前打开操作是为写而打开 FIFO
O_NONBLOCK disable :阻塞直到有相应进程为读而打开该 FIFO
O_NONBLOCK enable :立刻返回失败,错误码为 ENXIO
这是什么意思呢?举个例子,A进程以读的方式打开命名管道,那么此时A进程就会进入阻塞,直到B进程以写的方式打开该管道文件,A才继续运行。

 同样,假如A以写的方式打开命名管道,此时A也会进入阻塞,直到B进程以读的方式打开管道文件,A才继续运行。

通信演示(文件拷贝) 

实际上,进程间通信,不仅仅可以实现数据的传输,还可以让一个进程给另一个进程发送指令,使之根据不同指令执行不同方法。同样,也可以比如说让另一个进程实现文件拷贝。如下:

我要创建两个进程server进程与client进程,client进程负责将log.txt文件的数据写入到管道,而server进程则创建一个newlog.txt的文件,然后从管道中将数据读到newlog.txt中,从而实现文件拷贝。

log.txt文件的内容

 通信原理:

 通信实现

为了确保两个进程打开的管道文件名称不会出错,这里我们自定义一个头文件,并使两个进程共用。如下:

#include<iostream>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<cerrno>
#include<cstring>
#include<cassert>
using namespace std;

#define PIPENAME "./mypipe"

 接下来是server.cc的实现,如下:

#include"comm.h"

int main()
{
    umask(0000);
    //1、创建管道文件
    int n=mkfifo(PIPENAME,0664);
    if(n== -1)
    {
        cerr<<"mkfalse:"<<errno<<strerror(errno)<<endl;
        return 1;  
    }
    cout<<"mkpipe success"<<endl;
    //2、读方式打开管道文件
    int rfd=open(PIPENAME,O_RDONLY);
    if(rfd<0)
    {
        cerr<<"open false:"<<errno<<strerror(errno)<<endl;
        return 2;  
    }
    cout<<"open pipe success"<<endl;

    //3、以写的方式打开newlog.txt文件,如果不存在则创建
    int wfd=open("newlog.txt",O_WRONLY | O_CREAT,0666);
    assert(wfd>=0);
    //4、读取管道数据,并写入newlog.txt
    char buf[128];
    while(1)
    {
        //4.1每次读取前将buf清空
        buf[0]='\0';
        size_t s=read(rfd,buf,sizeof(buf)-1);//(从管道读取)
        if(s>0)
        {
            buf[s]='\0';
            //将数据写入newlog.txt
            write(wfd,buf,strlen(buf));
        }
        else if(s == 0)
        {
            cout<<"数据读取完毕,server读端关闭"<<endl;
            close(rfd);
            break;
        }
        else
        {
            cout<<"数据读取出错"<<endl;
            return -1;
        }
    }
    cout<<"结束通信,完成拷贝"<<endl;
    close(wfd);
    unlink(PIPENAME);//删除管道文件
    return 0;

}

client.cc的实现:

#include"comm.h"

int main()
{
    //1、以写的方式打开管道文件
    int wfd=open(PIPENAME,O_WRONLY);
    if(wfd<0)
    {
         cerr<<"open false:"<<errno<<strerror(errno)<<endl;
        return 2;  
    }
    cout<<"open pipe success"<<endl;
    //2、以读的方式打开log.txt文件
    int fd=open("log.txt",O_RDONLY);
    assert(fd>=0);
    //3、读取文件数据,并写入管道
    char buf[128];
    while(1)
    {
        buf[0]='\0';//置空
        size_t s=read(fd,buf,sizeof(buf)-1);//读取文件数据
        if(s>0)
        {
            buf[s]='\0';
            //写入管道
            write(wfd,buf,strlen(buf));   
        }
        else if(s == 0)
        {
            cout<<"读取文件结尾"<<endl;
            break;
        }
        else{
            cout<<"读取文件数据出错"<<endl;
            return -1;
        }
    }
    cout<<"client写端关闭"<<endl;
    close(wfd);
    close(fd);
    
    return 0;
}

我们来观看运行结果:

在这里,举这个例子的原因在于,假如我们将这里的管道想象成网络,将client想象成我们的Windows Xshell,将server想象成centos服务器,那么此时我们实现的就是文件的上传功能,反过来,我们实现的就是文件的下载功能。

 命名管道与匿名管道的区别

匿名管道由pipe函数创建并打开,适用于具有血缘关系的进程,且该管道文件没有名称

命名管道由mkfifo函数创建,由open函数打开,可以自定义命名,并且可以实现任意进程之间的通信。但两者通信的底层实际并无差别

system V 共享内存

共享内存通信原理

不管是什么方式,实现进程间通信的前提都是让不同的进程看到同一份资源。共享内存也不例外。而共享内存的原理则是在内存中申请一块空间,然后通过各进程对应的页表映射给不同的进程,此时两个进程,就会看到同一份资源。而在内存中申请的这块空间,就叫共享内存。如下图所示:

 共享内存数据结构

由于系统中可能存在大量的进程通信,这也就意味着系统中会存在大量的共享内存。因此,OS一定会对其进行管理,那么OS如何管理呢?六个字:先描述,再组织。即OS会给每一个共享内存,开辟一个结构体对象,该结构体内包含了共享内存的所有属性等相关信息,然后通过某种数据结构(链表等),将所有的结构体链接起来。该结构体名为:struct shmid_ds,如下:

struct shmid_ds {
 struct ipc_perm shm_perm; /* operation perms */
 int shm_segsz; /* size of segment (bytes) */
 __kernel_time_t shm_atime; /* last attach time */
 __kernel_time_t shm_dtime; /* last detach time */
 __kernel_time_t shm_ctime; /* last change time */
 __kernel_ipc_pid_t shm_cpid; /* pid of creator */
 __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
 unsigned short shm_nattch; /* no. of current attaches */
 unsigned short shm_unused; /* compatibility */
 void *shm_unused2; /* ditto - used by DIPC */
 void *shm_unused3; /* unused */
};

那么此时就出现了一个问题,既然系统中存在那么多共享内存,那么又是如何知道各个进程对应的是哪个呢?实际上每个共享内存在申请时,都会有一个key值,这个值就是为了标识共享内存的唯一性。两个进程通过该key值,就会找到各自对应的“共享资源”。我们发现,shmid_ds的第一个成员,也是一个结构体变量,而key就存在该结构体shm_perm中。如下:

struct ipc_perm
 10 {
 11   __kernel_key_t  key;//这就是用来标识共享内存的key
 12   __kernel_uid_t  uid;
 13   __kernel_gid_t  gid;
 14   __kernel_uid_t  cuid;
 15   __kernel_gid_t  cgid;
 16   __kernel_mode_t mode;
 17   unsigned short  seq;
 18 };

(shmid_ds存在于/usr/include/linux/shm.h下、shm_perm存在于/usr/include/linux/ipc.h下)

通信步骤

整体来说,分为:1、创建共享内存。2、将共享内存与进程进行关联。3、进行通信。4、取消关联。5、释放共享内存。

共享内存的创建

系统调用函数:shmget

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

对于第一个参数,就是我们上面提到的key,用来确保该共享内存是唯一的。第二个参数就是该共享内存的大小。第三个参数表示创建共享内存的方式选项。当共享内存创建成功时,会返回共享内存的id值(identifier),失败返回-1。

对于key值,我们不是随便传的,因为要确保该key值是唯一存在的,不会与系统内别的key值发生冲突,因此,我们需要用到函数:ftok

ftok函数

#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);

该函数表示:根据传入的已存在的路径名pathname,以及一个整数标识符proj_id,来生成一个发生冲突概率极低的key值。

这里需要注意的是:在进行通信的进程,必须保证传入的pathname以及proj_id是同一个,这样才能生成同一个key,从而找到同一份共享资源,实现通信。

关于shmget的第三个参数常见组合

一般常用组合:

组合代表含义
IPC_CREAT创建一个共享内存,如果该共享内存已存在,则获取已存在的共享内存的id,若不存在,则创建一个新的共享内存,并返回该共享内存的id(该选项无法保证一定是最新创建的共享内存)
IPC_CREAT | IPC_EXCL创建一个共享内存,若该共享内存不存在,则创建一个新共享内存并返回其id,若已存在,则立马保存返回。(该组合保证了创建出来的共享内存,一定是最新的)

当然,我们也可以在第三个参数后面加上权限,比如IPC_CREAT | 0666,用来控制创建出来的共享内存的权限。

代码演示

接下来,我们来创建一个共享内存,并打印其对应的key值以及id值。

 运行结果如下:

 我们可以通过命令行指令来查看关于共享内存的相关信息

IPC命令行指令查看信息

指令作用
ipcs列出消息队列、共享内存、信号量相关信息
ipcs -m只列出共享内存相关信息
ipcrm -m shmid删除id值为shmid的共享内存

 我们发现与我们打印出来的结果一致(key转为16进制,就是这里显示的key)

另外:perms就是我们设置的权限、bytes为大小,nattch表示关联的进程数(此时还未关联,所以为0),status表示该共享内存的状态。

共享内存的释放

我们发现,当进程结束后,共享内存依然存在,也就意味着:共享内存的生命周期随OS,不随进程。因此我们还需要进行删除,如果不进行删除,再次运行上面的代码就会报错(IPC_CREAT|IPC_EXCL)

 释放共享内存有两种方式,一种是上面讲的用命令行指令:ipcrm -m shmid,来进行删除。这里就不进行演示了,很简单。零另外一种就是用系统调用函数:shmctl

shmctl

#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

第一个参数为共享内存对应的id值,cmd表示具体的函数控制操作,第三个参数为输出型参数,即把共享内存的相关属性,输出到结构体buf中。

该函数调用成功返回0,失败返回-1

对于cmd,常见选项如下:

cmd值代表含义
IPC_RMID删除这块共享内存
IPC_STAT

得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中

IPC_SET改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内

因此,我们可以在末尾加上如下代码:

 关联与去关联

关联操作也就是将内存中的共享内存,映射到进程对应的地址空间。该操作需使用函数shmat

shmat函数

 #include <sys/types.h>
 #include <sys/shm.h>

 void *shmat(int shmid, const void *shmaddr, int shmflg);

首先,第一个参数就是共享内存的id值,第二个参数表示:指定一个区域,将共享内存映射到此处,一般我们设置为nullptr,设置为nulptr表示让OS自己选择区域映射。第三个参数表示关联共享内存时设置的一些属性。一般如下:

shmflg参数选项代表含义
SHM_RDONLY只读模式,只可以进行读取操作
0读写都可

当关联成功时,会返回映射到进程地址空间的起始地址,失败返回-1。

shmdt函数

该函数用来去关联。

 #include <sys/types.h>
 #include <sys/shm.h>

 int shmdt(const void *shmaddr);

这里的参数就是将要去关联的共享内存映射到进程地址空间的起始地址,也就是shmat函数的返回值。去关联成功时,返回0,失败返回-1.

代码演示

这里,我们通过管擦ipcs -m,可以看到nettch对应的值先是由0变成1,再由1变成0。

用共享内存实现客户端与服务端进程通信

既然了解了共享内存实现的原理,接下来我们将用此实现进程间通信:客户端不断地往共享内存写数据,服务端不断地从共享内存读数据,并将数据打印到显示器。在这里,我们将用一个类,来对实现的方法进行封装。基本框架如下所示:

接下来将对这些函数进行实现,如下所示:

将这些主要函数实现后,我们只需要在构造时,根据不同的type,从而执行不同的方法即可,如下所示:

  

 至此,一个共享内存的类就实现完毕,接下来只需要进行通信即可,客户端不断地写,服务端不断地读。如下所示:

 由于此时我们还没有运行客户端,进行写入,所以此时服务端读取永远是空信息。如下:

 因此,我们要再完善客户端,实现写入功能。这里简单的写一个,如下:

 

此时当我们两个进程都运行时,一方写入,一方读取,实现通信:

 当然,这里只是简单的实现通信,我们还可以在共享内存中加入管道,用来控制进程,当客户端写入完成后,服务端再进行读取。这里由于篇幅原因就不进行演示,可自行实验,有问题也可进行探讨。

管道VS共享内存

我们知道,管道通信的实现,需要借助于read、write函数,而该函数在前文我们讲过了已经,本质上其实是一个拷贝函数,如下图所示,如果要实现文件数据的传输,至少需要进行4次拷贝,而共享内存则只需两次即可,如下:

 因此:管道通信是进程间通信中最快的一种通信方式,但是,共享内存并不提供任何保护机制,即共享内存不会自带同步与互斥,而管道则自带同步与互斥。各有利弊。(同步与互斥会在后面章节讲解)


end.

生活原本沉闷,但跑起来就会有风!🌹

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

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

相关文章

【Apache 网页优化】

文章目录 一、Apahce 网页优化1、网页压缩2、网页缓存 二、Apachen的安全优化1、隐藏版本信息2、Apache 防盗链 一、Apahce 网页优化 1、网页压缩 1.检查是否安装 mod_deflate 模块 apachectl -t -D DUMP_MODULES | grep "deflate"2.如果没有安装mod_deflate 模块…

Java基础 流程控制语句

顺序结构 顺序结构就是程序从上到下逐行地执行。表达式语句都是顺序执行的。并且上一 行对某个变量的修改对下一行会产生影响。 public class StatementTest{public static void main(String[] args){int x 1;int y 2; System.out.println("x " x);System.out.p…

非科班自学一年心得,学弟学妹别瞎学了

大家好&#xff0c;我是帅地。 前两天我发了一篇亲学弟自学一年拿大厂 offer 的文章&#xff1a;非科班&#xff0c;帅地亲学弟自学一年拿到大厂offer了 不过那一篇只写了自己转行开发岗的心里变化&#xff0c; 这两天学弟又在知识星球发了一篇关于找工作的万字长文 说实话&…

ISO21434 项目网络安全管理

目录 一、概述 二、目标 三、输入 3.1 先决条件 3.2 进一步支持信息 四、要求和建议 4.1 网络安全责任 4.2 网络安全规划 4.3 裁剪 4.4 重用 4.5 非上下文组件 4.6 现成组件 4.7 网络安全案例&#xff08;Cybersecurity case&#xff09; 4.8 网络安全评估&#…

【惊叹】AI进步的速度太快,我们赶不上了?

文章目录 前言一、LoRA二、QLoRA1、环境准备2、推理就是直接 跑shscripts/generate.sh。3、前面的环境和数据都没问题了&#xff0c;运行scripts/generate.sh。 总结 前言 AI 领域的技术&#xff0c;真是隔一段时间就有一个新突破&#xff01; 全民都能训练大模型的时代&…

TypeScript算法题实战——剑指 Offer篇(3)

随着TypeScript的流行&#xff0c;越来越多的开发者开始使用TypeScript来解决算法问题。 在本文中&#xff0c;我们将使用TypeScript来解决剑指offer的算法题。这些问题涵盖了各种各样的主题&#xff0c;包括数组、字符串、链表、树、排序和搜索等。我们将使用TypeScript的强类…

【MySQL高级篇笔记 (中-索引的数据结构) 】

此笔记为尚硅谷MySQL高级篇部分内容 目录 一、索引及其优缺点 1、索引概述 2、优点 3、缺点 二、InnoDB中索引的推演 1、设计索引 1.一个简单的索引设计方案 2.InnoDB中的索引方案 2、常见索引概念 1. 聚簇索引 2. 二级索引&#xff08;辅助索引、非聚簇索引&#…

Node.js详解(一):基础知识

文章目录 一、Node.js介绍二、Node.js的优势三、Node.js的特点1、V8虚拟机2、事件驱动3、异步、非堵塞I/O 四、NodeJS带来的对系统瓶颈的解决方案1. 并发连接2. I/O阻塞 五、NodeJS的优缺点1、优点&#xff1a;2、缺点&#xff1a; 六、适合NodeJS的场景1、RESTful API2、统一W…

VMware、Ubuntu安装以及虚拟机复制粘贴问题

安装VMware 下载阿里云链接&#xff08;16 pro&#xff09;&#xff1a;VMware https://www.aliyundrive.com/s/ot9dhPNdSwC 安装&#xff1a;选一下安装地址&#xff0c;一直下一步即可。&#xff08;可能会要求重启电脑&#xff0c;重启即可&#xff09; 然后点击“许可证”…

Java 高级应用-多线程-(四)FutureTask的介绍及使用

Java多线程之FutureTask的介绍及使用 FutureTask属于java.util.concurrent 包&#xff1b;FutureTask表示可取消的异步计算。FutureTask类提供了一个Future的基本实现 &#xff0c;具有启动和取消计算的方法&#xff0c;查询计算是否完整&#xff0c;并检索计算结果。结果只能…

Camtasia2023试用版新功能介绍

Camtasia 2023在易用性更进一步&#xff0c;再一次降低了制作精美视频的门槛&#xff0c;下面看一看&#xff0c;Camtasia 2023有哪些的新功能&#xff01;包括影像、音效、鼠标移动轨迹、解说声音等等内容的录制&#xff0c;并且软件还可以提供即时播放和编辑压缩的功能&#…

如何监控电动车充电桩能耗?

一 背景 随着新能源汽车的快速发展&#xff0c;像特斯拉、BYD、蔚来、小鹏和理想等品牌的电动汽车在我们的日常生活中越来越多了&#xff0c;可见电动汽车如今已逐渐被我们所认可了。同汽油车需要加油一样&#xff0c;电动汽车需要充电&#xff0c;如此一来&#xff0c;电动汽…

第14届蓝桥杯省赛真题剖析-2023年5月7日Scratch编程中级组

[导读]&#xff1a;超平老师的《Scratch蓝桥杯真题解析100讲》已经全部完成&#xff0c;后续会不定期解读蓝桥杯真题&#xff0c;这是Scratch蓝桥杯真题解析第142讲。 第14届蓝桥杯Scratch省赛真题&#xff0c;这是2023年5月7日举办的省赛中级组试题&#xff0c;比赛仍然采取线…

洗地机充电底座语音芯片选型?NV040DS语音芯片

一、洗地机语音提示功能的价值 洗地机充电底座加入语音提示功能&#xff0c;主要是为了提高洗地机的智能化程度和使用便利性&#xff01; 1. 提高使用效率&#xff1a;底座语音提示充电状态可以使用户更方便地掌握底座电量和洗地机的使用情况&#xff0c;从而更快捷地对底座进…

ProtoBuf 语法(二)

系列文章 ProtoBuf 语法&#xff08;一&#xff09; ProtoBuf 语法&#xff08;三&#xff09; 文章目录 八、更新消息8.1 更新规则8.2 reserved 保留字段8.3 验证错误删除字段造成的数据损坏8.4 未知字段及其获取方法8.5 验证未知字段 八、更新消息 8.1 更新规则 如果现有的…

mysql中的count(1)、count(*)、count(id)哪个更快?

今天和大家聊一下mysql中的count()方法 我们日常开发中&#xff0c;经常会用到count()命令&#xff0c;有的人用count(*)&#xff0c;有的人用count(1)&#xff0c;还有的人用count(id)&#xff0c;那么这几种写法都有什么区别呢&#xff1f;哪种方法效率更高呢&#xff1f;今…

LangChain 查询使用指「北」

一只鹦鹉加上一根链条&#xff0c;组成了时下最流行的 AI 话题热门榜选手——LangChain。 LangChain 是一种 AI 代理工具&#xff0c;可以为以 ChatGPT 为代表的额大语言模型&#xff08;LLM&#xff09;增添更多功能。此外&#xff0c;LangChain 还具备 token 和上下文管理功能…

提高水泵可靠度与生产效率:故障诊断系统实践解析

水泵作为工厂生产线中不可或缺的设备之一&#xff0c;其正常运行对于生产效率和设备可靠性至关重要。然而&#xff0c;水泵故障可能会导致设备停机和生产中断&#xff0c;给企业带来巨大损失。 图.水泵&#xff08;iStock&#xff09; 为了解决这一问题&#xff0c;水泵健康管理…

机器人中的数值优化(四)—— 线搜索求步长(附程序实现)

本系列文章主要是我在学习《数值优化》过程中的一些笔记和相关思考&#xff0c;主要的学习资料是深蓝学院的课程《机器人中的数值优化》和高立编著的《数值最优化方法》等&#xff0c;本系列文章篇数较多&#xff0c;不定期更新&#xff0c;上半部分介绍无约束优化&#xff0c;…

CentOS7下更改、移动mysql数据存储的位置 附os 错误码13问题

1、看一下目前mysql数据存储的位 select datadir;从这个结果我们可以看出&#xff0c;当前mysql的datadir是在/var/lib/mysql 目录里的 2、关掉连接mysql的各种程序服务 systemctl stop mysqld 或 service mysql stop 3、在期望的位置创建目录&#xff08;datadir的新位置&a…