Linux:共享内存介绍(进程间通信)

共享内存

  • 共享内存原理介绍
  • 共享内存系统调用接口
    • shmget 创建共享内存段
    • ftok 生成唯一键 key
    • 开始创建共享内存
    • 指令 ipcs -m 查看共享内存
    • 指令 ipcrm -m 删除共享内存段
    • shmctl 控制创建的共享内存
      • 通过系统调用来删除共享内存
    • 共享内存权限问题
    • 关联/去关联共享内存
    • 封装处理

共享内存原理介绍

system V 共享内存是两个独立的进程之间完成通信的一个物理内存块。

直接看概念是带蒙的,下面来讲讲其中的缘由:

一个正在运行进程,由下面几个方面构成:进程的 PCB、进程地址空间页表。进程的数据通过页表映射到物理内存特定的位置。每个进程通过页表的映射,在物理内存的位置都会不一样。由此才能保证进程的独立性!

为了保证两个独立的进程能够进行通信,就要让这两个进程看到同一块空间。匿名管道和命名管道都是通过创建内存级的文件,来让两个独立的进程看到同一份资源。

那么共享内存又是如何操作的呢?

共享内存就是通过系统调用接口,在物理内存中开辟一块空间。在这里假设有两个独立的进程分别为:进程A 和 进程B。

前面提到,每个进程都有属于自己的进程地址空间。这个进程地址空间包含有:栈区、堆区、代码区… 在这里主要说一下在栈区 和 堆区之间的 共享区

为了让 进程A 和 进程B 能够通信,可以将创建的共享内存,通过页表的映射方式分别映射到 进程A 和 进程B 的共享区

在这里插入图片描述
此时,共享内存的地址就通过页表映射到进程地址空间的共享区,用户就可以通过共享区拿到对应的起始地址。这样就可以使 进程A 和进程B 看到同一份资源,进而就可以对两个独立的进程实现通信了

进程完成通信后,共享内存又是如何释放资源的呢?

对于创建共享内来说,释放空间是比较简单的。只需要找到进程,进程再通过页表映射到物理内存开辟的共享内存的地址进行修改,然后再对这块空间进行释放即可。

因此使用共享内存,通常这几步:创建共享内存、将独立的进程对共享内存进行关联通信、取消进程与共享内存的关联、释放共享内存

共享内存系统调用接口

shmget 创建共享内存段

创建共享内存需要用到系统调用接口:shmget

使用这个接口需要包含头文件:#include<sys/ipc.h>#include <sys/shm.h>

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

参数介绍:

  • key:用于唯一标识共享内存段(介绍另一个接口时,详细介绍这个参数)
  • size:创建共享内存段的大小,以字节为单位
  • shmflg:标志位,常用的标志位有两个 IPC_CREATIPC_EXCL
  • 返回值:成功创建共享内存返回共享内存的 描述符(后续介绍);失败返回 -1

IPC_CREAT 标志位表示:创建一个共享内存段。如果共享内存段不存在,创建一个新的共享内存段;如果共享内存段存在,返回这个共享内存段。

IPC_EXCL 通常要配合 IC_CREAT 一起使用,加上 IPC_EXCL 标志位表示:创建一个新的共享内存段。如果这个共享内存段不存在,创建一个新的共享内存段;如果这个共享内存段存在,直接出错返回。

下面再来介绍,第二个参数唯一键: key

ftok 生成唯一键 key

这个key值一般是通过 ftok 函数来生成的,ftok 函数内部会使用一些算法生成唯一的 key 值。

使用 ftok函数需要包含头文件:#include <sys/types.h>#include <sys/ipc.h>

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

参数介绍:

  • pathname:一个已存在的文件路径名。这个文件用于生成键,但其内容并不重要
  • proj_id:一个非负整数,作为项目标识符,与 pathname 一起用于生成键
  • 返回值:设置成功返回唯一键 key;失败返回 -1

进程A 和 进程B 可以通过共享内存进行通信。在操作系统中,存在这么多进程,是不是都是可以通过共享内存进行通信呢?

答案是可以的。一对对的进程都可以进行共享内存进行通信,这样将会开辟多个共享内存。针对如此多的共享内存,操作系统是需要对其进行做管理的。为了管理如此多的共享内存,操作系统会先对这些共享内存进行描述,创建属于共享内存结构体对象。假设创建:struct shm 结构体,每个结构体内都存储着共享内存的属性(指向共享内存的指针、共享内存大小、创建者、权限是多少、当前有多少进程正在使用这个共享内存等等),最重要的是这些属性中会存在着 struct shm* 指针,指向下一个共享内存的结构体属性。正是有了这些结构体指针,操作系统对共享内存的 删除 和 更改 就变成对 链表 的 增删查改!完成组织的操作。

因此,可以这样说:共享内存 == 共享内存的内核数据结构 + 真正在物理内存中开辟的内存空间

但是接下来就会出现这样的情况:
在这里插入图片描述

在物理内存中,存在着多个共享内存,操作系统为了管理这些共享内存会创建对应的 struct shm 结构体,形成链表的方式组织管理起来。面对如此多的共享内存,进程A 为了和 进程B 进行通信,就必须找到同一块资源。也就是要让 进程A 和 进程B 达成共识性的东西

这也是为什么要使用到 ftok 函数了。 进程A 和 进程B 约定,调用 ftok 函数一起传入一个两个进程都知道的文件路径,然后再传入一个项目标识符,最后生成一个唯一键 key

在这里假设 进程A 来创建一个共享内存(一般是谁创建谁来设置 唯一键 key),创建好一个共享内存后,进程A 将唯一键 key 设置到共享内存结构体的属性中。由于 进程A 和 进程B 是有约定的,所以在 进程B 使用 ftok 函数的时候,也会生成同一个 key 值。通过这个key值,进程A 和 进程B 就可以在诸多共享内存中找到 进程A 创建的共享内存,通信就可以开始。
在这里插入图片描述

因此,进程A 和 进程B 内部都要调用 ftok 函数来生成这样的唯一键,才能找到对应的共享内存

提了这么多理论上的点,下面来实操演示下:

  1. 创建三个文件:server.cc、 client.cc 、comm.hpp。
    comm.hpp 文件用来实现一些函数,为两个源文件提供使用:
#pragma once#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/ipc.h>//文件路径和项目id可以随意设置,但是要保证通信的进程之间都一样
#define PATHNAME "."  //文件路径
#define PROJID 0x6666 //项目id值#define NUM 64key_t getKey()
{key_t n = ftok(PATHNAME, PROJID);if(n == -1){std::cout << errno << ":" << strerror(errno) << std::endl;return errno;}return n;
}std::string toHex(int x)
{char buffer[NUM];snprintf(buffer, NUM - 1, "0x%x", x);//将整除x转换为16进制字符串显示return buffer;
}
  1. 实现server.cc 和 client.cc 文件,两个文件都包含 comm.hpp文件,分别调用 getKey 函数。检验不同进程之间生成的唯一键是否相同
//server.cc 文件
#include "comm.hpp"int main()
{   //创建唯一键keykey_t key = getKey();std::cout << "key:" << toHex(key) << std::endl;
}
//client.cc 文件
#include "comm.hpp"int main()
{   //创建唯一键keykey_t key = getKey();std::cout << "key:" << toHex(key) << std::endl;
}

编译运行,查看结果:
在这里插入图片描述

共识达成后,接下来就是创建共享内存的时候了。

接下来我会标注每一段代码都在哪个文件实现,防止老铁们迷路。整体代码会在篇尾全部给出。

开始创建共享内存

  1. 在 comm.hpp 文件增加两个函数 :createShm 函数用于创建共享内存、 getShm 函数用于获取共享内存

createShm 函数和 getShm 函数的实现逻辑其实都差不多,最大差异就是创建共享内存的时候是要开辟一个全新的共享内存段。获取函数只需要获取已经存在的共享内存即可。为了防止代码冗余,再实现多一个函数 createShmHelper ,用于createShm 函数和 getShm 函数 调用。

const int gsize = 4096;int createShmHelper(key_t key, int size, int flag)
{int shmid = shmget(key, size, flag);//创建共享内存段if(shmid == -1){std::cout << errno << strerror(errno) << std::endl;exit(2);}return shmid;
}int createShm(key_t key, int size)
{return = createShmHelper(key, gsize, IPC_CREAT|IPC_EXCL); //创建一块全新的共享内存段
}int getShm(key_t key, int size)
{return = createShmHelper(key, gsize, IPC_CREAT); //获取创建的共享内存块
}
  1. server.cc 文件创建共享内存段,client.cc 文件来获取共享内存段。
//servet.cc文件
#include "comm.hpp"int main()
{   //1. 定义唯一键keykey_t key = getKey();std::cout << "key:" << toHex(key) << std::endl;//2.创建共享内存段int shmid = createShm(key, gsize);std::cout << "shmid:" << toHex(shmid) << std::endl;return 0;
}
//client.cc文件
#include "comm.hpp"int main()
{   //1.创建唯一键keykey_t key = getKey();std::cout << "key:" << toHex(key) << std::endl;//2.获取共享内存块int shmid = getShm(key, gsize);std::cout << "shmid:" << toHex(shmid) << std::endl;return 0;
}
  1. 编译 server.cc 和 client.cc 文件,运行结果如下:

在这里插入图片描述

如果只是创建共享内存,那么还是比较简单的。但是,当我们再次运行 server 文件时会出现下面这种情况:

在这里插入图片描述

创建的共享内存还存在!共享内存再次被创建之后,共享内存的生命周期不会随着进程的退出而结束。它会一直被维护在操作系统中,直到操作系统退出。要对原来的共享内存先进行释放,才能继续创建共享内存

下面来介绍两个Linux 指令,分别是 :ipcs 查看共享内存段 、ipcrm 删除共享内存段。

指令 ipcs -m 查看共享内存

ipcs -m 指令通常用来查询共享内存段的信息:

ipcs -m

在这里插入图片描述
可以看到,标志位 32768 就是kunkun 这个用户通过 server 进程创建的共享内存段。此时的 server进程已经完全退出了,但是开辟共享内存依旧存在。

那么如何去释放这个空间呢?下面来介绍另一个指令:

指令 ipcrm -m 删除共享内存段

ipcrm -m 共享内存段标志位
  • -m 选项:删除用指定 标志位 的共享内存段

注意:-m 选项后面加上的是共享内存断的标志位,不是唯一键 key

下面将 32768 标志位的共享内存段释放:
在这里插入图片描述

除了指令的方式删除共享内存段,还可以通过系统调用的方式去删除共享内存段

shmctl 控制创建的共享内存

shmctl 函数是Linux中用于控制共享内存操作的一个系统调用函数,通过这个函数可以对共享内存进行 写、读、删除等操作。

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

参数介绍:

  1. shmid 参数:共享内存标识符
  2. cmd 参数:类似于位图,传入控制宏参数,指定shmctl函数对共享内存执行的具体操作
  3. buf 参数:指向 shmid_ds 结构体的指针,shmid_ds 结构体包含了共享内存的各种信息
  4. 返回值:成功返回0;失败返回 -1

这里主要来介绍如何对共享内存进行删除操作,其他像是 cmd 参数中诸多宏的用法,有感兴趣的老铁可以自行去使用使用。

通过系统调用来删除共享内存

删除共享内存,需要用到 shmctl 函数的第二个参数 cmd 的 IPC_RMID

在 comm.hpp 文件增加一个删除的函数,方便 server.cc 文件对共享内存进行删除的操作

//comm.hpp
void delShm(int shmid)
{//删除共享内存int n = shmctl(shmid, IPC_RMID, nullptr);assert(-1);(void)n;std::cout << "共享内存已经被删除\n";
}

对server.cc 文件内容进行修改:

//server.cc 文件
int main()
{   //1. 定义唯一键keykey_t key = getKey();std::cout << "key:" << toHex(key) << std::endl;//2.创建共享内存段int shmid = createShm(key, gsize);std::cout << "shmid:" << toHex(shmid) << std::endl;sleep(3);//停顿3秒模拟实现共享内存使用效果//删除共享内存delShm(shmid);return 0;
}

再来编译运行看看:

在这里插入图片描述

共享内存权限问题

前面介绍共享内存的操作都是如何创建、查看 和 删除共享内存,没有提到权限的问题。由于接下来要使用这块共享内存,对此要讲一下关于共享内存权限访问问题:

通过先前的方式创建一个共享内存,仔细观察一下这块共享内存的信息:
在这里插入图片描述
标红处表示:kunkun这个用户创建的的共享内存是没有读写权限的。尽管这块空间是kunkun用户创建,kunkun本身也没有读写权限,是不能使用这块空间的。但是创建这块共享内存的主人是可以对这块空间进行释放的

如何解决呢?

只需要在创建共享内存的时候给上对应的权限即可,修改 comm.hpp 文件:

int createShm(key_t key, int size)
{umask(0);//将权限掩码设置为0int shmid = createShmHelper(key, gsize, IPC_CREAT|IPC_EXCL| 0666); //创建一块新的共享内存段,赋予666的权限,0666表示八进制return shmid;
}

共享内存的权限设置犹如文件权限那般,受到权限掩码的影响(要设置的权限 & ~umask 得到最终权限)

编译运行看看结果:
在这里插入图片描述

创建的共享内存权限已经被设置为 666

关联/去关联共享内存

共享内存的创建、释放都有了,接下来的问题就是如何关联共享内存。

来介绍两个接口 shmatshmdt,这个接口是用来 关联去关联 一块共享内存空间。使用这两个接口需要包含头文件:#include<sys/types.h>#include<sys/shm.h>

先来介绍第一个接口 shmat ,用于关联共享内存:

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

at 是 attach 单词缩写,表示关联的意思。

参数作用:

  • shmid:要关联的共享内存
  • shmaddr:将共享内存挂接到进程地址空间的确定位置
  • shmflg:设置共享内存读写
  • 返回值:挂接成功后,返回共享内存映射在进程地址空间的起始地址(虚拟地址)

一般的,shmaddr 参数我们都不会自己去手动设置,直接传 nullptr 让OS去帮我们分配;shmflg 参数 是通过传标志位的方式来控制这块共享内存读写,例如 :SHM_RDONLY 宏表示只读。没有其他要求,在这里我们传 0 即可。

shmat 的系统接口的返回值 void* 很像C语言的 malloc 函数的返回值,我们可以像 malloc 那般去使用这个返回值。

再来介绍一下 shmdt 接口,将进程 去关联 共享内存:

int shmdt(const void *shmaddr);

dt 是单词 detach 缩写,表示分离的意思。

参数作用:

  • shmaddr:表示已经被挂载到进程地址空间的共享内存的起始地址
  • 返回值:去关联成功返回0,失败返回-1

下面来编写代码,将两个独立的进程 关联 和 去关联到同一块共享内存:

先在 comm.hpp 文件中去编写两个函数:attachShm关联共享内存、detachShm 去关联共享内存

//comm.hpp
//关联
char* attachShm(int shmid)
{char* start = (char*)shmat(shmid, nullptr, 0);if(start == nullptr){exit(errno);}return start;
}//去关联
void detachShm(char* start)
{int n = shmdt(start);if(n == -1) exit(errno);
}

通过这两个接口,server进程 和 client进程就可以关联到同一块共享内存进行通信了。下面来编写这两个进程的代码:

//server.cc
int main()
{   //1. 定义唯一键keykey_t key = getKey();std::cout << "key:" << toHex(key) << std::endl;//2.创建共享内存段int shmid = createShm(key, gsize);std::cout << "shmid:" << toHex(shmid) << std::endl;//将当前进程与共享内存关联起来char* start = attachShm(shmid);sleep(15);//停顿5秒模拟实现共享内存使用效果//将当前进程与共享内存去关联detachShm(start);//删除共享内存delShm(shmid);return 0;
}
//client.cc
int main()
{   //1.创建唯一键keykey_t key = getKey();std::cout << "key:" << toHex(key) << std::endl;//2.获取共享内存块int shmid = getShm(key, gsize);std::cout << "shmid:" << toHex(shmid) << std::endl;//关联共享内存char* start = attachShm(shmid);//使用共享内存sleep(10);//去关联detachShm(start);return 0;
}

先将 server进程 跑起来,创建共享内存,再将 client进程跑起来;由于,server进程运行的时间比 client进程 运行的时间长,client进程会先退出,通过先后顺序来观察进程关联共享内存的现象。

下面将两个进程跑起来:

  1. server创建共享内存,并且与这个共享内存关联起来:
    在这里插入图片描述2. client 进程运行起来关联到这块共享内存,此时的关联数变成2:
    在这里插入图片描述3. 经过一段时间后,client 进程会先去关联再退出,关联数会变成1:
    在这里插入图片描述4. 再后来就是 server进程去关联,关联数会变成1,最后 server进程释放这块共享内存:
    在这里插入图片描述

封装处理

创建共享内存、关联共享内存、去关联共享内存、删除共享内存,对于创建与删除 都是由 serve进程在处理,关联与去关联操作server进程与client 进程都是在重复操作。

对此可以将上述操作都封装成一个类,利用构造函数去创建与关联共享内存析构函数用来去关联与删除共享内存的操作。在这里要注意一下,创建与删除共享内存的操作只需要一个进程负责即可。

在 comm.hpp 文件中,定义两个宏,用于标识谁来创建与删除共享内存;实现 Init 类,去封装上面实现的 创建、删除、关联、去关联 函数接口:

#define SERVER 0
#define CLIENT 1class Init
{
public:Init(int type): _type(type){// 获取唯一键key_t key = getKey();std::cout << "key:" << toHex(key) << std::endl;if (_type == SERVER) // 符合SERVER创建{// 创建共享内存_shmid = createShm(key, gsize);std::cout << "进程创建共享内存成功,shmid:" << toHex(_shmid) << std::endl;}else{// 获取共享内存_shmid = getShm(key, gsize);std::cout << "进程已经获取到共享内存:shmid:" << toHex(_shmid) << std::endl;}// 关联共享内存_start = attachShm(_shmid);}char* getStart() { return _start; } //用于获取共享内存地址~Init(){// 去关联共享内存detachShm(_start);if (_type == SERVER){// 删除共享内存delShm(_shmid);}}
private:int _type;    // 用于标识谁来创建、删除共享内存int _shmid;   // 共享内存编号char *_start; // 共享内存地址(虚拟地址)
};

对此,我们在 server.cc 和 client.cc 文件中只需要创建 Init 对象就可以实现共享内存的创建、关联操作;Init 对象生命周期结束,调用析构函数完成对应的共享内存 去关联、删除操作。

有兴趣的小伙伴可以尝试利用下面的一个简单示例,来实现两个独立的进程通过共享内存完成一次通信过程:

client进程 向共享内存中逐个写入 1~26 个字符,server 从共享内存中获取client 写入的字符串并输出到终端。完成写的工作,client 进程写完后会先退出,server进程随后退出。

//server.cc
int main()
{Init init(SERVER);char *start = init.getStart();int n = 0;while(n <= 30){cout <<"client -> server# "<< start << endl; //读取字符串sleep(1);n++;}return 0;
}
//client.cc
int main()
{Init init(CLIENT);char *start = init.getStart();char c = 'A';while(c <= 'Z'){start[c - 'A'] = c;c++;start[c - 'A'] = '\0';sleep(1);}return 0;
}    

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

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

相关文章

数据采集项目结案报告

常州嘉爵机械机床采集项目案例 项目背景 常州市嘉爵机械配件厂 响应国家政策&#xff0c;申报智能车间&#xff0c;优化管理 车间设备包括&#xff1a;发那科机床、三菱机床。 项目需求调研分析 采集设备工艺参数&#xff0c;计算设备稼动率。 车间设备情况&#xff1a; …

HarmonyOS应用开发深度指南:从基础到高级实践

1. HarmonyOS开发概述 HarmonyOS是华为推出的分布式操作系统,旨在为不同设备提供统一的体验。它支持多种编程语言,包括ArkTS、JS、C/C++和Java。开发者需要了解HarmonyOS的分布式架构,包括Ability、Service、Data Ability等核心概念。 了解HarmonyOS的分布式架构:HarmonyO…

在视频号上面怎么卖货?只需要开一个店铺即可!具体怎么操作?

大家好&#xff0c;我是电商小V 最近这段时间在视频号上面卖货的热度可以说是非常大的&#xff0c;也是创业者常常提起的话题&#xff0c;大家之所以对视频号卖货非常感兴趣那是因为视频号和抖音处于一个赛道&#xff0c;也是朝着电商的方向发展&#xff0c; 所以说大家对于腾讯…

cmake使用(01)

顶层CMakeLists.txt cmake_minimum_required (VERSION 3.5)# 配置 交叉编译 放置在 project() 命令之前# /opt/fslc-wayland/2.5.2/sysroots/aarch64-fslc-linux/usr/bin/make: error # while loading shared libraries: libdl.so.2: cannot open shared object file: # No su…

c++ 模板类的泛化版本,只声明,不定义,可以么?可以

王建伟老师的课本&#xff0c;413 页讲解过这个情况。只要不创建泛化模板的对象即可。最近看源码时&#xff0c;也出现了这种情况&#xff0c;大师们也这么写。故简化逻辑&#xff0c;测试一下&#xff1a; 谢谢

家庭海外仓系统:做好标准化管理,小空间也能做出高收益

家庭海外仓凭借其运营模式灵活&#xff0c;合作成本低的独有特点&#xff0c;还是被很多跨境卖家所接受的。不过家庭海外仓的盈利也面临着一些问题。 首先&#xff0c;家庭海外仓的仓储空间有限&#xff0c;很难通过规模效应放大收益。家庭海外仓通常只能存储少量货物&#xf…

个人参与场外期权交易的最全指南

个人参与场外期权交易的最全指南 一、引言 场外期权作为金融市场中的一大亮点&#xff0c;为个人投资者提供了多样化的风险管理及投资策略选择。本文将详细探讨个人如何安全、有效地参与场外期权交易。 文章来源/&#xff1a;财智财经 二、理解场外期权 场外期权是双方通过协…

fastjson反序列化漏洞复现

靶机IP&#xff1a;192.168.253.134 攻击机IP&#xff1a;192.168.142.44 1、靶机环境搭建 靶机&#xff1a;http://caiyun.feixin.10086.cn/dl/095CteuquNKVq 提取密码:NNPD RCE&#xff1a;http://caiyun.feixin.10086.cn/dl/095CuIsJQOw14 提取密码:J2vd 靶机账号密码&…

OBS+nginx+nginx-http-flv-module实现阿里云的推流和拉流

背景&#xff1a;需要将球机视频推送到阿里云nginx&#xff0c;使用网页和移动端进行播放&#xff0c;以前视频格式为RTMP&#xff0c;但是在网页上面播放RTMP格式需要安装flash插件&#xff0c;chrome浏览器不给安装&#xff0c;调研后发现可以使用nginx的模块nginx-http-flv-…

工业物联网网关助推企业数字化转型,解决设备互联互通问题-天拓四方

随着科技的飞速发展&#xff0c;物联网技术已经渗透到工业领域的每一个角落。作为连接物理世界和数字世界的桥梁&#xff0c;工业物联网网关在推动企业数字化转型中发挥着至关重要的作用。数字化转型已经成为企业提升竞争力的必由之路&#xff0c;然而&#xff0c;在转型过程中…

升级最新版openssh-9.7p1及openssl-1.1.1h详细步骤及常见问题总结

近期因为openssh相继被漏洞扫描工具扫出存在漏洞&#xff0c;所以考虑升级操作系统中的openssh和openssl为最新版本&#xff0c;来避免漏洞风险。期间的升级过程及遇到的疑难问题&#xff0c;特此记录下来&#xff0c;供有需要的人参考。 本次目标是升级 openssh 为 9.7p1 版本…

计算机网络ppt和课后题总结(上)

试在下列条件下比较电路交换和分组交换。要传送的报文共 x(bit)。从源点到终点共经过 k 段链路&#xff0c;每段链路的传播时延为 d(s)&#xff0c;数据率为 b(b/s)。在电路交换时电路的建立时间为 s(s)。在分组交换时分组长度为 p(bit)&#xff0c;且各结点的排队等待时间可忽…

深入理解 Vue Router 及其 `router` 和 `route` 变量

深入理解 Vue Router 及其 router 和 route 变量 在使用 Vue.js 进行单页面应用开发时&#xff0c;Vue Router 是一个不可或缺的工具。它使得我们可以轻松地管理应用中的路由&#xff0c;提供了流畅的用户体验。然而&#xff0c;在实际开发中&#xff0c;许多开发者可能会混淆…

YOLOv8+PyQt5苹果叶病害检测(可以重新训练,yolov8模型,从图像、视频和摄像头三种路径识别检测)

效果视频&#xff1a;YOLOv8PyQt5苹果叶病害检测系统完整资源集合 资源包含可视化的苹果叶病害检测系统&#xff0c;基于最新的YOLOv8训练的苹果叶病害检测模型&#xff0c;和基于PyQt5制作的可视苹果叶病害系统&#xff0c;包含登陆页面和检测页面&#xff0c;该系统可自动检…

操作符:->

在一个指针变量指向一个结构体时常常会用->操作符来使用结构体内部的成员&#xff0c; 下面是我们没有使用指针时&#xff0c;如何调用结构体内的成员&#xff0c; #include<stdio.h>struct stu {char name[20];int age;char number[20]; };int main() {struct stu …

python实现——分类类型数据挖掘任务(图形识别分类任务)

分类类型数据挖掘任务 基于卷积神经网络&#xff08;CNN&#xff09;的岩石图像分类。有一岩石图片数据集&#xff0c;共300张岩石图片&#xff0c;图片尺寸224x224。岩石种类有砾岩&#xff08;Conglomerate&#xff09;、安山岩&#xff08;Andesite&#xff09;、花岗岩&am…

学会这14大招,30天涨粉两三千没问题!沈阳新媒体运营培训

很多小白在刚转入公司做新媒体时&#xff0c;基本都是从帮助公司运营账号开始的。但不同于个人号&#xff0c;一个企业本身是没有ip属性的&#xff0c;它的风格、调性等&#xff0c;都需要通过你的运营&#xff0c;让它变成一个活灵活现的、赋予独立个性人设的账号。 目前&…

Isaac Lab支持的强化学习框架介绍

在Isaac Lab中使用rl_games强化学习框架进行机械臂训练实验 python source/standalone/workflows/rl_games/train.py --taskIsaac-Franka-Cabinet-Direct-v0 使用 RL 代理进行培训 — Isaac Lab 文档 --- Training with an RL Agent — Isaac Lab documentation (isaac-sim.g…

能匠教育影视后期学员江颢:机电工程系的男大学生的意外收获!

江颢,一个热爱学习的大三学生。他是机电工程系的学生,因为女朋友喜欢拍照,经常让他剪辑视频,刚开始也只是用剪映马马虎虎剪辑,技术有限,总是剪不出想要的感觉和意境,女朋友也觉得不太满意。所以想提升下剪辑能力,后面,偶然发现能匠教育这个影视后期学习和接单信息。他一开始只是…

重学java 60.IO流 字节流 ① File类

明年此日青云去&#xff0c;却笑人间举子忙 —— 24.6.4 知识回顾 1 .HashMap a.特点:无序,无索引,key唯一,线程不安全,可以存null键null值 b.数据结构:哈希表 c.方法:put remove get keyset entryset values containsKey 2.LinkedHashMap : a.特点:有…