第九十七章:文件锁
斩灵期涉及内核源码:
一
林小源走进一个大厅,看到两个进程正在争夺同一扇门。一个进程想读门后的数据,另一个想往门后写数据。它们互不相让,推搡着对方。
"停下!"一个威严的声音响起。一个身穿锁链甲胄的身影出现在门前——它的胸口挂着一把巨大的锁,锁面上刻着"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 锁有死锁检测——如果内核发现一个新的锁请求会导致死锁,它会拒绝这个请求,返回 错误。"
/*
* 文件锁的类型:
*
* 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");#include <stdio.h>
/*
* 文件锁的类型:
*
* 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; /* 持有者 */
};
int main() {
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");
return 0;
}道藏笔记
内核启示
文件锁控制多个进程对文件的并发访问,规则很直观:共享锁多个进程可以同时持有,用于读取;独占锁只有一个进程能持有,用于写入;两者不能共存。
锁有两种粒度。flock 是 BSD 传下来的,锁定整个文件,简单但粗。fcntl 是 POSIX 标准,能锁定文件的某个区域——比如字节 0 到 100,两个进程锁不同区域互不影响,灵活得多。fcntl 还有一个区别:它是进程级别的,进程结束锁自动释放;flock 是文件描述符级别的,关闭 fd 才释放。
Linux 实际使用的是"建议锁"——只对主动申请锁的进程有效。如果一个进程不申请锁就直接访问文件,内核不会阻止它。这靠的是所有进程都守规矩。"强制锁"理论上对所有进程有效,但 Linux 中已经很少用了,有性能问题,还可能造成死锁。
说到死锁,fcntl 锁有死锁检测——如果内核发现一个新锁请求会导致死锁,会拒绝并返回 EDEADLK。
文件锁之试
本章对比 flock 和 POSIX 记录锁时,哪种锁能锁定文件的某个区域并带死锁检测?