【Linux】共享内存

🌻个人主页路飞雪吖~

🌠专栏:Linux


目录

☃️共享内存

[🪄 shmget函数 用来创建共享内存](#🪄 shmget函数 用来创建共享内存)

✨共享内存的管理指令:

[🌠 shmid VS key](#🌠 shmid VS key)

✨共享内存函数

[🍔 shmget() 创建共享内存](#🍔 shmget() 创建共享内存)

[🍔 shmctl() 用于控制共享内存](#🍔 shmctl() 用于控制共享内存)

[🍔 shmat() 将共享内存段连接到自己的进程地址空间](#🍔 shmat() 将共享内存段连接到自己的进程地址空间)

[🍔 shmdt() 将共享内存段与当前进程脱离](#🍔 shmdt() 将共享内存段与当前进程脱离)

✨共享内存的特点:

✨共享内存实现:


☃️共享内存

system V 共享内存

通过系统调用,1.开辟一段物理内存空间,把新开辟的物理内存映射到某一个进程的虚拟地址空间的共享区中的某一个位置【与动态库的加载原理类似】,映射完成之后,2.这个进程对用户返回内存空间的虚拟起始地址,3.另外一个进程类似,也对用户返回内存空间的虚拟起始地址。

让两个进程通过各自的地址空间,映射到同一块物理内存 ---- 共享内存。【先让不同的进程,看到同一份资源】。

共享内存 = 共享内存的内核数据结构 + 内存块

操作系统里可以有多个不同的进程,使用不同的共享内存来通信。共享内存 在任何时刻,可以在OS内部同时存在很多个,所以OS要对共享内存进行管理【先描述,再组织】,共享内存要有唯一的标识符【标识符 --- 用户传入的key】。

🪄 shmget函数 用来创建共享内存

IPC_CREAT:若单独使用,如果shm共享内存不存在,就创建;存在就获取它,并返回 --- 保证调用进程能拿到共享内存。

IPC_EXCL:单独使用无意义;

IPC_CREAT | IPC_EXCL :若shm共享内存不存在,就创建它;若存在,出错返回 --- 只要成功,一定是新的共享内存,不拿老的共享内存。

key_t key :系统调用接口的参数;

1.必须是由用户输入;

2.如何保证shm的唯一性?--- 让用户传入唯一的键值;

这个key,为什么要用户传入?内核自己生成不就行了吗?若内核自己生成 另一个进程就无法判断前一个进程使用的是哪一个shm共享内存,所以要让用户自己传入。

3.怎么保证不同进程看到的是同一个共享内存?

在进程1 和 进程2 自己的源代码上定义一个全局的【int key 要保证key的唯一性不被修改】,这个全局的key分别被两个进程包含,在进程1创建时,把全局的key设置进内核里,设置成功后,进程2也就知道是哪一块共享内存了【key在编码上进程1和进程2都知道】。

如何设置key?设置的key会不会冲突?----- 系统调用 ftok()
🌠共享内存 VS 命名管道 是如何让不同的进程看到同一份资源的?

共享内存 :通过 ftok() 【路径 + 项目ID】;

命名管道 : 通过文件路径

Makefile:

cpp 复制代码
SERVER=server
CLIENT=client 
cc=g++
SERVER_SRC=Server.cc
Client_SRC=Client.cc

.PHONY:all
all: $(SERVER) $(CLIENT)

$(SERVER):$(SERVER_SRC)
	$(cc) -o $@ $^ -std=c++11
$(CLIENT):$(Client_SRC)
	$(cc) -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f $(SERVER) $(CLIENT)

Comm.hpp:

cpp 复制代码
#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

const std::string gpath = "/home/zxl/study/stu0512";
int gprojID = 0x6666;
int gshmsize = 4096;

std::string ToHex(key_t k)
{
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "%0x", k);
    return buffer;
}

Client.cc

cpp 复制代码
#include <iostream>
#include "Comm.hpp"

int main()
{
    key_t k = ::ftok(gpath.c_str(), gprojID);
    std::cout << "k : " << ToHex(k) << std::endl;
    return 0;
}

Server.cc

cpp 复制代码
#include <iostream>
#include "Comm.hpp"

int main()
{
    // 1. 创建 key
    key_t k = ::ftok(gpath.c_str(), gprojID);
    if(k < 0)
    {
        std::cerr << "fork error" << std::endl;
        return 1;
    }
    std::cout << "k : " << ToHex(k) << std::endl;

    // 2. 创建共享内存 && 获取
    int shmid = ::shmget(k, gshmsize, IPC_CREAT | IPC_EXCL);
    if(shmid < 0)
    {
        std::cout << "shmget error" << std::endl;
        return 2;
    }
    std::cout << "shmid : " << shmid << std::endl;


    return 0;
}

共享内存的生命周期:随内核!

  1. 用户必须让OS释放;

• 指令释放

• 代码编写(共享内存函数)

  1. OS重启。

✨共享内存的管理指令:

ipcs -m:查看共享内存的属性

ipcrm -m 1 :删除指定的共享内存

shmid 是按照顺序排序的 --- 类似于数组下标

🌠****shmid VS key

• shmid : 只给用户用的一个标识shm的标识符【类似 fd 和 FILE*】。

• key :只作为内核中,区分shm唯一性的标识符,不作为用户管理 shm 的 id 值【类似文件描述符表中,struct file 的地址】。

✨共享内存函数

🍔 shmget() 创建共享内存

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

参数:

• key : 这个共享内存段名字

• size :共享内存大小

• shmflg :由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的。

• 返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1。

🌻****注意:共享内存也有权限!!!

🍔 shmctl() 用于控制共享内存

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

参数:

• shmid:由shmget返回的共享内存标识码;

• cmd:将要采取的动作(有三个可取值);

• buf:指向一个保存着共享的模式状态和访问权限的数据结构;

返回值:成功返回 0,失败返回 -1

|--------------|--------------------------------------------|
| 命令 | 说明 |
| IPC_STAT | 把shmid_ds结构中的数据设置为共享内存的当前关联值 |
| IPC_SET | 在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds 数据结构中给 |
| IPC_RMID | 删除共享内存段 |

对共享内存 用户层,进行完整的生命周期的管理,创建 --到--> 释放。

Server.cc :IPC_RMID 删除共享内存

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include "Comm.hpp"

int main()
{
    // 1. 创建 key
    key_t k = ::ftok(gpath.c_str(), gprojID);
    if(k < 0)
    {
        std::cerr << "fork error" << std::endl;
        return 1;
    }
    std::cout << "k : " << ToHex(k) << std::endl;

    // 2. 创建共享内存 && 获取
    // 注意:共享内存也有权限!
    int shmid = ::shmget(k, gshmsize, IPC_CREAT | IPC_EXCL | gmode);
    if(shmid < 0)
    {
        std::cout << "shmget error" << std::endl;
        return 2;
    }
    std::cout << "shmid : " << shmid << std::endl;

    sleep(1);

    // n. 删除共享内存
    shmctl(shmid, IPC_RMID, nullptr);
    std::cout << "delete shm done" << std::endl;

    sleep(5);


    return 0;
}

🍔 shmat() 将共享内存段连接到自己的进程地址空间

这个进程应该如何与共享内存关联呢? --- shmat () 系统调用

在底层:把申请好的共享内存 和 基于现在的进程地址空间里 开辟一个地址空间【虚拟地址】,要知道共享内存的开始和长度,虚拟地址空间在内核当中【mm_struct --> vm_area_struct,里面插入一个新的节点,构建一个start,再加一个偏移量】就能够映射到地址空间,同时填充一下页表。

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

• shmid:共享内存标识;

• shmaddr :指定连接的地址;【用户指定,挂接到什么虚拟地址】

• shmflg:它的两个可能取值是 SHM_RND 和 SHM_RDONLY ;

• 返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1。
🌠说明:

**•**shmaddr为NULL,核⼼⾃动选择⼀个地址

**•**shmaddr不为NULL且shmflg⽆SHM_RND标记,则以shmaddr为连接地址。

**•**shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会⾃动向下调整为SHMLBA的整数倍。

**•**公式:shmaddr - (shmaddr % SHMLBA)

**•**shmflg=SHM_RDONLY,表⽰连接操作⽤来只读共享内存

**Server.cc :**将共享内存段连接到自己的进程地址空间

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include "Comm.hpp"

int main()
{
    // 1. 创建 key
    key_t k = ::ftok(gpath.c_str(), gprojID);
    if(k < 0)
    {
        std::cerr << "fork error" << std::endl;
        return 1;
    }
    std::cout << "k : " << ToHex(k) << std::endl;

    // 2. 创建共享内存 && 获取
    // 注意:共享内存也有权限!
    int shmid = ::shmget(k, gshmsize, IPC_CREAT | IPC_EXCL | gmode);
    if(shmid < 0)
    {
        std::cout << "shmget error" << std::endl;
        return 2;
    }
    std::cout << "shmid : " << shmid << std::endl;

    sleep(1);

    // 3. 共享内存挂接到自己的地址空间中
    void *ret = shmat(shmid, nullptr, 0);
    std::cout << "attach done:" << (long long)ret << std::endl;

    sleep(1);

    // n. 删除共享内存
    shmctl(shmid, IPC_RMID, nullptr);
    std::cout << "delete shm done" << std::endl;

    sleep(5);


    return 0;
}

🌠小贴士:

void *ret = shmat(shmid, nullptr, 0); // 挂接失败,原因:

int shmid = ::shmget(k, gshmsize, IPC_CREAT | IPC_EXCL | gmode); // gmode:权限的大小【0600 自己设置】

perms: 的大小表示是否允许,创建的共享内存,是否允许

注意:共享内存也有权限,和文件一样。

🍔 shmdt() 将共享内存段与当前进程脱离

当不使用共享内存时,要去关联【把映射关系去掉】。

int shmdt(const void *shmaddr);

参数:

• shmaddr:由shmat所返回的指针;

• 返回值:成功返回 0,失败返回 -1;

注意:将共享内存段与当前进程脱离不等于删除共享内存段。

**Server.cc :**将共享内存段与当前进程脱离

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include "Comm.hpp"

int main()
{
    // 1. 创建 key
    key_t k = ::ftok(gpath.c_str(), gprojID);
    if(k < 0)
    {
        std::cerr << "fork error" << std::endl;
        return 1;
    }
    std::cout << "k : " << ToHex(k) << std::endl;

    // 2. 创建共享内存 && 获取
    // 注意:共享内存也有权限!
    int shmid = ::shmget(k, gshmsize, IPC_CREAT | IPC_EXCL | gmode);
    if(shmid < 0)
    {
        std::cout << "shmget error" << std::endl;
        return 2;
    }
    std::cout << "shmid : " << shmid << std::endl;

    sleep(1);

    // 3. 共享内存挂接到自己的地址空间中
    void *ret = shmat(shmid, nullptr, 0);
    std::cout << "attach done:" << (long long)ret << std::endl;

    sleep(1);

    // 4. 将共享内存段与当前进程脱离
    ::shmdt(ret);
    std::cout << "detach done: "  << std::endl;
    
    sleep(1);

    // n. 删除共享内存
    shmctl(shmid, IPC_RMID, nullptr);
    std::cout << "delete shm done" << std::endl;

    sleep(1);


    return 0;
}

建立完共享资源后【让不同的进程看见同一块资源】,该如何进行通信?

🌠小贴士:

操作系统,申请空间,是按照块为单位的:4KB,1KB,2KB,4MB;

当你申请一个4097个字节的空间【4096 --> 4KB】,对于操作系统来讲,在内部会申请 4096 * 2 的空间,但是用户只能用 4097 个字节,这样子就会造成空间浪费。

🌠小贴士:

strinfo[ch - 'A'] = ch; // 这里操作shm的时候,怎么没有用系统调用?

管道建立一旦完成之后,不管命名还是匿名,都需要用 read() 和 write() 来进行调用,把数据从用户拷贝到文件的缓冲去里面,然后从文件的缓冲区里面,通过read() 和 write() 拷贝到自己的用户缓冲区。【管道的缓冲区,没有指针的概念,用的是文件描述符struct file 只能通过系统调用来进行文件读写】

共享内存这里为什么不用系统调用呢?进程的地址空间当中【代码区、数据区、字符常量区、未/已初始化数据区、堆区、栈区、堆栈之间的共享区】这些都是用户空间!【当共享内存被挂接到虚拟地址空间上之后,用户可以直接使用,不需要使用系统调用,如同malloc出来的空间一样】。

✨共享内存的特点:

1. 通信速度最快的; 【将共享内存挂接到进程虚拟地址空间里面,就意味着作为共享内存,在用户空间拿虚拟地址,直接就能访问,往共享内存里面写东西,另一个进程也能立马获取】

2. 让两个进程在各自的用户空间共享内存块,但是 没有加任何的保护机制!【有无数据都一直读,另一个进程退出,也不会对当前进程限制,会一直读】

3. 共享内存,保护机制,需要由用户完成保护 --- 信号量 --- 命名管道。

共享内存就是这两个进程的共享资源,共享资源被加保护,叫做临界资源。访问公共资源的代码,叫临界区,其余的正常代码为非临界区。

把共享资源变成临界资源,保护起来,往往需要给临界区代码进行加锁。

进程具有独立性,两个进程要进行通信,就要让这两个进程能看到同一份资源,当读和写不完整【还没写完就读】,就会造成数据不一致的问题,为此就有了 临界区 && 临界资源 && 加锁 && 同步!

✨共享内存实现:

Makefile:

cpp 复制代码
SERVER=server
CLIENT=client 
cc=g++
SERVER_SRC=Server.cc
Client_SRC=Client.cc

.PHONY:all
all: $(SERVER) $(CLIENT)

$(SERVER):$(SERVER_SRC)
	$(cc) -o $@ $^ -std=c++11
$(CLIENT):$(Client_SRC)
	$(cc) -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f $(SERVER) $(CLIENT)

Fifo.hpp:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

const std::string gpipeFile = "./fifo"; // 公共文件
const mode_t gfifomode = 0600;
const int gdefault = -1;
const int gsize = 1024;
const int gForRead = O_RDONLY;
const int gForWrite = O_WRONLY;

class Fifo
{
private:
    void OpenFifo(int flag)
    {
        // 如果读端打开文件时,写端还没有打开,读端对应的open就会阻塞
        _fd = ::open(gpipeFile.c_str(), flag);
        if (_fd < 0)
        {
            std::cerr << "open error" << std::endl;
        }
    }

public:
    Fifo() : _fd(-1)
    {
        umask(0);
        int n = ::mkfifo(gpipeFile.c_str(), gfifomode);
        if (n < 0)
        {
            std::cerr << "mkfifo error" << std::endl;
            return;
        }
        std::cout << "mkfifo success" << std::endl;

        // sleep(10);
    }

    bool OpenPipeForwrite()
    {
        OpenFifo(gForWrite);
        if (_fd < 0)
            return false;
        return true;
    }

    bool OpenPipeForRead()
    {
        OpenFifo(gForRead);
        if (_fd < 0)
            return false;
        return true;
    }

    int wait()
    {
        int code = 0;
        ssize_t n = ::read(_fd, &code, sizeof(code));
        if (n == sizeof(code))
        {
            return 0;
        }
        else if (n == 0)
        {
            return 1;
        }
        else
        {
            return 2;
        }
    }

    void Signal()
    {
        int code = 1;
        ::write(_fd, &code, sizeof(code));
    }

    ~Fifo()
    {
        if (_fd >= 0)
            ::close(_fd);
        int n = ::unlink(gpipeFile.c_str());
        if (n < 0)
        {
            std::cerr << "unlink error" << std::endl;
            return;
        }
        std::cout << "unlink success" << std::endl;
    }

private:
    int _fd;
};

Fifo gpipe;

ShareMemory.hpp:

cpp 复制代码
#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "StructFile.hpp"


const std::string gpath = "/home/zxl/study/stu0512";
int gprojID = 0x6666;
// 操作系统,申请空间,是按照块为单位的:4KB、1KB、2KB、4MB
int gshmsize = 4096;
mode_t gmode = 0600;

std::string ToHex(key_t k)
{
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "%0x", k);
    return buffer;
}

class ShareMemory
{
private:
    void CreateShmHelper(int shmflg)
    {
        _key = ::ftok(gpath.c_str(), gprojID);
        if (_key < 0)
        {
            std::cerr << "fork error" << std::endl;
            return;
        }

        _shmid = ::shmget(_key, gshmsize, shmflg);
        if (_shmid < 0)
        {
            std::cout << "shmget error" << std::endl;
            return;
        }
        std::cout << "shmid : " << _shmid << std::endl;
    }

public:
    ShareMemory() : _shmid(-1), _key(0), _addr(nullptr)
    {
    }
    ~ShareMemory() {}

    // 创建共享内存
    void CreateShm()
    {
        if (_shmid == -1)
            CreateShmHelper(IPC_CREAT | IPC_EXCL | gmode);
    }

    // 获取
    void GetShm()
    {
        CreateShmHelper(IPC_CREAT);
    }

    // 挂接
    void AttachShm()
    {
        _addr = shmat(_shmid, nullptr, 0);
        if ((long long)_addr == -1)
        {
            std::cout << "attach error" << std::endl;
        }
    }

    // 将共享内存段与当前进程脱离
    void DetachShm()
    {
        if (_addr != nullptr)
            ::shmdt(_addr);
        std::cout << "detach done: " << std::endl;
    }

    // 删除共享内存
    void DeleteShm()
    {
        shmctl(_shmid, IPC_RMID, nullptr);
        std::cout << "delete shm done" << std::endl;
    }

    void *GetAddr()
    {
        return _addr;
    }

    void ShmMeta()
    {
    }

private:
    int _shmid;
    key_t _key;
    void *_addr;
};

ShareMemory shm;

StructFile.hpp:

cpp 复制代码
#include <iostream>

struct data
{
    // 409 --> 4KB
    char status[32];
    char lasttime[48];
    char image[4000];
};

Time.hpp:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <ctime>

std::string GetCurrTime()
{
    time_t t = time(nullptr);
    struct tm *curr = ::localtime(&t);

    char currtime[32];
    snprintf(currtime, sizeof(currtime), "%d-%d-%d %d:%d:%d",
        curr->tm_year + 1900,
        curr->tm_mon + 1,
        curr->tm_mday,
        curr->tm_hour,
        curr->tm_min,
        curr->tm_sec
    );
    return currtime;
}

Client.cc

cpp 复制代码
#include <iostream>
#include <string.h>
#include "ShareMemory.hpp"
#include "Time.hpp"
#include "Fifo.hpp"


int main()
{
    shm.GetShm();
    shm.AttachShm();
    gpipe.OpenPipeForwrite();
    // sleep(10);
    // std::cout << "Client attach done" << std::endl;

    // 在这里进行IPC
    // char *strinfo = (char*)shm.GetAddr();
    // printf("Client虚拟地址:%p\n", strinfo);

    // char ch = 'A';
    // while(ch <= 'Z')
    // {
    //     sleep(3);
    //     strinfo[ch - 'A'] = ch;// 这里操作shm的时候,怎么没有用系统调用?
    //     ch++;
    // }

    // 把共享内存当做一个结构体
    struct data *image = (struct data *)shm.GetAddr(); 
    char ch = 'A';
    while (ch <= 'Z')
    {
        strcpy(image->status, "最新");// 在缓冲区里面拷贝内容
        strcpy(image->lasttime, GetCurrTime().c_str());
        strcpy(image->image, "zxxxxxxxxxxxxxxxxxx");

        gpipe.Signal();
        
        sleep(3);
    }

    shm.DetachShm();
    // std::cout << "Client detach done" << std::endl;;
    // sleep(10);

    return 0;
}

// int main()
// {
//     key_t k = ::ftok(gpath.c_str(), gprojID);
//     std::cout << "k : " << ToHex(k) << std::endl;
//     return 0;
// }

Server.cc

cpp 复制代码
#include <iostream>
#include <string.h>
#include <unistd.h>
#include "ShareMemory.hpp"
#include "Time.hpp"
#include "Fifo.hpp"


int main()
{
    //std::cout << "time: " << GetCurrTime() << std::endl; 
    shm.CreateShm();
    shm.AttachShm();

    gpipe.OpenPipeForRead();

    // sleep(10);
    // std::cout << "Server attach done" << std::endl;
    
    //sleep(10);

    // 在这里进行IPC
    // char *strinfo = (char*)shm.GetAddr();
    // printf("Server虚拟地址:%p\n", strinfo);

    // 若共享资源被保护起来了,访问公共资源的代码 ---- 临界区
    // while(true)
    // {
    //     printf("%s\n", strinfo);
    //     sleep(1);
    // }

    // 把共享内存当做一个结构体
    struct data *image = (struct data*)shm.GetAddr();
    
    while(true)
    {
        gpipe.wait();// 等待写入完成,再输出
        
        printf("status: %s\n", image->status);
        printf("lasttime: %s\n", image->lasttime);
        printf("image: %s\n", image->image);
        strcpy(image->status, "过期");

        //sleep(5);
    }


    // sleep(10);

    shm.DetachShm();
    // std::cout << "Server detach done" << std::endl;
    // sleep(10);
    shm.DeleteShm();
    // std::cout << "delete shm" << std::endl;

    return 0;
}

// int main()
// {
//     // 1. 创建 key
//     key_t k = ::ftok(gpath.c_str(), gprojID);
//     if(k < 0)
//     {
//         std::cerr << "fork error" << std::endl;
//         return 1;
//     }
//     std::cout << "k : " << ToHex(k) << std::endl;

//     // 2. 创建共享内存 && 获取
//     // 注意:共享内存也有权限!
//     int shmid = ::shmget(k, gshmsize, IPC_CREAT | IPC_EXCL | gmode);
//     if(shmid < 0)
//     {
//         std::cout << "shmget error" << std::endl;
//         return 2;
//     }
//     std::cout << "shmid : " << shmid << std::endl;

//     sleep(5);

//     // 3. 共享内存挂接到自己的地址空间中
//     void *ret = shmat(shmid, nullptr, 0);
//     std::cout << "attach done:" << (long long)ret << std::endl;

//     sleep(5);

//     // 4. 将共享内存段与当前进程脱离
//     ::shmdt(ret);
//     std::cout << "detach done: "  << std::endl;

//     sleep(5);

//     // 在这里进行通信

//     // n. 删除共享内存
//     shmctl(shmid, IPC_RMID, nullptr);
//     std::cout << "delete shm done" << std::endl;

//     sleep(5);

//     return 0;
// }

如若对你有帮助,记得关注、收藏、点赞哦~ 您的支持是我最大的动力🌹🌹🌹🌹!!!

若有误,望各位,在评论区留言或者私信我 指点迷津!!!谢谢 ヾ(≧▽≦*)o \( •̀ ω •́ )/

相关推荐
DKPT2 分钟前
Java桥接模式实现方式与测试方法
java·笔记·学习·设计模式·桥接模式
好好学习啊天天向上32 分钟前
世上最全:ubuntu 上及天河超算上源码编译llvm遇到的坑,cmake,ninja完整过程
linux·运维·ubuntu·自动性能优化
好奇的菜鸟1 小时前
如何在IntelliJ IDEA中设置数据库连接全局共享
java·数据库·intellij-idea
tan180°2 小时前
MySQL表的操作(3)
linux·数据库·c++·vscode·后端·mysql
典学长编程2 小时前
Linux操作系统从入门到精通!第二天(命令行)
linux·运维·chrome
wuk9982 小时前
基于MATLAB编制的锂离子电池伪二维模型
linux·windows·github
DuelCode3 小时前
Windows VMWare Centos Docker部署Springboot 应用实现文件上传返回文件http链接
java·spring boot·mysql·nginx·docker·centos·mybatis
优创学社23 小时前
基于springboot的社区生鲜团购系统
java·spring boot·后端
幽络源小助理3 小时前
SpringBoot基于Mysql的商业辅助决策系统设计与实现
java·vue.js·spring boot·后端·mysql·spring
猴哥源码3 小时前
基于Java+springboot 的车险理赔信息管理系统
java·spring boot