文件锁保护共同协议中的特定对象
文件锁协调的是一组参与者:大家约定锁哪个对象、何时获取锁,以及哪些操作必须放在临界区内。Linux 提供多种锁接口,它们的所有权和释放规则并不相同。选一个文件名,再调用名称里带 lock 的函数,并不足以构成正确协议;还必须明确底层对象、锁的种类,以及相关每份引用的寿命,否则看起来都成功加锁的程序仍可能同时修改同一份逻辑数据。 坏掉的样子 在本实验使用的 Linux 本地文件系统上,flock 提供建议式互斥。另一个请求冲突锁的进程会等待或失败,但完全没有请求锁的普通写入仍然可以进行。因此,文件锁不会赋予内容隐私,也不会自动限制所有能够打开文件的程序。只有参与者实际遵循共同协议,互斥才能保护需要保持一致的数据操作。 如果更新通过新 inode 替换原路径,直接锁数据文件就会出现对象分离。已有锁仍关联旧的打开文件描述,后续打开者却访问替换后的文件,并可能成功获取另一把锁。两个写者都以为自己独占同一份逻辑文档,实际锁住的却是不同对象。路径名称稳定,并不意味着路径背后的文件身份稳定。 传统 POSIX 记录锁还具有容易忽略的寿命规则:锁属于进程,该进程关闭任何一个指向该文件的描述符,都会释放它在该文件上持有的记录锁。因此,某个库独立打开并关闭同一 inode,也可能使程序其他位置持有的锁失效。这与通过打开文件描述拥有锁的 flock 不同,不能把两套释放规则混用。还要注意,通过任意复制引用显式解锁,也会释放同一把 flock;其他复制描述符仍然打开,并不能抵消显式解锁。 快速验证 把脚本保存为 locking.py,在一次性 Linux 环境中运行 python3 locking.py。实测环境为 Linux 6.12.76、Python 3.11.2 和本地 tmpfs。每个竞争锁的请求都在独立子进程中执行,并使用非阻塞获取,因此预期冲突会产生可检查结果,不会让演示一直挂起: from pathlib import Path from tempfile import TemporaryDirectory import fcntl,os,subprocess,sys probe_code=r''' import fcntl,sys with open(sys.argv[1],'r+') as handle: try: if sys.argv[2]=='flock':fcntl.flock(handle,fcntl.LOCK_EX|fcntl.LOCK_NB) else:fcntl.lockf(handle,fcntl.LOCK_EX|fcntl.LOCK_NB,1,int(sys.argv[3]),0) except BlockingIOError:print('blocked') else:print('acquired') ''' def probe(path,kind='flock',start=0): result=subprocess.run([sys.executable,'-c',probe_code,str(path),kind,str(start)],capture_output=True,text=True,timeout=5) assert result.returncode==0,result.stderr return result.stdout.strip() with TemporaryDirectory(prefix='xbcatm-locks-') as directory: root=Path(directory);target=root/'data';target.write_text('original') handle=target.open('r+');fcntl.flock(handle,fcntl.LOCK_EX) assert probe(target)=='blocked' subprocess.run([sys.executable,'-c',"from pathlib import Path;import sys;Path(sys.argv[1]).write_text('uncooperative')",str(target)],capture_output=True,text=True,check=True) handle.seek(0);assert handle.read()=='uncooperative' duplicate=os.dup(handle.fileno());handle.close() try:assert probe(target)=='blocked' finally:os.close(duplicate) assert probe(target)=='acquired' print('flock blocks cooperating contenders but not ordinary writes; dup retains the same lock') with target.open('r+') as handle: fcntl.flock(handle,fcntl.LOCK_EX) replacement=root/'replacement';replacement.write_text('new inode');replacement.replace(target) assert os.fstat(handle.fileno()).st_ino!=target.stat().st_ino assert probe(target)=='acquired' lockfile=root/'stable.lock';lockfile.touch() with lockfile.open('r+') as handle: fcntl.flock(handle,fcntl.LOCK_EX) replacement.write_text('next inode');replacement.replace(target) assert probe(lockfile)=='blocked' assert probe(lockfile)=='acquired' print('replacing locked data changes the inode; a separate stable lock file protects that replacement protocol') first=os.open(target,os.O_RDWR);second=os.open(target,os.O_RDWR) try: fcntl.lockf(first,fcntl.LOCK_EX|fcntl.LOCK_NB,1,0,0) assert probe(target,'record',0)=='blocked' and probe(target,'record',1)=='acquired' os.close(second);second=None assert probe(target,'record',0)=='acquired' print('POSIX record locks permit disjoint bytes; closing another descriptor to the inode releases process-owned locks') finally: if second is not None:os.close(second) os.close(first) 首先,父进程持有排他 flock,遵守协议的子进程报告 blocked。另一个子进程完全不请求锁就写入文件,父进程随后读到修改后的内容。同一个文件、同一组权限下的两个结果,分别证明互斥确实存在,以及它的建议式边界。不能只展示一个竞争者失败,就推断所有普通读写也会被锁阻止。 ...