Linux进程间通信——探索共享内存—— 剖析原理, 学习接口应用

        前言:本节内容主要讲解进程间通信的, systemV版本下的共享内存。 共享内存,顾名思义, 其实就是一块内存, 它不同于管道是一个文件。 所以它的传输速度是很快的。 因为管道是文件,有缓冲区, 而共享内存只是一块内存, 可以直接与进程挂接, 直接拷贝。

        ps:本节内容适合了解一些管道知识的友友们进行观看哦

目录

进程间通信的本质

共享内存的原理

共享内存相关接口

shmget——创建共享内存

shmflg

key

shmat——进程挂接

 ​编辑

shmdt——进程去关联

 指令

ipcs -m

应用

准备文件

准备makefile

Com.hpp

GetKey——封装获取key值的函数

CreatShareMemory——获取共享内存

CreatShm和GetShm——获取共享内存

processa.cpp

processb.cpp

运行程序

key和shimd的区别

共享内存的生命周期


进程间通信的本质

        进程间通信的本质就是——先让不同的进程, 看到同一份资源。(这是为了给通信奠定前提条件)

共享内存的原理

        上面的这一张图, 其实就是叫做共享内存。 因为共享内存的本质, 就是两个进程能够看到同一份资源, 而上面, 就做到了两个进程看到同一份资源。  其中左边是一个进程的PCB, 然后右边是一个进程的PCB。 同时这两个PCB都有它们对应的虚拟地址空间然后物理内存中有一块共享资源, 两个进程都可以通过页表的映射看到这份共享资源。—— 这就是共享内存。 

        对于上面这一张图来说, 我们的第一步就是申请内存。 第二步然后就是将申请好的内存分别挂接到两个进程的进程地址空间, 然后返回我们的地址空间的地址即可。但是这是申请共享内存的流程。 如果我们要释放共享内存呢?

        我们申请是由操作系统在内存中开辟一段地址空间, 然后把虚拟地址到物理地址之间的映射关系在页表中一填, 最后再挂接。 那么共享内存就创建好了。 ——而释放空间内存其实就是反过来, 一定是如何创建的, 那么就如何释放的。 ——所以第一步就是进程和共享内存去关联, 就是将页表之中虚拟地址和物理地址的映射关系去掉 然后就释放共享内存。 

        现在, 有一个问题就是——上面的操作是由进程直接做的吗? 答案是不是的, 进程没有资格直接访问共享内存的, 必须由操作系统来做。 

        那么我们就能看到一个典型的现象。 ——我们的进程需要通信, 然后操作系统就帮进程建立信道。 ——这里进程就是需求方, 而操作系统就是执行方。 进程解决为什么我们要建立共享内存操作系统解决的是如何建立共享内存。 那么就意味着我们的操作系统是必须要给进程提供系统调用!!!

        那么,我们的操作系统当中有着各种通信, 所以一定有着非常多的共享内存, 所以操作系统要不要将共享内存管理起来呢?——答案是需要的, 而管理的方式就是先描述再组织。 也就是说, 一定会有内核数据结构来描述共享内存

共享内存相关接口

shmget——创建共享内存

        上面的shmget函数, 就是创建共享内存的接口。 使用它需要包含sys/ipc.h、sys/shm.h头文件。 同时三个参数, 一个返回值int类型。 ——其中, size和返回值比较好说, size就是我们要创建的共享内存的大小, 返回值int就是共享内存标识符。 key和shmflg是没有办法一下子说清的, 下面我们来谈一谈这两个参数的相关问题:

shmflg

        shmflag是创建的共享内存的模式, 这里说两种常用的使用选项:

        IPC_CREAT(单独使用)如果你申请的共享内存, 不存在, 就创建, 存在就获取并返回。

        IPC_CREAT | IPC_EXCL如果你申请的共享内存不存在, 就创建, 存在就出错返回。 ——这个选项就可以确保如果我们申请内存成功了, 这个内存一定是新的!!!(IPC_EXCL不单独使用!

key

        在谈key之前我们先思考一个问题, 就是对于一块共享内存, 我们如何知道他存不存在呢? 或者说我们怎么保证让不同的进程看到同一个共享内存呢?——带着这两个问题, 我们来谈key:

        1、key是一个数字, 这个数字是几, 不重要, 关键是它必须在内核种具有唯一性, 能够让不同进程进行唯一性标识。

        2、第一个进程可以根据key创建共享内存, 第二个之后的进程, 他们只要拿着同一个key, 就可以和第一个进程看到同一个共享内存了!!!

        3、对于一个创建好的共享内存, key在哪?——这个可以直接挑明, 其实就在共享内存的描述对象中!!!

        4、命名管道是通过路径 + 文件名来确定唯一的管道的!!!而我们的key, key类似于路径, 同样具有唯一性。共享内存就是通过key来确定唯一性!!!

         现在有一个问题, 第一次创建共享内存的时候的key, 是如何有的呢?

        这个函数, 就是用来在第一次创建共享内存时, 用来生成key的函数。 所以,key不是直接定义的!!!

        上面的图中, key是如何创建出来的呢? 我们通过上面的蓝色框框其实可以看到, 这个函数是通过转化路径名和一个项目标识符来生成一个key

        那么问题来了, 我们的key能不能随便定义一个出来呢?——理论上是可以的, 因为我们两个进程看到同一个资源, 本质上就是需要拿着同一把钥匙, 而key就是这一把钥匙。 ——所以我们不需要把ftok想的太复杂, 这个函数当生成key的时候不需要去系统中查找哪个key使用过, 哪个没有使用过。 而是类似于哈希函数, 使用了一套算法, pathname和proj_id进行数值计算即可!!

        那么, 为什么不能让操作系统自己去生成呢?为什么要让用户自己去设置呢?——虽然操作系统做这些工作很简单, 但是如果操作系统给我们生成了这么一个key, 我们进行通信的时候, 我们怎么把这个key交给另一个进程呢? ——这里可不可以直接将返回的key交给另一个进程呢这个不可以的, 因为我们现在要解决的就是进程间通信的问题, 而想要将key交给另一个进程, 需要进程通信, 这就陷入了一个死循环。 所以, 我们的key, 就势必不能由操作系统自己决定。 ——这个也就说明, 我们的ftok, 与其说是用户自己指定的key, 不如说是用户之间约定的key!!!只要有两个用户, 他们同时使用进程, 同时使用进程, 使用同一个路径, 同一个id, 那么他们就能够进行通信!!!

shmat——进程挂接

 

        什么是进程挂接?我们要访问一个共享内存, 虽然共享内存是操作系统的, 虽然我们使用shmid只是获得了共享内存的编号。 但是既然是内存, 那么如果我们操作系统提供一个系统接口, 通过shmid找到目标的共享内存, 然后让这个共享内存和进程的地址空间通过页表建立了联系。 那么我们的进程, 是不是就相当于能够访问这块共享内存了? 而这个过程, 就叫做挂接!!!

        而一个共享内存可以有多个进程和它挂接起来, 这个数量, 就是nattch, 我们如何观察到这个nattch, 可以使用ipcs -m指令。ps:讲解在指令部分

shmdt——进程去关联

        有进程挂接, 那么就有进程去关联, 也就是下面的shmdt, 传送的参数是shmat返回的地址空间的地址。 

 

        这里我们思考的是我们只有这个共享内存的起始地址, 这个函数是如何知道我们要取消关联的共享内存有多大呢? ——这个问题和realloc, free等函数类似, 他们都是只需要传送首地址, 不需要传送大小或者末尾地址, 这就说明了一定有我们在用户层看不到的东西, 在管理着我们的内存!!!

        并且, 我们的去关联的流程就是——根据页表找到我们的物理内存, 把页表清掉, 根据inode属性, 让物理内存减减, 如果要释放, 就按照属性里面的大小进行释放即可!

 指令

ipcs -m

        ipcs -m可以查看一个共享内存的各个属性:

  • shimd:共享内存的编号。
  • perms:共享内存的权限。
  • bytes:共享内存的大小, 这个大小是以4096唯一个单位的, 也就是说, 虽然我们上面显示我们创建了4097个大小的共享内存, 但其实操作系统给我们开辟的是8k多字节。 从c语言的角度, 这部分多申请的空间叫做cookie。 ——实际上, 我们c语言在申请堆空间的时候,要不要管理所谓的堆空间呢?——答案是肯定要的, 所以就要先描述再组织, 所以我们c语言多申请的那部分空间, 也有对应的属性!!!
  • nattch:代表了该共享内存的挂接数量。

应用

准备文件

ps:Log.hpp我们在之前的文章已经实现过了, 所以这里直接拿来用了, 不会再实现一遍, 不会的友友可以去前面的文章看一看:linux进程间通信——学习与应用命名管道, 日志程序的使用与实现-CSDN博客

准备makefile

有两个cpp文件需要编译。所以需要.PHONY, 然后编译两个文件。 最后clean, 删除两个文件。

.PHONY:all
all:processa.exe processb.exe processa.exe:processa.cppg++ -o $@ $^ -std=c++11
processb.exe:processb.cpp g++ -o $@ $^ -std=c++11.PHONY:clean
clean: rm -f processa.exe processb.exe

Com.hpp

GetKey——封装获取key值的函数

我们知道ftok函数的第一个参数是路径, 第二个参数是项目id。 我们就可以先定义两个常量——pathname和proj_id;然后, 我们又可以创建一个log函数, 让log函数来打印日志; ftok需要包含头文件sys/ipc.h以及sys/types.h。 同时其他的string, cstring, cerrno, 等等都包含进来需要用到。——然后其中的pathname其实就是传给ftok的路径函数, 其中的proj_id就是项目id。 通过这两个我们就可以获得一个key值。 用来创建共享内存。 

#pragma once#include"Log.hpp"
#include<cerrno>
#include<cstring>
#include<string>
#include<iostream>#include<sys/ipc.h>
#include<sys/types.h>using namespace std;const string pathname = "/home/_mian_yang";
const int proj_id = 0x88881; Log log;//获取key
key_t GetKey()
{key_t k = ftok(pathname.c_str(), proj_id);   //ftok拿到k函数if (k == -1)  //如果k == -1, 那么就拿到k失败。{log(Fatal, "ftok error: %s", strerror(errno));  //打印错误信息exit(1);}log(Info, "ftok success! Key is: #d", k);  //创建k成功, 返回kreturn k; }

CreatShareMemory——获取共享内存

        这是第二层封装, 封装了GetKey函数。 用来获取共享内存。 外层函数传递flag, 也就是贡献内存的打开方式——这个打开方式就是用来区分共享内存共享内存是已经创建好的还是第一次创建的。在本次实验中, 我们prcessa进程用来第一次创建共享内存, 而processb用来获取已经创建好的共享内存。 那么这两个进程内部调用CreatShareMemory的时候就要传递不同的flag: processa进程传递IPC_CREAT | IPC_EXCL | 权限, processb传递IPC_CREAT 

另外, 因为shmget创建共享内存的函数需要传递一个size用来规定共享内存的大小, 所以, 我们可以在上面创建一个size常量。 另外使用shmget也需要包含一下sys/shm.h头文件

//
#pragma once#include"Log.hpp"
#include<cerrno>
#include<cstring>
#include<iostream>
#include<sys/ipc.h>
#include<sys/types.h>
#include<string>
#include<sys/shm.h>
using namespace std;const int size = 4096;   //4kb
const string pathname = "/home/_mian_yang";
const int proj_id = 0x88881; 
Log log;//获取key
key_t GetKey()
{key_t k = ftok(pathname.c_str(), proj_id);   //ftok拿到k函数if (k == -1)  //如果k == -1, 那么就拿到k失败。{log(Fatal, "ftok error: %s", strerror(errno));  //打印错误信息exit(1);}log(Info, "ftok success! Key is: #d", k);  //创建k成功, 返回kreturn k; }int CreatShareMemory(int flag)
{key_t key = GetKey(); //获取keyint shmid = shmget(key, size, flag);if (shmid == -1){log(Fatal, "creat share memory error: %s", strerror(errno)); //创建共享内存失败, 打印错误消息exit(1);}log(Info, "creat share memory is success! shmget is: %d", shmid); //创建共享内存成功, 打印共享内存的的shmidreturn shmid;
}

CreatShm和GetShm——获取共享内存


int GreatShm()
{return GreatShareMemory(IPC_CREAT | IPC_EXCL | 0666); //第一次创建需要判断是否存在}int GetShm()
{return GreatShareMemory(IPC_CREAT); //获取的时候不需要判断是否存在
}

processa.cpp

        我们在本次实验中要实现a来打印, b来输入的实验效果。所以我们的a要从内存中读到数据。 在b中把数据写入内存。 

        另外, 我们设计让a来创建共享内存, 创建好共享内存后, 要知道, 我们平时向内存中读数据, 必须要获取这个内存的地址,但是我们现在只有共享内存的编号shmid,  那么如何获取这个地址? ——这就用到了挂接, 我们将共享内存挂接到当前进程的虚拟地址上, 那么进程访问虚拟地址就是在访问共享内存!!!(注:这里面使用了shmctl, 这个函数的使用难度很大, 这里博主使用了IPC_STAT选项, 目的是为了获取shmid对应共享内存中的数据, 那么获取到那里呢, 就需要提前创建一个描述共享内存的结构体, 博主定义的是shmds, 所以就有了下面的代码)

#include"Com.hpp"
#include"Log.hpp"extern Log log; //拿到Com.hpp里面创建的哪个全局的静态对象, int main()
{//先创建共享内存int shmid = GreatShm();//函数内部自动判断, 无需手动判断//挂接共享内存到到vm_area_structchar* shmaddr = (char*)shmat(shmid, nullptr, 0); //这个返回值, 就是//共享内存的虚拟地址, 我们利用虚拟地址, 就可以访问共享内存struct shmid_ds shmds; //创建一个结构体, 用来获取共享内存的数据while(true){cout << "client say@ " << shmaddr << endl;sleep(1);shmctl(shmid, IPC_STAT, &shmds); //读取共享内存的各个属性//将各个数据打出来!cout << "shm size: " << shmds.shm_segsz << endl;cout << "shm nattch: " << shmds.shm_nattch << endl;cout << "shm key: " << shmds.shm_perm.__key << endl;}    shmdt(shmaddr);shmctl(shmid, IPC_RMID, nullptr); //删除return 0;
}

processb.cpp

b进程我们用来读取数据, 同样需要挂接。 不同的是我们要创建一个缓冲区buffer, 来接收我们要打印的数据,然后向内存中将缓冲区的数据写进去。 

#include"Com.hpp"
#include"Log.hpp"int main()
{int shmid = GetShm();//获取共享内存//向里面写东西, 写东西就需要获得这个共享内存的地址, 那么就需要挂接char* shmaddr = (char*)shmat(shmid, nullptr, 0); //挂接地址随机, 模式为0while (true){//先创建一个缓冲区string str;cin >> str;  //向缓冲区种写入数据snprintf(shmaddr, str.size(), "%s", str.c_str()); //将缓冲区的数据写入//共享内存}return 0;
}

运行程序

如下图是我们甘冈打开两个进程:可以看到, 此时的挂接个数是2.

然后我们在b进程输入一个你好啊, 观察一下——

我们就会发现数据已经打印进去了。 然后我们如果退出进程b, 挂接数会减到1:

——以上就是共享内存接口的相关应用。 

经过上面的实验, 我们可以很明显的看出来, 共享内存是没有同步机制的!!!!——而想要让共享内存拥有同步机制的效果, 可以使用管道!!!
 

key和shimd的区别

        了解了应用后, 我们就可以深究一下内部的原理了。

        首先我们谈的就是请问key和shmid有什么区别呢?我们知道, key是在操作系统标定唯一性的, shmid是在我们的进程里面的,用来标识资源的唯一性的。也就是说,key是操作系统层面的, 只有在创建共享内存的时候会用到key, 其他时候用不到。 

共享内存的生命周期

        我们第一次运行程序processa.exe的时候, key_t会创建成功, 并且共享内存被创建出来。但是当我们第二次运行processa.exe的时候, 我们会看到下面这种情况:

        也就是说, 我们的共享内存并没有随进程的退出而退出。 这里我们可以使用ipcs -m查看当前系统中所有的共享资源

        我们输入上面ipcs -m的指令可以看到我们刚刚创建的共享内存, 也就是说, 即便我们进程推出了, 但是我们的ipc资源, 还是存在的。 这说明如果我们不主动把共享内存关掉, 操作系统也不会给我们关。——这就是共享内存的特性。 ——也就是说, 贡献内存的生命周期是随着内核的!!!(管道文件的生命周期是随着进程的!!!)用户不主动关闭, 共享内存会一直存在, 除非内核重启!!!

        释放的方式分为两种——一种是使用指令进行释放, 另一种就是上面程序中使用的进程内函数调用shmctl进行控制释放共享内存。 

  •         指令释放:ipcrm -m 共享资源的shmid
  •         内部调用:shmctl(shmid, IPC_RMID, nullptr);

 

——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!

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

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

相关文章

《深入理解 Java 线程池:高效管理线程的利器》

线程池 1. 什么是线程池&#xff1f; ​ 线程池内部维护了若干个线程&#xff0c;没有任务的时候&#xff0c;这些线程都处于等待空闲状态。如果有新的线程任务&#xff0c;就分配一个空闲线程执行。如果所有线程都处于忙碌状态&#xff0c;线程池会创建一个新线程进行处理或…

不得不说 Sam‘s Club 的数字化做得挺好

因正好有东西要退货就顺便看了下订单如何退货。 但发现 Sam’s Club 的所有交易都能够从后台查到&#xff0c;同时还提供了个 CSV 文件的下载。 打开下载文件就能看到全部的数字化的交易记录。 就拿加油这个事情来说&#xff0c;能够非常清楚这一年在 Sam’s Club 加油多少加…

【无人机设计与控制】基于粒子群算法的三维无人机航迹规划

摘要 本文研究了基于粒子群算法&#xff08;PSO&#xff09;的三维无人机航迹规划问题。通过粒子群优化算法&#xff0c;无人机能够在复杂三维环境中进行路径规划&#xff0c;以避开障碍并实现最优路径飞行。该方法有效结合了无人机的飞行动力学特性与环境约束&#xff0c;能够…

【GlobalMapper精品教程】087:基于DEM的山脊线提取

文章目录 一、山脊线介绍二、加载实验数据三、山脊线提取一、山脊线介绍 山脊线是指沿山脊走向布设的路线,通常也是山脊最高点的连线,它在地形表示和测绘科学技术中具有重要地位。 山脊线是大体上沿分水岭布设的路线,也被称为山脊的最高棱线。在等高线地图上,山脊线表现为…

计算机网络(七) —— https协议与网络安全证书

目录 一&#xff0c;关于https 二&#xff0c;关于加密 2.1 明文&#xff0c;密钥 2.2 对称和非对称加密 2.3 数据摘要&#xff0c;数据指纹&#xff0c;数字签名 三&#xff0c;https过程过程探究 四&#xff0c;证书 4.1 CA认证 4.2 证书大致内容和申请流程 4.3 签…

fastadmin 定义通用搜索默认值,实现datetimerange默认搜索今日的列表数据,列表查询慢解决方案

文章目录 前言实例完结 前言 fastadmin默认会展示全部列表数据&#xff0c;随着数据越来越多&#xff0c;一次性查询出全部的数据会导致列表查询越来越慢 sql优化可以查看我这篇文章&#xff1a;分享最全的sql优化解决方案 除了基本的sql优化查询速度外&#xff0c;我们还可…

网络爬虫到底难在哪里?

如果你是自己做爬虫脚本开发&#xff0c;那确实难&#xff0c;因为你需要掌握Python、HTML、JS、xpath、database等技术&#xff0c;而且还要处理反爬、动态网页、逆向等情况&#xff0c;不然压根不知道怎么去写代码&#xff0c;这些技术和经验储备起码得要个三五年。 比如这几…

Cursor免费 GPT-4 IDE 工具的保姆级使用教程

Cursor免费 GPT-4 IDE 工具的保姆级使用教程 简介 Cursor 是一款基于人工智能技术的代码生成工具。 它利用先进的自然语言处理和深度学习算法&#xff0c;可根据用户的输入或需求&#xff0c;自动生成高质量代码。 不管是初学者&#xff0c;还是资深开发者&#xff0c;Curs…

低代码技术:简化应用开发的未来

近年来&#xff0c;低代码技术作为一种新兴的应用开发方法&#xff0c;受到了广泛关注。低代码平台通过图形化的界面和预设的模块&#xff0c;使得用户能够以较少的代码编写工作创建应用程序。这一技术的发展&#xff0c;标志着软件开发过程中的一个重要变革。 低代码技术的基…

Koa (下一代web框架) 【Node.js进阶】

koa (中文网) 是基于 Node.js 平台的下一代 web 开发框架&#xff0c;致力于成为应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石&#xff1b; 利用 async 函数 丢弃回调函数&#xff0c;并增强错误处理&#xff0c;koa 没有任何预置的中间件&#xff0c;可快速…

二叉树的前中后序遍历(迭代法)( 含leetcode上三道【前中后序】遍历题目)

文章目录 前序遍历&#xff08;迭代法&#xff09;中序遍历&#xff08;迭代法&#xff09;后序遍历&#xff08;迭代法&#xff09;总结 为什么可以用迭代法&#xff08;非递归的方式&#xff09;来实现二叉树的前后中序遍历呢&#xff1f; 在队列与栈专题我们就感受到了&…

KeyCode及KeyCode分发机制

文章目录 需求场景纯KeyCode 快捷操作KeyCode 按键响应操作、拦截 一、工作中常用KeyCode二、KeyCode大全三、KeyCode 响应事件事件输入流程事件响应源码分析源码举例说明 需求场景 纯KeyCode 快捷操作 经常在代码中实现返回、Home 、音量加减、截屏 等功能实现&#xff0c;代…

SpringBoot(40) — SpringBoot整合MyBatis-plus

前言 在上节中我们对MyBatis-plus特性有了一个整体的认识&#xff0c;然后也大致讲了些MyBatis与MyBatis-plus的不同之处。大家感兴趣的话&#xff0c;可参考以下文章 SpringBoot(39) — MyBatis-plus简介 这节我们来讲讲SpringBoot项目如何快速接入MyBatis-plus框架。 今天涉及…

开源网安多城联动、多形式开展网安周公益活动,传播网络安全知识

9月9日至15日&#xff0c;以“网络安全为人民&#xff0c;网络安全靠人民”为主题的2024年国家网络安全宣传周将在全国范围内统一开展&#xff0c;通过多样的形式、丰富的内容&#xff0c;助力全社会网络安全意识和防护技能提升。开源网安今年继续为各地企业、群众带来了丰富的…

BOE(京东方)领先科技赋能体育产业全面向新 以击剑、电竞、健身三大应用场景诠释未来健康运动新生活

巴黎全球体育盛会虽已闭幕&#xff0c;但世界范围内的运动热潮并未消退。9月12日&#xff0c;在北京恒通国际商务园&#xff08;UBP&#xff09;的之所ICC&#xff0c;BOE&#xff08;京东方&#xff09;开启了以“屏实力 FUN肆热爱”为主题的“科技赋能体育”互动体验活动。活…

esp32 wifi 联网后,用http 发送hello 用pc 浏览器查看网页

参考chatgpt Esp32可以配置为http服务器&#xff0c;可以socket编程。为了免除编写针对各种操作系统的app。完全可以用浏览器仿问esp32服务器&#xff0c;获取esp32的各种数据&#xff0c;甚至esp的音频&#xff0c;视频。也可以利用浏览器对esp进行各种操作。但esp不能主动仿…

vue3前端开发-小兔鲜超市-本地购物车列表页面的统计计算

vue3前端开发-小兔鲜超市-本地购物车列表页面的统计计算&#xff01;这一次&#xff0c;实现了一些本地购物车列表页面的&#xff0c;简单的计算。 代码如下所示&#xff1a; import { computed, ref } from vue import { defineStore } from pinia export const useCartStor…

web基础—dvwa靶场(八)SQL Injection(Blind)

SQL Injection(Blind)&#xff08;SQL注入之盲注&#xff09; SQL Injection(Blind)&#xff0c;SQL盲注&#xff0c;相比于常规的SQL注入&#xff0c;他不会将返回具体的数据信息或语法信息&#xff0c;只会将服务器包装后的信息返回到页面中。 常规SQL注入与SQL盲注详细对比…

css 样式简单学习(一)

目录 1. css 介绍 1.1 css 样式 1.2 css代码风格 1.2.1 书写格式 1.2.2 样式大小写​编辑 1.2.3 空格规范 2. 基础选择器 2.1 选择器的作用​编辑 2.2 选择器的分类 2.3 基础选择器 2.3.1 标签选择器​编辑 2.3.2 类选择器​编辑 2.3.3 类选择器-多类名​编辑 2.…

Unity 百度AI实现无绿幕拍照抠像功能(详解版)

目录 一、前言 1.抠像效果 2.去哪找百度ai抠图 3.基础流程跳过 二、获取AccessToken 1.什么是Token 2.为什么要获取Token 3.如何获取token 4.解析json 5.完整代码 三、抠像 1.准备地址 2.建立链接&#xff0c;和基本配置 3.图片格式转换 4.开始上传 5.获取回复…