0


【Linux】IPC 进程间通信(二)(共享内存)

✨ ****莫道浮云终蔽日,总有云开雾散时 **** 🌏

📃个人主页:island1314

🔥个人专栏:Linux—登神长阶

⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞


**一、初识共享内存 🚀 **

1. 理解

进程间通信的本质是:先让不同的进程,看到同一份资源

共享内存区是最快的IPC形式

  1. 一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核
  2. 换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据

🚀 共享内存是一种进程间通信(IPC)机制,它允许多个进程直接访问同一块内存区域,从而实现高效的数据交换。

特点如下:

  • 不同于其他形式的进程间通信(如管道、消息队列等),共享内存无需数据复制,而是通过映射到进程的虚拟内存空间来进行读写操作
  • 共享内存的本质就是开辟一块物理内存,让多个进程映射同一块物理内存到自己的地址空间进行访问,实现数据共享的
  • 共享内存的操作是非进程安全的,多个进程同时对共享内存读写是有可能会造成数据的交叉写入或读取,造成数据混乱
  • 共享内存的删除操作并非直接删除,而是拒绝后续映射,只有在当前映射链接数为0时,表示没有进程访问了,才会真正被删除
  • 共享内存生命周期随内核,只要不删除,就一直存在于内核中,除非重启系统(当然这里指的是非手动操作,可以手动删除)

2. 工作原理

共享内存的工作原理基于操作系统的内存管理系统。具体步骤如下:

  1. 创建共享内存区域:一个进程首先通过系统调用(如 shmget 在Unix/Linux中)请求操作系统为它创建一个共享内存区域。这块内存区域在物理内存中分配,并且通过进程的虚拟内存映射到各个进程的地址空间中。
  2. 映射共享内存:一旦共享内存区域创建成功,其他进程可以通过 shmat 系统调用将这块共享内存映射到它们的地址空间中。所有映射到这块内存的进程可以直接读取和修改数据。
  3. 数据共享:因为所有进程直接操作的是同一块内存区域,它们之间可以快速地交换数据,而无需通过数据复制或其他中介手段。
  4. 解除映射和删除共享内存:使用完共享内存后,进程通过 shmdt 解除映射,操作系统可以通过 shmctl 删除共享内存区域。

  • 进程在进行动态库加载时,动态库会通过页表映射到进程地址空间的共享区中。如果有多个进程要加载同一个动态库,动态库加载到内存后会被这些进程共同使用。

🔥 所以根据动态库加载的原理,操作系统可以在内存中创建一个共享内存空间,再通过页表映射到两个进程的共享区中,这样两个进程就可以看到同一份资源了

注意:

  • 共享内存在系统中可以同时存在多份,供不同对进程进行通信
  • 共享内存不是简单的一段内存空间,它也要有描述并管理共享内存的数据结构和匹配算法

3. 为什么共享内存可以在 OS 内部同时存在多个

  1. 资源共享:让多个进程高效地共享数据,减少内存使用和提高性能
  2. 数据一致性:操作系统需要确保在并发访问时候,数据的一致性和完整性,以防止数据竞争和不一致的情况发生
  3. 权限控制:共享内存可以帮助 OS 控制特点进程来访问内存区域,从而提高安全性
  4. 资源分配:OS 负责分配共享内存地物理内存空间,确保系统资源的有效利用

二、共享内存函数 **🖊 **

  • ftok: 测试代码生成一个唯一的key
  • shmget:测试代码创建一个共享内存段
  • shmat: 测试代码将共享内存段连接到当前进程并写入数据
  • ****shmdt: ****测试代码将共享内存段与当前进程脱离
  • shmctl: 测试代码获取共享内存段的状态,并最终删除它

1. 创建 shmid - shmget 函数

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

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

功能:创建一个新的共享内存段,或者获取一个已存在的共享内存段

参数:

  1. key:共享内存段的标识符。用于命名共享内存段,在服务器和客户端之间共享。由用户自己给值确定,通常使用 ftok 函数随机定值
  2. size:共享内存段的大小,建议是页大小(一般是4096字节)的整数倍
  3. shmflg:权限标志和控制标志,可以组合使用:(如何实现操作?) 1. IPC_CREAT:如果共享内存段不存在,则创建它;如果存在,则返回其标识符2. IPC_EXCL:单独使用没意义,只有和IPC_CREAT组合才有意义3. IPC_CREAT | IPC_EXCL:如果共享内存段不存在则创建它;如果已存在,则返回错误。4. 权限标志(如文件权限)给出访问权限(如 0666 表示用户、组、其他都可读写)

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

2. 标识 key - ftok 函数

在理解这个函数之前,我们先来想一想为什么共享内存的 key 值要用户传递?就不能让内核自动生成嘛?

  1. 灵活性和控制:用户可以自定义和管理 key 值,避免自动生成的 key 可能带来的冲突。
  2. 进程间共享与同步:用户指定 key 值有助于进程间共享同一块内存区域。
  3. 简化内核设计:减少内核的复杂性和管理开销。
  4. 调试和维护:有助于开发人员理解和调试应用。

由于不能让内核生成,因此那就只能自己创建,并且需要让这两个进程都能看到

  • 但是让用户自己设定一个又不好,因为既没有一定的规律,还可能出现大量重复的key,然后导致创建shm失败,因此我们就使用 ftok 函数 (公共路径 + 公共项目 ID) 来创建 key 值,可以极大地 减少 key 值冲突

ftok 函数 原型

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

key_t ftok(const char *pathname, int proj_id) 

功能:用于根据文件的属性(如inode编号)生成一个唯一的键值

参数:

  1. pathname 是文件路径名,指向系统中的一个现有文件或目录(任意路径,通信进程使用相同的路径即可)
  2. proj_id 是项目标识符,通常为一个字符或整数(任意字符或整数)。即相同路径和项目标识符生成的唯一键值是相同的。

返回值:成功返回 key 唯一键值;失败返回 -1,并设置 errno 来指示错误

共享内存被删除后,则其他线程直接无法通信 ❓

  • 这句话是错误地,共享内存的删除操作并非直接删除,而是拒绝后续映射,只有在当前映射链接数为0时,表示没有进程访问了,才会真正被删除

shmid vs key

  • key:属于用户形成,内核使用的一个字段,用户不能用key来进行shm的管理。它是内核用来进行区分shm的唯一性的。
  • shmid:内核给用户返回的一个标识符,用来进行用户级对共享内存进行管理的id值。

key 保证了操作系统内的唯一性,shmid 只在你的进程内,用来表示资源的唯一性
只有在 shmget() 函数 时候用 key,大部分情况用户访问共享内存,都用的是** shmid**

3. 挂接 -- shmat 函数

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

功能:将共享内存段连接到进程地址空间

参数

  1. shmid:共享内存标识
  2. shmaddr:指定共享内存连接到当前进程的地址空间的起始地址
  3. shmflg:指定连接共享内存的权限标志,常用的权限标志有SHM_RDONLY(只读连接)和SHM_RDONLY,其他情况默认为读写模式(参数传0)

返回值:成功返回指向共享内存的起始地址;失败返回-1

shmaddr 说明:

  1. shmaddr为NULL,核心自动选择一个地址
  2. shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
  3. shmaddr不为NULL且 shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr -(shmaddr % SHMLBA)

4. 去挂接 -- shmdt 函数

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

功能:将共享内存段与当前进程脱离

参数:

  • shmaddr: 由shmat所返回的指针

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

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

5. 控制 -- shmctl 函数

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

功能:控制共享内存段

参数:

  1. shmid:共享内存段标识符,由 shmget 返回。内核当中获取共享内存的属性
  2. cmd:1. IPC_STAT:获取共享内存段的当前关联值。2. IPC_SET:设置共享内存段的当前关联值(需要足够权限)。3. IPC_RMID:删除共享内存段。
  3. buf:指向 shmid_ds 结构的指针。struct shmid_ds buf;创建一下

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

三、共享内存的释放♻️

🐸 共享内存和文件不同,不会随着进程的结束而自动释放,需要我们手动释放(指令或者其他系统调用),否则会一直存在内存中,直到系统重启。

  • 共享内存的生命周期随内核,文件的生命周期随进程。

① 系统命令 ipcs -m 查看已存在的共享内存

② 系统命令 ipcrm -m 共享内存标识符(shmid) 删除指定的共享内存

**四、共享内存使用案例 🔖 **

情况一:(实现内****存共享)

ShareMemory.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdalign.h>
#include <unistd.h>

const std::string gpath = "/home/lighthouse/112";
int gprojId = 0x6666;
// 操作系统,申请空间,是按照块为单位:4kb 1kb
int gshmsize = 4096; // 共享内存大小,建议使用 4096 的整数倍
mode_t gmode = 0600;

class ShareMemory
{
private:
    int CreateShmHelper(int shmflg)
    {
        key_t k = ::ftok(gpath.c_str(), gprojId);
        if(k < 0)
        {
            std::cerr << "ftok error"  << std::endl;
            return -1;
        }
        int shmid = ::shmget(k, gshmsize, shmflg);
        if(shmid < 0)
        {
            std::cerr << "shmget error" << std::endl;
            return -2;
        }
        std::cout << "shmid: " << shmid << std::endl;
        return shmid;
    }

public:
    ShareMemory(){}
    ~ShareMemory(){}
    int CreateShm()
    {
        return CreateShmHelper(IPC_CREAT | IPC_EXCL | gmode);
    }
    int GetShm()
    {
        return CreateShmHelper(IPC_CREAT);
    }
    void* AttachShm(int shmid)
    {
        void *ret = shmat(shmid, nullptr, 0);
        if((long long) ret == -1)
        {
            return nullptr;
        }
        return ret;
    }
    void DetachShm(void *ret)
    {
        ::shmdt(ret);
        std::cout << "detach done " << std::endl; 
    }
    void DeleteShm(int shmid)
    {
        shmctl(shmid, IPC_RMID, nullptr);
    }

    void ShmMeta()
    {
    }

};

ShareMemory shm;

Server.cc -- 读取

#include <iostream>
#include <unistd.h>
#include <string.h>
#include "ShareMemory.hpp"

int main()
{
    int shmid = shm.CreateShm();
    
    void *addr = shm.AttachShm(shmid);
    sleep(5);
    std::cout << "server attach done" << std::endl;

    shm.DetachShm(addr);
    std::cout << "server detach done" << std::endl;
    sleep(5);

    shm.DeleteShm(shmid);
    std::cout <<  "server delete done" << std::endl;
    return 0;
}

Client.cc -- 写入

#include <iostream>
#include "ShareMemory.hpp"

int main()
{
    int shmid = shm.GetShm();
    
    void *addr = shm.AttachShm(shmid);
    sleep(5);
    std::cout << "client attach done" << std::endl;
    // addr -> 写入
    shm.DetachShm(addr);
    std::cout << "client detach done" << std::endl;
    sleep(5);
    return 0;
}

演示结果如下:

共享内存演示

演示一种错误情况,代码如下:

共享演示错误情况

情况二:(实现IPC,发送固定字符串)

ShareMemory.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdalign.h>
#include <unistd.h>

const std::string gpath = "/home/lighthouse/112";
int gprojId = 0x6666;
// 操作系统,申请空间,是按照块为单位:4kb 1kb
int gshmsize = 4096; // 共享内存大小,建议使用 4096 的整数倍
mode_t gmode = 0600;

class ShareMemory
{
private:
    void CreateShmHelper(int shmflg)
    {
        _key = ::ftok(gpath.c_str(), gprojId);
        if(_key < 0)
        {
            std::cerr << "ftok error"  << std::endl;
            return ;
        }
        _shmid = ::shmget(_key, gshmsize, shmflg);
        if(_shmid < 0)
        {
            std::cerr << "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);
    }

    void *GetAddr()
    {
        return _addr;
    }

    void ShmMeta()
    {
    }

private: //写时拷贝,系统上会自己去区分的
    int _shmid;
    key_t _key;
    void *_addr;
};

ShareMemory shm;

Server.cc -- 读取

int main()
{
    shm.CreateShm();
    shm.AttachShm();
    // 进行IPC
    char *strinfo =(char*)shm.GetAddr();

    //检测: Server 和 client 映射虚拟地址不同
    // sleep(5);
    // printf("server 虚拟地址:%p\n", strinfo); 
    // sleep(5);
    
    while(true)
    {
       printf("%s\n", strinfo);
       sleep(1);
    }

    shm.DetachShm();
    shm.DeleteShm();
    return 0;
}

Client.cc -- 写入

int main()
{
    shm.GetShm();
    shm.AttachShm();
    
    // 进行 IPC 通信
    char *strinfo = (char*)shm.GetAddr();

    // sleep(5);
    // printf("client 虚拟地址:%p\n", strinfo);
    // sleep(5);

    char ch = 'A';
    while(ch <= 'Z')
    {
        sleep(3);
        strinfo[ch - 'A'] = ch; // 这里操作共享内存没用系统调用
        ch++;
    }

    shm.DetachShm();
    return 0;
}

演示结果如下:

共享内存通信演示

  • 结论:两个进程可以在各自的用户空间共享内存块,但是没有加任何保护机制

注意: server 和 client 映射虚拟地址不同

情况三:(实现IPC,发送结构化信息)

给情况二中的ShareMemory.hpp文件 增加如下代码

struct data
{
    char status[32];
    char lasttime[48];
    char image[4000];
};

** Time.hpp**

#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;
}

Server.cc -- 读取

int main()
{
    shm.CreateShm();
    shm.AttachShm();

    // 进行IPC
    struct data *image =(struct data*)shm.GetAddr();
    
    while(true)
    {
        printf("status: %s\n", image->status);
        printf("lasttime: %s\n", image->lasttime);
        printf("image: %s\n", image->image);

        strcpy(image->status, "过期");
        sleep(1);
    }

    shm.DetachShm();
    shm.DeleteShm();
    return 0;
}

Client.cc -- 写入

int main()
{
    shm.GetShm();
    shm.AttachShm();
    
    struct data *image = (struct data*)shm.GetAddr();

    int cnt = 2;
    while(cnt--)
    {
        strcpy(image->status, "最新");
        strcpy(image->lasttime, GetCurrTime().c_str());
        strcpy(image->image, "IsLand1314");

        sleep(3);
    }

    shm.DetachShm();
    return 0;
}

演示结果如下:

共享内存结构化信息演示

情况四:(实现进程同步,通过管道来发信息)

Fifo.hpp

#pragma once

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

// 模拟进程间同步的过程!

const std::string gpipeFile = "./fifo";
const mode_t gfifomode = 0600;
const int gdefultfd = -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;
    }
    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;

Server.cc -- 读取

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

    gpipe.OpenPipeForRead();
    // 进行IPC
    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(1);
    }

    shm.DetachShm();
    shm.DeleteShm();
    return 0;
}

Client.cc -- 写入

int main()
{
    shm.GetShm();
    shm.AttachShm();
    gpipe.OpenPipeForWrite();

    struct data *image = (struct data*)shm.GetAddr();

    //int cnt = 2;
    while(true)
    {
        strcpy(image->status, "最新");
        strcpy(image->lasttime, GetCurrTime().c_str());
        strcpy(image->image, "IsLand1314");
        gpipe.Signal();
        sleep(3);
    }

    shm.DetachShm();
    return 0;
}

演示结果如下:

共享内存和管道演示

题目

  1. 使用代码创建一个共享内存, 支持两个进程进行通信
  2. 进程A 向共享内存当中写 “i am process A”
  3. 进程B 从共享内存当中读出内容,并且打印到标准输出

client.c 文件 (写入)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <unistd.h>
#define SHM_KEY 1234   // 共享内存的键值
#define SHM_SIZE 1024  // 共享内存的大小

int main()
{
    // 1. 建立
    int shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget failed");
        exit(1);
    }
    
    // 2. 挂接
    char *shm_ptr = (char *)shmat(shmid, NULL, 0);
    if (shm_ptr == (char *)-1) {
        perror("shmat failed");
        exit(1);
    }

    // 3. 写数据
    const char *message = "i am process A";
    strcpy(shm_ptr, message);
    printf("Process A: Data write to shared memory %s\n", message);

    sleep(5); // 等待进程B读取数据

    // 4. 去挂接
    if (shmdt(shm_ptr) == -1) {
        perror("shmdt failed");
        exit(1);
    }

    return 0;
}

server.c 文件 (写出)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <unistd.h>

#define SHM_KEY 1234   // 共享内存的键值
#define SHM_SIZE 1024  // 共享内存的大小

int main()
{
    // 1. 建立
    int shmid = shmget(SHM_KEY, SHM_SIZE, 0666);
    if (shmid == -1) {
        perror("shmget failed");
        exit(1);
    }

    
    // 2. 将共享内存附加到当前进程的地址空间
    char *shm_ptr = (char *)shmat(shmid, NULL, 0);
    if (shm_ptr == (char *)-1) {
        perror("shmat failed");
        exit(1);
    }

    // 3. 从共享内存中读取数据并打印
    printf("Process B: Data read from shared memory: %s\n", shm_ptr);

    // 4. 断开共享内存
    if (shmdt(shm_ptr) == -1) {
        perror("shmdt failed");
        exit(1);
    }

    // 5. 删除共享内存
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl failed");
        exit(1);
    }

    return 0;
}

运行结果如下:

**五、共享内存的优缺点 **

优点

  1. 高效性:访问共享内存时没有任何系统调用,因为共享内存被映射到了进程地址空间的共享区,其是用户级别的,不需要将数据拷贝到管道中再拷贝回进程地址空间中,大大减少了拷贝次数,所以共享内存通信速度最快。适合高效的数据交换场景
  2. 低开销:与管道或消息队列等IPC机制相比,使用共享内存可以大大减少内核空间的上下文切换和内存复制开销,减少了性能损耗
  3. 数据共享简便:多个进程可以同时访问同一块内存区域,方便进行实时的数据交换

缺点

  1. 同步问题:共享内存的最大挑战是同步问题。多个进程可能同时访问和修改共享内存,导致数据竞争和不一致。因此,开发者必须显式地使用同步机制,如互斥锁(mutexes)或信号量(semaphores)来确保对共享内存的访问是安全的。
  2. 复杂性:管理共享内存的生命周期和同步机制需要额外的编程工作,这比管道或消息队列等简单的IPC机制更复杂
  3. 地址空间限制:每个进程只能将共享内存映射到自己的虚拟地址空间,而虚拟地址空间的大小是有限的,过大的共享内存可能会超出限制
  4. 安全性问题:由于多个进程共享同一块内存区域,若不小心操作,可能会导致进程间的干扰或数据泄露。因此,共享内存的访问需要严格的权限管理
  5. 保护机制:共享内存没有任何保护机制,客户端向共享内存中写数据时,还没有写完,服务端就会从共享内存中读取数据,导致数据不一致问题

六、总结 📖

共享内存是一种高效的进程间通信方式,允许多个进程访问同一块物理内存区域。它比管道和消息队列更快,因为数据不需要复制。使用共享内存时,进程间需同步访问,防止数据竞争和不一致性问题

★,°*:.☆( ̄▽ ̄)/$:*.°★ 】那么本篇到此就结束啦,如果我的这篇博客可以给你提供有益的参考和启示,可以三连支持一下 !!**

标签: linux c++ 算法

本文转载自: https://blog.csdn.net/island1314/article/details/143429025
版权归原作者 IsLand1314~ 所有, 如有侵权,请联系我们删除。

“【Linux】IPC 进程间通信(二)(共享内存)”的评论:

还没有评论