【Linux】System V 共享内存

文章目录

  • 一、System V共享内存的原理
      • 共享内存的内核数据结构
  • 二、共享内存的使用
      • 1. 创建
          • shmget()系统调用创建shm
          • 在命令行中查询共享内存
      • 2. 释放
          • 使用命令释放共享内存资源
          • 使用shmctl释放共享内存资源
      • 3. 关联
      • 4. 去关联
  • 三、用共享内存实现server&client通信

一、System V共享内存的原理

共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存。

共享内存的内核数据结构

在系统当中可能会有大量的进程在进行通信,因此系统当中就可能存在大量的共享内存,那么操作系统必然要对其进行管理,所以共享内存除了在内存当中真正开辟空间之外,系统一定还要为共享内存维护相关的内核数据结构。

共享内存的数据结构如下:

/* Obsolete, used only for backwards compatibility and libc5 compiles */
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 */
};

可以看到上面共享内存数据结构的第一个成员是shm_perm,shm_perm是一个ipc_perm类型的结构体变量,每个共享内存的key值存储在shm_perm这个结构体变量当中,其中ipc_perm结构体的定义如下:

/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct ipc_perm
{
    __kernel_key_t  key;
    __kernel_uid_t  uid;
    __kernel_gid_t  gid;
    __kernel_uid_t  cuid;
    __kernel_gid_t  cgid;
    __kernel_mode_t mode;
    unsigned short  seq;
};

在Linux系统中,key 是一个关键字段,用于标识 System V IPC 对象的唯一性。对于共享内存(Shared Memory),key 的唯一性保证了在系统范围内的IPC对象之间的区分。key 是一个32位的整数值,用于唯一标识一个IPC对象,包括共享内存。开发者在创建IPC对象时可以指定一个 key 值,而系统会根据这个值来唯一地标识该对象。它的特性如下:

  • 唯一性: 每个IPC对象的 key 都应该是唯一的。在系统中,通过不同的 key 值来区分不同的共享内存段。如果两个共享内存段的 key 相同,它们将被视为同一IPC对象。

  • 用户自定义: 开发者可以自己选择 key 的值,通常可以使用 ftok() 函数将文件路径和项目标识符(project identifier)转换为 key 值。这种方法可以确保在不同的程序中使用相同的文件路径和项目标识符生成相同的 key 值,从而在不同的进程间共享同一个IPC对象。

我们待会实现server&client通信就用到了这种方法,pathname和proj_id被server和client进程共用,以便使用 ftok() 来生成出相同的key,唯一确定共享的内存。

const std::string pathname="/home/chen/linux-learning/shm";
const int proj_id = 0x11223344;
  
// 共享内存的大小,建议设计成4096的整数倍
const int size = 4096;
  
key_t GetKey()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    if(key < 0)
    {
        std::cerr << "errno: " << errno 
                  << ", errstring: " << strerror(errno) 
                  << std::endl;
        exit(1);
    }
}

二、共享内存的使用

1. 创建

shmget()系统调用创建shm

创建共享内存我们需要用shmget函数,shmget函数的函数原型如下:
请添加图片描述

shmget的参数说明:

  • 第一个参数key,表示待创建共享内存在系统当中的唯一标识。
  • 第二个参数size,表示待创建共享内存的大小。
  • 第三个参数shmflg,表示创建共享内存的方式。

shmget的返回值说明:

  • shmget调用成功,返回一个有效的共享内存标识符(用户层标识符)。
  • shmget调用失败,返回-1。

注意:
我们把具有标定某种资源能力的东西叫做句柄,而这里shmget函数的返回值实际上就是共享内存的句柄,这个句柄可以在用户层标识共享内存,当共享内存被创建后,我们在后续使用共享内存的相关接口时,都是需要通过这个句柄对指定共享内存进行各种操作

说明一下参数key和shmflg:

  1. 关于传入shmget函数的第一个参数key,需要我们使用ftok函数进行获取

ftok函数的函数原型如下:
请添加图片描述
ftok函数的作用就是,将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值。将来在用shm实现进程间通信的时候,不同的进程使用相同的pathname和proj_id,通过ftok得到的key都是相同的,把key给到shmget即可获得相同的共享内存句柄。

const std::string pathname="/home/chen/linux-learning/shm";
const int proj_id = 0x11223344;
const int size = 4096;// 共享内存的大小,建议设计成4096的整数倍

key_t GetKey()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    if(key < 0)
    {
        std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        exit(1);
    }
    return key;
}

[!Question] 为什么要用户自己设置key,而不是操作系统帮我们做?
ftok函数的作用就是,将一个已存在的路径名pathname和一个整数标识符proj_id转换成一个key值。将来在用shm实现进程间通信的时候,不同的进程使用相同的pathnameproj_id,通过ftok得到的key都是相同的,而且这个key只有这些进程自己知道,把key给到shmget即可获得相同的共享内存的句柄,以使用约定的那块共享内存。


  1. 第三个参数shmflg,常用的组合方式有以下两种:

组合方式作用
IPC_CREAT如果内核中不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则直接返回该共享内存的句柄
PC_CREAT|IPC_EXCL如果内核中不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;如果存在这样的共享内存,则出错返回

甚至可以设置权限 (int32位中的最低的9位) 指定授予所有者、组和全局的权限。格式和含义与open(2)的mode参数相同。
请添加图片描述

在命令行中查询共享内存

使用ipcs命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:

  • -q:列出消息队列相关信息。
  • -m:列出共享内存相关信息。
  • -s:列出信号量相关信息。

ipcs命令输出的每列信息的含义如下:

标题含义
key系统区别各个共享内存的唯一标识
shmid共享内存的用户层id(句柄)
owner共享内存的拥有者
perms共享内存的权限
bytes共享内存的大小
nattch关联共享内存的进程数
status共享内存的状态

[!Attention] 注意:

  • key: 不要在应用层使用,key只用来在内核中标识shm的唯一性! - 类比文件描述符 fd
  • shmid:应用这个共享内存的时候,我们使用shmid来进行操作共享内存。 - 类比 FILE*

2. 释放

如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此),同时也说明了IPC资源是由Linux内核提供并维护的。

使用命令释放共享内存资源

可以使用ipcrm -m shmid命令释放指定id的共享内存资源

ipcrm -m [shmid]

请添加图片描述

使用shmctl释放共享内存资源

shmctl 用于对共享内存段进行控制操作,可以实现共享内存的删除功能。下面是 shmctl 系统调用的函数原型:

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

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • 参数说明:

    1. shmid: 共享内存标识符,它是由 shmget 函数返回的标识符,用于唯一标识一个共享内存段。
    2. cmd: 控制命令,表示对共享内存的执行的操作。可以使用以下命令:
      • IPC_STAT: 获取共享内存的状态信息,将共享内存的信息填充到 buf 中。
      • IPC_SET: 设置共享内存的状态信息,使用 buf 中提供的信息。
      • IPC_RMID: 删除共享内存段,释放资源。
    3. buf: 一个指向 struct shmid_ds 结构的指针,用于传递或接收共享内存的状态信息。
  • 返回值说明:

    • 成功时,返回0。
    • 失败时,返回-1,并设置全局变量 errno 来指示错误的原因。

3. 关联

将共享内存连接到进程地址空间我们需要用shmat函数,shmat函数的函数原型如下:

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

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmat函数的参数说明

    • 第一个参数shmid,表示待关联共享内存的用户级标识符。
    • 第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置。
    • 第三个参数shmflg,表示关联共享内存时设置的某些属性。
  • shmat函数的返回值说明

    • shmat调用成功,返回共享内存映射到进程地址空间中的起始地址。
    • shmat调用失败,返回(void*)-1。

4. 去关联

取消共享内存与进程地址空间之间的关联我们需要用shmdt函数,shmdt函数的函数原型如下:

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

int shmdt(const void *shmaddr);
  • shmdt函数的参数说明
    待去关联共享内存的起始地址,即调用shmat函数时得到的起始地址。

  • shmdt函数的返回值说明

    • shmdt调用成功,返回0。
    • shmdt调用失败,返回-1。

三、用共享内存实现server&client通信

在知道了共享内存的创建、关联、去关联以及释放后,现在可以尝试让两个进程通过共享内存进行通信了。

为了让服务端和客户端在使用ftok函数获取key值时,能够得到同一种key值,那么服务端和客户端传入ftok函数的路径名和和整数标识符必须相同,这样才能生成同一种key值,进而找到同一个共享资源进行挂接。这里我们可以将这些需要共用的信息放入一个头文件当中,服务端和客户端共用这个头文件即可。

共用的头文件:comm.h

#pragma once

#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

const std::string pathname = "/home/chen/linux-learning/shm";
const int proj_id = 0x112233;

const std::string filename = "fifo";

// 共享内存的大小,建议设计成4096的整数倍
const int size = 4096;

key_t GetKey()
{
    key_t key = ftok(pathname.c_str(), proj_id);
    if (key < 0)
    {
        std::cerr << "ftok, errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        exit(1);
    }

    return key;
}

std::string ToHex(int id)
{
    char buffer[1024];
    snprintf(buffer, sizeof(buffer), "0x%x", id);
    return buffer;
}


int CreateShmHelper(key_t key, int flag)
{
    int shmid = shmget(key, size, flag);
    if (shmid < 0)
    {
        std::cerr << "shmget, errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        exit(2);
    }
}

int CreateShm(key_t key)
{
    // 如果内核中不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;
    // 如果存在这样的共享内存,则出错返回
    return CreateShmHelper(key, IPC_CREAT | IPC_EXCL | 0666);
}

int GetShm(key_t key)
{
    // 如果内核中不存在键值与key相等的共享内存,则新建一个共享内存并返回该共享内存的句柄;
    // 如果存在这样的共享内存,则直接返回该共享内存的句柄
    return CreateShmHelper(key, IPC_CREAT);
}

bool MakeFifo()
{
    int n = mkfifo(filename.c_str(), 0666);
    if (n < 0)
    {
        std::cerr << "mkfifo, errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
        return false;
    }

    std::cout << "mkfifo success... read" << std::endl;
    return true;
}

server端负责创建共享内存,server和client挂接到同一块共享内存之后,client先向共享内存写入‘a’,再通过命名管道fifo来通知server写入完毕,server端的read就读到了管道中的数据,开始打印共享内存中的内容。

server.cpp:

#include "comm.hpp"

class Init
{
public:
    Init()
    {
        // 使用管道通信
        bool r = MakeFifo();
        if (!r) exit(1);

        key_t key = GetKey(); //获取key值
        std::cout << "key: " << key << std::endl;
        // sleep(3);

        // key vs shmid
        // key: 不要在应用层使用,只用来在内核中标识shm的唯一性!  类比fd
        // shmid:应用这个共享内存的时候,我们使用shmid来进行操作共享内存   类比FILE*

        shmid = CreateShm(key);
        std::cout << "shmid: " << shmid << std::endl;
        // sleep(5);

        std::cout << "开始将shm映射到进程的地址空间中" << std::endl;
        s = (char*)shmat(shmid, nullptr, 0);
        std::cout << "映射完成" << std::endl;

        fd = open(filename.c_str(), O_RDONLY); // 打开管道,阻塞等待
        std::cout << "fd: " << fd << std::endl;
    }
    ~Init()
    {
        // sleep(5);
        shmdt(s);
        std::cout << "开始将shm从进程的地址空间中移除" << std::endl;

        // sleep(5);
        shmctl(shmid, IPC_RMID, nullptr);
        std::cout << "开始将shm从OS中删除" << std::endl;

        close(fd);
        unlink(filename.c_str());
    }
public:
    int shmid;
    int fd;
    char* s;
};

int main()
{
    Init init;

    // todo
    while (true)
    {
        // wait
        int code = 0;
        ssize_t n = read(init.fd, &code, sizeof(code));
        if (n > 0)
        {
            std::cout << "共享内存的内容:" << init.s << std::endl;
            sleep(1);
        }
        else if (n == 0)
        {
            break;
        }
    }

    return 0;
}

client.cpp:

#include <iostream>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

#include "comm.hpp"

int main()
{
    key_t key = GetKey();
    int shmid = GetShm(key);
    char* s = (char*)shmat(shmid, nullptr, 0);
    std::cout << "attach shm done" << std::endl;
    int fd = open(filename.c_str(), O_WRONLY);

    // sleep(10);
    char c = 'a';
    for (; c <= 'e'; c++)
    {
        s[c - 'a'] = c;
        std::cout << "client write: " << c << " done!" << std::endl;
        sleep(1);

        // 通知server写入完毕
        int code = 1;
        write(fd, &code, sizeof(code));
    }

    shmdt(s);
    std::cout << "detach shm done" << std::endl;

    //sleep(5);
    close(fd);
    std::cout << "管道写端关闭!!" << std::endl;
    //sleep(5);

    return 0;
}

[!Question] 两个问题:

  1. 为什么server一定会等待client把要写进共享内存中的数据写完?
    实际上server端的read系统调用在读取管道数据的时候,是阻塞等待的,就是说server会卡在read来等待client向管道里写数据,管道里有数据write才会读到数据并返回。也就是说管道里没有数据,程序就卡在read这里了。

  1. 不启动client,server就不会继续,为什么?
    请添加图片描述
    因为server要打开管道的读端,但是会阻塞等待,因为要等到client把管道的写端打开,server中的open才会停止阻塞等待并返回。

梳理一下就是:

  • 在创建一个命名管道(FIFO)之后,如果进程1打开了读端,而其他进程迟迟不打>开它的写端,open 函数在进程1中通常会阻塞,等待直到有其他进程打开了端。

  • 在默认情况下,打开一个管道的读端或写端,如果对应的另一端没有被打开,打开操作会一直阻塞,直到另一端被打开为止。这是因为管道的通信是基于两个进程之间的协作的,当一个进程试图打开读端时,它可能期望有其他进程打开相应的写端,并且在没有写端打开的情况下,读端打开可能会被阻塞。

  • 启动了client之后,server正常打印。当client把管道的写端的fd关闭的时候,server这边的read会直接返回0,server中的死循环被break,然后调用init的析构进行资源清理,然后正常退出。

运行之前可以使用以下监控脚本时刻关注共享内存的资源分配情况:

while :; do ipcs -m;echo "###################################";sleep 1;done

请添加图片描述

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

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

相关文章

【架构】Docker实现集群主从缩容【案例4/4】

实现集群主从缩容【4/4】 接上一节&#xff0c;在当前机器为4主4从的架构上&#xff0c;减缩容量为3主3从架构。即实现删除6387和6388. 示意图如下&#xff1a; 第一步&#xff1a;查看集群情况&#xff08;第一次&#xff09; redis-cli --cluster check 127.0.0.1:6387roo…

负载均衡技术助力企业数字化转型和高可用架构实现

文章目录 什么是高可用架构什么是负载均衡负载均衡设备硬件负载均衡的代表产品软件负载均衡的代表产品 负载均衡基于OSI模型分层负载均衡在网络层负载均衡在传输层负载均衡在应用层 优先考虑负载均衡的场景硬件负载均衡的缺点云负载均衡正在成为最佳选择企业数字化转型对负载均…

Spring-boot项目+Rancher6.3部署+Nacos配置中心+Rureka注册中心+Harbor镜像仓库+NFS存储

目录 一、项目概述二、环境三、部署流程3.1 Harbor部署3.1.1 docker安装3.1.2 docker-compose安装3.1.3 安装证书3.1.4 Harbor下载配置安装 3.2 NFS存储搭建3.3 Rancher平台配置3.3.1 NFS存储相关配置3.3.2 Harbor相关配置3.3.3 Nacos部署及相关配置3.3.4 工作负载deployment配…

石油化工设备状态监测与健康管理

在石油化工行业&#xff0c;设备长时间稳定运行至关重要&#xff0c;而PreMaint作为智能监测与健康管理领域的领军者&#xff0c;正在为该行业设备状态的实时监测、健康管理以及智能诊断提供全新的解决方案。 一、状态监测的必要性 石化行业设备的特殊性质要求对其状态进行持续…

如何在 Mac 中运行 Office 办公软件

虽然 Office 软件也有 Mac 版本的&#xff0c;但是有蛮多小伙伴用起来还是感觉不得劲&#xff0c;毕竟接触了太久的 Windows&#xff0c;所以想要使用 Windows 版本的 Office 软件。 今天就给大家介绍一下怎么在 Mac 电脑中运行 Windows 版本的办公软件&#xff0c;在这里就需…

【EEG信号处理】ERP相关

ERP&#xff0c;全称为event-related potential&#xff0c;中文是事件相关电位。 首先要明确的一点是&#xff0c;ERP是根据脑电图EEG得到的&#xff0c;他是EEG的一部分&#xff0c;是最常用的时域分析方法 可能有一部分是介绍不到的&#xff0c;望谅解 在维基百科中给的定义…

手机屏幕生产厂污废水处理需要哪些工艺设备

随着手机行业的快速发展&#xff0c;手机屏幕生产厂的规模也越来越大&#xff0c;但同时也带来了大量的污废水排放问题。为了保护环境和人类的健康&#xff0c;手机屏幕生产厂需要采取适当的工艺设备来处理污废水。本文将介绍手机屏幕生产厂污废水处理所需的工艺设备。 首先&am…

Spring:JDBCTemplate 的源码分析

一&#xff1a;JdbcTemplate的简介 JdbcTemplate 是 Spring Template设置模式中的一员。类似的还有 TransactionTemplate、 MongoTemplate 等。通过 JdbcTemplate 我们可以使得 Spring 访问数据库的过程简单化。 二&#xff1a;执行SQL语句的方法 1&#xff1a;在JdbcTempla…

提升编程效率的利器: 解析Google Guava库之集合篇RangeSet范围集合(五)

在编程中&#xff0c;我们经常需要处理各种范围集合&#xff0c;例如时间范围、数字范围等。传统的集合类库往往只能处理离散的元素集合&#xff0c;对于范围集合的处理则显得力不从心。为了解决这个问题&#xff0c;Google的Guava库提供了一种强大的数据结构——RangeSet&…

一文掌握 Golang 加密:crypto/cipher 标准库全面指南

一文掌握 Golang 加密&#xff1a;crypto/cipher 标准库全面指南 引言Golang 和加密简介crypto/cipher 库概览使用 crypto/cipher 实现加密高级功能和技巧最佳实践和性能优化总结资源推荐 引言 在现代软件开发领域&#xff0c;安全性是一个不容忽视的重要议题。随着信息技术的…

黑盒测试用例的具体设计方法(7种)

7种常见的黑盒测设用例设计方法&#xff0c;分别是等价类、边界值、错误猜测法、场景设计法、因果图、判定表、正交排列。 &#xff08;一&#xff09;等价类 1.概念 依据需求将输入&#xff08;特殊情况下会考虑输出&#xff09;划分为若干个等价类&#xff0c;从等价类中选…

备战蓝桥杯---数据结构与STL应用(基础实战篇1)

话不多说&#xff0c;直接上题&#xff1a; 当然我们可以用队列&#xff0c;但是其插入复杂度为N,总的复杂度为n^2,肯定会超时&#xff0c;于是我们可以用链表来写&#xff0c;同时把其存在数组中&#xff0c;这样节点的访问复杂度也为o(1).下面是AC代码&#xff1a; 下面我们来…

Vim实战:使用Vim实现图像分类任务(一)

文章目录 摘要安装包安装timm 数据增强Cutout和MixupEMA项目结构编译安装Vim环境环境安装过程安装库文件 计算mean和std生成数据集 摘要 论文&#xff1a;https://arxiv.org/pdf/2401.09417v1.pdf 翻译&#xff1a; 近年来&#xff0c;随着深度学习的发展&#xff0c;视觉模型…

项目解决方案:高清视频监控联网设计方案

目 录 一、客户需求 二、网络拓扑图 三、方案描述 四、服务器配置 五、方案优势 1. 多级控制 2. 平台可堆叠使用 3. 支持主流接入协议 4. 多种终端显示 5. 视频质量诊断 6. 客户端功能强大 7. 一机一档 一、客户需求 客户现场存在两个网络环境&#xff0c…

25考研北大软微该怎么做?

25考研想准备北大软微&#xff0c;那肯定要认真准备了 考软微需要多少实力 现在的软微已经不是以前的软微了&#xff0c;基本上所有考计算机的同学都知道&#xff0c;已经没有什么信息优势了&#xff0c;只有实打实的有实力的选手才建议报考。 因为软微的专业课也是11408&am…

HarmonyOS4.0系统性深入开发31创建列表(List)

创建列表&#xff08;List&#xff09; 概述 列表是一种复杂的容器&#xff0c;当列表项达到一定数量&#xff0c;内容超过屏幕大小时&#xff0c;可以自动提供滚动功能。它适合用于呈现同类数据类型或数据类型集&#xff0c;例如图片和文本。在列表中显示数据集合是许多应用…

[DotNetGuide]C#/.NET/.NET Core优秀项目和框架精选

前言 注意&#xff1a;排名不分先后&#xff0c;都是十分优秀的开源项目和框架&#xff0c;每周定期更新分享&#xff08;欢迎关注公众号&#xff1a;追逐时光者&#xff0c;第一时间获取每周精选分享资讯&#x1f514;&#xff09;。 帮助开发者发现功能强大、性能优越、创新前…

R语言学习case7:ggplot基础画图(核密度图)

step1: 导入ggplot2库文件 library(ggplot2)step2&#xff1a;带入自带的iris数据集 iris <- datasets::irisstep3&#xff1a;查看数据信息 dim(iris)维度为 [150,5] head(iris)查看数据前6行的信息 step4&#xff1a;画图展示 plot2 <- ggplot(iris,aes(Sepal.W…

基于C#制作一个连连看小游戏

基于C#制作一个连连看小游戏,实现:难易度选择、关卡选择、倒计时进度条、得分计算、音效播放等功能。 目录 引言游戏规则开发环境准备游戏界面设计游戏逻辑实现图片加载与显示鼠标事件处理游戏优化与扩展添加关卡与难度选择说明</

wpf 数据转换(Bytes 转 KB MB GB)

效果 后端 using ProCleanTool.Model; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Data;namespace P…