文件系统-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)    收藏  举报

导航