1. 基本概念
github项目地址:https://github.com/superwujc
Linux文件锁用于同步多个进程对同一文件执行的IO操作,防止出现竞争状态。
文件锁分为建议性锁与强制性锁:
- 建议性锁用于协同多进程,即多个已知进程间的同步;每个进程都按照加锁,读写文件,解锁的步骤对同一文件执行IO操作;若文件已被其他进程锁定,则当前进程将等待或以失败返回;建议性锁并不能阻止其他进程在文件已加锁的情况下,不获得锁而强制执行与锁的类型相冲突的IO操作。
- 强制性锁除可用于协同多进程外,还可用于保护文件内容,以防止其他进程强制读写已被当前进程加锁的文件。
2. 实现详解
Linux用户空间文件锁主要通过flock(2)或fcntl(2)系统调用实现:
2.1 - flock(2)
#include <sys/file.h>
int flock(int fd, int operation);
fd指定用于引用文件的文件描述符
operation指定对该文件执行的相关锁操作
- LOCK_SH:设置共享(读)锁
- LOCK_EX:设置独占(写)锁
- LOCK_UN:解锁
- 默认情况下,若其他进程已对fd指定的文件加锁,则当前进程对该文件加锁时将被阻塞,直到对该文件加锁的进程执行解锁;若LOCK_SH或LOCK_EX与该标志进行按位或操作,则当前进程立即以失败返回,并将errno设置为EWOULDBLOCK/EAGAIN
进程对未加锁的文件执行解锁操作,或对已解锁的文件再次执行解锁操作,都不会产生错误。
对于同一文件,多个进程都可以设置共享锁,但在任一时间点,仅单一进程可以对该文件设置独占锁,且其他进程无法对该文件设置共享锁与独占锁,否则将以EWOULDBLOCK/EAGAIN错误失败,即同一文件的独占锁排斥所有其他类型的锁。
2.2 - fcntl(2)
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, struct flock *flockstr);
fd指定用于引用文件的文件描述符
flockstr指定锁的属性
struct flock {
short l_type;
short l_whence;
off_t l_start;
off_t l_len;
pid_t l_pid;
};
l_type指定锁的类型,可以设置为F_RDLCK/F_WRLCK/F_UNLCK,含义分别与flock(2)的LOCK_SH/LOCK_EX/LOCK_UN一致,且加锁的规则与flock(2)相同,即共享锁数量任意,独占锁单一且排他。
l_whence,l_start,l_len共同设置锁定区域:
l_whence与lseek(2)的whence参数含义相同,可以设置为SEEK_SET/SEEK_CUR/SEEK_END分别表示文件起始/文件当前偏移/文件末尾
l_start指定相对于l_whence的起始字节偏移数;l_whence为SEEK_CUR或SEEK_END时,l_start可以指定为负值
l_len指定从l_start与l_whence计算得出的偏移值开始,锁定区域的字节长度
- 值为0,表示从l_start与l_whence计算得出的偏移值开始,至文件末尾,而不论文件长度的变化
- 值为正数,表示[l_start, l_start+l_len-1]
- 值为负数,表示[l_start+l_len, l_start-1]
锁定的区域可以超过文件末尾,但l_start与l_len为负数时,与l_whence计算得出的偏移值不能超过文件起始位置,即字节0。
cmd指定对文件区域设置锁的方式
- F_SETLK:加锁(F_RDLCK/F_WRLCK)或解锁(F_UNLCK);若该操作与其他进程对该文件区域的锁相冲突,则返回-1,并将errno设置为EACCES或EAGAIN。
- F_SETLKW:与F_SETLK相同,但与其他进程对该文件区域的锁相冲突时将阻塞,等待解锁;等待过程中若被信号中断,则返回-1,并将errno设置为EINTR。
- F_GETLK:检查是否可对文件指定区域加锁,但并不实际执行锁定操作,此时l_type值必须为F_RDLCK或F_WRLCK;若当前进程可以对文件内的指定区域加锁,则通过l_type返回F_UNLCK;若与其他进程的锁相冲突,则分别通过l_type返回锁的类型,l_whence,l_start,l_len返回锁定区域,l_pid返回锁定该文件区域的进程PID。
对文件区域解锁将立即返回,对并未加锁的区域解锁不会产生错误。
由于独占锁排斥所有其他类型的锁,因此若某进程已对某文件设置了共享锁,而其他进程请求对该文件设置独占锁时,将出现锁饥饿而可能被无限阻塞。
共享锁与独占锁之间没有优先级关系,对于多个对同一文件请求设置锁的进程,内核将按进程调度的顺序而非请求锁的顺序处理。
2.3 - flock(2)与fcntl(2)设置文件锁的语义区别
1. flock(2)仅可对整个文件加锁;fcntl(2)可对从单一字节到整个文件范围内的任意区域加锁。
2. 通过flock(2)设置文件锁不受文件的打开访问模式标志影响;通过fcntl(2)设置文件锁时,锁的类型必须与文件的打开访问模式标志一致,即F_RDLCK/F_WRLCK分别对应于O_RDONLY/O_WRONLY;先后设置读写锁时,访问模式标志应为O_RDWR。
3. 同一进程可以通过再次调用flock(2)或fcntl(2)的方式,对同一文件的共享锁与独占锁之间进行相互转换;内核保证fcntl(2)的原子性,但不保证flock(2)的原子性。
4. 通过flock(2)设置的文件锁与系统文件表项相关联,而非进程的文件描述符或文件(inode)自身;通过fcntl(2)设置的文件锁与进程的文件描述符表项与系统的inode表项相关联,而非系统文件表项。
对同一文件执行多次open(2)将产生多个不同的文件描述符,每个文件描述符各自指向一个独立的文件表项,但这些文件表项都指向相同的inode表项;对文件描述符调用dup(2)/dup2(2)/fcntl(2)等执行复制操作,或对进程调用fork(2),将产生多个不同的文件描述符,但这些文件描述符都指向相同的文件表项。
- 对于flock(2),调用dup(2)等复制的文件描述符,以及fork(2)继承的文件描述符,以及未设置close-on-exec标志位的文件描述符,都与原fd引用相同的文件表项;仅当所有引用该文件表项的文件描述符都关闭后,相应的文件锁才会自动释放。
- 对于fcntl(2),由于文件描述符表是每进程属性,因此通过fork(2)创建的子进程与其父进程各自具有独立的文件描述符表,文件锁不会被继承;关闭dup(2)等调用复制的文件描述符,以及设置了close-on-exec标志而执行exec(3)的情况,都将导致与文件描述符相关联的文件锁被释放。
- 同一进程对同一文件多次调用open(2)获得引用该文件的多个文件描述符时,flock(2)将区分对待这些文件描述符,而fcntl(2)将视为同一文件(inode);由于这些文件描述符引用不同的文件表项,对不同的文件描述符先后多次调用flock(2)可能导致进程锁定自身对文件的IO操作,最终将阻塞或以失败返回;fcntl(2)不会出现该情况。
5. 进程的加锁操作即将导致多个进程死锁的情况出现时,内核将对fcntl(2)执行检测,选择一个进程使其以EDEADLK错误返回;flock(2)不会被执行检测。
6. flock(2)仅支持建议性锁,而fcntl(2)同时支持建议性锁与强制性锁。
2.4 - 强制性锁
2.4.1 - 启用方式
对于设置了强制性锁的文件,内核将在进程尝试对文件执行IO时,检查该文件是否已被其他进程设置了与IO请求类型相冲突的锁。
启用强制性锁需要文件系统属性与文件自身属性的支持:
- 文件系统需要开启mount(2)的MS_MANDLOCK挂载标志
- 文件需要开启set-group-ID(S_ISGID/02000)标志位,并关闭组的可执行/搜索标志位(S_IXGRP/00010)
2.4.2 - 问题与限制
- 对文件设置强制性共享锁并不能阻止其他读取该文件
- 对文件设置强制性锁并不能阻止其他进程删除该文件
- 非特权进程可以对文件设置强制性锁,可能导致其他访问该文件区域的进程拒绝服务
- 对于设置了强制性锁的文件,内核对每一个尝试对该文件区域执行IO操作的系统调用都将执行检查;系统中包含大量强制性锁时,将额外消耗较多的系统资源
- 使用强制性锁可能导致内核竞争条件,受影响的系统调用包括read(2)/write(2)/readv(2)/writev(2)/open(2)/creat(2)/mmap(2)/truncate(2)/ftruncate(2)等
2.5 - stdio库函数与文件锁
包含用户空间缓冲区的stdio库函数与flock(2)/fcntl(2)设置的文件锁共同使用时,可能出现文件加锁前输入缓冲区已被填满而仍可写入,或文件解锁后输出缓冲区已被清空而无法读取的情况,可以通过以下方式避免该问题:
- 使用read(2)/write(2)等IO系统调用替代stdio库函数
- 加锁前对文件流执行fflush(3),解锁后再次对文件流执行fflush(3)
- 调用setbuf(3)等禁用stdio缓冲区
2.6 - 查看文件锁
Linux下可以通过/proc/locks文件与lslocks(8)命令查看系统中的文件锁;/proc/locks文件的每一列表示的含义分别为:
1. 文件锁的序号
2. 加锁的方式,POSIX表示fcntl(2),FLOCK表示flock(2)
3. 锁的模式,ADVISORY表示建议性锁,MANDATORY表示强制性锁
4. 锁的类型,READ表示共享锁,WRITE表示独占锁
5. 加锁进程的PID
6. 由冒号分隔的加锁文件所在的文件系统标识,主设备号:次设备号:锁文件inode号
7. 锁定区域的起始字节偏移,对于flock(2),该字段总为0
8. 锁定区域的末尾字节偏移,对于flock(2),该字段总为EOF
2.7 - 文件锁的其他设置方式(不推荐)
1. open(file, O_CREAT | O_EXCL, ...)与unlink(file)
通过指定O_CREAT | O_EXCL标志而原子性创建锁文件的方式加锁,通过删除文件的方式解锁。
若open(2)调用以EEXIST失败,则表示其他进程已独占性创建并打开该文件;该方式具有以下限制:
- 若open(2)调用失败,则需继续调用以获得文件锁;可以通过fcntl(2)的F_SETLKW操作解决该问题
- 若当前进程在删除文件之前异常终止,则其他进程对已存在的锁文件调用open(2)将失败而无法获得锁
- 该加锁方式需要文件系统操作,速度慢于fcntl(2)提供的记录锁
- 无死锁检测,若通过不同的锁文件加锁,则可能出现无限死锁的情况
2. link(file, lockfile)与unlink(lockfile)
通过为现有文件创建硬链接的方式加锁,通过删除硬链接的方式解锁;该方式的限制为:创建硬链接需要原路径名与链接路径名位于同一文件系统,且每个进程在创建的锁文件名称需要通过某种协定保持唯一性,且link(2)调用失败时,需继续调用以获得文件锁。
3. open(file, O_CREAT | O_TRUNC | O_WRONLY, 0)与unlink(file)
对已存在的文件调用open(2),若指定了O_TRUNC标志且当前进程对文件没有写入权限,则调用将失败。
该方式的限制为,若调用失败而无法加锁,则需继续调用,且特权用户不受文件访问权限的影响。
2.8 - 杂项
glibc基于fcntl(2)实现了lockf(3)函数:
#include <unistd.h>
int lockf(int fd, int cmd, off_t len);
该函数的加锁类型仅支持独占锁,且分别设置默认值l_whence为SEEK_CUR,以及l_start为0,len参数一致。
util-linux工具包基于flock(2)实现了flock(1)工具。
flock(2)与fcntl(2)对文件锁的内部实现在Linux下并无交互,对同一文件混合使用这两种方式可能出现未定义的行为。
3 - 示例程序:单实例进程
操作系统与内核版本
# lsb_release -a
LSB Version: :core-4.1-amd64:core-4.1-noarch:cxx-4.1-amd64:cxx-4.1-noarch:desktop-4.1-amd64:desktop-4.1-noarch:languages-4.1-amd64:languages-4.1-noarch:printing-4.1-amd64:printing-4.1-noarch
Distributor ID: CentOS
Description: CentOS Linux release 7.4.1708 (Core)
Release: 7.4.1708
Codename: Core
# uname -r
3.10.0-693.21.1.el7.x86_64
gcc与glibc版本
# gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,obj-c++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install --enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
Thread model: posix
gcc version 4.8.5 20150623 (Red Hat 4.8.5-16) (GCC)
# ldd /bin/ls
linux-vdso.so.1 => (0x00007ffd230a3000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007ff394398000)
libcap.so.2 => /lib64/libcap.so.2 (0x00007ff394193000)
libacl.so.1 => /lib64/libacl.so.1 (0x00007ff393f89000)
libc.so.6 => /lib64/libc.so.6 (0x00007ff393bc6000)
libpcre.so.1 => /lib64/libpcre.so.1 (0x00007ff393964000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007ff39375f000)
/lib64/ld-linux-x86-64.so.2 (0x00005591acf14000)
libattr.so.1 => /lib64/libattr.so.1 (0x00007ff39355a000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007ff39333e000)
# /lib64/libc.so.6
GNU C Library (GNU libc) stable release version 2.17, by Roland McGrath et al.
Copyright (C) 2012 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 4.8.5 20150623 (Red Hat 4.8.5-16).
Compiled on a Linux 3.10.0 system on 2017-11-30.
Available extensions:
The C stubs add-on version 2.1.2.
crypt add-on version 2.1 by Michael Glad and others
GNU Libidn by Simon Josefsson
Native POSIX Threads Library by Ulrich Drepper et al
BIND-8.2.3-T5B
RT using linux kernel aio
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<http://www.gnu.org/software/libc/bugs.html>.
程序代码:single_instance.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define BUF_SIZE 16
#define lockfile "/var/run/daemon.pid"
#define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while (0)
static void addlock(void);
int main(void)
{
setbuf(stdout, NULL);
addlock();
printf("OK\n");
for ( ; ; )
sleep(1);
}
static void addlock(void)
{
int lockfd, flags;
struct flock fl_w;
char buf[BUF_SIZE];
lockfd = open(lockfile, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
if (lockfd == -1)
ERR_EXIT("open() failed");
flags = fcntl(lockfd, F_GETFD);
if (flags == -1)
ERR_EXIT("fcntl() to get flags failed");
flags |= FD_CLOEXEC;
if (fcntl(lockfd, F_SETFD, flags) == -1)
ERR_EXIT("fcntl() to set flags failed");
fl_w.l_type = F_WRLCK;
fl_w.l_whence = SEEK_SET;
fl_w.l_start = 0;
fl_w.l_len = 0;
if (fcntl(lockfd, F_SETLK, &fl_w) == -1)
ERR_EXIT("fcntl() to set lock failed");
if (ftruncate(lockfd, 0) == -1)
ERR_EXIT("ftruncate() failed");
snprintf(buf, BUF_SIZE, "%ld\n", (long)getpid());
if (write(lockfd, buf, strlen(buf)) != strlen(buf))
ERR_EXIT("write() failed");
return ;
}
守护进程通常仅允许运行程序的单个实例,加锁文件通常位于/var/run/目录下,且加锁文件中包含锁定文件的进程PID。
对于通过执行exec(3)重启自身的进程,需要设置引用加锁文件的文件描述符的close-on-exec标志位,以防止文件描述符未随exec(3)关闭导致文件锁未被释放而无法启动程序。
编译程序并运行
# gcc single_instance.c -o single_instance
# ./single_instance &
[2] 3953
# OK
# cat /var/run/daemon.pid
3953
# ./single_instance
fcntl() to set lock failed: Resource temporarily unavailable
4. 参考
flock(1)
flock(2)
fcntl(2)
flockfile(3)
lslocks(8)
《UNIX环境高级编程》13.5 14.3
《The Linux Programming Interface》Chapter 55
https://lwn.net/Articles/667210/
Documentation/filesystems/mandatory-locking.txt