文件系统-4-文件锁flock
基于msm-5.4
一、简介
对于文件系统的文件,存在多线程或进程同时访问的问题,如果没有锁机制,则可能导致文件数据的损坏或不一致。文件锁分为 劝告锁(Advisory Lock)和 强制锁(Mandatory Lock) 两种类型。
(1) 劝告锁是一种建议性的锁,通过该锁告诉访问者现在该文件被其他进程访问,但不强制阻止访问。
(2) 强制锁则是在有进程对文件锁定的情况下,其他进程是无法访问该文件的。
在使用模式上,这两种锁都可以分为共享锁和排他锁两种:
(1) 共享锁(Shared Lock): 在任意时间内,一个文件的共享锁可以被多个进程拥有,共享锁不会导致系统进程的阻塞(类似读锁)。
(2) 排他锁(Exclusive Lock): 在任意时间内,一个文件的排他锁只能被一个进程拥有(类似写锁)。
读文件可以使用共享锁,写文件可以使用排他锁。
在Linux中,文件锁的特性是通过 flock() 和 fcnt1() 两个函数对外提供的。这两个函数都可以实现文件加锁和解锁的流程,但是后者要比前者的特性更加丰富,粒度也更细。
二、相关结构
1. struct inode
struct inode { //fs.h
...
struct file_lock_context *i_flctx; //文件锁上下文
...
};
检查 i_flctx 是否为 NULL, 可以判断此文件上是否有持有锁。
2. struct file_lock_context
struct file_lock_context { //fs.h spinlock_t flc_lock; struct list_head flc_flock; //flock锁记录 struct list_head flc_posix; //POSIX锁记录 struct list_head flc_lease; //有租约锁 };
各类型锁链表,不为空表示有持相关锁。
3. struct file_lock
struct file_lock { struct file_lock *fl_next; //链表指针 struct list_head fl_list; //在上下文链表中 struct hlist_node fl_link; //hash 链 struct file *fl_file; //指向 file 结构 struct pid *fl_pid; //持有者进程 struct list_head fl_blocked_member; //阻塞队列链 // 锁属性 unsigned char fl_type; //F_RDLCK / F_WRLCK unsigned int fl_flags; //FL_POSIX/FL_FLOCK/FL_LEASE 等 unsigned char fl_reason; //为什么被 break(租约) // 锁范围 loff_t fl_start; loff_t fl_end; // 回调 const struct file_lock_operations *fl_ops; const struct lock_manager_operations *fl_lm_ops; // ...其他字段 };
每个具体锁使用一个此结构。
三、flock
1. 系统调用
#include <sys/file.h> int flock(int fd, int operation);
作用: 对已打开的文件添加或移除建议锁。
参数 operation 可以是以下之一:
LOCK_SH: 添加共享锁。同一时间,多个进程可以持有同一文件的共享锁。
LOCK_EX: 添加排他锁。同一时间,同一文件只能有一个进程持有排他锁。
LOCK_UN: 移除当前进程持有的锁。
若卡锁不想阻塞,需要在这三个标志上再或上 LOCK_NB 标志。
单个文件不能同时拥有共享锁和排他锁####。一个进程也只能对一个文件持有一种类型的锁(共享锁或排他锁),对已锁定的文件后续调用 `flock()` 会将现有锁转换为新的锁模式。
flock() 创建的锁与已打开的文件描述符(fd)相关联,这意味着重复的文件描述符(例如 fork()/dup() 创建的)指向同一个锁,并且可以使用这些文件描述符中的任何一个来修改或释放该锁。此外,锁的释放方式有两种:一是对这些重复的文件描述符执行显式的 `LOCK_UN` 操作; 二是当所有此类文件描述符都已关闭时。
如果一个进程通过 open(2)(或类似的方式)为同一个文件获取了多个文件描述符,那么这些文件描述符在 flock() 的处理中是独立的。使用其中一个文件描述符尝试对文件加锁,可能会被该调用进程已经通过另一个文件描述符加的锁所拒绝。
由 flock() 创建的锁在 execve() 调用后仍然有效。无论文件以何种模式打开,都可以对文件施加共享锁或独占锁。
返回值:成功时返回零,出错时返回 -1,并设置相应的错误代码 errno。
2. 执行路径
SYSCALL_DEFINE2(flock, unsigned int, fd, unsigned int, cmd) //fs/locks.c locks_lock_file_wait //linux/fs.h locks_lock_inode_wait //fs/locks.c flock_lock_inode_wait //fs/locks.c wait_event_interruptible(fl->fl_wait, ...) //休眠状态
其中 fl 是 struct file_lock 类型。
3. 总结说明
持 flock() 失败阻塞锁后是休眠状态。
四、fcntl
1. 系统调用
#include <unistd.h> #include <fcntl.h> int fcntl(int fd, int cmd, ... /* arg */ );
fcntl()不仅可以用于锁操作,还可以用于其他操作,这主要依赖参数cmd的值。对于文件锁,cmd 可以是 F_GETLK、F_SETLK 、FSETLKW(W是wait休眠等待的意思)。对于文件锁操作,第3个参数的类型为 struct flock。此时函数签名为:
int fcntl(int fd, int cmd, struct flock *lock);
lock 参数是文件锁的详细属性信息,它描述了我们想要添加什么类型的文件锁,要加锁的范围等。
struct flock { //fcntl.h short l_type; //F_RDLCK,F_WRLCK,F_UNLCK short l_whence; __kernel_off_t l_start; __kernel_off_t l_len; __kernel_pid_t l_pid; };
成员介绍:
l_type: F_RDLCK 表示读锁(共享锁), F_WRLCK 表示写锁(排他锁), F_UNLCK 表示解锁。
l_whence: 是 l_start 的参考基准,和 lseek 一样,取值有3个:SEEK_SET 表示相对文件开头,SEEK_CUR 表示相对当前文件偏移,SEEK_END 相对文件末尾。
等效起点计算:start_offset = base(l_whence) + l_start, 实际工程上常用 SEEK_SET,不容易出错。
l_start: 锁定区间的起始偏移(单位字节)。可以是0, 可正可负, 与 l_whence 一起决定最终起点。
l_len: 锁定长度(单位字节)。若大于0,锁 [start, start + l_len) 这段; 若等于0,锁从 start 一直到文件末尾 EOF, 并随文件增长延伸;小于0,表示向前锁定一段,从 start+len 到 start。
最常见写法是 l_len = 0,表示“从某点到文件末尾全锁住”。
l_pid: 含义与 cmd 参数有关。当 cmd 取 F_SETLK/F_SETLKW 时通常可忽略,F_GETLK时由内核填写“冲突锁持有者 PID”。
2. 执行路径
(1) fcntl上锁/解锁
SYSCALL_DEFINE3(fcntl, fd, cmd, arg) //fs/fcntl.c do_fcntl //fs/fcntl.c case F_OFD_SETLK: case F_OFD_SETLKW: case F_SETLK: //上锁和解锁都对应这个字段 case F_SETLKW: fcntl_setlk(fd, filp, cmd, &flock) //fs/locks.c do_lock_file_wait //fs/locks.c for (;;) { vfs_lock_file //fs/locks.c if (filp->f_op->lock) //若文件没定义lock回调则走posix的 return filp->f_op->lock(filp, cmd, fl); else return posix_lock_file(filp, fl, conf); wait_event_interruptible(fl->fl_wait,...) //休眠 }
(2) fcntl持锁查询
SYSCALL_DEFINE3(fcntl, fd, cmd, arg) //fs/fcntl.c do_fcntl //fs/fcntl.c case F_OFD_GETLK: case F_GETLK: fcntl_getlk //fs/locks.c vfs_test_lock //fs/locks.c if (filp->f_op->lock) return filp->f_op->lock() posix_test_lock()
3. 总结说明
fcntl 锁是“建议锁”,不是强制锁,所有进程都要自觉遵守,只要不遵守约定就不受约束。
四、相关调试
1. /proc/locks
用于查看哪些进程持有文件锁
/ # cat /proc/locks 1: POSIX ADVISORY WRITE 510 00:14:26268 0 EOF 2: POSIX ADVISORY WRITE 446 fe:10:302 0 EOF # 格式示例: # 10: POSIX ADVISORY READ 12345 08:03:98765 0 EOF # ^ ^ ^ ^ ^ ^ ^ ^ ^^ # 锁号 类型 建议型 锁类型 PID 设备号 inode 范围
上述两个表示 POSIX 类型的建议锁(ADVISORY), 是排它性锁(WRITE).
查看被锁的是哪个文件,通过下面这种方法找到多个文件:
/ # find / -inum 26268 2>/dev/null /dev/vsomeip/vsomeip.lck //ls -i 确认是 26268 /sys/devices/virtual/block/ram4/queue/zoned //ls -i 确认是 26268 / # / # stat -Lc '%n dev=%t:%T ino=%i' /dev/vsomeip/vsomeip.lck /sys/devices/virtual/block/ram4/queue/zoned /dev/vsomeip/vsomeip.lck dev=0:0 ino=26268 /sys/devices/virtual/block/ram4/queue/zoned dev=0:0 ino=26268
注:这里的 %t:%T 不是“文件所在文件系统的设备号”,而是“设备文件的 rdev 主次设备号”。对普通文件(或很多伪文件节点)它经常显示 0:0,所以两个路径都变成了 0:0,看起来像同一设备。
正确应该使用下面这个,看真正的文件系统设备号 + inode:
/ # stat -Lc '%n fs_dev_dec=%d fs_dev_hex=%D ino=%i type=%F' /dev/vsomeip/vsomeip.lck /sys/devices/virtual/block/ram4/queue/zoned /dev/vsomeip/vsomeip.lck fs_dev_dec=20 fs_dev_hex=14 ino=26268 type=regular empty file /sys/devices/virtual/block/ram4/queue/zoned fs_dev_dec=22 fs_dev_hex=16 ino=26268 type=regular file
这是正常现象,原因是 inode 号不是全局唯一,只在同一个文件系统实例内唯一####。即使 inode 都是 26268,也可能是不同文件系统里的“同号 inode”,这在 sysfs、tmpfs、devtmpfs 这种伪文件系统里很常见。单看 inode 号永远不够,必须带上设备号一起判定。
使用这个方法确认是哪个文件:
法1:
/ # lsof -p 510 | grep 26268 haha@2.0-ser 510 root 24w REG 0,20 0 26268 /dev/vsomeip/vsomeip.lck
法2:
/ # stat -Lc 'dev=%t:%T inode=%i path=%n -> %N' /proc/510/fd/* ... dev=0:0 inode=26268 path=/proc/510/fd/24 -> /proc/510/fd/24 ... # or / # for f in /proc/510/fd/*; do stat -Lc '%n dev=%t:%T inode=%i -> %N' "$f"; done | grep 'inode=26268' /proc/510/fd/24 dev=0:0 inode=26268 -> /proc/510/fd/24 # / # ls -l /proc/510/fd/24 l-wx------ 1 root root 64 2026-04-13 15:45 /proc/510/fd/24 -> /dev/vsomeip/vsomeip.lck
法2的这个设备号有时竟然匹配不上:
/ # stat -Lc 'dev=%t:%T inode=%i path=%n -> %N' /proc/446/fd/* ... dev=0:0 inode=302 path=/proc/446/fd/4 -> /proc/446/fd/4 / # ls -l /proc/446/fd/4 lrwx------ 1 radio radio 64 2026-04-13 15:45 /proc/446/fd/4 -> /data/vendor/ipa/ipacm.pid
但是法1的这个能设备号能对上, 254,16 == fe:10
/ # lsof -p 446 | grep 302 ipacm 446 radio 4u REG 254,16 0 302 /data/vendor/ipa/ipacm.pid
可以看第一个锁对应的文件在哪个文件系统中, 看16进制的 00:14 对应哪个挂载点:
/ # cat /proc/510/mountinfo | awk '$3=="0:20"{print}' 19 29 0:20 / /dev rw,nosuid,relatime shared:2 - tmpfs tmpfs rw,seclabel,mode=755
2. filelock events
/sys/kernel/debug/tracing/events/filelock # ls break_lease_block break_lease_unblock fcntl_setlk flock_lock_inode generic_delete_lease locks_get_lock_context posix_lock_inode break_lease_noblock generic_add_lease leases_conflict locks_remove_posix time_out_leases
锁相关 trace events:
(1) locks_get_lock_context
作用:记录 inode->i_flctx(文件锁上下文)获取/创建情况。常用于判断“这次请求是否新建了锁上下文”。
触发函数:locks_get_lock_context(inode, type) 尾部。
典型调用路径:
fcntl(F_SETLK/F_SETLKW) -> sys_fcntl -> do_fcntl -> fcntl_setlk -> do_lock_file_wait -> vfs_lock_file -> posix_lock_file -> posix_lock_inode -> locks_get_lock_context -> trace_locks_get_lock_context
flock() -> sys_flock -> locks_lock_file_wait -> flock_lock_inode -> locks_get_lock_context -> trace
lease 路径里 generic_add_lease 也会调用它。
(2) posix_lock_inode
作用:记录一次 POSIX 锁操作(加锁/解锁/冲突)结果,包含 fl_type/fl_start/fl_end/fl_flags/ret。
触发函数:posix_lock_inode(...) 返回前。
调用路径:
fcntl(F_SETLK/F_SETLKW/F_OFD_SETLK/F_OFD_SETLKW) -> fcntl_setlk -> do_lock_file_wait -> vfs_lock_file -> posix_lock_file -> posix_lock_inode -> trace_posix_lock_inode
(3) fcntl_setlk
作用:记录 fcntl_setlk() 这层接口的最终返回值,便于区分“参数校验失败”与“真正加锁阶段失败”。
触发函数:fcntl_setlk(...) 的 out: 路径。
调用路径:
fcntl(F_SETLK/F_SETLKW/F_OFD_SETLK/F_OFD_SETLKW) -> sys_fcntl -> do_fcntl -> fcntl_setlk -> trace_fcntl_setlk
备注:这是 fcntl_setlk(非 compat 64 分支)里的 trace 点。
(4) locks_remove_posix
作用:记录 close 路径清理 POSIX 锁时的结果(FL_CLOSE 解锁)。
触发函数:locks_remove_posix(filp, owner) 末尾。
调用路径:
close(fd) -> filp_close -> fput -> locks_remove_file -> locks_remove_posix -> vfs_lock_file(...F_UNLCK...) -> trace_locks_remove_posix
(5) flock_lock_inode
作用:记录 BSD flock 加锁/解锁冲突结果。
触发函数:flock_lock_inode(...) 返回前。
调用路径:
flock(fd, LOCK_*) -> sys_flock -> locks_lock_file_wait -> flock_lock_inode -> trace_flock_lock_inode
close 清理 flock 时,locks_remove_flock 也会走到 flock_lock_inode(无 f_op->flock 时)。
lease 相关 trace events:
(6) break_lease_noblock
作用:记录 __break_lease 在 O_NONBLOCK 下遇到冲突 lease,立即返回 -EWOULDBLOCK 的时刻。
触发函数:__break_lease 中 if (mode & O_NONBLOCK) 分支。
调用路径:
open/truncate 触发 break lease -> break_lease/__break_lease -> trace_break_lease_noblock
(7) break_lease_block
作用:记录 break lease 进入阻塞等待前,把 new_fl 挂到 blocker 的 blocked 队列那一刻。
触发函数:__break_lease 中 locks_insert_block(...) 后。
调用路径:
open/truncate -> __break_lease -> 冲突 -> 插入等待 -> trace_break_lease_block
(8) break_lease_unblock
作用:记录阻塞等待醒来后,从 blocked 队列摘除并继续重试的时刻。
触发函数:__break_lease 中 wait_event... 返回后、locks_delete_block(new_fl) 前后。
调用路径:
open/truncate -> __break_lease -> wait -> wakeup -> trace_break_lease_unblock
(9) generic_delete_lease
作用:记录删除 lease 时找到的 victim(或未找到为 NULL)。
触发函数:generic_delete_lease(filp, owner) 中查找到 victim 后。
调用路径:
fcntl(F_SETLEASE, F_UNLCK) -> fcntl_setlease -> vfs_setlease -> generic_setlease -> generic_delete_lease -> trace_generic_delete_lease
以及 close 路径里 lease 清理可能触发相关删除逻辑。
(10) time_out_leases
作用:记录 lease 超时扫描中的每个 lease 条目,后续可能 downgrade 或 unlock。
触发函数:time_out_leases(inode, dispose) 循环内部。
调用路径:
__break_lease 开始会调用 time_out_leases
fcntl_getlease 里也会调用 time_out_leases
generic_add_lease 里也会先调用一次
在这些路径里都会触发 trace_time_out_leases
(11) generic_add_lease
作用:记录尝试新增/修改 lease 的入口状态(inode open 计数、lease 类型/flags 等)。
触发函数:generic_add_lease(...) 开头。
调用路径:
fcntl(F_SETLEASE, F_RDLCK/F_WRLCK) -> fcntl_setlease -> vfs_setlease -> generic_setlease -> generic_add_lease -> trace_generic_add_lease
(12) leases_conflict
作用:记录 lease 与 breaker 是否冲突的判定结果(conflict=true/false),用于诊断“为什么 break lease 没发生/发生了”。
触发函数:leases_conflict(lease, breaker) 末尾。
调用路径:
__break_lease -> any_leases_conflict -> leases_conflict -> trace_leases_conflict
generic_add_lease 中冲突扫描也会走到同一判定。
打印示例:
/ # P=/sys/kernel/tracing; echo filelock > $P/set_event; > $P/trace; echo 1 > $P/tracing_on; cat $P/trace_pipe app_process-8559 [004] .... 19316.820374: flock_lock_inode: fl=0x00000000736da099 dev=0xfc:0x3 ino=0x51576 fl_blocker=0x0000000000000000 fl_owner=0x000000007384c91a fl_pid=8559
fl_flags=FL_FLOCK|FL_SLEEP fl_type=F_UNLCK fl_start=0 fl_end=9223372036854775807 ret=0 app_process-8559 [004] .... 19316.820387: locks_get_lock_context: dev=0xfc:0x3 ino=0x491fc type=F_WRLCK ctx=000000009ae28193
五、实验
1. fcntl加排它锁实验
#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <fcntl.h> #define BUF LEN 4096 int main(int argc, char* argv[]) { int ret = 0; struct flock test_lock = { .l_whence = SEEK_SET, .l_type = F_WRLCK, //排它锁 }; int fd = open("a.txt", O_RDWR); if (fd < 0){ printf("open file failed, ret=%d, errno=%d: %s\n", fd, errno, strerror(errno)); goto OUT; } printf("before lock file\n"); //改F_SETLK重复持锁会失败 if (argc == 1) /* 实测,trace上这种阻塞在文件锁上是休眠状态(S) */ ret = fcntl(fd, F_SETLKW, &test_lock); //加锁操作,如果已经被加锁则等待 else ret = fcntl(fd, F_SETLK, &test_lock); //加锁操作,如果已经被加锁则返回失败 if (ret <0) { /* * argc != 1时报错打印: * Ubuntu: lock file failed, ret=-1, errno=11: Resource temporarily unavailable * Android: lock file failed, ret=-1, errno=11: Try again */ printf("lock file failed, ret=%d, errno=%d: %s\n", ret, errno, strerror(errno)); goto OUT; } printf("after lock file\n"); sleep(3); //休眠3秒,用于模拟访问碰撞 OUT: return(ret); }
Android.bp:
cc_binary { name: "flock_test", srcs: ["flock_test.cpp"], cflags: [ "-Wall", "-Werror", "-Wno-unused-function", "-Wno-unused-parameter", "-Wno-unused-variable", ], static_executable: true, static_libs: ["libc"], }
开多用例运行,后开的会阻塞。若将 F SETLKW 改为 F_SETLK,此时后到的不会被阻塞,而是会返回一个错误。
加锁后,若使用cat命令读取文件数据,仍然是可以正常读取数据,因为在Linux中默认使用的是劝告锁。如果进程没有对锁的状态进行询问而直接访问数据,则锁并不会保护数据。
为了对某个特定文件施行强制性上锁,需要使用强制锁。使用强制锁需要满足如下几个条件。
(1) 挂载文件系统时要指定 mand 选项(mount-0 mand)。
(2) 必须关闭文件的组成员执行位(chmod g-x file)。
(3) 必须打开文件的 SGID 位(chmod g+s file)。这里 SGID (Set Group ID)是文件/目录的一种特殊权限,用于用户临时获得组权限。
完成上述操作后,如果在一个窗口运行该程序,则在另一个窗口执行 ca t命令或 vim 命令查看文件数据时会被阻塞。
五、总结
1. 无论是 flock 还是 fcntl, 持文件锁失败进入阻塞时,都是休眠状态。
2. 文件锁相当于"自己定规则自己遵守", 只要有任何一方不遵守此规则,就可以无视锁的存在去访问文件。
posted on 2026-04-13 21:29 Hello-World3 阅读(2) 评论(0) 收藏 举报
浙公网安备 33010602011771号