【Linux】进程间通信-> 共享内存

共享内存原理

在C语言/C++中,malloc也可以在物理内存申请空间,将申请的物理内存空间通过页表映射到进程地址空间,将内存空间的起始地址(虚拟地址)返回,进而进程可以使用虚拟地址通过页表映射到物理内存的方式访问到申请的内存空间。

那么malloc和共享内存有什么区别呢?

区别就是malloc出来的空间,是进程在自己的堆上申请的空间,没有和其他进程建立映射关系,不能被其他进程共享!

  • 共享内存是用来进程间通信的!
  • 共享内存是一种通信方式,所有想通信的进程都可以使用!
  • OS中一定会可能存在很多的共享内存!

共享内存概念

共享内存是最快的 IPC 方式之一。允许多个进程共享同一块物理内存区域。进程可以通过将共享内存区域映射到自己的地址空间来访问这块内存。这样,多个进程可以直接在共享内存区域中读写数据,大大提高了通信效率。

共享内存函数

shmget函数

  • 创建共享内

成功,返回共享内存的标识符;失败,返回-1。

shmflg

int shmflg 权限标志

常用选项:

  1. IPC_CREAT:如果shm不存在,则创建;存在,则获取。
  2. IPC_EXCL:不能单独使用。

IPC_CREAT | IPC_EXCL:如果shm不存在创建,则创建;存在,则出错返回。(对用户来讲,如果创建成功,一定是一个新的shm)

进程要进行通信,首先要让不同的进程看到同一份资源。如何保证两个进程看到的就是同一份共享内存呢?

key

通过接口int shmget(key_t key,size_t size,int shmflg),第二个参数key来保证。

key_t类型其实就是一个32位的整数。

key是什么不重要,重要的是它能进行唯一性标识一个共享内存段

如何形成一个key呢?通过ftok函数来形成一个key。

ftok函数
  • 将文件路径名和项目标识符转换为System V IPC(进程间通信)key(键)

成功,返回成功创建的key;失败,返回-1。

如果进行通信的两个进程使用ftok函数,传递的两个参数是一样的,那么形成的key值也是同样的,就能找到同一个共享内存。一个进程通过key进行创建,另一个进程通过key获取。

makefile

.PHONY:all
all:shm_client shm_server

shm_client:shm_client.cc
	g++ -o $@ $^ -std=c++11
shm_server:shm_server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f shm_client shm_server

comm.hpp

#ifndef _COMM_HPP_
#define _COMM_HPP_

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cerrno>
#include <cstring>

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

#define PATHNAME "."
#define PROJ_ID 0x88 // 项目标识符,随便写的。
#define MAX_SIZE 4096 //共享内存大小。

//获取key
key_t getKey()
{
    key_t k = ftok(PATHNAME, PROJ_ID);//使用ftok可以获取同样的一个key!
    if (k < 0)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(1);
    }
    return k;
}

#endif

shm_server.cc

#include "comm.hpp"

int main()
{
    key_t k = getKey();
    printf("0x%x\n", k);

    return 0;
}

 shm_client.cc

#include "comm.hpp"

int main()
{
    key_t k = getKey();
    printf("0x%x\n", k);

    return 0;
}

运行结果

size

共享内存大小一般建议为4KB的整数倍,系统分配共享内存是以4KB大小为单位的!这里我们定义了共享内存大小为4096KB,如果定为,例如:4097KB,那么内核给用户的共享内存大小会自动向上取整,申请4096*2KB。注意:内核给用户的申请的大小,和用户能用的大小是两码事,虽然内核会申请4096*2KB,但是用户依旧只能用4097KB(ipcs -m 查看共享内存大小依旧为4097KB)。

创建共享内存

comm.hpp

//获取key
key_t getKey()
{
    key_t k = ftok(PATHNAME, PROJ_ID);//使用ftok可以获取同样的一个key!
    if (k < 0)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(1);
    }
    return k;
}
int getShmHelper(key_t k, int flags)
{
    int shmid = shmget(k, MAX_SIZE, flags);
    if (shmid < 0)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(2);
    }
    return shmid;
}
//获取共享内存
int getShm(key_t k)
{
    return getShmHelper(k, IPC_CREAT);
}
//创建共享内存
int createShm(key_t k)
{
    return getShmHelper(k, IPC_CREAT | IPC_EXCL);
}

shm_server.cc 

#include "comm.hpp"

int main()
{
    key_t k = getKey();
    printf("key: 0x%x\n", k);
    int shmid = createShm(k);
    printf("shmid: %d\n",shmid);
    
    return 0;
}

shm_client.cc

#include "comm.hpp"

int main()
{
    key_t k = getKey();
    printf("key: 0x%x\n", k);
    int shmid = getShm(k);
    printf("shmid: %d\n",shmid);
    
    return 0;
}

 运行结果

如何理解key和shmid,有什么区别? 

再谈key

基于上述理解,我们认识到:

我们先谈论两个小问题: 

C语言C++ malloc的时候,例如申请1024字节的内存空间,int *p = malloc(size_t 1024),释放free(p),对空间释放不能局部释放,所以free释放的时候要传入所申请空间的起始地址。

问题是怎么知道从p开始释放多少个字节呢?

虽然用户申请了1024个字节,但是OS为了管理用户申请的空间,还会额外申请一些空间,用来记录用户malloc所申请出来的内存空间的属性信息,例如内存块大小,是否被使用等信息。所以free的时候,会自己去额外申请的空间中搜索内存块大小属性,帮助用户去释放。

在创建进程的时候,OS为了管理进程,以先描述,后组织的方式,还创建了一个描述该进程的内核数据结构PCB,用来记录进程的相关属性,所以OS管理进程就通过PCB来管理。

基于对上面两个问题的理解,那么:

OS中一定会存在很多共享内存,那么OS也要对共享内存以先描述,后组织的方式管理起来!

所以,这里并不单单只是用户申请一块物理内存空间,OS也一定会额外申请一些空间用来记录共享内存的相关属性信息,例如共享内存的权限、容量等。

所以共享内存 = 物理内存块 + 共享内存的相关属性!

实际上,用户在申请共享内存的时候,OS除了给用户申请一个物理内存块之后,还会给共享内存申请一个数据结构对象,OS要管理共享内存,并不是直接通过管理这个物理内存块,而是通过管理这个数据结构对象来进行管理。

创建共享内存的时候,是通过key来保证共享内存在系统中是唯一的。那么key在哪呢?

在创建共享内存时,key就作为共享内存的相关属性被填入到了描述共享内存相关属性的数据结构对象中。

eg:

struct shm{

//相关属性

//...

key_t k;

}

(下面还会谈到)

所以,当我们上层调用ftok创建key,再调用shmget创建共享内存时,把key作为参数传入shmget中,本质是把key设置进创建好的共享内存对应的数据结构对象中某一个属性字段里。另一个进程再去获取共享内存时,并不是通过共享内存对应的物理内存块,而是遍历共享内存所对应的相关属性,查找自己的key和共享内存的key是否相等。

总结:key要通过shmget,设置进共享内存属性中!用来标识该共享内存在内核中的唯一性!

shmid与key类似于文件概念里的fd与inode。使用具有唯一性的key值来标定特定的共享内存,给上层返回一个特定的整数,使用户可以通过这个特定的整数来访问特定的共享内存。

当我们进程退出,再执行的时候,这里报了一个文件已经存在的错误。进程已经结束了,共享内存资源依旧存在。

  • 共享内存生命周期随OS,不是随进程的。除非显式的使用对应的命令或接口释放共享内存,否则共享内存会随着OS的运行一直存在。
  • 生命周期随OS,不随进程,也是所有System V版本进程间通信的共性!
  • 跟管道不一样,管道的生命周期随进程

查看IPC资源

  • ipcs -m/q/s

  • ipcs -m 查看系统中共享内存段的相关信息

删除共享内存

  • 使用命令:ipcrm -m + shmid

  • 删除共享内存也有相关接口,使用shmctl函数。

shmctl函数

  • 控制共享内存

获取共享内存的属性、设置共享内存的属性、删除共享内存都使用shmctl接口。

失败返回-1。

  • shmid:共享内存id。
  • cmd:控制共享内存的方式。

包含选项IPC_STAT、IPC_SET、IPC_RMID、IPC_INFO

其中IPC_RMID选项,就是删除共享内存。

  • buffer:共享内存的相关属性。

comm.hpp

//获取共享内存
//...
//创建共享内存
//...
//删除共享内存
int delShm(int shmid)
{
    if (shmctl(shmid, IPC_RMID, NULL) == -1)
    {
        std::cerr << errno << strerror(errno) << std::endl;
    }
}
//...

shm_server.cc

#include "comm.hpp"

int main()
{
    key_t k = getKey();
    printf("key: 0x%x\n", k);
    int shmid = createShm(k);
    printf("shmid: %d\n", shmid);

    sleep(5);
    delShm(shmid);

    return 0;
}

一般情况,共享内存谁创建谁删除。

至此,我们完成了共享内存的创建与删除工作,但是,目前所创建的共享内存还没有办法使用,因为共享内存还没有和进程关联起来。接下来,我们要完成共享内存与进程关联的工作。

shmat函数

  • 将共享内存段连接到进程的地址空间

成功,返回,共享内存的起始地址;失败,返回-1。

  • shmaddr:指定连接地址。

要将共享内存映射到哪一个地址空间。大部分情况下,这个参数可以不设置,因为我们并不清楚我们想将共享内存映射到虚拟地址空间中的哪个区域。设定位为NULL即可,系统会自动选择一个合适的地址来连接共享内存段。

  • shmflg:标志位,用于控制共享内存段的连接方式和权限等。

常用的标志位为:

SHM_RDONLY:表示以只读方式连接共享内存。如果不设置(0),默认以读写方式连接共享内存段,即进程可以对连接后的共享内存段进行读和写操作。

comm.hpp

//获取共享内存
//...
//创建共享内存
//...
//挂接共享内存
void *attachShm(int shmid)
{
    void *mem = shmat(shmid, nullptr, 0);
    if ((long long)mem == -1L) // linux为64位系统,指针大小为8
    {
        std::cerr << errno << ": " << strerror(errno) << std::endl;
        exit(3);
    }
}
//删除共享内存
//...

 shm_server.cc

#include "comm.hpp"

int main()
{
    key_t k = getKey();
    printf("key: 0x%x\n", k);
    int shmid = createShm(k);
    printf("shmid: %d\n", shmid);
    
    char *start = (char*)attachShm(shmid);
    printf("attch success,address start:%p\n",start);

    delShm(shmid);

    return 0;
}

此时,Permission denied 权限被拒绝了。创建共享内存的时候,添加权限即可。

comm.hpp

int getShmHelper(key_t k, int flags)
{
    int shmid = shmget(k, MAX_SIZE, flags);
    if (shmid < 0)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(2);
    }
    return shmid;
}
//获取共享内存
int getShm(key_t k)
{
    return getShmHelper(k, IPC_CREAT | 0600);
}
int createShm(key_t k)
{
    return getShmHelper(k, IPC_CREAT | IPC_EXCL | 0600);
}
//挂接共享内存
void *attachShm(int shmid)
{
    void *mem = shmat(shmid, nullptr, 0);
    if ((long long)mem == -1L) // linux为64位系统,指针大小为8
    {
        std::cerr << errno << ": " << strerror(errno) << std::endl;
        exit(3);
    }
}
//删除共享内存
//...

shm_server.cc

#include "comm.hpp"

int main()
{
    key_t k = getKey();
    printf("key: 0x%x\n", k);
    int shmid = createShm(k);
    printf("shmid: %d\n", shmid);
    
    sleep(2);

    char *start = (char*)attachShm(shmid);
    printf("attch success,address start:%p\n",start);
    
    sleep(2);

    delShm(shmid);

    return 0;
}

下面我们写一个监控脚本监测运行一下。 

进程和共享内存挂接工作也已经完成了。接下来就可以进行通信了,当通信结束以后,要先去关联,将进程和共享内存的关联去掉,再删除共享内存。

shmdt函数

  • 去掉进程和共享内存的关联性

成功,返回0;失败,返回-1。

comm.hpp

//创建共享内存
//...
//挂接共享内存
//...
//共享内存去关联
void detachShm(void *start)
{
    if (shmdt(start) == -1)
    {
        std::cerr << errno << ": " << strerror(errno) << std::endl;
    }
}
//删除共享内存
//...

shm_server.cc 

#include "comm.hpp"

int main()
{
    key_t k = getKey();
    printf("key: 0x%x\n", k);
    int shmid = createShm(k);
    printf("shmid: %d\n", shmid);
    
    sleep(2);    
    char *start = (char*)attachShm(shmid);
    printf("attch success,address start:%p\n",start);
    
    sleep(2);
    
    //通信
    //...
    
    //去关联
    detachShm(start);
    
    sleep(2);
    delShm(shmid);

    return 0;
}

要通信,让client也和共享内存挂接起来。

 shm_client.cc

#include "comm.hpp"

int main()
{
    key_t k = getKey();
    printf("key: 0x%x\n", k);
    int shmid = getShm(k);
    printf("shmid: %d\n",shmid);
    
    sleep(5);    
    char *start = (char*)attachShm(shmid);
    printf("attch success,address start:%p\n",start);
    
    sleep(5);
    
    //通信
    //...
    
    //去关联
    detachShm(start);
    sleep(2);

    return 0;
}

共享内存直接就可以当作缓冲区,可以直接向共享内存输入输出。

共享内存通信

comm.hpp

#ifndef _COMM_HPP_
#define _COMM_HPP_

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cerrno>
#include <cstring>

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

#define PATHNAME "."
#define PROJ_ID 0x66  // 项目标识符,随便写的。
#define MAX_SIZE 4096 // byte

key_t getKey()
{
    key_t k = ftok(PATHNAME, PROJ_ID); // 使用ftok可以获取同样的一个key!
    if (k < 0)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(1);
    }
    return k;
}
int getShmHelper(key_t k, int flags)
{
    int shmid = shmget(k, MAX_SIZE, flags);
    if (shmid < 0)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(2);
    }
    return shmid;
}
int getShm(key_t k)
{
    return getShmHelper(k, IPC_CREAT | 0600);
}
int createShm(key_t k)
{
    return getShmHelper(k, IPC_CREAT | IPC_EXCL | 0600);
}
void *attachShm(int shmid)
{
    void *mem = shmat(shmid, nullptr, 0);
    if ((long long)mem == -1L) // linux为64位系统,指针大小为8
    {
        std::cerr << errno << ": " << strerror(errno) << std::endl;
        exit(3);
    }
}
void detachShm(void *start)
{
    if (shmdt(start) == -1)
    {
        std::cerr << errno << ": " << strerror(errno) << std::endl;
    }
}
int delShm(int shmid)
{
    if (shmctl(shmid, IPC_RMID, NULL) == -1)
    {
        std::cerr << errno << strerror(errno) << std::endl;
    }
}
#endif

shm_client.cc

#include "comm.hpp"

int main()
{
    key_t k = getKey();
    printf("key: 0x%x\n", k);
    int shmid = getShm(k);
    printf("shmid: %d\n", shmid);

    sleep(5);
    char *start = (char *)attachShm(shmid);
    printf("attch success,address start:%p\n", start);

    //sleep(5);

    // 通信
    const char *message = "Hello server,我是另一个进程,我正在和你通信!";
    pid_t id = getpid();
    int cnt = 1;
    while (true)
    {
        sleep(1);
        snprintf(start, MAX_SIZE, "%s[pid:%d][消息编号:%d]", message, id, cnt++);
    }
    // 不需要再定义缓冲区,写到共享内存中。共享内存就可以当作用户层缓冲区,可以直接写入。跟管道不同!
    // char buffer[1024];
    // while (true)
    // {
    //     snprintf(start, sizeof(buffer), "%s[pid:%d][消息编号:%d]", message, id, cnt++);
    //     memcpy(start, buffer, strlen(buffer) + 1);
    // }

    // 去关联
    detachShm(start);
    //sleep(2);

    return 0;
}

 shm_server.cc

#include "comm.hpp"

int main()
{
    key_t k = getKey();
    printf("key: 0x%x\n", k);
    int shmid = createShm(k);
    printf("shmid: %d\n", shmid);

    // sleep(5);
    char *start = (char *)attachShm(shmid);
    printf("attch success,address start:%p\n", start);

    // sleep(5);

    // 通信
    while (true)
    {
        //共享内存当字符串
        printf("client say : %s\n", start);
        sleep(1);
    }
    // 管道读取方式:
    // char buffer[]; read(pipefd, buffer, ...)

    // 去关联
    detachShm(start);

    // sleep(5);
    delShm(shmid);

    return 0;
}

至此,就完成了基于共享内存的client、server端的通信。

共享内存特点

  • 优点:所有进程间通信,速度最快!

因为,共享内存被进程共享,当一个进程向共享内存区域写入数据后,其他进程可以立即从该区域读取到最新的数据,能大大的减少数据拷贝次数,无需像其他通信方式那样经过内核空间的中转和数据拷贝,从而大大减少了数据传输的时间开销,提高了通信效率。

同样的代码,使用管道和共享内存的方式通信,考虑键盘输入输出,分别需要几次拷贝?

  • 缺点:没有进行同步与互斥操作,没有对数据做任何保护!

将client端改为每3秒写一次,而此时server读端为每1秒读一次,读比写快:

server端读取一次之后,3秒内一直在重复读取!跟管道不同,管道会阻塞,等待写入。当然,也可以设计出对数据做保护的共享内存,这里就不演示了。

共享内存的数据结构

shmctl可以允许用户调用特定接口获取指定id的共享内存的属性。

其中shmid_ds结构体是OS暴露给用户的一部分用户级数据结构,存放着共享内存的相关属性,也就是上面我们所说的OS为了管理共享内存额外申请空间创建的数据结构对象,当然,此数据结构是用户级的,跟内核中的还有一点差异,不过类似。

上面我们已经知道key是要被设置进共享内存属性中的!在shmid_ds中也可以看到key做了封装,封装到了shm_perm结构体中。

我们也可以获取共享内存的相关属性:

//...    
while (true)
{
    printf("client say : %s\n", start);
    struct shmid_ds ds;
    shmctl(shmid, IPC_STAT, &ds);
    printf("获取属性:size:%d,pid:%d,myself:%d,key: ", ds.shm_segsz, ds.shm_cpid, getpid(),ds.shm_perm._key);
    sleep(1);
}
//...

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

高仿CSDN编辑器,前端博客模板

高仿CSDN编辑器纯前端模板&#xff0c;使用的js、html、vue、axios等技术&#xff0c;网络请求库已进行封装&#xff0c;可以按需调整界面,需要源码联系(4k左右)。 1.支持代码高亮 2.支持目录点击定位 3.支持文件上传、图片上传&#xff08;需要自己写后端接口&#xff09; 4.M…

国产低代码框架zdppy开发笔记002 标准的接口响应

前言 通过前面的学习, 我们已经知道了zdppy_api和zdppy_req的基本用法, 接下来我们会在学习中多次用到这两个框架. 我们已经知道了该如何响应一个字符串,但是我们该如何响应json数据呢? 在zdppy_api中,我们定义了一组规范的API响应, 我们慢慢来看看. 规范的响应 首先来看…

实用技巧:关于 AD修改原理图库如何同步更新到有原理图 的解决方法

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/144738332 长沙红胖子Qt&#xff08;长沙创微智科&#xff09;博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV…

牛客周赛 Round 74

D. 预知 题目链接 题意有点绕&#xff0c;简单来说是其中一堆牌&#xff0c;问最少预知几张才能保证任取两张都不会导致种类重复。一开始对每张牌种类不是已知的&#xff0c;已知的是每种牌的牌数。 思路就是相当于把其中一种明牌&#xff0c;保证任取两张都不会导致种类重复…

【linux学习指南】SIGCHLD信号

文章目录 &#x1f4dd;SIGCHLD信号&#x1f6a9;总结 &#x1f4dd;SIGCHLD信号 进程⼀章讲过⽤wait和waitpid函数清理僵⼫进程,⽗进程可以阻塞等待⼦进程结束,也可以⾮阻塞地查询是否有⼦进程结束等待清理(也就是轮询的⽅式)。采⽤第⼀种⽅式,⽗进程阻塞了就不能处理⾃⼰的⼯…

AI助力SEO优化的关键词策略解析

内容概要 在数字营销的快速发展中&#xff0c;人工智能&#xff08;AI&#xff09;正逐步成为提升搜索引擎优化&#xff08;SEO&#xff09;效果的重要工具。关键词策略是SEO成功的关键要素之一&#xff0c;而AI技术的应用使得这一过程更加高效和精准。在关键词研究中&#xf…

PHP-Casbin v4.0.0 发布,支持 ACL、RBAC、ABAC 等模型的访问控制框架

PHP-Casbin 是一个用 PHP 语言打造的轻量级开源访问控制框架&#xff0c;支持 ACL、RBAC、ABAC 多种模型。它采用了元模型的设计思想&#xff0c;支持多种经典的访问控制方案&#xff0c;如基于角色的访问控制 RBAC、基于属性的访问控制 ABAC 等。 更新内容&#xff1a; http…

解决Git中没有小绿勾与红叉叉的问题

一、检查自己的软件 必须安装Git和Tortoisegit&#xff08;也就是俗称的小乌龟&#xff09;这两个软件。 Git的下载地址&#xff1a; CNPM Binaries Mirrorhttps://registry.npmmirror.com/binary.html?pathgit-for-windows/ 寻找与自己电脑相配的软件版本就可以了。 Tor…

搭建跨境电商企业博客的指南

在跨境电商领域&#xff0c;企业博客不仅是展示品牌形象的窗口&#xff0c;也是连接全球客户的重要桥梁。一个精心搭建的企业博客能够提升品牌知名度、增强客户信任&#xff0c;并促进销售。 搭建企业博客的必要性 1. 建立品牌权威&#xff1a;通过高质量的内容&#xff0c;企…

渗透学习笔记(十一)Burp Suite 总结

声明&#xff01; 学习视频来自B站up主 泷羽sec 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章&#xff0c;笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&#xff0c;其他均与本人以及泷羽sec团队无关&#…

课设CLion连接Ubuntu14makeQt项目出错解决汇总

在这之前需要注意以下几点&#xff1a; 1、需要 确保CLion能连接Ubuntu14 2、cmakelist.txt文件配置 3、知道部署路径&#xff1a; 问题一&#xff1a;/usr/bin/ld: cannot open output file GreedySnake: Is a directory 否则就会出现make以后应该生成一个可执行文件&…

【GO基础学习】gin的使用

文章目录 模版使用流程参数传递路由分组数据解析和绑定gin中间件 模版使用流程 package mainimport ("net/http""github.com/gin-gonic/gin" )func main() {// 1.创建路由r : gin.Default()// 2.绑定路由规则&#xff0c;执行的函数// gin.Context&#x…

磁编码器(Magnetic Encoder)

磁编码器&#xff08;Magnetic Encoder&#xff09;是一种传感器&#xff0c;它通过检测磁性材料的磁场变化来测量旋转或线性位置。编写用于读取磁编码器数据的C语言程序时&#xff0c;您需要根据具体的硬件接口和编码器类型进行调整。以下是一个基本的框架&#xff0c;假设我们…

Qt Creator项目构建配置说明

QT安装好之后&#xff0c;在安装目录的Tools\QtCreator\bin下找到qtcreator.exe文件并双击打开 点击文件-新建文件或项目 选择Qt Widgets Application 设置项目名称以及路径 make工具选择qmake&#xff08;cmake还未尝试过&#xff09; 设置主界面对应类的名称、父类&#…

智能边缘计算×软硬件一体化:开启全场景效能革命新征程(企业开发者作品)

边缘智能技术快速迭代&#xff0c;并与行业深度融合。它正重塑产业格局&#xff0c;催生新产品、新体验&#xff0c;带动终端需求增长。为促进边缘智能技术的进步与发展&#xff0c;拓展开发者的思路与能力&#xff0c;挖掘边缘智能应用的创新与潜能&#xff0c;高通技术公司联…

【React】- 跨域PDF预览、下载(改文件名)、打印

我们经常会碰到跨域来方位PDF&#xff0c;同时需要下载、打印的需求&#xff0c;通常由于浏览器的安全策略&#xff0c;可以预览&#xff0c;但是下载和打印可能会受限&#xff0c;这时候怎么办呢&#xff1f; 1.创建一个隐藏的标签 要下载 iframe 中的 PDF 文件&#xff0c;…

Ps:创建数据驱动的图像

在设计实践中&#xff0c;常常需要处理大量内容变化但设计格式统一的任务&#xff0c;例如批量生成名片、工作证、学生证、胸牌、奖状或证书甚至图册。这些工作如果逐一手动制作&#xff0c;不仅耗时费力&#xff0c;还容易出错。 为解决这一问题&#xff0c;Photoshop 提供了强…

Kotlin 协程基础知识总结六 —— 协程 Flow 的综合应用

1、项目描述与搭建 &#xff08;P92~P94&#xff09;我们会将几个 Flow 的应用实例放在同一个 Demo 中&#xff0c;主页就是一个 Activity 里包含一个按钮&#xff0c;点击按钮跳转到对应的功能展示页面上。整体架构采用一个 Activity 多个 Fragment 的结构&#xff0c;结合 J…

环,域,体,整区,理想,极大理想,

环&#xff1a; 定义&#xff1a; 加法交换群 乘法半群 分配律 域的定义&#xff1a; 加法交换群 乘法群&#xff08;去掉0元是交换群&#xff09; 分配律 Eg:比如整数集合不是域&#xff0c;因为对于乘法来说&#xff0c;去掉0后没有单位元了&#xff0c;但是是环 Eg…

关于Flutter应用国际化语言的设置

目录 1. Locale配置 2. 用户切换/启动自动加载缓存里面的locale 由于最近在开发app国际化设置的时候遇到一些问题&#xff0c;所以做出一些总结。 1. Locale配置 具体的初始化配置可以参考文档&#xff1a;i18n | Flutter 中文文档 - Flutter 中文开发者网站 - Flutter 值得…