MMAP 实现了一个可以跨进程通信打Boss的组件(二)

简介: MMAP 实现了一个可以跨进程通信打Boss的组件

干Boss


image.png

通过前辈留下的印记,我们发现Boss主要有三个技能

  • initMMAP
  • setData
  • getData

好,我们打Boss前主要就是,见招拆招,只要能躲过这三个技能,我们就可以无伤通关,下面开始拆招

拆招


同样我们通过前辈的了解,发现boss mmap有六个参数

void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);
  • addr 代表映射的虚拟内存起始地址;
  • length 代表该映射长度;
  • prot 描述了这块新的内存区域的访问权限;
  • flags 描述了该映射的类型;
  • fd 代表文件描述符;
  • offset 代表文件内的偏移值;

关键在于对length、prot、flags、 fd四个参数的调整,来让boss变化形态。boss的核心设计如下

m_ptr = (char *) ::mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
  • m_ptr 将boss指针交给它,后面通过它来打boss
  • m_size 设计boss怒气值,怒气值满了就死掉,不是血条哈,因为这个boss免疫任何伤害,但就怕被人给它东西,一给东西就涨怒气值,自己气自己,你说气人不
  • PROT_READ | PROT_WRITE,设计可读可写
  • MAP_SHARED,设计共享
  • m_fd,boss真身的位置,这个boss比较狡猾,喜欢保护自己,如果突然断电了,这boss能跑掉,你只能下次宰它了

这样一来,我们的boss对象就有着落了,为了打他方便,我们把所有变量都存起来,然后设计一些工具方法来揍他

class MMIPC {
    int m_fd = -1;  //文件句柄,boss的真身
    string m_path;  //文件的路径,用于初始化文件句柄,boss真身的位置
    char *m_ptr;    //mmap内存的指针,boss的替身,对它的伤害最终会作用到真身上
    size_t m_size;  //mmap内存映射大小,boss的怒气值,越打越大,满了会自杀,估计是被气死了
    size_t default_mmap_size; //系统规定的默认缓存页大小,怒气值的大小限制就靠它了
    size_t m_position = 0;  //当前mmap内存数据记录的位置,当前怒气值位置
public:
    ~MMIPC() { doCleanMemoryCache(true); } //对象销毁时自动调用
    void doCleanMemoryCache(bool forceClean); //boss死的时候要清理战场
    bool open();//链接Boss真身
    void close();//关闭真身的链接,可以打扫战场了
    bool truncate(size_t size);//刷新怒气值
    bool mmap();//构建怒气值槽
    void reloadMmap(const string &path);//多个人来打boss时,重新链接boss
    void setData(const string &key, const string &value);//打他
    string getData(const string &key, const string &value);//同步怒气值给别的人
    bool isFileValid() { return m_fd >= 0; }//检查这个boss能不能被打
};

通过如上,我们是不是终于摸透了Boss,那就打吧开始,还墨迹啥。

开始


由于initMMAP无法限制游戏角色调几次,所以需要设计一个多次调用的兼容逻辑,防止重新加载出现异常,于是在initMMAP中,调用如下函数

void MMIPC::reloadMmap(const string &path) {
    m_path = path;
    // 重复加载时,文件句柄已经有效,所以先清理下缓存
    if (isFileValid()) {
        doCleanMemoryCache(false);
    }
    // 获取系统默认缓存页大小,也就是boss的怒气槽大小
    default_mmap_size = getPageSize();
    // 如果打开文件失败就直接打印错误日志
    if (!open()) {
        ALOGD("fail to open [%s], %d(%s)", m_path.c_str(), errno, strerror(errno));
    } else {
        // 如果打开成功,这里通过m_fd获取实际文件大小,重新给m_size赋值
        getFileSize(m_fd, m_size);
        // 如果文件大小小于一个缓存页大小,且不等0
        if (m_size < default_mmap_size || (m_size % default_mmap_size != 0)) {
            // 获取比m_size 小 m_size*default_mmap_size分之一的值,这个大小说明:在怒气槽即将满时,给boss留点空间,好逃跑不是
            size_t roundSize = (m_size / default_mmap_size + 1) * default_mmap_size;
            // 看下方函数解释
            truncate(roundSize);
        } else {
            // 如果 m_size 大于一个缓存页大小,重新创建一个boss文件真身,继续干他
            auto ret = mmap();
            if (!ret) {
                // 如果创建替身失败了,boss就怂了
                doCleanMemoryCache(true);
            }
        }
    }
}
// 此函数的目的是为了改变文件大小,也就是boss怒气槽的大小
bool MMIPC::truncate(size_t size) {
    // 文件句柄无效,还打个p,直接收拾装备回家吧,boss跑了
    if (!isFileValid()) {
        return false;
    }
    // 如果怒气槽大小和当前大小一样,直接返回,不需要更新
    if (size == m_size) {
        return true;
    }
    // 如果传进来的大小和旧的不一样,记录下当前怒气槽大小
    auto oldSize = m_size;
    // 直接改变当前怒气槽大小
    m_size = size;
    // 按照m_size开始改变文件m_fd怒气槽的大小
    if (::ftruncate(m_fd, static_cast<off_t>(m_size)) != 0) {
        ALOGE("truncate failed [%s] to size %zu, %s", m_path.c_str(), m_size, strerror(errno));
        m_size = oldSize;
        return false;
    }
    // 改变成功后,判断下旧的是否比现在的大
    if (oldSize > m_size) {
        // 如果比现在的大,改变文件的读写位置,并在合适的位置进行填充0
        if (!zeroFillFile(m_fd, oldSize, m_size - oldSize)) {
            ALOGE("zeroFile fail [%s] to size %zu, %s", m_path.c_str(), m_size,
                  strerror(errno));
            m_size = oldSize;
            return false;
        }
    }
    // 如果发现boss有替身,解除替身的引用,把替身受到的伤害,写入文件里
    if (m_ptr) {
        if (munmap(m_ptr, oldSize) != 0) {
            ALOGE("munmap fail [%s], %s", m_path.c_str(), strerror(errno));
        }
    }
    // 继续创建新的替身,又可以愉快的揍他了
    auto ret = mmap();
    if (!ret) {
        // 如果创建失败,则释放掉
        doCleanMemoryCache(true);
    }
    return ret;
}

boss替身在这里

bool MMIPC::mmap() {
    m_ptr = (char *) ::mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
    if (m_ptr == MAP_FAILED) {
        ALOGD("mmap failed");
        m_ptr = nullptr;
        return false;
    }
    ALOGD("mmap success");
    return true;
}

boss真身在这里

bool MMIPC::open() {
    //判断文件是否已经打开过
    if (isFileValid()) {
        return true;
    }
    //O_RDWR 可读可写,O_CREAT 如果没有则创建
    m_fd = ::open(m_path.c_str(), O_RDWR | O_CREAT, S_IRWXU);
    if (!isFileValid()) {
        //fail to open
        return false;
    }
    //输出文件句柄的值,可以判断m_fd的值是否==-1,来判断是否获取失败
    ALOGD("open m_fd[%d], %s", m_fd, m_path.c_str());
    return true;
}
void MMIPC::close() {
    if (isFileValid()) {
        // 关闭
        if (::close(m_fd) == 0) {
            m_fd = -1;
        } else {
            //fail to close
        }
    }
}
  • open 通过游戏角色给的路径m_path,我们定位到boss的真身位置,并获取boss文件句柄
  • close 关闭boss文件句柄

真实伤害


void MMIPC::setData(const string &key, const string &value) {
    // 组合伤害,憋大招
    string content = key + ":" + value;
    if (m_position != 0) {
        content = "," + content;
    }
    ALOGD("setData content=%s", content.c_str());
    size_t numberOfBytes = content.length();
    // 如果伤害溢出,则抛异常给上面
    if (m_position + numberOfBytes > m_size) {
        auto msg = "m_position: " + to_string(m_position) + ", numberOfBytes: " +
                   to_string(numberOfBytes) +
                   ", m_size: " + to_string(m_size);
        throw out_of_range(msg);
    }
    // 对替身施加伤害,每次都在上次的m_position之后,累加伤害值
    memcpy(m_ptr + m_position, (void *) content.c_str(), numberOfBytes);
    // 刷新上次的m_position
    m_position += numberOfBytes;
    ALOGD("setData success m_ptr=%s", m_ptr);
}
// 其他跨进程的角色也可以同步到伤害的值
string MMIPC::getData(const string &key, const string &value) {
    return m_ptr;
}

验证


image.png

  • App被初始化两次,多进程初始化后都能见到Boss
  • MainActivty,揍了两下
  • OtherProcessActivity,获取MainActivty在主进程中打的伤害,完美跨进程通信

这里面看到两个有价值的信息

  • m_fd = 78 两个进程是同样的虚拟地址,有个疑问,它们是一个虚拟地址么?(答案:不是的,有问题需要自己去探索,去搜吧)
  • 缓存页的大小都是 4096,也就是4kb

复盘


打完boss我们做个小复盘,以便后面打的更快更爽。

做的好的

  • 首先值得肯定是完成了,很多时候完成是第一步,设计过渡应避免
  • 层级划分清晰,各个模块职责分明,功能实现干净利索不拖泥大水

做的不好的

  • Boss真身在Sdcard,被别人上大号,连接上后,一刀抢走怎么办
  • 伤害值,传输过程被别人拦截怎么办,本来你砍了1万点伤害,被别人改成1点
  • 如果跨线程、跨进程都来了伤害,你怎么保证这些伤害都能正常到Boss身上

如何改进

  • 有个办法就是将 真身放到data/data/包名目录下,有人说你为啥不用 mmap参数中 MAP_ANONYMOUS 属性,因为它只能父子进程通信,应用的子进程其实是zygote 的子进程,非你的,我把你当兄弟,你把我当儿子,但data/data下也有风险,如果root手机,你的通信数据就有可能被人篡改
  • 如何防止被篡改呢?建议pb+加密,pb保证传输快,效率高,加密保证不被篡改
  • 如果跨线程可以通过线程锁来实现,如果跨进程怎么办,我看binder实现中用到了MAP_PRIVATE,这个参数有个好处就是,当你往映射内存里写的时候,内核会先copy一份出来再写,这边避免其他进程读的时候不能读,这样一来是不是就好了呢,当然需要验证的(已验证,MAP_PRIVATE方式,拷贝再写入的值,并不会被刷的文件中,也就无法实现共享),多进程的写入,需要写测试用例验证。

以上改进,Doing .......

游戏副本在哪?


说了这么多,游戏副本在哪?欢迎跟我一起来打Boss

github.com/ibaozi-cn/m…


目录
相关文章
|
1天前
|
存储 Unix Linux
进程间通信方式-----管道通信
【10月更文挑战第29天】管道通信是一种重要的进程间通信机制,它为进程间的数据传输和同步提供了一种简单有效的方法。通过合理地使用管道通信,可以实现不同进程之间的协作,提高系统的整体性能和效率。
|
1天前
|
消息中间件 存储 供应链
进程间通信方式-----消息队列通信
【10月更文挑战第29天】消息队列通信是一种强大而灵活的进程间通信机制,它通过异步通信、解耦和缓冲等特性,为分布式系统和多进程应用提供了高效的通信方式。在实际应用中,需要根据具体的需求和场景,合理地选择和使用消息队列,以充分发挥其优势,同时注意其可能带来的复杂性和性能开销等问题。
|
23天前
|
存储 Python
Python中的多进程通信实践指南
Python中的多进程通信实践指南
14 0
|
2月前
|
Java Android开发 数据安全/隐私保护
Android中多进程通信有几种方式?需要注意哪些问题?
本文介绍了Android中的多进程通信(IPC),探讨了IPC的重要性及其实现方式,如Intent、Binder、AIDL等,并通过一个使用Binder机制的示例详细说明了其实现过程。
256 4
|
3月前
|
Linux
Linux源码阅读笔记13-进程通信组件中
Linux源码阅读笔记13-进程通信组件中
|
3月前
|
消息中间件 安全 Java
Linux源码阅读笔记13-进程通信组件上
Linux源码阅读笔记13-进程通信组件上
|
3月前
|
消息中间件 存储 安全
python多进程并发编程之互斥锁与进程间的通信
python多进程并发编程之互斥锁与进程间的通信
|
3月前
|
存储 C语言
【C语言】进程间通信之存储映射区mmap
【C语言】进程间通信之存储映射区mmap
40 0
|
4月前
|
运维 关系型数据库 MySQL
掌握taskset:优化你的Linux进程,提升系统性能
在多核处理器成为现代计算标准的今天,运维人员和性能调优人员面临着如何有效利用这些处理能力的挑战。优化进程运行的位置不仅可以提高性能,还能更好地管理和分配系统资源。 其中,taskset命令是一个强大的工具,它允许管理员将进程绑定到特定的CPU核心,减少上下文切换的开销,从而提升整体效率。
掌握taskset:优化你的Linux进程,提升系统性能
|
4月前
|
弹性计算 Linux 区块链
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
144 4
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)

相关实验场景

更多