MMKVの原理-プロセス間でどのように実現するか(2)

14512 ワード

次のページに続きます.https://blog.csdn.net/lin20044140410/article/details/104450727
mmkvの使用では、マルチスレッドがあるに違いない.マルチプロセスの同期問題では、同期問題があればロックが使用されるに違いない.だから、まずmmkvのロックの使用から言えば、mmkv処理スレッドの同期ではmutex反発ロックが使用されている.例えば、セットからmmkvのc++層のオブジェクトを取得すると、ロックが追加される.複数のスレッドが同時に操作される可能性があるからだ.
プロセス間の同期処理にはflockファイルロックが使用する、例えばライトポインタの同期処理、メモリリビルド時などである.
以下のロックの使用はnative層である.
1,まず反発ロックがどのように使われているかを見て,このdemoはスレッドロックの使用を示し,さらにmmkvのロックの使用時にどのようにカプセル化されているかを引き出す.
queue m_queue;

void *dequeue(void *args) {
    if (!m_queue.empty()) {
        printf("dequeue data :%d 
", m_queue.front()); __android_log_print(ANDROID_LOG_DEBUG,"mmap","nativeThreadTest,dequeue data :%d
", m_queue.front()); m_queue.pop(); } else { __android_log_print(ANDROID_LOG_DEBUG,"mmap","nativeThreadTest,no data"); } return 0; } extern "C" JNIEXPORT void JNICALL Java_com_test_mmkvdemo_MainActivity_nativeThreadTest(JNIEnv *env, jobject instance) { for (int i = 0; i < 5; ++i) { m_queue.push(i); } pthread_t tid[10]; for (int i = 0; i < 10; ++i) { pthread_create(&tid[i], 0, dequeue, &m_queue); } __android_log_print(ANDROID_LOG_DEBUG,"mmap","nativeThreadTest,m_queue," ); }

このdemoでは、まず1つのキューに5つの要素を配置し、それから10のスレッドを起こしてキューの要素を取ります.
2020-02-24 21:49:29.213 20930-20949/? D/mmap: nativeThreadTest,dequeue data :0 ,tid= 3529775472
2020-02-24 21:49:29.214 20930-20948/? D/mmap: nativeThreadTest,dequeue data :1 ,tid= 3530815856
2020-02-24 21:49:29.214 20930-20950/? D/mmap: nativeThreadTest,dequeue data :1 ,tid= 3528735088
2020-02-24 21:49:29.214 20930-20951/? D/mmap: nativeThreadTest,dequeue data :3 ,tid= 3527694704
2020-02-24 21:49:29.215 20930-20953/? D/mmap: nativeThreadTest,dequeue data :4 ,tid= 3525613936
2020-02-24 21:49:29.215 20930-20954/? D/mmap: nativeThreadTest,dequeue data :4 ,tid= 3524573552
2020-02-24 21:49:29.215 20930-20952/? D/mmap: nativeThreadTest,dequeue data :-374526740 ,tid= 3526654320
2020-02-24 21:49:29.216 20930-20956/? D/mmap: nativeThreadTest,dequeue data :-374526724 ,tid= 3522492784
2020-02-24 21:49:29.216 20930-20930/? D/mmap: nativeThreadTest,m_queue,
2020-02-24 21:49:29.216 20930-20955/? D/mmap: nativeThreadTest,dequeue data :-374526708 ,tid= 3523533168
2020-02-24 21:49:29.216 20930-20957/? D/mmap: nativeThreadTest,dequeue data :-374526692 ,tid= 3521452400

出力から見ると、スレッドが取り出したデータが重複し、未知のデータが取り出される可能性がある.これは、マルチスレッドが共有データを操作してもロックされず、汚れた読み取りを招くためである.
popを操作する方法に反発ロックを加えると、汚れた読み取りの問題を解決することができます.ここではpthead_を使用しています.mutex_t反発量、このロックはデフォルトでは非再帰ロック、すなわち再入不可であり、再入不可ロックの場合、1つのスレッド内で同じようなロックを複数回取得するとデッドロックが発生するためpthread_mutex_tロック初期化時に属性pthread_を設定する必要があるmutexattr_t,これを再帰ロックに設定する. pthread_mutexattr_settype(&m_attr, PTHREAD_MUTEX_RECURSIVE);
queue m_queue;
pthread_mutex_t m_mutex;
pthread_mutexattr_t m_attr;

void *dequeue(void *args) {
    pthread_mutex_lock(&m_mutex);
    pthread_t tid = pthread_self();
    if (!m_queue.empty()) {
        printf("dequeue data :%d 
", m_queue.front()); __android_log_print(ANDROID_LOG_DEBUG,"mmap","nativeThreadTest,dequeue data :%d ,tid= %u
", m_queue.front(), (unsigned int)tid); m_queue.pop(); } else { __android_log_print(ANDROID_LOG_DEBUG,"mmap","nativeThreadTest,no data,tid= %u
",(unsigned int)tid); } pthread_mutex_unlock(&m_mutex); return 0; } extern "C" JNIEXPORT void JNICALL Java_com_test_mmkvdemo_MainActivity_nativeThreadTest(JNIEnv *env, jobject instance) { pthread_mutexattr_init(&m_attr); pthread_mutexattr_settype(&m_attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init(&m_mutex, &m_attr); pthread_mutexattr_destroy(&m_attr); for (int i = 0; i < 5; ++i) { m_queue.push(i); } pthread_t tid[10]; for (int i = 0; i < 10; ++i) { pthread_create(&tid[i], 0, dequeue, &m_queue); } pthread_mutex_destroy(&m_mutex); __android_log_print(ANDROID_LOG_DEBUG,"mmap","nativeThreadTest,m_queue," ); }

ロック使用中にpthread_を呼び出すmutex_lock(&m_mutex); 鍵をかけてpthreadを呼び出すのを忘れないでください.mutex_unlock(&m_mutex);ロック解除、うっかりロック解除を忘れてしまったら悲劇に違いないし、ロックを追加するには、そんなに多くのコードを書く必要があり、不快になるに違いないので、ロックのパッケージがあり、ロックの自動管理を実現します.
まずc++クラスの構造方法と解析方法でカプセル化し,構造関数でロックおよび属性の初期化を行い,解析関数でロックオブジェクトを破棄する.
class ThreadLock {
private:
    pthread_mutex_t m_lock;

public:
    ThreadLock() {
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
        pthread_mutex_init(&m_lock, &attr);
        pthread_mutexattr_destroy(&attr);
    }

    ~ThreadLock() {
        pthread_mutex_destroy(&m_lock);
    }

    void lock() {
        auto result = pthread_mutex_lock(&m_lock);
        if (result != 0) {
            //failed.
        }
    }

    bool try_lock() {
        auto result = pthread_mutex_lock(&m_lock);
        return  (result == 0);
    }

    void unlock() {
        auto result = pthread_mutex_unlock(&m_lock);
        if (0 != result){
            //failed.
        }
    }
};

このようにカプセル化すると、簡単になるが、unlockを忘れることによって悲劇を招く可能性があり、使用中にlockの後、unlockを忘れることがないようにするためには、classを作成し、ThreadLockのオブジェクトを持たせることで、このような構造方法で鍵をかけ、その解析方法で鍵を解く必要がある.
template 
class ScopedLock {
    T *m_lock;

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

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

public:
    ScopedLock(T *oLock) : m_lock(oLock) {
        assert(m_lock);
        lock();
    }

    ~ScopedLock() {
        unlock();
        m_lock = nullptr;
    }

    void lock() {
        if (m_lock) {
            m_lock->lock();
        }
    }

    bool try_lock() {
        if (m_lock) {
            return m_lock->try_lock();
        }
        return false;
    }

    void unlock() {
        if (m_lock) {
            m_lock->unlock();
        }
    }
};

このようにパッケージング後、使用方法は、コードブロックにロックオブジェクトを作成するだけでロックを実現し、コードブロックが終了すると、ロックオブジェクトのライフサイクルが終了するにつれて、その構造関数を呼び出してロックを解放することである.
{
   ThreadLocak * thread_lock = new ThreadLock();
   ScopedLock scoped_lock(thread_lock);
}

ScopedLockクラスはテンプレートクラスであるが、実際にmmkvではファイルロックにおけるリードロックとライトロック、およびスレッドロックがこのテンプレートクラスによって使用する.
上記のパッケージはロックの使用に非常に便利であり、mmkvはまたロックの使用をより便利にし、それはマクロ関数を定義してロックの使用を簡略化することである.
#define SCOPEDLOCK(lock) _SCOPEDLOCK(lock, __COUNTER__)
#define _SCOPEDLOCK(lock, counter) __SCOPEDLOCK(lock, counter)
#define __SCOPEDLOCK(lock, counter) ScopedLock __scopedLock##counter(&lock)

1番目のマクロ関数、ThreadLockオブジェクトを転送すると、2番目のマクロ関数が呼び出されます.
2番目のマクロ関数は、入力されたThreadLockパラメータに加えて、コンパイラ内のマクロ定義を追加します.COUNTER__コンパイル時に具体的な値0,1,2,...に置き換えられます.
このマクロ関数が1つのコンパイルユニットで何回呼び出されたかを記録するカウンタに相当する.
3つ目のマクロ関数は、特定のロックオブジェクトを作成することです.
ScopedLock __scopedLock##counter(&lock)
decltype(lock)はlockの特定のタイプを取得し、カウント値を加えるとScopedLockタイプのロックオブジェクトが得られ、
1回目の呼び出し_scopedLock0,
2回目の呼び出し_scopedLock1,
3回目の呼び出し_scopedLock2 ,....
 
2、次にファイルロックを見てください.
pthread_mutexはプロセスロックとして使用できますが、andoridバージョンのpthread_mutexは丈夫ではありませんpthreadを追加したらmutexロックのプロセスはkillされ、システムはクリーンアップ作業を行わず、このロックはずっと存在し、他のロックを待つプロセスは餓死する.
https://github.com/Tencent/MMKV/wiki/android_ipc
マルチプロセスの実装ではファイルロックが用いられ、マルチプロセスが同時に1つのファイルを操作する場合、Aプロセスがこのファイルを書く可能性があり、Bプロセスがこのファイルを読むと、Bプロセスが汚いデータを読み取ることになる.Bプロセスが最新のデータを読み取ることができ、データの完全性を保証するために、ファイルロックを使用してマルチプロセス操作ファイルの同期を完了する.
ファイルロックの使用:
#include 
//  open        
string m_path;
int m_fd = open(m_path.c_str(), O_RDWR | O_CREAT, S_IRWXU);
//           
int flock(m_fd, operation);

パラメータoperationはロックタイプで、flockは2つのロックタイプLOCK_をサポートします.SH,LOCK_EX
LOCK_SH、共有ロック、複数のプロセスを同時に使用することができ、リードロックとして使用することができる.
LOCK_EX、排他ロック、同時に1つのプロセスしか使用できず、書き込みロックとして使用できる.
LOCK_UN,ロック解除
LOCK_BNは、ブロック要求ではなく、読み書きロックと併用.
flockを使用して1つのファイルに対してリードロック、あるいはライトロックをかけて、すべてブロックすることができて、例えばAプロセスは1つのファイルを持つライトロックに対して、Bプロセスはこのファイルに対してロックを書きたいと思って、ブロックして、ブロックされたくないならば、LOCK_に協力することができますBN属性使用、すなわちLOCK_BN | LOCK_EX.
flockにはいくつかの特徴があります.
1)flockは、1つのファイルに対して複数回のロックをサポートする、状態ロックであるためカウンタがない、何回のロックを加えても1回だけロックを解除する必要がある.したがって、mmkvにおいてflockをカプセル化する際にカウンタを付けることは、何回かロックをかけたことを保証し、何回かロックを解除することである.
2)ロックのアップグレード、ダウングレード、1つのプロセスが1つのファイルにリードロックを加えた後、再びflock操作を実行すると、転送されたoperationはLOCK_EX、それではこのプロセスはファイルのリードロックを書くロックにアップグレードして、これはロックのアップグレードで、逆にロックのダウングレードで、しかしファイルのロックのダウングレードは行うことができなくて、彼が再帰を支持しないため、ダウングレードするとロックがありません.
上記の問題を解決するために、mmkvはファイルロックをカプセル化し、読み書きロックカウンタを追加し、再帰をサポートする.
ソースコードを結合してmmkvがどのように多いかを見ます.
InterProcessLock.h
class FileLock {
    int m_fd;
    size_t m_sharedLockCount;
    size_t m_exclusiveLockCount;

    bool doLock(LockType lockType, bool wait);
}

読み書きロックカウンタを追加し、doLockを呼び出すことでファイルをロックする.locktypeはロックのタイプ、2番目のパラメータがブロックするかどうかを決定する.ポイントはdoLockがどのように実現したのかです
ロックの処理ロジック:
1),//先判断上的是什么钥匙//上读钥匙,カウンター先加1,如果加1后,读钥匙カウンター大了1,说明已上过上读钥匙了,实现回归,这种情况不在执行钥匙,只增加了钥匙,如果这文件书的写钥匙カウンター大了0,说明已上写锁了,现在仍用写锁了,所以不//実行上读钥的操作,ロックがダウングレードされるため、//最初の読み込みロックの場合、後でロック操作が実行されます
2),/書き込みロック、カウンタに1を付けて、書き込みロックをかけた場合、直接return.//リードロックカウンタが0より大きい場合は、書き込みロックを試してみます.失敗した場合は、別のプロセスがこのファイルのリードロックを使用していることを示します.//この場合は、まず自分のリードロックを解放してから、ブロックの形でロックを書きます.デッドロックを避けるためです.//仮に、Aプロセスは自分のリードロックを解いていないとして、直接書き込みロックはここでブロックされています.もし他のプロセスBプロセスがちょうど読み取りロックを解放したら、現在のプロセス//Aプロセスに書き込みロック//は成功します.しかし、他のプロセスBが統合されても書き込みロックをかけなければなりません.Aプロセスの読み取りロックが解放されていないため、Bプロセスに書き込みロックの操作がずっと//ブロックされています.これがAです.Bプロセスはいずれも相手のリードロックの解放を待っており、デッドロックを招く.
bool FileLock::doLock(LockType lockType, bool wait) {
    if (!isFileLockValid()) {
        return false;
    }
    bool unLockFirstIfNeeded = false;
   //         
//   ,     1,   1 ,       1 ,         , 
     ,           ,        ,              0,
         ,       ,   
//        ,      ,      
//         ,         
    if (lockType == SharedLockType) {
        m_sharedLockCount++;
        // don't want shared-lock to break any existing locks
        if (m_sharedLockCount > 1 || m_exclusiveLockCount > 0) {
            return true;
        }
    } else {
//   ,     1,          ,  return.
//         0,         ,     ,                 ,
//                ,           ,             .
//  ,A          ,           ,      B         ,      //A     
//    ,  ,      B       ,  A         ,   B          
//   ,   A,B            ,     .
        m_exclusiveLockCount++;
        // don't want exclusive-lock to break existing exclusive-locks
        if (m_exclusiveLockCount > 1) {
            return true;
        }
        // prevent deadlock
        if (m_sharedLockCount > 0) {
            unLockFirstIfNeeded = true;
        }
    }
    return platformLock(lockType, wait, unLockFirstIfNeeded);
}

ロック解除の処理ロジックを参照してください.
1),//解読ロックであれば、リードロックカウンタは0で、直接戻ります.//もしリードロックカウンタが1を減らした後、さらに大きくなったら、再帰ロックを実現するために、前回のリードロックを何回か解除しなければならないので、直接//戻ります.//ここに書き込みロックがあれば、直接戻ります.LOCK_を実行すればUN、書き込みロックも解除しました.
2),//書き込みロックを解除した場合、カウンタは0、直接戻り、//カウンタが1を減らして0より大きい場合、直接戻り//そうでなければ、書き込みロックカウンタは0になったが、リードロックがあっても、直接LOCK_UNではなく、ロックのダウングレードをします.
bool FileLock::unlock(LockType lockType) {
    if (!isFileLockValid()) {
        return false;
    }
    bool unlockToSharedLock = false;
//      ,      0,    ,
//         1 ,    ,       ,       ,       ,    
//  ,
//       ,     ,      LOCK_UN,       .
    if (lockType == SharedLockType) {
        if (m_sharedLockCount == 0) {
            return false;
        }
        m_sharedLockCount--;
        // don't want shared-lock to break any existing locks
        if (m_sharedLockCount > 0 || m_exclusiveLockCount > 0) {
            return true;
        }
    } else {
//     ,    0,    ,
//      1   0,    
//  ,        0 ,     ,     LOCK_UN,       ,
        if (m_exclusiveLockCount == 0) {
            return false;
        }
        m_exclusiveLockCount--;
        if (m_exclusiveLockCount > 0) {
            return true;
        }
        // restore shared-lock when all exclusive-locks are done
        if (m_sharedLockCount > 0) {
            unlockToSharedLock = true;
        }
    }
    return platformUnLock(unlockToSharedLock);
}

3、これでファイルロックがカプセル化され、このファイルロックを利用すれば同じ時間に1つのプロセスでfileを操作することができますが、Aプロセスがファイルを修正した後、Bプロセスはどのようにこの修正を知っていますか?
上記の問題に対して、mmkvはやっていますか?実際には私たちが想像していたのとは違って、mmkvはkey-valueデータを保存するdefaultファイルに枷をかけず、鍵をかけた.crcチェックファイルこの検査書類は上記の問題を解決するために来たのです.
struct MMKVMetaInfo {
    uint32_t m_crcDigest = 0;
    uint32_t m_version = 1;
    uint32_t m_sequence = 0; // full write-back count
    unsigned char m_vector[AES_KEY_LEN] = {0};
}

このmmkv.default.crcファイルにはmmkvという検査コードがあります.defaultファイルのMD 5値は、ファイルが合法かどうかを判断することができる.
もう1つのシーケンス番号は、シーケンス化ファイルが再削除されると、拡張操作が行われ、このシーケンス番号が増加します.
具体的にソースコードはどうやって作ったのですか?
1)mmkvが初期化されると、crcファイルが読み出され、その中の検証コード、シリアル番号、
2)データを読み込む場合はcheckLoadDataでチェックする.
void MMKV::checkLoadData() {
   //      ,     ,        ,         ,
//  ,        ,
    MMKVMetaInfo metaInfo;
    metaInfo.read(m_metaFile.getMemory());
    if (m_metaInfo.m_sequence != metaInfo.m_sequence) {
        SCOPEDLOCK(m_sharedProcessLock);

        clearMemoryState();
        loadFromFile();
        notifyContentChanged();
    }else if (m_metaInfo.m_crcDigest != metaInfo.m_crcDigest) {
    //      ,          ,       ,        ,         
//          ,       ,          ,
//      ,       k-v,       ,            .
        size_t fileSize = 0;
        if (m_isAshmem) {
            fileSize = m_size;
        } else {
            struct stat st = {0};
            if (fstat(m_fd, &st) != -1) {
                fileSize = (size_t) st.st_size;
            }
        }

        if (m_size != fileSize) {
            MMKVInfo("file size has changed [%s] from %zu to %zu",             
           m_mmapID.c_str(), m_size,
                     fileSize);
            clearMemoryState();
            loadFromFile();
        } else {
            partialLoadFromFile();
        }
    }
}

したがって、ファイルを検証することにより、データを読み出す際に、検証を行うことで、複数のプロセスのデータ同期が実現する.