Linux文件锁用于同步多个进程对同一文件执行的IO操作,防止出现竞争状态。 文件锁分为建议性锁与强制性锁:
建议性锁用于协同多进程,即多个已知进程间的同步;每个进程都按照加锁,读写文件,解锁的步骤对同一文件执行IO操作;若文件已被其他进程锁定,则当前进程将等待或以失败返回;建议性锁并不能阻止其他进程在文件已加锁的情况下,不获得锁而强制执行与锁的类型相冲突的IO操作。 强制性锁除可用于协同多进程外,还可用于保护文件内容,以防止其他进程强制读写已被当前进程加锁的文件。
一个处理多个进程间文件共享冲突、解决crontab 中单任务多实例问题的利器.
除了多种语言提供 flock 系统调用或函数,linux shell 中也提供了 flock 命令。
flock 命令最大的用途就是实现对 crontab 任务的串行化。在 crontab 任务中,有可能出现某个任务的执行时间超过了 crontab 中为此任务设定的执行周期,这就导致了当前的任务实例还未执行完成,crontab 又启动了同一任务的另外一个实例,这通常不是用户所期望的行为。极端情况下,如果某个任务执行异常一直未返回,crontab 不会处理这种情形,会继续启动新的实例,而新的实例很可能又会异常,这样就导致 crontab 对同一任务不断的启动新的实例,最终导致系统内存被耗尽,影响到整个操作系统的运行。为了防止crontab 任务出现多实例的情况,可以使用 flock 命令将crontab 中任务的周期性执行串行化。
在将corntab 中任务串行化时,flock 通过对一个中间文件加文件锁来间接实现同一时刻某个任务只有一个实例运行的目标。对应的 crontab 中任务的描述形式如下:
* * * * * flock -xn /tmp/mytest.lock -c 'php /home/fdipzone/php/test.php'
这里的定时任务是每分钟执行一次,但是任务中并未直接执行目标命令 ‘php /home/fdipzone/php/test.php’ ,而是将命令作为 flock 的 -c 选项的参数。flock 命令中,-x 表示对文件加上排他锁,-n 表示文件使用非阻塞模式,-c 选项指明加锁成功后要执行的命令。因而上面flock 命令的整体含义就是:如果对 /tmp/mytest.lock 文件(如果文件不存在, flock 命令会自动创建)加锁成功就执行后面的命令,否则不执行。
假如上面 php 命令要执行2分钟,而crontab 任务每分钟就会执行一次,如果当前 php 命令正在执行,说明 flock 已经锁定了文件 /tmp/mytest.lock,crontab 到了再次执行任务的时间时,会发现文件已经被加了锁。由于设置的是非阻塞模式的文件锁,flock 会在加锁失败时直接返回,并不执行php 命令,这样就使 php 命令得以顺序执行,crontab 任务就不会出现同时有两个实例运行的情况了,达到了串行化目的。
结语 flock 在多进程共享文件时非常有用,避免了用各种“奇技淫巧”来模拟多线程中的读写锁,这样模拟出来的锁通常会有缺陷。在多进程共享文件时,强烈建议使用flock 文件件锁。一言以蔽之:简单、易用、高效。
Linux用户空间文件锁主要通过flock(2)或fcntl(2)系统调用实现:
flock(2)
#include <sys/file.h> int flock(int fd, int operation);fd指定用于引用文件的文件描述符
operation指定对该文件执行的相关锁操作
进程对未加锁的文件执行解锁操作,或对已解锁的文件再次执行解锁操作,都不会产生错误。
对于同一文件,多个进程都可以设置共享锁,但在任一时间点,仅单一进程可以对该文件设置独占锁,且其他进程无法对该文件设置共享锁与独占锁,否则将以EWOULDBLOCK/EAGAIN错误失败,即同一文件的独占锁排斥所有其他类型的锁。
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计算得出的偏移值开始,锁定区域的字节长度
锁定的区域可以超过文件末尾,但l_start与l_len为负数时,与l_whence计算得出的偏移值不能超过文件起始位置,即字节0。
cmd指定对文件区域设置锁的方式
对文件区域解锁将立即返回,对并未加锁的区域解锁不会产生错误。
由于独占锁排斥所有其他类型的锁,因此若某进程已对某文件设置了共享锁,而其他进程请求对该文件设置独占锁时,将出现锁饥饿而可能被无限阻塞。
共享锁与独占锁之间没有优先级关系,对于多个对同一文件请求设置锁的进程,内核将按进程调度的顺序而非请求锁的顺序处理。
flock(2)与fcntl(2)设置文件锁的语义区别
对于设置了强制性锁的文件,内核将在进程尝试对文件执行IO时,检查该文件是否已被其他进程设置了与IO请求类型相冲突的锁。 启用强制性锁需要文件系统属性与文件自身属性的支持:
包含用户空间缓冲区的stdio库函数与flock(2)/fcntl(2)设置的文件锁共同使用时,可能出现文件加锁前输入缓冲区已被填满而仍可写入,或文件解锁后输出缓冲区已被清空而无法读取的情况,可以通过以下方式避免该问题:
Linux下可以通过/proc/locks文件与lslocks(8)命令查看系统中的文件锁;/proc/locks文件的每一列表示的含义分别为:
文件锁的其他设置方式(不推荐)
link(file, lockfile)与unlink(lockfile)
open(file, O_CREAT | O_TRUNC | O_WRONLY, 0)与unlink(file)
flock(1)命令包含3种形式: flock 命令选项 加锁文件或目录 -c 不带参数的命令或脚本 flock 命令选项 加锁文件或目录 不带参数的命令或脚本 命令或脚本的参数 flock 命令选项 引用锁文件的文件描述符 第2种形式中,若命令或脚本的参数为空字符串,则等同于第1种形式。 主要命令行选项与参数包括: -s, --shared:共享锁 -x, -e, --exclusive:独占锁,默认类型 -u, --unlock:解锁 -n, --nb, --nonblock:非阻塞,若指定的文件正在被其他进程锁定,则立即以失败返回 -w, --wait, --timeout seconds:若指定的文件正在被其他进程锁定,则等待指定的秒数;指定为0将被视为非阻塞 -o, --close:锁定文件后与执行命令前,关闭用于引用加锁文件的文件描述符 -E, --conflict-exit-code number:若指定-n时请求加锁的文件正在被其他进程锁定,或指定-w时等待超时,则以该选项的参数作为返回值 -c, --command command:运行无参数的命令 -h, --help:输出用法信息 -V, --version:输出版本信息 标准的util-linux对flock(1)的实现另包含无
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下并无交互,对同一文件混合使用这两种方式可能出现未定义的行为。
示例程序:单实例进程 操作系统与内核版本
# 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
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