接着上篇聊
此处为语雀内容卡片,点击链接查看:
回归正经
打游戏浪费生命,我们步入正轨,开始进一步改造进程间通信组件,上次我聊到三个痛点总结为两个维度:
- 安全性
- 同步性
安全性,我们通过对mmap映射文件路径的规范,挪至data/data目录下,保证不被其他应用访问到,后续可以通过pb+加密手段来防止root手机随意篡改映射文件内容。
今天我们先来聊聊同步,假设一种场景,就是在客户端Activity中,开启多个线程同时写入mmap内存中,看看会怎样
多线程写入
private fun testMultiThreadWriteMMIPC() { "set data".print(getProcessName()) val countDownLatch = CountDownLatch(2) Thread { var index = 10000 //重复写入一万次 repeat(10000) { index++ MMIPC.setData(index.toString(), index.toString()) } countDownLatch.countDown() }.start() Thread { var index = 20000 //重复写入一万次 repeat(10000) { index++ MMIPC.setData(index.toString(), index.toString()) } countDownLatch.countDown() }.start() //等待两个线程结束 countDownLatch.await() //打印写入数据的长度 MMIPC.getData("").length.toString().print(getProcessName()) }
输出日志
2022-07-20 22:20:46.743 9933-9933/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: init 2022-07-20 22:20:46.744 9933-9933/com.zzy.mmipc D/MMIPC->: open file /data/user/0/com.zzy.mmipc/files 2022-07-20 22:20:46.744 9933-9933/com.zzy.mmipc E/MMIPC->: pid[9933] 2022-07-20 22:20:46.744 9933-9933/com.zzy.mmipc D/MMIPC->: open m_fd[78], /data/user/0/com.zzy.mmipc/files/default_mmap.ipc 2022-07-20 22:20:46.744 9933-9933/com.zzy.mmipc D/MMIPC->: m_fd size[0] 2022-07-20 22:20:46.744 9933-9933/com.zzy.mmipc D/MMIPC->: getFileSize m_file_size 0, default_mmap_size 4161536 2022-07-20 22:20:46.744 9933-9933/com.zzy.mmipc D/MMIPC->: mmap success 2022-07-20 22:20:46.845 9933-9933/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: set data 2022-07-20 22:20:46.913 9933-9933/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: 237004
正常长度等于20000*12 = 240000,因为setData,我会将key和value进行预处理变成如下,所以一次setData的长度是12,那么重试两万次就是 24万
string content = key + ":" + value + ",";
有什么办法可以解决该问题呢,往下看
HandlerThread
利用Looper机制,实现单线程中让所有的任务按顺序执行,直接代码验证效果,创建一个dev-looper分支,然后代码改变如下
然后测试结果如下,这次是240000
2022-07-20 22:43:47.874 11336-11336/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: init 2022-07-20 22:43:47.897 11336-11336/com.zzy.mmipc D/MMIPC->: open file /data/user/0/com.zzy.mmipc/files 2022-07-20 22:43:47.897 11336-11336/com.zzy.mmipc E/MMIPC->: pid[11336] 2022-07-20 22:43:47.897 11336-11336/com.zzy.mmipc D/MMIPC->: open m_fd[76], /data/user/0/com.zzy.mmipc/files/default_mmap.ipc 2022-07-20 22:43:47.897 11336-11336/com.zzy.mmipc D/MMIPC->: m_fd size[0] 2022-07-20 22:43:47.897 11336-11336/com.zzy.mmipc D/MMIPC->: getFileSize m_file_size 0, default_mmap_size 4161536 2022-07-20 22:43:47.897 11336-11336/com.zzy.mmipc D/MMIPC->: mmap success 2022-07-20 22:43:48.265 11336-11336/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: set data 2022-07-20 22:43:56.241 11336-11336/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: 240000
我们想想,这样实现有什么问题,功能没什么问题,但失去了原本用C++实现的好处,那就是跨平台能力,如果再IOS端,我们如何实现呢?接下来我们考虑用C++的方式实现对多线程写入
互斥锁(mutex)
该锁限制同一时间只有一个线程访问数据,实现如下,下面跨进程的时候在详细了解mutex
//声明 pthread_mutex_t m_lock; //setData函数中使用 void MMIPC::setData(const string &key, const string &value) { //加锁 pthread_mutex_lock(&m_lock); string content = key + ":" + value + ","; // ALOGD("setData content=%s", content.c_str()); size_t numberOfBytes = content.length(); if (m_position + numberOfBytes > m_file_size) { auto msg = "m_position: " + to_string(m_position) + ", numberOfBytes: " + to_string(numberOfBytes) + ", m_file_size: " + to_string(m_file_size); throw out_of_range(msg); } m_position = strlen(m_ptr); memcpy(m_ptr + m_position, (void *) content.c_str(), numberOfBytes); // ALOGD("setData success m_ptr.len=%d", m_position + numberOfBytes); //释放锁 pthread_mutex_unlock(&m_lock); }
测试日志
2022-07-20 23:04:10.958 12322-12322/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: init 2022-07-20 23:04:10.959 12322-12322/com.zzy.mmipc D/MMIPC->: open file /data/user/0/com.zzy.mmipc/files 2022-07-20 23:04:10.959 12322-12322/com.zzy.mmipc E/MMIPC->: pid[12322] 2022-07-20 23:04:10.959 12322-12322/com.zzy.mmipc D/MMIPC->: open m_fd[78], /data/user/0/com.zzy.mmipc/files/default_mmap.ipc 2022-07-20 23:04:10.959 12322-12322/com.zzy.mmipc D/MMIPC->: m_fd size[0] 2022-07-20 23:04:10.959 12322-12322/com.zzy.mmipc D/MMIPC->: getFileSize m_file_size 0, default_mmap_size 4161536 2022-07-20 23:04:10.959 12322-12322/com.zzy.mmipc D/MMIPC->: mmap success 2022-07-20 23:04:11.047 12322-12322/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: set data 2022-07-20 23:04:11.187 12322-12322/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: 240000
同样得到正确的结果,结果一样,哪种方式更好呢?这个给你留个作业,自己去验证下两种实现方式的耗时,用耗时来判断哪种更好,我初步判断是差不多的,因为Looper循环起来也是一个个处理的,而这个锁也一样,处理完一个,就处理下一个,但上锁和释放锁是有一定耗时的,估计在量上去后Looper更优秀。所以后面我们验证完了以后,可以用C++实现一个Looper,这样既可以做到跨平台,也可以做到不用锁。这期我们先跳过,优先实现多进程的读写,那么多进程有哪些方式实现同步呢?
多进程写
通过资料的查询,目前找到几种方案,分别是Semaphores和Mutex以及文件锁,下面就一一实践。
Semaphores
信号量提供了一种有效的进程间通信形式。协作进程可以使用信号量来同步对资源的访问,最常见的是共享内存。信号量还可以保护以下可供多个进程使用的资源免受不受控制的访问
- 全局变量,例如文件变量、指针、计数器和数据结构。保护这些变量,防止多个进程同时访问。
- 硬件资源,例如磁盘和磁带驱动器。硬件资源需要受控访问,因为同时访问会导致数据损坏。
看过介绍,发现它可以保护资源被多个进程同时访问,所以先来对它有个详细的了解,然后再用代码实战一下,看看效果。
概述
信号量用于控制进程对共享资源的访问。计数信号量有一个正整数值,表示可以同时锁定信号量的进程数。
分类
- 命名信号量,命名信号量提供对多个进程之间资源的访问。
- 未命名信号量,未命名的信号量提供对单个进程内或相关进程之间的资源的多种访问。
一些信号量函数专门设计用于对命名或未命名信号量执行操作。
如何操作
- 信号量的值为正数,则锁定
- 信号量值递减,进程继续执行,如果信号量的值为零或负数,则请求锁的进程将等待(被阻塞),直到另一个进程解锁该资源。
可能会阻塞多个进程以等待资源变得可用。
另外
信号量是全局实体,不与任何特定进程相关联。从这个意义上说,信号量没有所有者,因此无法出于任何目的(例如,错误恢复)跟踪信号量的所有权。
仅当使用共享资源的所有进程通过在不可用时等待信号量并在放弃资源时增加信号量值来合作时,信号量保护才起作用。由于信号量缺乏所有者,因此无法确定其中一个合作进程是否已变得不合作。使用信号量的应用程序必须仔细详细说明协作任务。共享资源的所有进程必须就哪个信号量控制资源达成一致。
接口
功能 | 描述 |
sem_close | 释放指定的命名信号量 |
sem_destroy | 销毁一个未命名的信号量 |
sem_getvalue | 获取指定信号量的值 |
sem_init | 初始化一个未命名的信号量 |
sem_open | 打开/创建一个命名信号量供进程使用 |
sem_post | 解锁锁定的信号量 |
sem_trywait | 仅当信号量可以锁定信号量而无需等待另一个进程解锁它时才对信号量执行信号量锁定 |
sem_unlink | 删除指定的命名信号量 |
sem_wait | 对信号量执行信号量锁定 |
实战
通过上面的了解,我们先来改造下项目,实现一个跨进程写入的逻辑,如下:
子进程Activity改造
这样改造后,就可以实现多进程同时写操作,我们运行看下结果
2022-07-20 23:59:40.605 16531-16531/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: init 2022-07-20 23:59:40.606 16531-16531/com.zzy.mmipc D/MMIPC->: open file /data/user/0/com.zzy.mmipc/files 2022-07-20 23:59:40.606 16531-16531/com.zzy.mmipc E/MMIPC->: pid[16531] 2022-07-20 23:59:40.606 16531-16531/com.zzy.mmipc D/MMIPC->: open m_fd[78], /data/user/0/com.zzy.mmipc/files/default_mmap.ipc 2022-07-20 23:59:40.606 16531-16531/com.zzy.mmipc D/MMIPC->: m_fd size[0] 2022-07-20 23:59:40.606 16531-16531/com.zzy.mmipc D/MMIPC->: getFileSize m_file_size 0, default_mmap_size 4161536 2022-07-20 23:59:40.606 16531-16531/com.zzy.mmipc D/MMIPC->: mmap success 2022-07-20 23:59:40.704 16531-16531/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: set data 2022-07-20 23:59:40.860 16531-16531/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: 240000 2022-07-20 23:59:40.896 16564-16564/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc:childProcess 日志: init 2022-07-20 23:59:40.897 16564-16564/com.zzy.mmipc D/MMIPC->: open file /data/user/0/com.zzy.mmipc/files 2022-07-20 23:59:40.897 16564-16564/com.zzy.mmipc E/MMIPC->: pid[16564] 2022-07-20 23:59:40.897 16564-16564/com.zzy.mmipc D/MMIPC->: open m_fd[78], /data/user/0/com.zzy.mmipc/files/default_mmap.ipc 2022-07-20 23:59:40.897 16564-16564/com.zzy.mmipc D/MMIPC->: m_fd size[4161536] 2022-07-20 23:59:40.897 16564-16564/com.zzy.mmipc D/MMIPC->: getFileSize m_file_size 4161536, default_mmap_size 4161536 2022-07-20 23:59:40.897 16564-16564/com.zzy.mmipc D/MMIPC->: mmap success 2022-07-20 23:59:46.110 16564-16564/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc:childProcess 日志: 360000
发现由于MainActivity,写入太快了,导致后面子进程,初始化完成前已经写入完整,那我们加大MainActivity的写入次数。
2022-07-21 00:06:43.077 17809-17809/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: init 2022-07-21 00:06:43.080 17809-17809/com.zzy.mmipc D/MMIPC->: open file /data/user/0/com.zzy.mmipc/files 2022-07-21 00:06:43.080 17809-17809/com.zzy.mmipc E/MMIPC->: pid[17809] 2022-07-21 00:06:43.080 17809-17809/com.zzy.mmipc D/MMIPC->: open m_fd[76], /data/user/0/com.zzy.mmipc/files/default_mmap.ipc 2022-07-21 00:06:43.080 17809-17809/com.zzy.mmipc D/MMIPC->: m_fd size[0] 2022-07-21 00:06:43.080 17809-17809/com.zzy.mmipc D/MMIPC->: getFileSize m_file_size 0, default_mmap_size 4161536 2022-07-21 00:06:43.081 17809-17809/com.zzy.mmipc D/MMIPC->: mmap success 2022-07-21 00:06:43.472 17809-17809/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: set data 2022-07-21 00:06:43.600 17860-17860/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc:childProcess 日志: init 2022-07-21 00:06:43.602 17860-17860/com.zzy.mmipc D/MMIPC->: open file /data/user/0/com.zzy.mmipc/files 2022-07-21 00:06:43.602 17860-17860/com.zzy.mmipc E/MMIPC->: pid[17860] 2022-07-21 00:06:43.602 17860-17860/com.zzy.mmipc D/MMIPC->: open m_fd[76], /data/user/0/com.zzy.mmipc/files/default_mmap.ipc 2022-07-21 00:06:43.602 17860-17860/com.zzy.mmipc D/MMIPC->: m_fd size[4161536] 2022-07-21 00:06:43.602 17860-17860/com.zzy.mmipc D/MMIPC->: getFileSize m_file_size 4161536, default_mmap_size 4161536 2022-07-21 00:06:43.603 17860-17860/com.zzy.mmipc D/MMIPC->: mmap success 2022-07-21 00:07:05.648 17860-17860/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc:childProcess 日志: 1744548 2022-07-21 00:07:07.888 17809-17809/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: 1798260
可能是我手机性能太好,加了线程同步后,发现多进程写的时候并没有乱,于是我加大了重试次数,终于不负有心人,终于出错了,目前是重试15万次,正常应该是15*12 = 180万长度才对,打印的最终结果是1798260,差了1740,接下来我们想办法找回来
先用Semaphores试试,看能否找回1740,抱歉没实现成功,我们来用mutex试下
跨进程同步Mutex
开发中,Mutex能够保证多个线程对同一共享资源的互斥访问。互斥体(互斥对象)是一个程序对象,它被创建以便多个程序线程可以轮流共享同一资源,通常,当程序启动时,它会在开始时通过向系统请求给定资源来为给定资源创建互斥体,并且系统会为其返回唯一的名称或 ID。之后,任何需要该资源的线程都必须在使用该资源时使用互斥锁从其他线程锁定该资源。如果互斥锁已被锁定,则需要资源的线程通常由系统排队,然后在互斥锁解锁时获得控制权(再次,互斥锁在新线程使用资源期间被锁定)。
如果这个互斥对象我们使用共享内存创建,那就可以实现跨进程锁。那么如何创建呢?
struct shm_mutex { pthread_mutex_t mutex; pthread_mutexattr_t mutexattr; }; inline void ShmMutex::createShmMutex(string dir) { int fd = open(dir.c_str(), O_CREAT | O_RDWR, 0666); if (fd == -1) { ALOGD("open fail for mutex: fd[%d], %s", fd, dir.c_str()); return; } ALOGD("open success for mutex: fd[%d], %s", fd, dir.c_str()); ftruncate(fd, sizeof(struct shm_mutex)); shmMutex = static_cast<shm_mutex *>(mmap(NULL, sizeof(struct shm_mutex), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)); if (shmMutex == MAP_FAILED) { ALOGD("mutex mmap failed"); return; } if (close(fd) == 0) { fd = -1; } else { //fail to close ALOGD("mutex fd close failed"); } memset(shmMutex, 0, sizeof(struct shm_mutex)); pthread_mutexattr_init(&shmMutex->mutexattr); pthread_mutexattr_setpshared(&shmMutex->mutexattr, PTHREAD_PROCESS_SHARED); pthread_mutex_init(&shmMutex->mutex, &shmMutex->mutexattr); pthread_mutexattr_destroy(&shmMutex->mutexattr); }
我们通过mmap创建一个共享内存,然后通过 pthread_mutexattr_init 初始化共享内存中的mutexattr,再设置PTHREAD_PROCESS_SHARED 跨进程互斥锁,最终 pthread_mutex_init 初始化 共享互斥对象 mutex。这样多个进程就可以通过一个mutex对象来实现互斥。上面的逻辑只是初始化,怎么使用呢?
第一 ShmMutex mLock; 第二 在原来加锁的地方,加上ShmMutex锁 void MMIPC::setData(const string &key, const string &value) { AutoMutex autoMutex(mLock); string content = key + ":" + value + ","; // ALOGD("setData content=%s", content.c_str()); size_t numberOfBytes = content.length(); if (m_position + numberOfBytes > m_file_size) { auto msg = "m_position: " + to_string(m_position) + ", numberOfBytes: " + to_string(numberOfBytes) + ", m_file_size: " + to_string(m_file_size); throw out_of_range(msg); } m_position = strlen(m_ptr); memcpy(m_ptr + m_position, (void *) content.c_str(), numberOfBytes); // ALOGD("setData success m_ptr.len=%d", m_position + numberOfBytes); }
你是不是发现了 AutoMutex,这里用到一个黑科技,想想我们之前加锁是这样
pthread_mutex_lock(&count_mutex); pthread_mutex_unlock(&count_mutex);
lock、unlock 必须成对出现的,为什么呢?因为如果你加了锁之后,不释放,就会导致该资源无法被别人使用,从而产生死锁反应,那autoMutex怎么实现的自动释放呢?来为你揭晓答案
class ShmMutex { public: enum { PRIVATE = 0, SHARED = 1 }; ShmMutex(); ~ShmMutex(); // lock or unlock the mutex status_t lock(); void unlock(); // lock if possible; returns 0 on success, error otherwise status_t tryLock(); /** * 该方法,创建共享内存的Mutex,实现跨进程互斥锁 * @param dir */ void createShmMutex(string dir); // Manages the mutex automatically. It'll be locked when Autolock is // constructed and released when Autolock goes out of scope. class Autolock { public: inline Autolock(ShmMutex &mutex) : mLock(mutex) { mLock.lock(); } inline Autolock(ShmMutex *mutex) : mLock(*mutex) { mLock.lock(); } inline ~Autolock() { mLock.unlock(); } private: ShmMutex &mLock; }; private: ShmMutex(const ShmMutex &); ShmMutex &operator=(const ShmMutex &); struct shm_mutex *shmMutex; };
你看~Autolock()析构函数就明白了,它利用了C++的构造和析构函数特性,也就是说析构函数在对象被销毁时自动调用,而我们在setData中,autoMutex使用的栈内存,所以当函数出栈时,会自动释放栈内的对象。这样就完美实现了成对的锁。牛吹了半天,下面是验证的时候了,跑下封装好的代码,看看是否是正常输出180万的长度,请看
2022-07-23 18:44:31.241 12534-12534/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: init 2022-07-23 18:44:31.242 12534-12534/com.zzy.mmipc D/MMIPC->: root dir /data/user/0/com.zzy.mmipc/files 2022-07-23 18:44:31.242 12534-12534/com.zzy.mmipc D/MMIPC->: open success for mutex: fd[78], /data/user/0/com.zzy.mmipc/files/default_mutex.ipc 2022-07-23 18:44:31.242 12534-12534/com.zzy.mmipc E/MMIPC->: pid[12534] 2022-07-23 18:44:31.242 12534-12534/com.zzy.mmipc D/MMIPC->: open m_fd[78], /data/user/0/com.zzy.mmipc/files/default_mmap.ipc 2022-07-23 18:44:31.242 12534-12534/com.zzy.mmipc D/MMIPC->: m_fd size[0] 2022-07-23 18:44:31.242 12534-12534/com.zzy.mmipc D/MMIPC->: getFileSize m_file_size 0, default_mmap_size 4161536 2022-07-23 18:44:31.242 12534-12534/com.zzy.mmipc D/MMIPC->: mmap success 2022-07-23 18:44:31.369 12534-12534/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: set data 2022-07-23 18:44:31.472 12575-12575/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc:childProcess 日志: init 2022-07-23 18:44:31.473 12575-12575/com.zzy.mmipc D/MMIPC->: root dir /data/user/0/com.zzy.mmipc/files 2022-07-23 18:44:31.473 12575-12575/com.zzy.mmipc D/MMIPC->: open success for mutex: fd[76], /data/user/0/com.zzy.mmipc/files/default_mutex.ipc 2022-07-23 18:44:31.473 12575-12575/com.zzy.mmipc E/MMIPC->: pid[12575] 2022-07-23 18:44:31.473 12575-12575/com.zzy.mmipc D/MMIPC->: open m_fd[76], /data/user/0/com.zzy.mmipc/files/default_mmap.ipc 2022-07-23 18:44:31.473 12575-12575/com.zzy.mmipc D/MMIPC->: m_fd size[4161536] 2022-07-23 18:44:31.473 12575-12575/com.zzy.mmipc D/MMIPC->: getFileSize m_file_size 4161536, default_mmap_size 4161536 2022-07-23 18:44:31.473 12575-12575/com.zzy.mmipc D/MMIPC->: mmap success 2022-07-23 18:44:34.273 12534-12534/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: 1192464 2022-07-23 18:44:42.746 12575-12575/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc:childProcess 日志: 1800000
看到了没,1800000,完美实现跨进程同步。但这样就结束了吗?由于我们使用了跨进程的互斥锁,如果该进程在上锁后,并且未释放之前,进程崩溃了怎么办?咋整?我们能不能做到在进程崩溃前,自动释放呢?通过搜寻,我找到了答案,我们可以使用
pthread_mutexattr_setrobust() ,将 pthread 互斥锁初始化为“robust”,如果持有互斥锁的进程死了,下一个获取它的线程将收到 EOWNERDEAD(但仍然成功获取互斥锁),以便它知道执行任何清理。然后它需要使用 pthread_mutex_consistent() 通知获取的互斥锁再次一致。但一个不好的消息,如图:
那就是android 的ndk,没有这个函数,而且我看了google 官方项目Issue对这个做了回答: github.com/android/ndk…,意思是目前还没支持。怎么办呢?现在只剩下最后一条路,那就是文件锁了。