Linux通信:基于System V共享内存方式实现进程间通信

一、基于共享内存通信方式原理

 在虚拟地址空间中,堆栈之间的区域被称为共享内存区。不同进程间进行通信的前提是看到同一份资源,而共享内存区就是其中之一。操作系统既然可以发生缺页中断申请内存,同样也可以直接外物理内存中申请共享区空间资源,然后通过一定手段让不同进程看到同一块共享内存区,即让不同进程看到同一份资源!!

基于共享内存通信就是先让操作系统申请一块真实物理空间上的共享内存资源。然后让该物理空间挂接到不同进程各自的共享区上,并建立页表(虚拟地址共享区和真实物理空间的映射)! 这样不同进程就可以通过各自页表查找到同一块资源!

 删除时,只需要先将页表中的映射关系等删除,然后释放共享资源即可!!(共享内存通信方式是基于地址空间,不是基于文件。共享资源的生命周期随内核,所以需要用户手动释放)

在这里插入图片描述

如何保证通信间的进程看到的是同一块共享资源?
 共享区资源存在很多,为了确保不同进程看到的是同一份资源,不同的共享资源是不同唯一的,并且需要唯一的标识符(即key)

 共享资源存在很多,所以操作系统要管理共享内存资源(共享内存多大,还有多少空间可以,有多少个进程使用等)。先描述,在组织!操作系统会为每一个共享内存形成一个结构体对象shm,其中包含了共享内存的各自属性和数据。然后在通过链表或其他数据结构将所有的结构体对象组织起来。从而将对内存资源的管理转化为对链表的增删查改!

除此之外,操作系统会在shm结构体中维护一个key,用于标记共享内存,具有唯一性。操作系统不仅会将该标识值保存到结构体对象shm中,也会通过一些手段让打算进行通信的不同进程间也获取到该值(比如不同进程通过相同的文件路径,和一个相同的整型值,操作系统通过内部的相关转换算法就可以让不同进程获取到key

  • 查看共享内存挂载进程控制数目:ipcs -m + shmid。移除:ipcr, -m + shmid

二、共享内存优缺点

2.1 共享内存优缺点

  1. 共享内存的通信方式,不会提供同步机制。共享内存是直接裸露给所有使用者,一定要注意共享内存的使用安全问题!
  2. 共享内存可以提供较大空间。
  3. 共享内存通信方式是最快的。
  4. 共享内存通信方式是基于虚拟地址空间的,但在计算机网络中通信方式是基于文件的。共享内存方式很难和文件进行整合,导致共享内存的通信方式比较独立,导致在实际场景中用处相对不多!

2.2 基于文件通信 vs 共享内存通信

【基于文件的通信方式】:
 Linux下一切皆文件,并且所有的数据转移,本质上都是拷贝。

 所有基于文件的通信方式有如下过程:将键盘上的数据拷贝到用户缓冲区(暂定进程A缓存区)、进程A通过系统调用write将数据拷贝到操作系统内存缓冲区、另一个进程(暂定进程B)通过系统调用接口read将操作系统内核数据拷贝到自身缓冲区中、最后将缓存区数据拷贝到显示器文件中!(共5次拷贝)(当然printf()、scanf()等函数内部还存在语言级缓冲区,没算!)
在这里插入图片描述

【共享内存通信方式】:
&emsp基于共享内存的通信方式A进程可以直接从键盘数据直接写到共享内存中,进程B直接在从共享内存中读取数据打印到显示器上!在这里插入图片描述

  • 通过上述两种通信方式对比,共享内存通信方式是最快的通信方式,没有之一!!!

三、相关函数调用接口

2.1 创建共享内存(shmget)

//原型
 #include <sys/ipc.h>
 #include <sys/shm.h>
 
 int shmget(key_t key, size_t size, int shmflg);

2.1.1 key

key用于标识待申请的共享内存传递!理论上,该值可以随便填充,但为了减少偶然性,我们一般通过操作系统提供的一些算法接口tok生成。函数原型如下:

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

 key_t ftok(const char *pathname, int proj_id);

 我们只需要先让待进行通信的进程间约定特定的文件路径和proj_id,此处的proj_id用户可以自己随意填充。路径和proj_id都是数据,ftok会结合这两个数据,通过内部的转换算法生成一个整型值,该值可能会和已存在的数字冲突,但概率非常小。

2.1.2 size

size为待申请内存的空间大小,建议设置成为n*4096。原因在于操作系统申请资源的最小单位为4MB。

2.1.3 shmflg

shmget函数不仅可以创建共享内存,还可以获取已创建的共享内存的唯一标识key,具体有所传递的shmflg决定的。shmflg参数选项常见的有两种IPC_CREATIPC_EXCL

  1. IPC_CREAT:共享内存不存在就创建,否则就获取唯一标识key
  2. IPC_EXCL单独使用没意义。通常配合IPC_CREAT使用(IPC_CREAT | IPC_EXCL)。此时共享内存资源不存在就创建,否则出错返回,确保创建出的共享内存是全新的!!

 除此之外,shmflg参数还可以传递待创建资源的权限!!如:(IPC_CREAT | IPC_EXCL | 0664)

2.1.4 返回值

 由于共享内存的通信方式不是基于文件的,导致返回值虽然可以标记共享内存,但无法和文件描述符一样,进行各种重定向等工作!而网络通信方式是基于文件的,导致该种通信方式实用性大大受限!

2.2 共享内存段挂接到进程地址空间(shmat)

//原型
 #include <sys/shm.h>

 void *shmat(int shmid, const void *shmaddr, int shmflg);
//返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
  • shmid: 共享内存标识。
  • shmaddr:指定连接的地址。我们通常设为nullptr。表示让操作系统自动选择一块为使用的区域,开辟进程地址空间,然后将共享内存挂接到给地址空间上!
  • shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY,和权限相关。权限我们可以直接在shmget函数中给定。所以我们这里可以直接设为0

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

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

 int shmdt(const void *shmaddr);
  • shmaddr: 由shmat所返回的指针。返回值:成功返回0;失败返回-1
  • 注意:将共享内存段与当前进程脱离不等于删除共享内存段!!

2.4 用于控制共享内存(删除)(shmctl)

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

 int shmctl(int shmid, int cmd, struct shmid_ds *buf));
  • shmid:由shmget返回的共享内存标识码。
  • cmd:将要采取的动作(有三个可取值)
命令说明
IPC_RMID删除共享内存段
IPC_STAT(获取)将shmid_ds结构中的数据设置为共享内存当前的关联值
IPC_SET(设置)在进程权限足够的前提下,将共享内存的当前关联值设为shmid_ds数据结构中给出的数据
  • buf用于获取当前的共享内存的相关属性信息,具体如下:
struct shmid_ds {
    struct ipc_perm shm_perm;    /* Ownership and permissions */
    size_t          shm_segsz;   /* Size of segment (bytes) */
    time_t          shm_atime;   /* Last attach time */
    time_t          shm_dtime;   /* Last detach time */
    time_t          shm_ctime;   /* Last change time */
    pid_t           shm_cpid;    /* PID of creator */
    pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
    shmatt_t        shm_nattch;  /* No. of current attaches */
    ...
};

struct ipc_perm {
    key_t          __key;    /* Key supplied to shmget(2) */
    uid_t          uid;      /* Effective UID of owner */
    gid_t          gid;      /* Effective GID of owner */
    uid_t          cuid;     /* Effective UID of creator */
    gid_t          cgid;     /* Effective GID of creator */
    unsigned short mode;     /* Permissions + SHM_DEST and
                                SHM_LOCKED flags */
    unsigned short __seq;    /* Sequence number */
};

四、基于System V共享内存实现进程间通信

 接下来我们会创建两个进程clientserver,用于不同进程间通信测试!这里博主规定在server端创建共享内存、读取数据,client端仅向已创建好的共享内存中进行写入!
 由于共享内存方式的进程通信不具备同步机制。这也就意味着我们在写端向共享内存写数据还未写完前,读端就可以直接读数据。读端同理。这就会导致数据失效。所以我们在共享内存的基础上,创建命名管道。利用命名管道同步机制,来控制System V版本进程通信的同步机制!(向命名管道写入信号码,整型code;读端获取到code后遭读取共享内存中的数据)

3.1 GetKey()、CreaterShm、GetShm

 由于不同进程间通信都需要key,和唯一标识(暂时称为shmid)。所以我们可以先在xxx.chh文件中封装事先好相关接口。初次之外,我们还将封装了命名管道创建接口,具体如下:

const std::string pathname = "/home/wzy/Stu/lesson27/shm";//共享内存
const int proj_id = 0x11223344;
const int size = 4096; // 共享内存的大小,设置成为n*4096
const std::string filename = "fifo";//命名管道

key_t GetKey()//获取key值
{
    key_t key = ftok(pathname.c_str(), proj_id);
    if (key < 0)
    {
        std::cout << "errno:" << errno << " strerror" << strerror(errno) << std::endl;
        exit(0);
    }
    return key;
}

//将直接获取接唯一标识符、创建共享内存后间接唯一标识符接口统一
int GetShmHelper(key_t key, int flag)
{
    int shm = shmget(key, size, flag);
    std::cout << shm << " "  << "get" << std::endl;
    if (shm < 0)
    {
        std::cout << "errno:" << errno << " strerror" << strerror(errno) << std::endl;
        exit(2);
    }
    return shm;
}

//创建共享内存,获取唯一标识符
int CreaterShm(key_t key)
{
    return GetShmHelper(key, IPC_CREAT | IPC_EXCL | 0644);
}

//直接获取唯一标识符(共享内存已经存在)
int GetShm(key_t key)
{
    return GetShmHelper(key, IPC_CREAT);
}

//创建命名管道
bool Makefifo()
{
    int n = mkfifo(filename.c_str(), 0666);
    if (n < 0)
    {
        std::cerr << "errno: " << errno << "errstring: " << strerror(errno) << std::endl;
        return false;
    }
    std::cout << "creater fifo success!" << std::endl;
    return true;
}

3.2 server端创建共享内存、并建立联系

前置工作

server端的主要工作有:创建共享内存,将共享内存挂接到地址空间,打开命名管道读端、读取共享内存数据(具体合适读取有命名管道控制)、移除共享内存和地址空间映射关系、删除共享内存,关闭命名管道读端!
 所以我们可以将:创建共享内存,将共享内存挂接到地址空间、打开命名管道读端;移除共享内存和地址空间映射关系、删除共享内存、关闭命名管道读端;这些工作封装为一个类,让代码更加优雅!!

class Init
{
public:
    Init()
    {
    	// 1. 创建管道
        bool r = Makefifo(); 
        if (!r)
            exit(1);
            
        // 2.共享内存
        key_t key = GetKey();
        shmid = CreaterShm(key);

        // 3. 虚拟地址空间挂接
        std::cout << "server: 开始将shm映射到虚拟地址空间!" << std::endl;
        s = (char *)shmat(shmid, nullptr, 0);

        // 4. 打开管道
        rfd = open(filename.c_str(), O_RDONLY);
        if (rfd < 0)
        {
            std::cerr << "errno: " << errno << "errstring: " << strerror(errno) << std::endl;
            exit(1);
        }
    }

    ~Init()
    {
        // 1. 移除shm映射关系
        int shmrm = shmdt(s);
        if (shmrm == 0)
            std::cout << "server: 移除成功!" << std::endl;
        else
            std::cout << "server: 移除失败!" << std::endl;

        // 2. 删除共享内存空间
        int rm = shmctl(shmid, IPC_RMID, nullptr);
        if (rm < 0)
            std::cout << "从OS中移除失败" << std::endl;
        else
            std::cout << "从OS中成功移除" << std::endl;
            
          // 3. 关闭命名管道写端
        close(rfd);
    }

public:
    int shmid;
    char *s;
    int rfd;
};

整体代码

 下面我们直接定义Init类的变量,直接调用析构函数,自动完成:创建共享内存,将共享内存挂接到地址空间、打开命名管道读端3向工作!!

 接下来就是读取共享内存数据了。我们通过read读取命名管道中的数据;如果读取到命名管道数据,此时让server端开始读取命名管道中的数据。然后进入下一次循环!

 当上述工作做完后,自动调用Init类的变量的析构函数,回收资源!!

【代码如下】:

int main()
{
	//1. 创建Init类的变量
    Init init;
    
    //开始读取共享内存中的数据
    int code = 0;
    while (true)
    {
        ssize_t n = read(init.rfd, &code, sizeof(code));
        if (n > 0)
        {
            std::cout << "共享内存数据: " << init.s << std::endl;
            sleep(1);
        }
        else
        {
            break;
        }
    }
    return 0;
}

3.3 client端和共享内存建立联系

  既然server端已经创建好了共享内存和命名管道了, client端只需要直接获取该共享内存的唯一标识。然后将该共享内存挂接到自己的地址空间上即可。
 此时我们就可以直接向共享内存中写入数据。当 client端共享内存写完后,立即向命名管道发生任务码(暂定为code),告诉server端可以开始读取共享内存数据了。

 当上述工作做完后, client端的使命就已经完成。最后移除共享内存和地址空间映射关系、关闭命名管道写端即可。

【代码如下】:

int main()
{
	// 1. 获取唯一标识符shmid
    key_t key = GetKey();
    int shmid = GetShm(key);

    // 2. 虚拟地址空间挂接
    char *s = (char *)shmat(shmid, nullptr, 0);
	
	// 3.打开命名管道写端
    int wfd = open(filename.c_str(), O_WRONLY);
    if (wfd < 0)
    {
        std::cerr << "errno: " << errno << "errstring: " << strerror(errno) << std::endl;
        return 1;
    }

    //4. 开始写入数据
    char in = 'a';
    for (; in <= 'z'; in++)
    {
        s[in - 'a'] = in;
        sleep(1);
        
		//发生任务码,通知server端
        int code = 1;
        write(wfd, &code, sizeof(code));
    }

    // 5. 移除shm映射关系、关闭写端
    std::cout << "server:开始将shm从进程地址空间中移除" << std::endl;
    int shmrm = shmdt(s);
    if (shmrm == 0)
        std::cout << "client: 移除成功!" << std::endl;
    else
        std::cout << "client: 移除失败!" << std::endl;

    close(wfd);
    return 0;
}

五、所以代码

commit.chh】:

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

const std::string pathname = "/home/wzy/Stu/lesson27/shm";//共享内存
const int proj_id = 0x11223344;
const int size = 4096; // 共享内存的大小,设置成为n*4096
const std::string filename = "fifo";//命名管道

key_t GetKey()//获取key值
{
    key_t key = ftok(pathname.c_str(), proj_id);
    if (key < 0)
    {
        std::cout << "errno:" << errno << " strerror" << strerror(errno) << std::endl;
        exit(0);
    }
    return key;
}

//将直接获取接唯一标识符、创建共享内存后间接唯一标识符接口统一
int GetShmHelper(key_t key, int flag)
{
    int shm = shmget(key, size, flag);
    std::cout << shm << " "  << "get" << std::endl;
    if (shm < 0)
    {
        std::cout << "errno:" << errno << " strerror" << strerror(errno) << std::endl;
        exit(2);
    }
    return shm;
}

//创建共享内存,获取唯一标识符
int CreaterShm(key_t key)
{
    return GetShmHelper(key, IPC_CREAT | IPC_EXCL | 0644);
}

//直接获取唯一标识符(共享内存已经存在)
int GetShm(key_t key)
{
    return GetShmHelper(key, IPC_CREAT);
}

//创建命名管道
bool Makefifo()
{
    int n = mkfifo(filename.c_str(), 0666);
    if (n < 0)
    {
        std::cerr << "errno: " << errno << "errstring: " << strerror(errno) << std::endl;
        return false;
    }
    std::cout << "creater fifo success!" << std::endl;
    return true;
}

server.cc端】:

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

class Init
{
public:
    Init()
    {
    	// 1. 创建管道
        bool r = Makefifo(); 
        if (!r)
            exit(1);
            
        // 2.共享内存
        key_t key = GetKey();
        shmid = CreaterShm(key);

        // 3. 虚拟地址空间挂接
        std::cout << "server: 开始将shm映射到虚拟地址空间!" << std::endl;
        s = (char *)shmat(shmid, nullptr, 0);

        // 4. 打开管道
        rfd = open(filename.c_str(), O_RDONLY);
        if (rfd < 0)
        {
            std::cerr << "errno: " << errno << "errstring: " << strerror(errno) << std::endl;
            exit(1);
        }
    }

    ~Init()
    {
        // 1. 移除shm映射关系
        int shmrm = shmdt(s);
        if (shmrm == 0)
            std::cout << "server: 移除成功!" << std::endl;
        else
            std::cout << "server: 移除失败!" << std::endl;

        // 2. 删除共享内存空间
        int rm = shmctl(shmid, IPC_RMID, nullptr);
        if (rm < 0)
            std::cout << "从OS中移除失败" << std::endl;
        else
            std::cout << "从OS中成功移除" << std::endl;
            
          // 3. 关闭命名管道写端
        close(rfd);
    }

public:
    int shmid;
    char *s;
    int rfd;
};


int main()
{
	//1. 创建Init类的变量
    Init init;
    
    //开始读取共享内存中的数据
    int code = 0;
    while (true)
    {
        ssize_t n = read(init.rfd, &code, sizeof(code));
        if (n > 0)
        {
            std::cout << "共享内存数据: " << init.s << std::endl;
            sleep(1);
        }
        else
        {
            break;
        }
    }
    return 0;
}

client端】:

#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.chh"

int main()
{
	// 1. 获取唯一标识符shmid
    key_t key = GetKey();
    int shmid = GetShm(key);

    // 2. 虚拟地址空间挂接
    char *s = (char *)shmat(shmid, nullptr, 0);
	
	// 3.打开命名管道写端
    int wfd = open(filename.c_str(), O_WRONLY);
    if (wfd < 0)
    {
        std::cerr << "errno: " << errno << "errstring: " << strerror(errno) << std::endl;
        return 1;
    }

    //4. 开始写入数据
    char in = 'a';
    for (; in <= 'z'; in++)
    {
        s[in - 'a'] = in;
        sleep(1);
        
		//发生任务码,通知server端
        int code = 1;
        write(wfd, &code, sizeof(code));
    }

    // 5. 移除shm映射关系、关闭写端
    std::cout << "server:开始将shm从进程地址空间中移除" << std::endl;
    int shmrm = shmdt(s);
    if (shmrm == 0)
        std::cout << "client: 移除成功!" << std::endl;
    else
        std::cout << "client: 移除失败!" << std::endl;

    close(wfd);
    return 0;
}

六、所有代码(debug版本)

 下面博主在上述代码中,增加调试信息。

commit.chh】:

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

const std::string pathname = "/home/wzy/Stu/lesson27/shm";
const int proj_id = 0x11223344;
const int size = 4096; // 共享内存的大小,设置成为n*4096
const std::string filename = "fifo";

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

int GetShmHelper(key_t key, int flag)
{
    int shm = shmget(key, size, flag);
    std::cout << shm << " "  << "get" << std::endl;
    if (shm < 0)
    {
        std::cout << "errno:" << errno << " strerror" << strerror(errno) << std::endl;
        exit(2);
    }
    return shm;
}

int CreaterShm(key_t key)
{
    return GetShmHelper(key, IPC_CREAT | IPC_EXCL | 0644);
}

int GetShm(key_t key)
{
    return GetShmHelper(key, IPC_CREAT);
}

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

bool Makefifo()
{
    int n = mkfifo(filename.c_str(), 0666);
    if (n < 0)
    {
        std::cerr << "errno: " << errno << "errstring: " << strerror(errno) << std::endl;
        return false;
    }
    std::cout << "creater fifo success!" << std::endl;
    return true;
}

server.cc端】:

#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.chh"

class Init
{
public:
    Init()
    {
        bool r = Makefifo(); // 创建管道
        if (!r)
            exit(1);
        // 创建共享内存
        key_t key = GetKey();
        shmid = CreaterShm(key);
        std::cout << "server: key:" << key << ", shmid:" << shmid << std::endl;

        // 虚拟地址空间挂接
        std::cout << "server: 开始将shm映射到虚拟地址空间!" << std::endl;
        s = (char *)shmat(shmid, nullptr, 0);
        std::cout << s << std::endl;
        // sleep(10);

        // 打开管道
        rfd = open(filename.c_str(), O_RDONLY);
        std::cout << filename.c_str() << ":" << rfd << std::endl;
        if (rfd < 0)
        {
            std::cerr << "errno: " << errno << "errstring: " << strerror(errno) << std::endl;
            exit(1);
        }
    }

    ~Init()
    {
        // 移除shm映射关系
        std::cout << "server: 开始将shm从进程地址空间中移除" << std::endl;
        int shmrm = shmdt(s);
        if (shmrm == 0)
            std::cout << "server: 移除成功!" << std::endl;
        else
            std::cout << "server: 移除失败!" << std::endl;
        // sleep(2);

        // 删除共享内存空间
        std::cout << "开始将shm从OS中删除" << std::endl;
        int rm = shmctl(shmid, IPC_RMID, nullptr);
        if (rm < 0)
            std::cout << "从OS中移除失败" << std::endl;
        else
            std::cout << "从OS中成功移除" << std::endl;
        close(rfd);
    }

public:
    int shmid;
    char *s;
    int rfd;
};

int main()
{

    Init init;

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

    return 0;
}

client端】:

#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.chh"

int main()
{
    key_t key = GetKey();
    int shmid = GetShm(key);
    std::cout << "client: key:" << key << ", shmid:" << shmid << std::endl;

    // 虚拟地址空间挂接
    std::cout << "client: 开始将shm映射到虚拟地址空间!" << std::endl;
    char *s = (char *)shmat(shmid, nullptr, 0);
    std::cout << s << std::endl;
    // sleep(3);

    int wfd = open(filename.c_str(), O_WRONLY);
    if (wfd < 0)
    {
        std::cerr << "errno: " << errno << "errstring: " << strerror(errno) << std::endl;
        return 1;
    }

    // TODO
    char in = 'a';
    for (; in <= 'z'; in++)
    {
        s[in - 'a'] = in;
        sleep(1);
        int code = 1;
        write(wfd, &code, sizeof(code));
    }

    sleep(2);
    // 移除shm映射关系
    std::cout << "server:开始将shm从进程地址空间中移除" << std::endl;
    int shmrm = shmdt(s);
    if (shmrm == 0)
        std::cout << "client: 移除成功!" << std::endl;
    else
        std::cout << "client: 移除失败!" << std::endl;

    close(wfd);
    return 0;
}

5.1 最终结果

请添加图片描述

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

独享你的盛夏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值