Linux中的文件锁定简介
1. 概述
文件锁定是一种互斥机制,以确保文件可以被多个进程以安全的方式读取/写入。
在本教程中,我们将了解多进程系统中的调解更新问题。然后,我们将介绍 Linux 中的两种锁。
在此过程中,我们将通过示例学习一些与文件锁定相关的命令。
2. 调解更新问题
中间更新是**并发系统中典型的竞争条件 **问题。让我们看一个例子来更好地理解这个问题。
假设我们有一个balance.dat文件存储一个账户的余额,它的初始值为“ 100 ”。我们的并发系统有两个过程来更新余额值:
- 过程 A:读取当前值,减去 20 并将结果保存回文件。
- 过程 B:读取当前值,加 80 并将结果写回文件中。
显然,在执行了两个进程之后,我们期望文件的值是:100-20+80=160。
但是,在这种情况下可能会出现中间更新问题:
- 进程 A 读取文件的当前值 ( 100 ) 并准备进行进一步的计算。
- 进程 B 现在读取同一个文件并获取当前余额 ( 100 )。
- 进程 A 计算100-20并将结果80保存回文件。
- 进程 B 不知道余额自上次读取后已更新。因此,它仍然会使用旧值100来计算100+80并将结果180写入文件。
结果,我们在balance.dat文件中有180 ,而不是预期值160。
3. Linux 中的文件锁定
文件锁定是一种限制在多个进程之间访问文件的机制。它只允许一个进程在特定时间访问文件,从而避免了中间更新问题。
我们都知道rm -rf /在 Linux 中是一个非常危险的命令。如果我们以root用户执行命令,运行系统中的所有文件都将被删除。这是因为**Linux 通常不会自动锁定打开的文件。**但是,Linux 支持两种文件锁:建议锁和强制锁。
我们将很快介绍这两种锁定类型,但本文将主要关注咨询锁定。
3.1. 咨询锁定
建议锁定不是强制锁定方案。**只有当参与的进程通过显式获取锁进行合作时,它才会起作用。**否则,如果进程根本不知道锁,建议锁将被忽略。
一个例子可以帮助我们更容易地理解协作锁定方案。让我们回顾一下我们之前的余额示例。
- 首先,我们假设文件balance.dat仍然包含初始值“ 100 ”。
- 进程 A 获取balance.dat文件的排他锁,然后打开并读取该文件以获取当前值:100。
我们必须了解,咨询锁不是由操作系统或文件系统设置的。因此,即使进程 A 锁定了文件,进程 B 仍然可以通过系统调用自由地读取、写入甚至删除文件。
如果进程 B 执行文件操作而不尝试获取锁,我们说进程 B 不与进程 A 合作。
但是现在,让我们看看锁是如何为协作进程工作的:
- 进程 B 在读取文件之前尝试获取 balance.dat文件的锁(与进程 A 合作)。
- 由于进程 A 已锁定文件,进程 B 必须等待进程 A 释放锁定。
- 进程 A 计算 100-20并将80写回文件。
- 进程 A 释放锁。
- 进程 B 现在获取锁并读取文件,并获得更新后的值: 80。
- 进程 B 启动其逻辑并将结果 160 ( 80+80 ) 写回文件。
- 进程 B 释放锁,以便其他协作进程可以读取和写入文件。
我们将在后面的部分中看到如何使用flock命令来实现该示例。
3.2. 强制锁定
在我们开始研究强制文件锁定 之前,我们应该记住“强制锁定的 Linux 实现是不可靠的 ”。
与咨询锁定不同,**强制锁定不需要参与进程之间的任何合作。**一旦在文件上激活强制锁定,操作系统就会阻止其他进程读取或写入文件。
要在 Linux 中启用强制文件锁定,必须满足两个要求:
- 我们必须使用mand选项(mount -o mand FILESYSTEM MOUNT_POINT )**挂载 文件系统。
- 我们必须打开set-group-ID 位并关闭我们将要锁定的文件的 group-execute 位(chmod g+s,gx FILE)。
4. 检查系统中的所有锁
在本节中,让我们看一下检查正在运行的系统中当前获取的锁的两种方法。
4.1. lslocks命令
lslocks 命令是*util-linux *软件包的成员,可用于所有 Linux 发行版。它可以列出我们系统中所有当前持有的文件锁。
让我们看一个示例输出:
$ lslocks
COMMAND PID TYPE SIZE MODE M START END PATH
lvmetad 298 POSIX 4B WRITE 0 0 0 /run/lvmetad.pid
containerd 665 FLOCK 128K WRITE 0 0 0 /var/lib/docker/...
chromium 184029 POSIX 9.4M WRITE 0 1073741824 1073742335 /home/kent/.config/chromium/Default/History
nextcloud 961 POSIX 32K READ 0 128 128 /home/kent/Nextcloud/._sync_0e131dbf228b.db-shm
dockerd 630 FLOCK 16K WRITE 0 0 0 /var/lib/docker/buildkit/snapshots.db
dropbox 369159 FLOCK 10M WRITE 0 0 0 /home/kent/.dropbox/logs/1/1-4ede-5e20dd8d.tmp
...
在上面的列表中,我们可以看到系统中所有当前被锁定的文件。我们还可以看到每个锁的详细信息,比如锁的类型,哪个进程持有锁。
4.2. /proc/locks
/proc/locks不是命令。相反,它是procfs 虚拟文件系统中的一个文件。该文件包含所有当前文件锁。lslocks 命令也依赖此文件来生成列表。
要获取 /proc/locks的信息,我们执行“ cat /proc/locks ”:
$ cat /proc/locks
1: FLOCK ADVISORY WRITE 369159 08:12:22417368 0 EOF
2: POSIX ADVISORY WRITE 321130 00:2e:30761 0 EOF
3: POSIX ADVISORY WRITE 184029 08:12:21760394 0 EOF
4: POSIX ADVISORY WRITE 184029 08:12:21633968 1073741824 1073742335
5: POSIX ADVISORY WRITE 184029 08:12:21760401 0 EOF
6: POSIX ADVISORY WRITE 184029 08:12:21891515 0 EOF
7: POSIX ADVISORY WRITE 184029 08:12:21633928 0 EOF
...
让我们选择第一行来了解锁信息在*/proc/locks*文件系统中是如何组织的:
1: FLOCK ADVISORY WRITE 369159 08:12:22417368 0 EOF
-1- --2-- ---3--- --4-- ---5-- -------6------ -7- -8-
- 第一列是序列号。
- 第二个字段表示使用的锁的类,例如FLOCK(来自flock 系统调用)或POSIX(来自 lockf 、 fcntl系统调用)。
- 此列用于锁定类型。它可以有两个值:ADVISORY或MANDATORY。
- 第四个字段显示锁是写锁还是读锁。
- 然后我们有持有锁的进程的ID。
- 该字段包含一个冒号分隔值字符串,以“ major-device:minor-device:inode ”格式显示锁定文件的 id 。
- 此列与最后一列一起显示被锁定文件的锁定区域的开始和结束。在此示例行中,整个文件被锁定。
5. flock命令介绍
util-linux包也提供了flock 命令。该实用程序允许我们在 shell 脚本或命令行中管理咨询文件锁定。
基本使用语法是:
flock FILE_TO_LOCK COMMAND
接下来,让我们用 flock命令演示我们的余额更新示例。
除了包含当前余额值的balance.dat文本文件外,我们还需要两个进程 A 和 B 来更新文件中的余额。
我们首先创建一个简单的 shell 脚本update_balance.sh来处理两个进程的余额更新逻辑:
#!/bin/bash
file="balance.dat"
value=$(cat $file)
echo "Read current balance:$value"
#sleep 10 seconds to simulate business calculation
progress=10
while [[ $progress -lt 101 ]]; do
echo -n -e "\033[77DCalculating new balance..$progress%"
sleep 1
progress=$((10+progress))
done
echo ""
value=$((value+$1))
echo "Write new balance ($value) back to $file."
echo $value > "$file"
echo "Done."
我们创建一个简单的shell脚本a.sh来模拟进程A:
#!/bin/bash
#-----------------------------------------
# process A: lock the file and subtract 20
# from the current balance
#-----------------------------------------
flock --verbose balance.dat ./update_balance.sh '-20'
现在让我们启动进程A进行测试:
$ ./a.sh
flock: getting lock took 0.000002 seconds
flock: executing ./update_balance.sh
Read current balance:100
Calculating new balance..100%
Write new balance (80) back to balance.dat.
Done.
通过输出,我们可以看到flock命令首先获取了文件balance.dat的锁,然后update_balance.sh脚本读取并更新了该文件。 在其运行期间,我们可以通过lslocks命令查看锁信息:
$ lslocks | grep 'balance'
flock 825712 FLOCK 4B WRITE 0 0 0 /tmp/test/balance.dat
输出显示,flock命令对整个文件*/tmp/test/balance.dat持有WRITE*锁。
5.1. 非合作过程的flock示范
我们已经了解到,咨询锁只有在参与的进程合作时才起作用。让我们将余额重置为 100,并检查如果我们为进程 A 获取文件的咨询锁但以非合作方式启动进程 B 会发生什么。
现在让我们创建一个简单的 shell 脚本b_non-cooperative.sh:
#!/bin/bash
#----------------------------------------
# process B: add 80 to the current balance in a
# non-cooperative way
#----------------------------------------
./update_balance.sh '80'
我们看到进程 B 调用update_balance.sh没有尝试获取余额数据文件上的锁。 让我们在 GIF 动画中演示这个场景:
我们看到如果进程 B 启动时没有与进程 A 合作,进程 A 获取的咨询锁将被忽略。
因此,我们在balance.dat中有 180,而不是 160 。
5.2. 使用协同过程演示flock
最后,让我们创建另一个协作进程 B,b.sh,看看咨询锁是如何工作的:
#!/bin/bash
#----------------------------------------
# process B: add 80 to the current balance
# in a cooperative way
#----------------------------------------
flock --verbose balance.dat ./update_balance.sh '80'
同样,我们在 GIF 动画中展示演示:
在演示中,我们做了两个流程来配合。
我们注意到,当进程 B 尝试获取balance.dat文件的锁时,它等待进程 A 释放锁。因此,建议锁定起作用了,我们在余额数据文件中得到了预期的结果 160。