# 说明
# 背景
在多进程共享的应用程序中,通过锁
来对同一个计算资源进行协同管理是非常常见的做法,无论在单机或多机的系统、数据库、文件系统中,都需要依赖锁
机制来避免并发访问导致的不确定结果。
文件锁是一种互斥机制,可确保多个进程以安全的方式读取/写入同一个文件。之所以要对这些多进程业务进行控制,是因为这些进程的调度是不可预期的,这种时序上的不可预期会对同一个文件资源产生竞争性访问,从而带来预期外的结果。
# 中间更新问题(interceding update)
中间更新是并发系统中典型的竞争条件问题。 举例说明:
|
|
linux 中的文件锁
文件锁定是一种限制在多个进程之间访问文件的机制。它只允许一个进程在特定时间访问文件,从而避免中间更新(interceding update)
问题。linux
支持两种文件锁:
协同锁(Advisory Locking)
强制锁(mandatory locks)
# 协同锁
协同锁不是强制锁定方案。只有当参与的进程通过显式获取锁进行合作时,它才会起作用;否则,如果进程根本不知道锁,协同锁将被忽略。
仍以之前的示例说明:
|
|
# 强制锁
在了解强制文件锁定之前,需要了解的是:Linux 的强制锁实现是不可靠的
因强制锁存在 BUG,且自 linux 4.5 以来,该功能被认为很少使用,
强制锁已成为可选功能,由配置选项(CONFIG_MANDATORY_FILE_LOCKING)配置; 后续将逐步删除该功能。
与协同锁不同,强制锁不需要参与进程之间的任何合作。一旦在文件上激活强制锁定,操作系统就会阻止其他进程读取或写入文件。要在 linux
中启用强制文件锁定,必须满足两个要求:
- 必须使用
mand
选项挂载文件系统1
$ mount -o mand FILESYSTEM MOUNT_POINT
- 必须打开
set-group-ID
位并关闭我们将要锁定的文件的group-execute
位1
$ chmod g+s,g-x FILE
# 检查系统中的所有锁
检查正在运行的系统中当前获取的锁的两种方法:
lslockslslocks
命令是由 util-linux
软件包提供的,可用于所有 Linux
发行版,它可以列出我们系统中当前持有的所有文件锁。
|
|
在命令输出中,我们可以看到系统中所有当前被锁定的文件,以及每个锁的详细信息,比如锁的类型,哪个进程持有锁等。
/proc/locks/proc/locks
不是命令,它是 procfs
虚拟文件系统中的一个文件;该文件包含所有当前文件锁。lslocks
命令也依赖此文件来生成列表。
|
|
选取第一行来了解锁信息在 /proc/locks
文件系统中是如何组织的:
|
|
# 协同锁使用示例
util-linux
包也提供了flock
命令; flock
命令允许我们在 shell
脚本或命令行中管理协同文件锁,使用方式为:
|
|
# 获取协同锁
以更新 balance.dat
文本文件为例说明,还需要两个进程 A 和 B 来更新文件中的余额。首先创建一个简单的 shell
脚本 update_balance.sh
来处理两个进程的余额更新逻辑,脚本如下:
|
|
创建一个简单的 shell
脚本 a.sh
来模拟进程A
:
|
|
执行后结果:
|
|
脚本执行过程中,可以通过 lslocks
命令查看锁文件:
|
|
输出显示 flock
命令对整个文件 /tmp/test/balance.dat
持有一个 WRITE
锁。
# 非协作进程示例
协作锁只有在参与的进程协作时才起作用。将余额重置为 200,并测试如果进程 A 获取文件的协作锁但以非协作方式启动进程 B 会发生什么。
创建一个简单的 shell
脚本 b_non-cooperative.sh
:
|
|
进程 B 调用 update_balance.sh
没有尝试获取数据文件上的锁。
如果进程 B 启动时没有与进程 A 协作,进程 A 获取的协作锁将被忽略;因此在
balance.dat
中数字为 280,而不是 260。
# 协作进程示例
创建另一个协作进程 B,b.sh
,看看协作锁是如何工作的:
|
|
当进程 B 尝试获取
balance.dat
文件上的锁时,它等待进程 A 释放锁。因此,协同锁定起作用了,我们在数据文件中得到了预期的结果 260。
# flock 命令的使用
# flock 的语法
|
|
上面的第一种和第二种形式类似于 su
或 newgrp
的命令格式。他们锁定一个指定的文件或目录,如果尚不存在,则会创建(需要有适当的权限)。默认情况下,如果锁不能立即获得,flock
会一直等待,直到锁可用为止。
第三种形式通过文件描述符号使用打开的文件,后面有示例说明。
# 常见使用方式
|
|
# flock 和 fd 的结合使用
- 为什么将
flock
与文件描述符结合使用与更传统的锁定机制相比,使用
flock()
或与之密切相关的fcntl(LOCK_EX)
机制的主要优势之一是,无需在重新启动或其他非正常关机情况下执行清理动作。因为锁是通过文件描述符附加的;当该文件描述符关闭时(无论是通过正常关闭、SIGKILL 还是断电),不再持有锁。 - 使用举例1
1 2 3 4 5
## 特定版本之后的 bash,支持不用手动管理 fd 的分配 # this requires a very new bash -- 4.2 or so. exec {lock_fd}>filename # open filename, store FD number in lock_fd flock -x "$lock_fd" # pass that FD number to flock exec $lock_fd>&- # later: release the lock
- 使用举例2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
## 配置一个函数,用于获取锁 declare -A lock_fds=() # store FDs in an associative array getLock() { local file=$(readlink -f "$1") # declare locals; canonicalize name local op=$2 case $op in LOCK_UN) [[ ${lock_fds[$file]} ]] || return # if not locked, do nothing exec ${lock_fds[$file]}>&- # close the FD, releasing the lock unset lock_fds[$file] # ...and clear the map entry. ;; LOCK_EX) [[ ${lock_fds[$file]} ]] && return # if already locked, do nothing local new_lock_fd # don't leak this variable exec {new_lock_fd}>"$file" # open the file... flock -x "$new_lock_fd" # ...lock the fd... lock_fds[$file]=$new_lock_fd # ...and store the locked FD. ;; esac }
# flock() 系统调用
flock()
- 在打开的文件上应用或删除协同锁
# 实现
|
|
# 描述
对 fd
指定的打开文件应用或删除协同锁,参数 operation
是以下之一:
Tag | Description |
---|---|
LOCK_SH | Place a shared lock. More than one process may hold a shared lock for a given file at a given time. |
LOCK_EX | Place an exclusive lock. Only one process may hold an exclusive lock for a given file at a given time. |
LOCK_UN | Remove an existing lock held by this process. |
- 如果另一个进程持有不兼容的锁,则调用
flock()
可能会阻塞。要发出非阻塞请求,在上述任何操作中包含LOCK_NB(通过 ORing)
。 - 单个文件一般不会同时拥有共享锁和排他锁。
- 由
flock()
创建的锁与打开的文件表条目相(open file table entry)
关联。这意味着重复的文件描述符(例如,由fork(2)
或dup(2)
创建)引用同一个锁,并且可以使用这些描述符中的任何一个来修改或释放该锁。此外,通过对这些重复描述符中的任何一个执行显式LOCK_UN
操作或在所有此类描述符已关闭时释放锁。 - 如果一个进程使用
open(2)
(或类似方法)为同一个文件获取多个描述符,这些描述符将由flock()
独立处理。 - 一个进程只能在文件上持有一种类型的锁(共享或独占)。对已锁定文件的后续
flock()
调用会将现有锁定转换为新锁定模式。 - 由
flock()
创建的锁在execve(2)
中被保留。 - 无论打开文件的模式如何,都可以在文件上放置共享锁或排他锁。
# 返回值和错误
成功时,返回零;出错时,返回 -1,并适当设置 errno。
错误内容如下:
Error Code | Description |
---|---|
EBADF | d is not a not an open file descriptor. |
EINTR | While waiting to acquire a lock, the call was interrupted by delivery of a signal caught by a handler. |
EINVAL | operation is invalid. |
ENOLCK | The kernel ran out of memory for allocating lock records. |
EWOULDBLOCK | The file is locked and the LOCK_NB flag was selected. |
# 其他
flock(2)
不会通过NFS
锁定文件。请改用fcntl(2)
:它可以在NFS
上运行。- 从内核
2.0
开始,flock(2)
本身作为系统调用实现,而不是在GNU C
库中模拟为对fcntl(2)
的调用。flock(2)
和fcntl(2)
放置的锁类型之间没有交互,flock(2)
不会检测到死锁。 flock(2)
仅设置协同锁;给文件适当的权限,进程可以忽略flock(2)
的使用并对文件正常执行I/O
操作。- 对于
forked进程
和dup(2)
,flock(2)
和fcntl(2)
锁具有不同的语义。在使用fcntl()
实现flock()
的系统上,flock()
的语义与本文描述
部分不同。 - 转换锁(共享到独占,反之亦然)不能保证是原子的:先移除现有锁,然后建立新锁。在这两个步骤之间,可能会授予另一个进程的挂起锁请求,如果指定了
LOCK_NB
,则转换要么阻塞,要么失败。
# 其他
# 参考内容
本文内容参考自: