Skip to content

第九十七章:文件锁

斩灵期

涉及内核源码:

林小源走进一个大厅,看到两个进程正在争夺同一扇门。一个进程想读门后的数据,另一个想往门后写数据。它们互不相让,推搡着对方。

"停下!"一个威严的声音响起。一个身穿锁链甲胄的身影出现在门前——它的胸口挂着一把巨大的锁,锁面上刻着"F_RDLCK"和"F_WRLCK"。

"我是文件锁。"那人的声音低沉而有力,"你们不能同时读写同一个文件。"

"为什么不能?"读取进程不服气,"我只是想读!"

"你是读取没问题。"文件锁转向写入进程,"但你——写入——必须等它读完。如果你在它读取的过程中写入,它可能读到一半旧数据、一半新数据。那叫数据竞争。"

写入进程不情愿地后退了一步。

文件锁转向林小源:"文件锁的规则很简单:共享锁——多个进程可以同时持有,用于读取。独占锁——只有一个进程可以持有,用于写入。共享锁和独占锁不能共存。"

"所以刚才那个场景,读取进程应该申请共享锁?"

"对。"文件锁说,"如果两个进程都想读取,它们可以同时持有共享锁,互不影响。但如果有一个进程想写入,它必须等所有共享锁释放后,才能申请独占锁。"

林小源跟着文件锁走进大厅的深处,看到一面墙上挂着两把不同的锁——一把标着"flock",另一把标着"fcntl"。

"这两把锁有什么区别?"林小源问。

"粒度不同。"文件锁取下 flock 锁,"flock 锁定整个文件——要么全锁,要么不锁。简单,但粗粒度。如果两个进程想访问同一个文件的不同区域,用 flock 就只能排队等待。"

它又取下 fcntl 锁:"fcntl 可以锁定文件的某个区域——比如字节 0 到 100,或者字节 100 到 200。两个进程可以同时锁定不同区域,互不影响。"

"更灵活。"林小源说。

"但更复杂。"文件锁说,"fcntl 需要指定锁的起始位置、长度、类型。而且 fcntl 锁是进程级别的——进程结束后,锁自动释放。flock 锁是文件描述符级别的——关闭文件描述符才释放。"

林小源看着两把锁,思考了一会儿:"还有别的区别吗?"

"flock 是 BSD 传下来的,fcntl 是 POSIX 标准。"文件锁说,"在实际使用中,fcntl 更常用,因为它支持记录锁——锁定文件的某个区域。"

林小源注意到大厅的角落里有一面告示牌,上面写着"建议锁"和"强制锁"。

"这是什么?"他问。

文件锁的表情变得严肃:"这是两种锁的执行方式。建议锁——Advisory Lock——只对使用锁的进程有效。如果一个进程不申请锁就直接访问文件,内核不会阻止它。"

"那锁有什么用?"

"靠自觉。"文件锁说,"所有进程都遵守规则,先申请锁再访问文件。如果有一个进程不守规矩,锁就形同虚设。"

"强制锁呢?"

"强制锁——Mandatory Lock——对所有进程有效。"文件锁说,"即使进程不申请锁,内核也会阻止违反锁的访问。但强制锁在 Linux 中已经很少用了——它有性能问题,而且可能造成死锁。"

"死锁?"林小源警觉起来。

"对。"文件锁指着墙上的一个图示,"进程 A 持有文件 1 的锁,等待文件 2 的锁。进程 B 持有文件 2 的锁,等待文件 1 的锁。两者互相等待,永远无法继续。这就是死锁。"

"内核能检测死锁吗?"

"能。"文件锁说,"fcntl 锁有死锁检测——如果内核发现一个新的锁请求会导致死锁,它会拒绝这个请求,返回 错误。"

c
/*
 * 文件锁的类型:
 *
 * 1. 共享锁 (F_RDLCK)
 *    多个进程可以同时持有
 *    用于读取
 *
 * 2. 独占锁 (F_WRLCK)
 *    只有一个进程可以持有
 *    用于写入
 *
 * 3. 解锁 (F_UNLCK)
 *    释放锁
 *
 * 锁的粒度:
 *   记录锁: 锁定文件的某个区域
 *   整个文件: 锁定整个文件
 *
 * fcntl 锁:
 *   fcntl(fd, F_SETLK, &lock)
 *   fcntl(fd, F_GETLK, &lock)
 *
 * flock 锁:
 *   flock(fd, LOCK_SH)
 *   flock(fd, LOCK_EX)
 */

struct file_lock {
    int type;       /* F_RDLCK, F_WRLCK, F_UNLCK */
    int start;      /* 起始偏移 */
    int length;     /* 长度 */
    int pid;        /* 持有者 */
};

printf("=== 文件锁 — 并发访问控制 ===\n\n");

printf("文件锁的类型:\n\n");

printf("1. 共享锁 (F_RDLCK):\n");
printf("   多个进程可以同时持有\n");
printf("   用于读取\n\n");

printf("2. 独占锁 (F_WRLCK):\n");
printf("   只有一个进程可以持有\n");
printf("   用于写入\n\n");

printf("3. 解锁 (F_UNLCK):\n");
printf("   释放锁\n\n");

printf("--- 锁的兼容性 ---\n");
printf("请求 \\ 持有  共享锁  独占锁\n");
printf("共享锁       允许    拒绝\n");
printf("独占锁       拒绝    拒绝\n\n");

printf("--- 记录锁 ---\n");
printf("锁可以锁定文件的某个区域:\n");
printf("  进程 A: 锁定 0-100\n");
printf("  进程 B: 锁定 100-200\n");
printf("  两者不冲突\n\n");

printf("--- fcntl 锁 ---\n");
printf("struct flock lock = {\n");
printf("  .l_type = F_WRLCK,\n");
printf("  .l_whence = SEEK_SET,\n");
printf("  .l_start = 0,\n");
printf("  .l_len = 0,  /* 整个文件 */\n");
printf("};\n");
printf("fcntl(fd, F_SETLK, &lock);\n\n");

printf("--- flock 锁 ---\n");
printf("flock(fd, LOCK_SH);  /* 共享锁 */\n");
printf("flock(fd, LOCK_EX);  /* 独占锁 */\n");
printf("flock(fd, LOCK_UN);  /* 解锁 */\n\n");

printf("--- 死锁检测 ---\n");
printf("内核检测死锁:\n");
printf("  进程 A 等待进程 B 释放锁\n");
printf("  进程 B 等待进程 A 释放锁\n");
printf("  内核拒绝其中一个请求\n");

道藏笔记

内核启示

文件锁控制多个进程对文件的并发访问,规则很直观:共享锁多个进程可以同时持有,用于读取;独占锁只有一个进程能持有,用于写入;两者不能共存。

锁有两种粒度。flock 是 BSD 传下来的,锁定整个文件,简单但粗。fcntl 是 POSIX 标准,能锁定文件的某个区域——比如字节 0 到 100,两个进程锁不同区域互不影响,灵活得多。fcntl 还有一个区别:它是进程级别的,进程结束锁自动释放;flock 是文件描述符级别的,关闭 fd 才释放。

Linux 实际使用的是"建议锁"——只对主动申请锁的进程有效。如果一个进程不申请锁就直接访问文件,内核不会阻止它。这靠的是所有进程都守规矩。"强制锁"理论上对所有进程有效,但 Linux 中已经很少用了,有性能问题,还可能造成死锁。

说到死锁,fcntl 锁有死锁检测——如果内核发现一个新锁请求会导致死锁,会拒绝并返回 EDEADLK。


破关试炼

文件锁之试

本章对比 flock 和 POSIX 记录锁时,哪种锁能锁定文件的某个区域并带死锁检测?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道