多线程、多进程同时操作MMAP,会怎么样?

3,135 阅读24分钟

接着上篇聊

此处为语雀内容卡片,点击链接查看:

www.yuque.com/ibaozi/dhzi…

回归正经

打游戏浪费生命,我们步入正轨,开始进一步改造进程间通信组件,上次我聊到三个痛点总结为两个维度:

  • 安全性
  • 同步性

安全性,我们通过对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…,意思是目前还没支持。怎么办呢?现在只剩下最后一条路,那就是文件锁了。

文件锁flock

在linux 系统中,flock函数是为解决多进程对同一文件的读写冲突的,而flock函数只能锁定整个文件,无法锁定文件的某一区域。且flock可以保证robust,这也是我们选择它最终目的。

int flock(int fd,int operation);
  • fd 文件描述
  • operation 锁的模式
    • LOCK_SH 建立共享锁定。多个进程可同时对同一个文件作共享锁定。
    • LOCK_EX 建立互斥锁定。多个进程中一个文件同时只有一个互斥锁定。
    • LOCK_UN 解除文件锁定状态。
    • LOCK_NB 无法建立锁定时,此操作可不被阻断,马上返回进程。通常与LOCK_SH或LOCK_EX 做组合
  • 单一文件无法同时建立共享锁定和互斥锁定,且当使用dup()或fork()时文件描述符也不会继承此种锁定。
  • 返回值 返回0表示成功,若有错误则返回-1,错误代码存于errno。

代码实现

void MMIPC::setData(const string &key, const string &value) {
//    AutoMutex autoMutex(m_mutex_lock);
    // 上跨进程写锁
    flock(m_fd, LOCK_EX);
    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);
    // 解锁
    flock(m_fd, LOCK_UN);
}

运行效果

2022-07-24 12:54:32.259 10191-10191/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: set data
2022-07-24 12:54:32.521 10221-10221/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc:childProcess 日志: init
2022-07-24 12:54:32.523 10221-10221/com.zzy.mmipc D/MMIPC->: root dir /data/user/0/com.zzy.mmipc/files
2022-07-24 12:54:32.524 10221-10221/com.zzy.mmipc D/MMIPC->: open success for mutex: fd[76], /data/user/0/com.zzy.mmipc/files/default_mutex.ipc
2022-07-24 12:54:32.524 10221-10221/com.zzy.mmipc E/MMIPC->: pid[10221]
2022-07-24 12:54:32.524 10221-10221/com.zzy.mmipc D/MMIPC->: open m_fd[76], /data/user/0/com.zzy.mmipc/files/default_mmap.ipc
2022-07-24 12:54:32.524 10221-10221/com.zzy.mmipc D/MMIPC->: m_fd size[4194304]
2022-07-24 12:54:32.524 10221-10221/com.zzy.mmipc D/MMIPC->: getFileSize m_file_size 4194304, default_mmap_size 4194304
2022-07-24 12:54:32.524 10221-10221/com.zzy.mmipc D/MMIPC->: mmap success
2022-07-24 12:54:44.677 10221-10221/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc:childProcess 日志: 2796540
2022-07-24 12:55:52.774 10191-10191/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: 3600000

怎么是36万?不应该是18万么,是的,因为我跑了两遍,一遍不放心不是,万一出问题呢。接着之前的经验,为了避免自己手欠无法正常的配对使用锁,我们封装下文件锁flock,如下

//
// Created by 张占永 on 2022/7/24.
//

#include <fcntl.h>
#include <sys/file.h>
#include <unistd.h>
#include <cerrno>
#include "AndroidLog.h"

#ifndef MMIPC_FILELOCK_H
#define MMIPC_FILELOCK_H


enum LockType {
    SharedLockType,
    ExclusiveLockType,
};

class FileLock {
    int m_fd;

    bool isFileLockValid() { return m_fd >= 0; }

public:
    explicit FileLock(int fd);

    bool lock(LockType lockType);

    bool try_lock(LockType lockType);

    bool unlock();

    // just forbid it for possibly misuse
    explicit FileLock(const FileLock &other) = delete;

    FileLock &operator=(const FileLock &other) = delete;

    class Autolock {
    public:
        inline Autolock(FileLock &flock, LockType lockType) : mFLock(flock), m_lockType(lockType) {
            mFLock.lock(m_lockType);
        }

        inline Autolock(FileLock *flock, LockType lockType) : mFLock(*flock), m_lockType(lockType) {
            mFLock.lock(m_lockType);
        }

        inline ~Autolock() { mFLock.unlock(); }

    private:
        FileLock &mFLock;
        LockType m_lockType;
    };

};

inline FileLock::FileLock(int fd)
        : m_fd(fd) {
}

inline bool FileLock::lock(LockType lockType) {
    if (!isFileLockValid()) {
        return false;
    }
    int res = flock(m_fd, lockType == SharedLockType ? LOCK_SH : LOCK_EX);
    if (res == -1) {
        ALOGD("flock lock fail: fd[%d]", m_fd);
    }
    return res == 0;
}

inline bool FileLock::try_lock(LockType lockType) {
    if (!isFileLockValid()) {
        return false;
    }
    int res = flock(m_fd, lockType == SharedLockType ? LOCK_SH | LOCK_NB : LOCK_EX | LOCK_NB);
    if (res == -1) {
        ALOGD("flock try_lock fail: fd[%d]", m_fd);
    }
    return res == 0;
}

inline bool FileLock::unlock() {
    if (!isFileLockValid()) {
        return false;
    }
    int res = flock(m_fd, LOCK_UN);
    if (res == -1) {
        ALOGD("flock unlock fail: fd[%d]", m_fd);
    }
    return res == 0;
}

typedef FileLock::Autolock AutoFileLock;

#endif //MMIPC_FILELOCK_H

然后调用如下

void MMIPC::setData(const string &key, const string &value) {
//    AutoMutex autoMutex(m_mutex_lock);
    // 加文件锁
    AutoFileLock autoFileLock(m_file_lock, ExclusiveLockType);
    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);
}

运行一下,看看如何

2022-07-24 13:36:29.715 28670-28670/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: init
2022-07-24 13:36:29.716 28670-28670/com.zzy.mmipc D/MMIPC->: root dir /data/user/0/com.zzy.mmipc/files
2022-07-24 13:36:29.716 28670-28670/com.zzy.mmipc D/MMIPC->: open success for mutex: fd[75], /data/user/0/com.zzy.mmipc/files/default_mutex.ipc
2022-07-24 13:36:29.716 28670-28670/com.zzy.mmipc E/MMIPC->: pid[28670]
2022-07-24 13:36:29.717 28670-28670/com.zzy.mmipc D/MMIPC->: open m_fd[75], /data/user/0/com.zzy.mmipc/files/default_mmap.ipc
2022-07-24 13:36:29.717 28670-28670/com.zzy.mmipc D/MMIPC->: m_fd size[0]
2022-07-24 13:36:29.717 28670-28670/com.zzy.mmipc D/MMIPC->: getFileSize m_file_size 0, default_mmap_size 4194304
2022-07-24 13:36:29.717 28670-28670/com.zzy.mmipc D/MMIPC->: mmap success
2022-07-24 13:36:29.929 28670-28670/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: set data
2022-07-24 13:36:30.414 28737-28737/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc:childProcess 日志: init
2022-07-24 13:36:30.415 28737-28737/com.zzy.mmipc D/MMIPC->: root dir /data/user/0/com.zzy.mmipc/files
2022-07-24 13:36:30.415 28737-28737/com.zzy.mmipc D/MMIPC->: open success for mutex: fd[76], /data/user/0/com.zzy.mmipc/files/default_mutex.ipc
2022-07-24 13:36:30.415 28737-28737/com.zzy.mmipc E/MMIPC->: pid[28737]
2022-07-24 13:36:30.415 28737-28737/com.zzy.mmipc D/MMIPC->: open m_fd[76], /data/user/0/com.zzy.mmipc/files/default_mmap.ipc
2022-07-24 13:36:30.415 28737-28737/com.zzy.mmipc D/MMIPC->: m_fd size[4194304]
2022-07-24 13:36:30.415 28737-28737/com.zzy.mmipc D/MMIPC->: getFileSize m_file_size 4194304, default_mmap_size 4194304
2022-07-24 13:36:30.415 28737-28737/com.zzy.mmipc D/MMIPC->: mmap success
2022-07-24 13:36:38.311 28737-28737/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc:childProcess 日志: 1463520
2022-07-24 13:36:51.944 28670-28670/com.zzy.mmipc D/MMIPC->: 进程:com.zzy.mmipc 日志: 1800000

好的,18万,完美。是不是可结束了?其实光有flock是不行的,还需要线程同步,这里又用到了mutex的线程锁,可以往上翻一翻看下,今天就到此结束。

总结

多线程如何保持结果一致性,我们做了两种实现方式:

  • Looper 实现
  • mutex 互斥锁

多进程如何保持结果一致性,我们做了三种:

  • Semaphores 找了大量的示例,没有测试成功,抱歉
  • mutext 实现了跨进程的互斥锁,但它无法保证robust,意思是在进程出现异常死掉时,无法释放锁,导致其他进程无法再次获取,从而产生不可预估的问题
  • flock 实现了跨进程的文件锁,但需要配合mutex线程锁来解决多线程问题

总之,目前为止,我们搞定了mmap的跨线程以及跨进程同步的问题,接下来就可以考虑如何设计传输的数据结构,下期我们来用pb做载体,体验pb的魅力。

源码地址

github.com/ibaozi-cn/m…

参加活动

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿