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<PosixFileLock*>(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<short>(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<short>(unlockToSharedLock ? F_RDLCK : F_UNLCK);判断是加读锁还是完全解锁。这个过程是unlock的过程。
总之,对于单进程,加锁解锁的次数要对应,锁升级时正确处理进程间死锁的情况,锁降级时能不能完全解锁,还是回归到读锁等情况。
然后对于两个进程,区别在于锁升级的情况,其他的都比较好理解。

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容