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

news2024/9/19 8:46:24

        前言:本节内容主要讲解进程间通信的, 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.cpp
	g++ -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成功, 返回k
	return 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成功, 返回k
	return k; 

}


int CreatShareMemory(int flag)
{
	key_t key = GetKey(); //获取key
	int 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); //创建共享内存成功, 打印共享内存的的shmid
	return 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_struct
    char* 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); //挂接地址随机, 模式为0
    while (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.coloradmin.cn/o/2145916.html

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

相关文章

Day99 代码随想录打卡|动态规划篇--- 01背包问题

题目&#xff08;卡玛网T46&#xff09;&#xff1a; 小明是一位科学家&#xff0c;他需要参加一场重要的国际科学大会&#xff0c;以展示自己的最新研究成果。他需要带一些研究材料&#xff0c;但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等&am…

LeRobot - 让现实机器人更易学

文章目录 一、关于 LeRobot特点模拟环境中预训练模型的示例 致谢教程 - Getting Started with Real-World Robots 二、安装三、Walkthrough1、可视化数据集2、LeRobotDataset的格式3、评估预先训练的策略4、训练你自己的政策复制最先进的&#xff08;SOTA&#xff09; 四、贡献…

Vue3 中 Aos 动画同时触发的解决办法

文章目录 问题现象解决之后的效果解决办法问题猜测 问题现象 我总共有四行数据&#xff0c;每一行都是一个动画&#xff0c;但是触发第一个之后其他三个也都触发了 我想要的效果是&#xff1a;动画从底部出现的时候触发一个动画&#xff0c;不要都触发掉 解决之后的效果 解决…

智慧卫生间系统:引领公共卫生间管理的新时代@卓振思众

随着城市化进程的加快&#xff0c;公共卫生间的使用频率不断增加。如何提升公共卫生间的使用体验、管理效率以及卫生水平&#xff0c;已成为各地政府和管理者关注的焦点。智慧卫生间系统应运而生&#xff0c;成为解决这一问题的重要工具。它结合了物联网技术和智能管理理念&…

四、Cookie 和 Session

文章目录 1. Cookie 饼干1.1 什么是 Cookie?1.2 如何创建 Cookie1.3 服务器如何获取 Cookie1.4 Cookie 值的修改1.5 浏览器查看 Cookie1.6 Cookie 生命控制&#xff08;指浏览器中Cookie的存在时间&#xff09;1.7 Cookie 有效路径 Path 的设置 2. Session 会话2.1 什么是 Ses…

Canopen-pn有线通信标准在汽车制造中至关重要

电子元件越来越多地被集成到车辆中&#xff0c;从而实现与物联网世界的连接。该行业中主要的高速串行接口方法包括控制器局域网 (CAN) 总线 。CAN 是运输应用中使用的一种强大的总线标准。它旨在允许微控制器(MCU) 和相关组件与彼此的应用程序进行通信。这无需系统具有主机即可…

从入门到精通,带你探索适合新手的视频剪辑工具

用视频来分享生活已经变成越来越多人的一种习惯&#xff0c;很多时候视频并不能一镜到底&#xff0c;所以还需要一些的修改、剪辑操作&#xff0c;那么这次我将介绍几款视频剪辑工具&#xff0c;希望能够让你分享的道路更加通畅。 1.FOXIT视频剪辑 连接直达>>https://w…

【项目设计】Facial-Hunter

目录 一、项目介绍 二、开发环境以及技术 三、项目架构设计 3.1 项目总体架构 3.2 客户端架构 3.3 主服务端架构 3.4 处理服务端架构 3.5 数据库设计 四、FaceNet 五、代码实现 一、项目介绍 该项目是基于深度学习与负载均衡的人脸识别系统 该项目主要由三个部分组…

【Elasticsearch系列十二】聚合-电视案例

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

反射的相关内容

目录 一、什么是反射 二、为什么会有反射 三、反射是如何工作的 获取类信息的三种方式&#xff1a; 例&#xff1a; 四、获取类信息并调用 1.获取 &#xff08;1&#xff09;获取变量 获取全部类信息 获取public修饰的 获取指定某一个 &#xff08;2&#xff09;…

【新手上路】衡石分析平台使用手册-租户管理

租户管理​ 衡石系统支持服务一个平台方和多个企业客户的租户模式&#xff0c;平台方管理租户&#xff0c;为租户提供数据&#xff0c;租户在系统内进行数据分析。 衡石系统增加工作空间的设计&#xff0c;在平台方和租户之间提供单向的传递通道&#xff0c;平台厂商可以轻松…

C++map,set,multiset,multimap详细介绍

目录 1. 关联式容器 2. 键值对 3. 树形结构的关联式容器 3.1 set set的介绍 set的使用 1. set的模板参数列表 2. set的构造 3. set的迭代器 4. set的容量 5. set的修改操作 6. set的使用举例 ​3.2 map map的介绍 map的使用 1. map的模板参数声明 2. map的构造 …

实例讲解电动汽车钥匙ON挡上下电控制策略及Simulink建模方法

在电动汽车VCU开发中&#xff0c;上下电控制是其中一个核心控制内容&#xff0c;也是其他控制功能的基础&#xff0c;而钥匙ON挡上下电又是整车上下电的基础。本文介绍电动汽车钥匙ON挡上下电的控制策略及Simulink建模方法。 目录 一、整车高压原理 二、钥匙ON挡上下电控制策…

计算机毕业设计 办公用品管理系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

Aegisub字幕自动化及函数篇(图文教程附有gif动图展示)(一)

目录 自动化介绍 bord 边框宽度 随机函数 fsvp 随机颜色 move 自动化介绍 自动化介绍:简单来说自动化能让所有字幕行快速拥有你指定的同一种特效 对时间不同的行应用相同的效果 只要设计好一个模板&#xff0c;然后让所有行都执行这个模板上的特效就好了 首先制作模板行…

kafka消息发送几种方式

同步发送 or 异步发送 消息发送根据是否需要处理发送的结果分为同步发送、异步发送。 同步发送&#xff1a;等待发送结果返回&#xff0c;这种方式是可靠的&#xff0c;因为异常能及时处理&#xff0c;但同步发送需要阻塞等待一条消息发送完才处理下一条&#xff0c;吞吐量差。…

esp32核心跑分程序

https://github.com/ochrin/coremark/tree/esp32 最近一直捣腾esp32s3 (Sense) 做微型摄像。过程中发现一款不错的跑分软件&#xff0c;特此记一笔。 其中针对esp32s3各类参数设定&#xff08;用idf.py menuconfig)&#xff0c;做个记录。 CPU Frequency去240MHz&#xff08…

探索网络世界:TCP/IP协议、Python Socket编程及日常生活比喻

网络介绍 &#x1f310; 网络就像是一个无形的纽带&#xff0c;把我们所有人&#x1f46b;&#x1f46b;&#x1f46b;紧紧相连。没有网络的世界&#xff0c;就像是失去了色彩的画布&#xff0c;多么的单调和无趣啊&#xff01;&#x1f3a8; 单机游戏 &#x1f3ae; 单机游…

ER 图 Entity-Relationship (ER) diagram 101 电子商城 数据库设计

起因&#xff0c; 目的: 客户需求, 就是要设计一个数据库。 过程&#xff0c; 关于工具: UI 设计&#xff0c;我最喜欢的工具其实是 Canva, 但是 Canva 没有合适的模板。我用的是 draw.io, 使用感受是&#xff0c;很垃圾。 各种快捷键不适应&#xff0c;箭头就是点不住&…

VuePress搭建文档网站/个人博客(详细配置)主题配置

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…