文件锁简介
在多数unix系统中,当多个进程/线程同时编辑一个文件时,该文件的最后状态取决于最后一个写该文件的进程。但对于有些应用程序,如数据库,各个进程需要保证它正在单独地写一个文件,这时就要用到文件锁。
文件锁(也叫记录锁)的作用是,当一个进程读写文件的某部分时,其他进程就无法修改同一文件区域。更合适的术语可能是字节范围锁,应为它锁定的是一个文件中的一个区域(也可以是整个文件。)
文件锁还分为建议性锁和强制性锁,这里主要介绍建议性锁。
能够实现文件锁的函数有flock、fcntl和lockf,主要是用前两个。flock和fcntl是系统调用,而lockf是库函数,实际上是fcntl的封装。flock和fcntl加文件锁,两者是不冲突,对应内核类型分别为FLOCK和POSIX。
文件锁的基本规则
-
文件锁是进程级别的锁,一个进程中的所有线程共享此进程的身份。
-
任意多个进程在一个给定的字节范围上,每个进程都可以持有一个共享性的读锁,但只能有一个进程持有一个独占性的写锁。
-
如果在一个给定的字节范围上,已经有一个或多个读锁,则不能在此范围上再加写锁。如果在一个给定的字节范围上已经有一个写锁,则不能在此范围上再加任何读锁或写锁。
-
对于一个进程而言,如果进程对某个文件区域已经有了一个锁,然后又试图在相同区域再加一个锁,在没有冲突的前提下,则新锁会替换旧锁。
-
加读锁时,该描述符必须是读打开,加写锁时,该描述符必须是写打开。
规则如表2-1所示:
表2-1 不同进程文件锁加锁规则
flock介绍
函数原型
#include <sys/file.h>
int flock(int fd, int operation);
fd是系统调用open返回的文件描述符
operation的选项如下:
-
LOCK_SH :表示要创建一个共享锁,在任意时间内,一个文件的共享锁可以被多个进程拥有
-
LOCK_EX :表示要创建一个排他锁,在任意时间内,一个文件的排他锁,只能被一个进程拥有
-
LOCK_UN : 表示删除该进程创建的锁即解锁
-
LOCK_NB : 非阻塞(与以上三种操作一起使用)
主要特性
-
只能加建议性锁。
-
只能对整个文件加锁,而不能对文件的某一区域加锁。
-
使用exec后,文件锁的状态不变。
-
flock锁是可以递归,即通过dup或者fork产生的两个fd,都可以加锁而不会产生死锁。因为其创建的锁是和文件打开表项(struct file)相关联的,而不是fd。这就意味着复制文件fd(通过fork或者dup)后,这两个fd都可以操作这把锁(例如通过一个fd加锁,通过另一个fd可以释放锁),也就是说子进程继承父进程的锁。但是加锁过程中,关闭其中一个fd,锁是不会被释放的(因为struct file并没有释放),只有关闭所有复制出的fd,锁才会被释放。
-
使用open两次打开同一个文件,得到的两个fd是独立的(因为底层对应两个struct file对象),通过其中一个fd加锁,通过另一个fd无法解锁,并且在前一个解锁前也无法加有冲突的锁。
-
flock在NFS文件系统上使用时,服务端NFSD将文件锁的类型由FLOCK改为POSIX。
-
不会进行死锁检查。
特性测试
open测试
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
#include <errno.h>
#include <string.h>
int main (int argc, char *argv[])
{
int ret;
int fd1 = open(argv[1],O_RDWR);
int fd2 = open(argv[1],O_RDWR);
printf("fd1: %d, fd2: %d\n", fd1, fd2);
ret = flock(fd1, LOCK_EX|LOCK_NB)
printf("get flock1 by fd1 %d, ret: %d", fd1, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
ret = flock(fd2, LOCK_EX|LOCK_NB);
printf("get flock2 by fd2 %d, ret: %d", fd2, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
return 0;
}
本地文件系统:
nfs导出:
dup测试
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
#include <errno.h>
#include <string.h>
int main (int argc, char *argv[])
{
int ret;
int fd1 = open(argv[1],O_RDWR);
int fd2 = dup(fd1);
printf("fd1: %d, fd2: %d\n", fd1, fd2);
ret = flock(fd1, LOCK_EX|LOCK_NB)
printf("get flock1 by fd1 %d, ret: %d", fd1, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
ret = flock(fd2, LOCK_EX|LOCK_NB);
printf("get flock2 by fd2 %d, ret: %d", fd2, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
return 0;
}
本地文件系统:
nfs导出:
fork测试
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
#include <errno.h>
#include <string.h>
int main (int argc, char ** argv)
{
int ret;
int pid;
int fd = open(argv[1],O_RDWR);
if ((pid = fork()) == 0){
ret = flock(fd,LOCK_EX|LOCK_NB);
printf("child get lock, fd: %d, ret: %d",fd, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
sleep(10);
printf("child exit\n");
exit(0);
}
ret = flock(fd,LOCK_EX|LOCK_NB);
printf("parent get lock, fd: %d, ret: %d", fd, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
waitpid(pid);
printf("parent exit\n");
return 0;
}
本地文件系统:
nfs导出:
死锁检查测试
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
void flock_set(int fd, char *file_name, char *process_type)
{
int ret;
printf("process %s pid %d start set flock for %s by fd %d.\n",
process_type, getpid(), file_name, fd);
ret = flock(fd, LOCK_EX);
printf("process %s pid %d set flock for %s by fd end, ret %d",
process_type, getpid(), file_name, fd, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
}
int main (int argc, char *argv[])
{
int pid;
int fd1, fd2;
printf("====test FL_FLOCK dead lock ====\n", argv[1]);
if ((pid = fork()) == 0){
fd1 = open(argv[1], O_WRONLY|O_CREAT);
fd2 = open(argv[2], O_WRONLY|O_CREAT);
flock_set(fd2, argv[2], "child");
sleep(1);
flock_set(fd1, argv[1], "child");
sleep(2);
printf("process child exit\n");
exit(0);
}
fd1 = open(argv[1], O_WRONLY|O_CREAT);
fd2 = open(argv[2], O_WRONLY|O_CREAT);
flock_set(fd1, argv[1], "parent");
sleep(1);
flock_set(fd2, argv[2], "parent");
waitpid(pid);
printf("process parent exit\n");
return 0;
}
测试结果如下:
父子进程互相等待死锁了,栈的信息如下
lockf介绍
#include <unistd.h>
int lockf(int fd, int cmd, off_t len);
fd为通过open返回的打开文件描述符。
cmd的取值如下:
-
F_LOCK :给文件加排他锁,若文件已被加锁,则会一直阻塞到锁被释放。
-
F_TLOCK :同F_LOCK,但若文件已被加锁,不会阻塞,并返回错误。
-
F_ULOCK :解锁。
-
F_TEST :测试文件是否被加锁,若文件没被加锁则返回0,否则返回-1。
len 为从文件当前位置的起始要锁住的长度。
lockf 只支持排他锁,不支持共享锁。
fcntl介绍
函数原型
#include <fcntl.h>
int fcntl(int fd, int cmd, struct flock *lock);
fd为通过open返回的打开文件描述符。
cmd的取值如下:
-
F_SETLK:申请锁(读锁F_RDLCK,写锁F_WRLCK)或者释放所(F_UNLCK),但是如果kernel无法将锁授予本进程(被其他进程持有),立即返回error,不会阻塞,并将冲突锁的信息,保存存在 struct flock中。
-
F_SETLKW:和F_SETLK几乎一样,唯一的区别是申请不到锁,就会阻塞。
-
F_GETLK:这个操作是获取锁的相关信息,并会修改我们传入的lock。进程可以通过此操作,来获取fd指向的那个文件的加锁信息。执行该操作时,lock中就保存了希望对文件的加锁信息(或者是测试是否可以加锁)。如果确实存和lock冲突的锁,内核会把冲突的锁的信息写到lock中,并将该锁拥有者的PID写入 l_pid字段中,然后返回;否则,就将lock中的l_type设置为 F_UNLCK,并保持 lock中其他信息不变返回,而不是对该文件真正加锁。
需要注意的是,F_GETLK 用于测试是否可以加锁,在 F_GETLK 测试可以加锁之后,F_SETLK 和 F_SETLKW 就会企图申请一个锁,但是这两者之间并不是一个原子操作,也就是说,在 F_SETLK 或者 F_SETLKW 还没有成功加锁之前,另外一个进程就有可能已经加上了一个锁。而且F_SETLKW 有可能导致程序长时间睡眠。还有,进程对某个文件拥有的各种类型的锁,会在相应的文件描述符被关闭时自动清除,进程运行结束后,其所加的各种锁也会自动清除。
flock结构如下:
struct flock {
...
short l_type; /* Type of lock: F_RDLCK,F_WRLCK,F_UNLCK */
short l_whence; /* How to interpret l_start: SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* Starting offset for lock */
off_t l_len; /* Number of bytes to lock */
pid_t l_pid; /* PID of process blocking our lock (F_GETLK only) */
...
};
flock结构说明:
-
锁类型:共享读锁F_RDLCK,独占写锁F_WRLCK,解锁F_UNLCK。
-
加锁或解锁区域的起始字节偏移量,由l_start和l_whence决定。
-
l_start是相对偏移量,l_whence决定了l_start的起点,l_whence可选用的值为SEEK_SET, SEEK_CUR, SEEK_END。
-
区域字节长度(l_len)
-
由F_GETLK获取已存在的冲突锁的进程PID(l_pid)
-
锁可以在文件尾处开始或者越过尾端开始,但是不能在文件起始位置之前开始
-
若l_len=0, 表示锁的范围可以扩大到最大可能偏移量,这意味着,不论往文件中追加多少数据,它们都处于锁的范围内
-
设置l_start和l_whence指向文件的起始位置,并且指定l_len=0,以实现对整个文件加锁(一般l_start=0, l_whence=SEEK_SET)
主要特性
-
加锁可递归,如果一个进程对一个文件区间已经有一个锁,后来又在同一区间再加一个锁,在没有冲突的前提下,则新锁将替换老锁。
-
加读锁(共享锁)文件必须是读打开,加写锁(排他锁)文件必须是写打开。
-
进程不能使用F_GETLK命令来测试它自己是否在文件的某一部分持有一个锁。F_GETLK命令定义说明,返回信息指示是否现存的锁阻止调用进程设置它自己的锁。因为,F_SETLK和F_SETLKW命令总是替换进程的现有锁,所以调用进程绝不会阻塞再自己持有的锁上,于是F_GETLK命令绝不会报告调用进程自己持有的锁。
-
进程终止时,他所建立的所有文件锁都会被释放,同flock。
-
任何时候关闭一个描述符时,则该进程通过这一描述符可以引用的文件上的任何一个锁都被释放(这些锁都是该进程设置的),与flock不同。例如:
fd1 = open(pathname, …);
fcntl(fd1, F_SETLK, …);
fd2 = dup(fd1);
close(fd2);
// 在close(fd2)后,在fd1上加的锁,会被释放。
// 如果将dup换为open,以打开同一文件的另一描述符,则效果也一样。
fd1 = open(pathname, …);
fcntl(fd1, F_SETLK, …);
fd2 = open(pathname, …);
close(fd2);
-
由fork产生的子进程不继承父进程所设置的锁,与flock不同。
-
在执行exec后,新程序可以继承原程序的锁,这点和flock是相同的。(如果对fd设置了close-on-exec,则exec前会关闭fd,相应文件的锁也会被释放)。
-
支持强制性锁:对一个特定文件打开其设置组的ID位(S_ISGID),并关闭其组执行位(S_IXGRP),则对该文件开启了强制性锁机制。再Linux中如果要使用强制性锁,则要在文件系统mount时,使用_omand打开该机制。
-
阻塞方式加锁时,会进行死锁检查。死锁链搜索深度为10步,超过该深度的不再进行死锁检查。
特性测试
open测试
#include <unistd.h>
#include <sys/file.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main (int argc, char *argv[])
{
int ret;
int fd1, fd2;
struct flock lock;
fd1 = open(argv[1], O_RDWR);
fd2 = open(argv[1], O_RDWR);
printf("fd1: %d, fd2: %d\n", fd1, fd2);
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
lock.l_type = F_WRLCK;
ret = fcntl(fd1, F_SETLK, &lock);
printf("get POSIX lock1 by fd1 %d, ret: %d", fd1, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
ret = fcntl(fd2, F_SETLK, &lock);
printf("get POSIX lock2 by fd2 %d, ret: %d", fd2, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
return 0;
}
本地文件系统:
nfs导出:
dup测试
#include <unistd.h>
#include <sys/file.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main (int argc, char *argv[])
{
int ret;
int fd1, fd2;
struct flock lock;
fd1 = open(argv[1], O_RDWR);
fd2 = dup(fd1);
printf("fd1: %d, fd2: %d\n", fd1, fd2);
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
lock.l_type = F_WRLCK;
ret = fcntl(fd1, F_SETLK, &lock);
printf("get POSIX lock1 by fd1 %d, ret: %d", fd1, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
ret = fcntl(fd2, F_SETLK, &lock);
printf("get POSIX lock2 by fd2 %d, ret: %d", fd2, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
return 0;
}
本地文件系统:
nfs导出:
fork测试
#include <unistd.h>
#include <sys/file.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main (int argc, char ** argv)
{
int ret;
int pid;
int fd;
struct flock lock;
fd = open(argv[1],O_RDWR);
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
lock.l_type = F_WRLCK;
if ((pid = fork()) == 0){
ret = fcntl(fd, F_SETLK, &lock);
printf("child set lock, fd: %d, ret: %d",fd, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
sleep(2);
printf("child exit\n");
exit(0);
}
ret = fcntl(fd, F_SETLK, &lock);
printf("parent set lock, fd: %d, ret: %d", fd, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
waitpid(pid);
printf("parent exit\n");
return 0;
}
本地文件系统:
nfs导出:
死锁检查测试
测试代码如下:
#include <unistd.h>
#include <sys/file.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
void lock_set(int fd, struct flock *lock, char *process_type)
{
int ret;
int lock_end = lock->l_start + lock->l_len;
printf("process %s pid %d start set lock[%d %d], by fd %d.\n",
process_type, lock->l_pid, lock->l_start, lock_end, fd);
ret = fcntl(fd, F_SETLKW, lock);
printf("process %s pid %d set lock[%d %d] by fd %d end, ret %d",
process_type, lock->l_pid, lock->l_start, lock_end, fd, ret);
if (ret == -1)
printf(" error(%d:%s).", errno, strerror(errno));
printf("\n");
}
int main (int argc, char *argv[])
{
int pid;
int fd;
struct flock lock;
fd = open(argv[1],O_RDWR);
lock.l_whence = SEEK_SET;
lock.l_type = F_WRLCK;
printf("====test FL_POSIX dead lock for %s====\n", argv[1]);
if ((pid = fork()) == 0){
lock.l_pid = getpid();
lock.l_start = 20;
lock.l_len = 10;
lock_set(fd, &lock, "child");
sleep(1);
lock.l_start = 1;
lock.l_len = 10;
lock_set(fd, &lock, "child");
sleep(2);
printf("process child exit\n");
exit(0);
}
lock.l_pid = getpid();
lock.l_start = 1;
lock.l_len = 10;
lock_set(fd, &lock, "parent");
sleep(1);
lock.l_start = 20;
lock.l_len = 10;
lock_set(fd, &lock, "parent");
waitpid(pid);
printf("process parent exit\n");
return 0;
}
测试结果如下:
自己进程检查到了死锁,直接返回,不再阻塞,最后父子都退出了。