文件锁的作用:当第一个进程正在读或修改文件的某个部分时,使用文件锁可以阻止其他进程修改文件的相同部分。

因此文件锁可用于多个进程之间进行同步,防止进程间的竞争状态。

Linux系统支持两组给文件加锁的不同API,分别是fcntlflock。本节主要记录flock的实现原理以及使用方式。

flock锁与文件表项相关

flock锁与文件表项相关,这意味着如果同一个进程通过调用open打开同名文件(这会生成一个新的文件表项)得到文件描述符,在该文件描述符上加flock锁时会当作两把锁,按照锁的互斥原则进行判断。这一点与fcntl记录锁不同fcntl锁在不同文件描述符上会被视为同一把锁,之间不会产生冲突。

而如果通过dupdup2以及fork得到的文件描述符,在该文件描述符加锁时会被认为是相同的锁,因为这些文件描述符指向相同的文件表项。此时如果通过任意一个文件描述符解锁,那么其他文件描述符上的锁也会被解除

如果不显式进行解锁,则需要所有文件描述符关闭之后才可以解锁,而fcntl锁则是指向相同i节点的任意一个文件描述符关闭,都会解锁

参见源码:fs/locks.c

/* Determine if lock sys_fl blocks lock caller_fl. Common functionality
 * checks for shared/exclusive status of overlapping locks.
 */
static bool locks_conflict(struct file_lock* caller_fl, struct file_lock* sys_fl)
{
    if (sys_fl->fl_type == F_WRLCK)
        return true;
    if (caller_fl->fl_type == F_WRLCK)
        return true;
    return false;
}
/* Determine if lock sys_fl blocks lock caller_fl. FLOCK specific
 * checking before calling the locks_conflict().
 */
/* 判断系统已存在的锁是否阻塞请求添加的锁 */
static bool flock_locks_conflict(struct file_lock* caller_fl, struct file_lock* sys_fl)
{
    /* FLOCK locks referring to the same filp do not conflict with
     * each other.
     */
    /* 如果请求添加的锁与系统已存在的锁的文件表项相同,则不阻塞,锁不冲突 */
    /* 这种情况一般出现在对duo或fork之后的文件描述符进行加锁 */
    if (caller_fl->fl_file == sys_fl->fl_file)
        return false;

    /* 如果文件表项不相同,则只要其中任何一个为写锁则阻塞 */
    return (caller_fl, sys_fl);
}
/* Try to create a FLOCK lock on filp. We always insert new FLOCK locks
 * after any leases, but before any posix locks.
 *
 * Note that if called with an FL_EXISTS argument, the caller may determine
 * whether or not a lock was successfully freed by testing the return
 * value for -ENOENT.
 */
static int flock_lock_inode(struct inode* inode, struct file_lock* request)
{
    // ... 
    list_for_each_entry(fl, &ctx->flc_flock, fl_list)
    {
        if (request->fl_file != fl->fl_file)
            continue;
        /* 这里:系统中存在锁与请求设置的锁的文件表项相同 */
        if (request->fl_type == fl->fl_type)
            // 锁类型相同,表示锁已存在,则跳出
            goto out;
        // 这里:表示存在锁并且与请求的锁文件表项相同但类型不同
        // 设置锁存在标志
        found = true;
        // 从系统锁列表中删除文件表项相同的锁fl,并将其添加到dispose链表中
        // 如果是请求锁则重新添加,解锁则清除dispose
        locks_delete_lock_ctx(fl, &dispose);
        break;
    }
    // 这里:表示存在锁并且与请求的锁文件表项相同但类型不同
    if (request->fl_type == F_UNLCK)
    {
        // 解锁
        if ((request->fl_flags & FL_EXISTS) && !found)
            error = -ENOENT;
        /* 如果是解锁并且锁已找到,执行out,out中会解锁 */
        goto out;
    }
 /* 这里:已经去掉了文件表项相同的锁,判断是否存在文件表项不同的锁,导致冲突 */
find_conflict:
    /* 遍历文件描述符指向的i节点中的flock链表 */
    list_for_each_entry(fl, &ctx->flc_flock, fl_list)
    {
        /* 判断是否有锁冲突,如果不冲突则继续循环 */
        if (!flock_locks_conflict(request, fl))
            continue;
        error = -EAGAIN;
        /* 这里:发生了所冲突,设置错误码FILE_LOCK_DEFERRED,休眠标志(FL_SLEEP) */
        /* 用于通知外层,当前进程需要休眠等待锁 */
        if (!(request->fl_flags & FL_SLEEP))
            goto out;
        error = FILE_LOCK_DEFERRED;
        /* 将当前请求的锁加入到阻塞的锁列表中 */
        locks_insert_block(fl, request, flock_locks_conflict);
        goto out;
    }
    /* 没有锁冲突,则添加当前锁到flock锁链表中 */
    if (request->fl_flags & FL_ACCESS)
        goto out;
    locks_copy_lock(new_fl, request);
    locks_move_blocks(new_fl, request);
    locks_insert_lock_ctx(new_fl, &ctx->flc_flock);
    new_fl = NULL;
    error = 0;

out:
    spin_unlock(&ctx->flc_lock);
    percpu_up_read(&file_rwsem);
    if (new_fl)
        locks_free_lock(new_fl);
    /* 清空dispose中的所有锁 */
    locks_dispose_list(&dispose);
    trace_flock_lock_inode(inode, request, error);
    return error;
}
/**
 * flock_lock_inode_wait - Apply a FLOCK-style lock to a file
 * @inode: inode of the file to apply to
 * @fl: The lock to be applied
 *
 * Apply a FLOCK style lock request to an inode.
 */
static int flock_lock_inode_wait(struct inode* inode, struct file_lock* fl)
{
    int error;
    might_sleep();
    for (;;)
    {
        error = flock_lock_inode(inode, fl);
        /* 当错误码为FILE_LOCK_DEFERRED时,继续循环 */
        if (error != FILE_LOCK_DEFERRED)
            break;
        error = wait_event_interruptible(fl->fl_wait, list_empty(&fl->fl_blocked_member));
        if (error)
            break;
    }
    locks_delete_block(fl);
    return error;
}

flock非显式解锁

如果flock锁不显式进行解锁,则需要所有文件描述符关闭之后才可以解锁,而fcntl锁则是指向相同i节点的任意一个文件描述符关闭,都会解锁

参见源码:fs/open.c, fs/file.c fs/file_table.c

/*
 * Careful here! We test whether the file pointer is NULL before
 * releasing the fd. This ensures that one clone task can't release
 * an fd while another clone is opening it.
 */
SYSCALL_DEFINE1(close, unsigned int, fd)
{
    int retval = __close_fd(current->files, fd);
    /* ... */
    return retval;
}
/*
 * The same warnings as for __alloc_fd()/__fd_install() apply here...
 */
int __close_fd(struct files_struct* files, unsigned fd)
{
    struct file* file;
    struct fdtable* fdt;

    // 从进程的文件管理结构体中获取文件描述符表
    spin_lock(&files->file_lock);
    fdt = files_fdtable(files);
    /* 判断要关闭的文件描述符是否大于已打开的最大文件描述符 */
    if (fd >= fdt->max_fds)
        goto out_unlock;
    /* 通过文件描述符表以及要关闭的文件描述符,获取内核维护的文件表项 */
    file = fdt->fd[fd];
    if (!file)
        goto out_unlock;
    rcu_assign_pointer(fdt->fd[fd], NULL);
    /* 获取到文件表项之后,释放文件描述符 */
    __put_unused_fd(files, fd);
    /* 释放完文件描述符,则对进程表中管理当前进程已打开文件的操作已经结束 */
    spin_unlock(&files->file_lock);
    // 内核处理文件表项
    return filp_close(file, files);

out_unlock:
    spin_unlock(&files->file_lock);
    return -EBADF;
}

/*
 * "id" is the POSIX thread ID. We use the
 * files pointer for this..
 */
int filp_close(struct file* filp, fl_owner_t id)
{
    int retval = 0;

    if (!file_count(filp))
    {
        printk(KERN_ERR "VFS: Close: file count is 0\n");
        return 0;
    }

    // 关闭文件时刷新
    if (filp->f_op->flush)
        retval = filp->f_op->flush(filp, id);

    if (likely(!(filp->f_mode & FMODE_PATH)))
    {
        dnotify_flush(filp, id);
        // 释放posix锁
        locks_remove_posix(filp, id);
    }
    // 进一步处理文件表项,判断是否引用计数为0,进而清理
    fput(filp);
    return retval;
}

void fput(struct file* file)
{
    // 文件引用计数检测
    if (atomic_long_dec_and_test(&file->f_count))
    {
        struct task_struct* task = current;

        if (likely(!in_interrupt() && !(task->flags & PF_KTHREAD)))
        {
            // 创建任务,并设置处理函数为____fput,____fput中会执行清理动作
            init_task_work(&file->f_u.fu_rcuhead, ____fput);
            // 添加任务并开始执行
            if (!task_work_add(task, &file->f_u.fu_rcuhead, true))
                return;
            /*
             * After this task has run exit_task_work(),
             * task_work_add() will fail.  Fall through to delayed
             * fput to avoid leaking *file.
             */
        }

        if (llist_add(&file->f_u.fu_llist, &delayed_fput_list))
            schedule_delayed_work(&delayed_fput_work, 1);
    }
}
static void ____fput(struct callback_head* work)
{
    __fput(container_of(work, struct file, f_u.fu_rcuhead));
}

/* the real guts of fput() - releasing the last reference to file
 */
static void __fput(struct file* file)
{
    /* ... */
    // 移除flock文件锁
    locks_remove_file(file);

    /* ... */
out:
    file_free(file);
}

其他

  1. flock锁只能对文件整体加锁,无法对文件内容加锁。
  2. flock锁不受文件打开模式(O_RDONLY, O_WRONLY, O_RDWR)影响,fcntl锁需要和打开模式保持一致。
  3. flock锁如果冲突会直接阻塞,不支持检测,fcntl锁除非设置命令为F_SETLKW才会阻塞,否则会出错返回。