mmkv框架源码浅析(中)

这篇是跟mmkv相关的分析,上篇主要是一些基本知识介绍。

五) 文件结构和文件锁

粗略介绍下文件结构,引用网上一张图:


图一

如上图,不同进程打开同一个文件时,拥有不同的文件描述符和file对象,但共享唯一的inode节点,其中f_count为引用计数。在同一个进程中,如果使用dup/close fd时,具体会影响f_count变化。
下图是fork一个进程时影响的文件描述表:


图二

其中文件锁的数据结构如下:

struct flcok 
{ 
    short int l_type; //锁定的状态
    short int l_whence;  //决定 l_start位置
    off_t l_start;  //锁定区域的开头位置
    off_t l_len;  //锁定区域大小
    pid_t l_pid;
};

int fcntl(int fd, int cmd, struct flock *lock);

其中l_type有F_WRLCK/F_RDLCK/F_UNLCK,而使用接口fcntl中的cmd有F_SETLK/F_SETLKW等,这里主要列出后面即将使用到的情况。

六) leveldb中怎么防止同一个进程被启动多次

这里拷贝leveldb中的代码来说明:

351 static int LockOrUnlock(int fd, bool lock) {
352   errno = 0;
353   struct flock f;
354   memset(&f, 0, sizeof(f));
355   f.l_type = (lock ? F_WRLCK : F_UNLCK); //加写锁或解锁
356   f.l_whence = SEEK_SET;
357   f.l_start = 0;
358   f.l_len = 0;        // Lock/unlock entire file
359   return fcntl(fd, F_SETLK, &f);
360 }

524   virtual Status LockFile(const std::string& fname, FileLock** lock) {
525     *lock = nullptr;
526     Status result;
527     int fd = open(fname.c_str(), O_RDWR | O_CREAT, 0644);
528     if (fd < 0) {
529       result = PosixError(fname, errno);
530     } else if (!locks_.Insert(fname)) {
531       close(fd);
532       result = Status::IOError("lock " + fname, "already held by process");
533     } else if (LockOrUnlock(fd, true) == -1) {
534       result = PosixError("lock " + fname, errno);
535       close(fd);
536       locks_.Remove(fname);
537     } else {
538       PosixFileLock* my_lock = new PosixFileLock;
539       my_lock->fd_ = fd;
540       my_lock->name_ = fname;
541       *lock = my_lock;
542     }
543     return result;
544   }
545 
546   virtual Status UnlockFile(FileLock* lock) {
547     PosixFileLock* my_lock = reinterpret_cast(lock);
548     Status result;
549     if (LockOrUnlock(my_lock->fd_, false) == -1) {
550       result = PosixError("unlock", errno);
551     }
552     locks_.Remove(my_lock->name_);
553     close(my_lock->fd_);
554     delete my_lock;
555     return result;
556   }

LockOrUnlock中后三个参数是用于加锁整个文件,以排他锁的方式,这样其他进程当以约定的格式对文件加写锁或读锁时,会返回错误。这里需要注意的是,以读写形式打开文件,如果以只读方式打开文件,那么只能加读锁,反之是写锁,代码还是挺好理解的。

七) 改造优化文件锁

如果让我实现一个能递归加解锁且能进行锁升降级的功能我会怎么去实现?
按照既有的约定,同一个进程中,可以对同一文件加锁,后一种会覆盖前一种,但前提是以读写形式打开文件,原因参考上面。
比如在同一个进程中,以读写形式打开文件,那么第一次加读锁时,后面加读锁或者写锁会覆盖前一种锁,加写锁也是如此。

当有两个进程协作时,还是以读写形式打开文件,进程p加读锁,进程q可以加读锁成功,但加写锁就不行;进程p加写锁,进程q不能加读锁或者写锁。

来看看mmkv中的文件锁是如何改造的,基本思路是上面说的:

 27 enum LockType {
 28     SharedLockType,
 29     ExclusiveLockType,
 30 };

 32 // a recursive POSIX file-lock wrapper
 33 // handles lock upgrade & downgrade correctly
 34 class FileLock {
 35     int m_fd;
 36     struct flock m_lockInfo;
 37     size_t m_sharedLockCount;
 38     size_t m_exclusiveLockCount;
 39 
 40     bool doLock(LockType lockType, int cmd);
 41 
 42     // just forbid it for possibly misuse
 43     FileLock(const FileLock &other) = delete;
 44 
 45     FileLock &operator=(const FileLock &other) = delete;
 46 
 47 public:
 48     FileLock(int fd);
 50     bool lock(LockType lockType);
 52     bool try_lock(LockType lockType);
 54     bool unlock(LockType lockType);
 55 };

以上是文件锁的类声明,其中的两个数据成员用于计数读和写。

 25 static short LockType2FlockType(LockType lockType) {
 26     switch (lockType) {
 27         case SharedLockType:
 28             return F_RDLCK;
 29         case ExclusiveLockType:
 30             return F_WRLCK;
 31     }
 32 }
 33 
 34 FileLock::FileLock(int fd) : m_fd(fd), m_sharedLockCount(0), m_exclusiveLockCount(0) {
 35     m_lockInfo.l_type = F_WRLCK;  //默认为写锁
 36     m_lockInfo.l_start = 0;
 37     m_lockInfo.l_whence = SEEK_SET;
 38     m_lockInfo.l_len = 0;
 39     m_lockInfo.l_pid = 0;
 40 }
 42 bool FileLock::doLock(LockType lockType, int cmd) {
 43     bool unLockFirstIfNeeded = false;
 44 
 45     if (lockType == SharedLockType) {
 46         m_sharedLockCount++;
 47         // don't want shared-lock to break any existing locks
 48         if (m_sharedLockCount > 1 || m_exclusiveLockCount > 0) {
 49             return true;
 50         }
 51     } else {
 52         m_exclusiveLockCount++;
 53         // don't want exclusive-lock to break existing exclusive-locks
 54         if (m_exclusiveLockCount > 1) {
 55             return true;
 56         }
 57         // prevent deadlock
 58         if (m_sharedLockCount > 0) {
 59             unLockFirstIfNeeded = true;
 60         }
 61     }
 62 
 63     m_lockInfo.l_type = LockType2FlockType(lockType);
 64     if (unLockFirstIfNeeded) {
 65         // try lock
 66         auto ret = fcntl(m_fd, F_SETLK, &m_lockInfo);
 67         if (ret == 0) {
 68             return true;
 69         }
 70         // lets be gentleman: unlock my shared-lock to prevent deadlock
 71         auto type = m_lockInfo.l_type;
 72         m_lockInfo.l_type = F_UNLCK;
 73         ret = fcntl(m_fd, F_SETLK, &m_lockInfo);
 74         if (ret != 0) {
 77         }
 78         m_lockInfo.l_type = type;
 79     }
 80 
 81     auto ret = fcntl(m_fd, cmd, &m_lockInfo);
 82     if (ret != 0) {
 84         return false;
 85     } else {
 86         return true;
 87     }
 88 }

 90 bool FileLock::lock(LockType lockType) {
 91     return doLock(lockType, F_SETLKW);
 92 }
 93 
 94 bool FileLock::try_lock(LockType lockType) {
 95     return doLock(lockType, F_SETLK);
 96 }

以上是加锁的实现,为了实现递归锁,需要使用计数的形式,这里以mmkv中的使用方式来说明如何正确实现递归和锁的升降级,部分代码如下:

 57 class InterProcessLock {
 58     FileLock *m_fileLock;
 59     LockType m_lockType;
 61};

 44 class MMKV {
 65     FileLock m_fileLock;
 66     InterProcessLock m_sharedProcessLock;
 67     InterProcessLock m_exclusiveProcessLock;
 217};

  59 MMKV::MMKV(const std::string &mmapID, int size, MMKVMode mode, string *cryptKey)
  65     , m_fileLock(m_metaFile.getFd())
  66     , m_sharedProcessLock(&m_fileLock, SharedLockType)
  67     , m_exclusiveProcessLock(&m_fileLock, ExclusiveLockType)
  99 }

使用方式SCOPEDLOCK(m_exclusiveProcessLock),一个mmkv对象持有指向同一文件锁的两种不同锁对象:共享锁和排他锁。

 98 bool FileLock::unlock(LockType lockType) {
 99     bool unlockToSharedLock = false;
100 
101     if (lockType == SharedLockType) {
102         if (m_sharedLockCount == 0) {
103             return false;
104         }
105         m_sharedLockCount--;
106         // don't want shared-lock to break any existing locks
107         if (m_sharedLockCount > 0 || m_exclusiveLockCount > 0) {
108             return true;
109         }
110     } else {
111         if (m_exclusiveLockCount == 0) {
112             return false;
113         }
114         m_exclusiveLockCount--;
115         if (m_exclusiveLockCount > 0) {
116             return true;
117         }
118         // restore shared-lock when all exclusive-locks are done
119         if (m_sharedLockCount > 0) {
120             unlockToSharedLock = true;
121         }
122     }
123 
124     m_lockInfo.l_type = static_cast(unlockToSharedLock ? F_RDLCK : F_UNLCK);
125     auto ret = fcntl(m_fd, F_SETLK, &m_lockInfo);
126     if (ret != 0) {
128         return false;
129     } else {
130         return true;
131     }
132 }

这里为了简化讨论分两步。当只有一个进程时,第一次加读锁时,使用方式SCOPEDLOCK(m_sharedProcessLock)m_sharedLockCount为一,加读锁成功;后续加读锁纯粹计数。而后面要加写锁时,使用SCOPEDLOCK(m_exclusiveProcessLock),此时m_exclusiveLockCount为1,判断得出unLockFirstIfNeeded为真,此时锁升级,先加写锁,如果成功,则返回true;这里并没有把对读次数减一,因为当锁降级时,还需要保留读锁;当加写锁失败时,需要解读锁,防止出现两个进程同时加写锁,导致死锁,这里有一方先退出即先解掉占有的读锁,这样另一方必定会加写锁成功。这个过程是lock的情况。

当解锁时,如果解读锁,m_sharedLockCount为零表示一种错误并返回;先对读计数减一,如果有读写计数大于零,表示进行了递归加锁,不释放锁处理,否则解锁(不再占有)。当解写锁时,如果m_exclusiveLockCount为零表示一种错误并返回,否则对m_exclusiveLockCount减一,如果大于零则表示之前进行的是递归加写锁;否则此时不再成为写锁,那么尝试解锁或者进行锁降级为读锁,取决于m_sharedLockCount是否大于零,最后根据
m_lockInfo.l_type = static_cast(unlockToSharedLock ? F_RDLCK : F_UNLCK);判断是加读锁还是完全解锁。这个过程是unlock的过程。
总之,对于单进程,加锁解锁的次数要对应,锁升级时正确处理进程间死锁的情况,锁降级时能不能完全解锁,还是回归到读锁等情况。
然后对于两个进程,区别在于锁升级的情况,其他的都比较好理解。

如果用两个不相关的进程demo测试文件锁的情况,约定文件以读写方式打开,其中一个进程对文件锁进行了加写锁成功,sleep一段时间,另一个进程cmd为F_SETLK,会出现Resource temporarily unavailable。当把获取锁的进程kill时,就能成功获取锁;

—————————
今天线上出现了玩家无法登陆游戏的问题,在游戏中的玩家没问题,后来日志报了几个错误,联系问题的时间点,应该是这里引起的。堆栈打印stack overflow,前几帧和后几帧信息打印出来了,中间跳过一堆,行号大概十几万行,跟同事一起分析了下代码,大概是队伍跟随问题导致存在环,那么遍历的时候就死循环了,工作线程再也无法脱离这个消息队列,包括发给这个消息队列的消息,可能之前没有测试到这种情况,而且不是很明显的死循环。后来让相关同事修复了这个bug。虽然这个不算是关键路径,但是因为一个消息队列独占一个工作线程,而且其他的消息处理不了,导致都超时,且占用更多内存,引起雪崩或者如上面的玩家无法进游戏的情况。

你可能感兴趣的:(mmkv框架源码浅析(中))