Linux fcntl与文件锁
文章目录
- 一、基本概念
- 2.1 基础
- 2.2 文件锁(读锁,写锁)
- 2.2 加锁范围(实际上就是锁的粒度)
- 二、相关函数和数据结构声明
- 三、实例
- 3.1 F_SETLK验证:写的时候不可以读。没写的时候可以同时读。
- 3.1.1运行结果
- 3.2 struct flock 中的pid参数
- 3.3 F_GETLK 文件加锁信息的获取
- 3.3.1 运行结果
- 3.3.2 如果,l_start, l_len指定的范围中有两个不同的锁呢。信息会返回什么
- 3.4 F_GETLKW 与F_GETLK的区别
- 3.4.1 验证一下 F_GETLKW 阻塞等待
- 3.4.2 验证结果
一、基本概念
2.1 基础
- 排他锁(互斥锁): 即同一时刻只有一个对象拥有该锁。
- 共享锁:即同一时刻可以被多个对象拥有。
2.2 文件锁(读锁,写锁)
- 读锁:其实就是共享锁。
- 写锁:其实就是排他锁。
也就是说,同一文件的某个范围内的数据,可以被多个操作者同时读。但是最多只有一个操作者可以同时写。当有操作者在写时,读锁也无法获取。
简单的说就是:
写的时候不可以读。没写的时候可以 同时读。如果已经获取了读锁,则写锁也将无法获取
2.2 加锁范围(实际上就是锁的粒度)
also known as byte-range, file-segment, or file-region locks
引用自:Ubuntu 20.4man fcntl,说明了锁的范围可以是字节范围,文件段,或者是整个文件范围内的锁。
二、相关函数和数据结构声明
#include
#include int fcntl(int fd, int cmd, ... /* arg */ );
cmd 参数的可选项中有,F_SETLK / F_SETLKW / F_GETLK 三项。这些参数与锁的信息获取和设置(加锁解锁)相关。
关于fcntl可以查看:Linux fcntl 函数
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(set by F_GETLK and F_OFD_GETLK) */...};
这里是从ubuntu 20.4版本的man fcntl中得到的。
// fcntl-linux.h 文件中的节选
/* For old implementation of BSD flock. */
#ifndef F_EXLCK
# define F_EXLCK 4 /* or 3 */
# define F_SHLCK 8 /* or 4 */
#endif
从fcntl-linux.h头文件中可以看到l_type除了以上的三种,还有F_SHLCK(共享锁)和F_EXLCK(排他锁)
这里也说了,这两个宏定义只是为了兼容BSD的老的实现。所以,其实F_EXLCK(排他锁)就是F_WRLCK(写锁),F_SHLCK(共享锁)就是F_RDLCK(读锁)。
三、实例
3.1 F_SETLK验证:写的时候不可以读。没写的时候可以同时读。
为了方便验证,我编写了下面的工具代码,可以方便的进行文件加锁验证。
//fcntl_lock.c
#include
#include
#include
#include
#include
#include
#include int
main(int argc, char **args)
{int fd = 0;struct flock fl= {0};char *str_filename, *str_lock_type, *str_start, *str_len, *str_pid;int lock_type, start, len, pid;if (argc < 5) {printf("usage: \n"" fcntl_lock [filename] [lock_type] [start] [len] [pid]\n"" lock_type: options is 0(F_RDLCK), 1(O_WRLCK), 2(O_UNLCK)\n"" start: start is base from the start of the file head\n\n"" Remark: the lock range is [start, start + len)\n"" pid: thie param is option, to set the l_pid elem for flock struct\n");exit(-1);}str_filename = args[1];str_lock_type = args[2];str_start = args[3];str_len = args[4];str_pid = argc >= 5 ? args[5] : NULL;// parse argslock_type = atoi(str_lock_type);start = atoi(str_start);len = atoi(str_len);pid = ( str_pid != NULL ? atoi(str_pid) : 0 );// checkif (lock_type != F_RDLCK && lock_type != F_WRLCK && lock_type != F_UNLCK) {printf("locktype=%d is unalivable\n", lock_type);exit(-1);}if (start < 0) {printf("start can't set %d, must be positive integer\n", start);exit(-1);}fl.l_whence = SEEK_SET;fl.l_type = lock_type;fl.l_start = start;fl.l_len = len;fl.l_pid = 0;str_pid != NULL ? pid: getpid();if (fl.l_pid < 0) {perror("getpid with fail");exit(-1);}fd = open(str_filename, O_CREAT|O_RDWR, S_IRWXU);if (fd < 0) {perror("open test file with fail");exit(-1);}if (fcntl(fd, F_SETLK, &fl) < 0) {perror("fcntl (F_SETLK) with fail");exit(-1);}printf("press any key to continue");getchar();close(fd);return 0;
}
3.1.1运行结果
- 验证多个操作者(实例这里操作者就是不同的进程)同时可以获取读锁。
// A terminal
hotice0@ubuntu:~/Documents/Unix_Program$ ./fcntl_lock test 0 0 10
press any key to continue
// B terminal
hotice0@ubuntu:~/Documents/Unix_Program$ ./fcntl_lock test 0 0 10
press any key to continue
可以看到,没有报错,都成功的通过F_SETLK成功的对文件的【0,10)偏移上(单位字节)的内容进行了加读锁。【备注:test是文件名,后面的0是F_RDLCK宏定义的实际值,再后的0和10代表,加锁的范围是 【0,10) 左闭右开 】
- 验证多个操作者都去获取写锁。
// A terminal
hotice0@ubuntu:~/Documents/Unix_Program$ ./fcntl_lock test 1 0 10
press any key to continue
// B terminal
hotice0@ubuntu:~/Documents/Unix_Program$ ./fcntl_lock test 1 0 10
fcntl (F_SETLK) with fail: Resource temporarily unavailable
可以看到,A成功的获取到了 文件的写锁,而B则获取失败,报错内容是“资源暂时不可用”。
- 验证加了写锁之后,读锁是否可以加成功。
// A terminal
hotice0@ubuntu:~/Documents/Unix_Program$ ./fcntl_lock test 1 0 10
press any key to continue
// B terminal
hotice0@ubuntu:~/Documents/Unix_Program$ ./fcntl_lock test 0 0 10
fcntl (F_SETLK) with fail: Resource temporarily unavailable
可以看到,A成功的获取了文件的写锁,而B尝试获取文件的读锁也失败了。
总结:到这,已经成功的证明了上面对读写锁,分别是排他和共享锁的说明是正确的。
备注:当然,如果A获取了文件[0, 10)的写锁,那么B尝试获取[0, 10)范围内的都会失败,获取范围外的可以成功。这里没有写出来,可以自己验证。
3.2 struct flock 中的pid参数
很奇怪的发现,struct flock有pid参数。经过我的验证,这个pid参数,在进行F_SETLK的时候,是可以不填充的,会被忽略系统会自动地将该字段填充为调用进程的pid。这个字段的设计是为了,能获取到是哪个用户导致当前文件的哪些内容被锁定了。
3.3 F_GETLK 文件加锁信息的获取
实际上,读取文件锁的信息的时候,只需要指定flock结构体的这两个参数
l_whence和l_start。
因为我这里结构体初始化为0了,所以l_len为0.也就是获取l_start指定的点的锁的信息。
// fcntl_lock_read.c
#include
#include
#include
#include
#include
#include
#include int
main(int argc, char **args)
{int fd = -1;struct flock fl = {0};char *str_filename, *str_start;if (argc < 3) {printf("usage:\n"" fcntl_lock_read [filename] [start]\n\n"" options:"" start: the offset of the start of the file\n");exit(-1);}str_filename = args[1];str_start = args[2];fl.l_whence = SEEK_SET;fl.l_start = atoi(str_start);fl.l_pid = -1;fl.l_len = 0;fd = open(str_filename, O_WRONLY);if (fd < 0) {perror("open test file with fail");exit(-1);}if (fcntl(fd, F_GETLK, &fl) < 0) {perror("fcntl F_GETLK with fail");exit(-1);}printf("filename: %s\n""pos: [%ld,%ld) byte\n""lock_type: %d OPTIONS: 0(F_RDLCK), 1(O_WRLCK), 2(O_UNLCK)\n""pid: %d\n",str_filename, fl.l_start, fl.l_start + fl.l_len, fl.l_type, fl.l_pid);close(fd);return 0;
}
3.3.1 运行结果
//A terminal 加文件[0,10)位置的写锁。
hotice0@ubuntu:~/Documents/Unix_Program$ ./fcntl_lock test 1 0 10
press any key to continue//B terminal 加文件[10, 15)位置的写锁
hotice0@ubuntu:~/Documents/Unix_Program$ ./fcntl_lock test 1 10 5
press any key to continue//C terminal 分别获取文件 0位置,5位置,10位置,15位置的锁的信息。// 0位置
hotice0@ubuntu:~/Documents/Unix_Program$ ./fcntl_lock_read test 0
filename: test
pos: [0,10) byte
lock_type: 1 OPTIONS: 0(F_RDLCK), 1(O_WRLCK), 2(O_UNLCK)
pid: 100368// 5位置
hotice0@ubuntu:~/Documents/Unix_Program$ ./fcntl_lock_read test 5
filename: test
pos: [0,10) byte
lock_type: 1 OPTIONS: 0(F_RDLCK), 1(O_WRLCK), 2(O_UNLCK)
pid: 100368// 10位置 写锁 锁的范围是10-15 锁的拥有进程pid是101297
hotice0@ubuntu:~/Documents/Unix_Program$ ./fcntl_lock_read test 10
filename: test
pos: [10,15) byte
lock_type: 1 OPTIONS: 0(F_RDLCK), 1(O_WRLCK), 2(O_UNLCK)
pid: 101297// 15位置 无锁
hotice0@ubuntu:~/Documents/Unix_Program$ ./fcntl_lock_read test 15
filename: test
pos: [15,15) byte
lock_type: 2 OPTIONS: 0(F_RDLCK), 1(O_WRLCK), 2(O_UNLCK)
pid: -1
从15位置,无锁的时候可以看到,其实获取锁的信息,如果无锁,只会更新flock结构体中的l_type成员(因为l_pid我填充的-1进去的。然后len我尝试过初始化为-1,发现如果没锁,不会修改该值)。
有锁时,就会返回锁类型、开始位置(当然是基于你的whence设置的值得到的相对开始位置,可能为负数正数)、长度、pid。
3.3.2 如果,l_start, l_len指定的范围中有两个不同的锁呢。信息会返回什么
...fl.l_whence = SEEK_SET;fl.l_start = atoi(str_start);fl.l_pid = -1;fl.l_len = 20; //我们修改一下这个值为20(原来是0),因为这种情况验证的少,所以,我没将len从参数传入。
...
//A terminal 加文件[0,10)位置的写锁。
hotice0@ubuntu:~/Documents/Unix_Program$ ./fcntl_lock test 1 0 10
press any key to continue//B terminal 加文件[11, 16)位置的写锁
hotice0@ubuntu:~/Documents/Unix_Program$ ./fcntl_lock test 1 11 5
press any key to continue//C terminal 获取文件[10, 30)位置的锁的信息
hotice0@ubuntu:~/Documents/Unix_Program$ ./fcntl_lock_read test 10
filename: test
pos: [11,16) byte
lock_type: 1 OPTIONS: 0(F_RDLCK), 1(O_WRLCK), 2(O_UNLCK)
pid: 101964
从终端C中输出的信息可以发现,获取到的锁,会是B上加的锁。
总结:F_GETLK的实现其实就是,从指定的start位置开始,向后遍历,例如10位置没锁,但是由于len不是0,则会继续向后查找(如果一直是无锁,直到start + len的位置才结束)。如果在向后查找的过程中,遇到了锁,就会直接返回这个锁的信息。
3.4 F_GETLKW 与F_GETLK的区别
F_SETLKW (struct flock *)
As for F_SETLK, but if a conflicting lock is held on the file,
then wait for that lock to be released. If a signal is caught
while waiting, then the call is interrupted and (after the
signal handler has returned) returns immediately (with return
value -1 and errno set to EINTR; see signal(7)).
引用自:https://man7.org/linux/man-pages/man2/fcntl.2.html
其大概意思就是,与F_SETLK相比,F_SETLKW(W指的应该就是wait)如果现在获取不到锁,则会等等待,直到其请求获取的锁被释放(准确的说法,应该是可以被获取),才会返回。如果在等待的期间,收到了一个signal(等到signal 回调处理函数返回),则本次系统调用会被中断并立即返回-1。
简单的理解其实就是会等到能获取到其想获取的锁的时候才会返回。(除了收到signal的情况)。
3.4.1 验证一下 F_GETLKW 阻塞等待
下面这个代码其实就是使用的上面fcntl_lock.c的代码,只是将原来的F_SETKL改成了F_SETKLW。
// fcntl_lock.c
#include
#include
#include
#include
#include
#include
#include int
main(int argc, char **args)
{int fd = 0;struct flock fl= {0};char *str_filename, *str_lock_type, *str_start, *str_len, *str_pid;int lock_type, start, len, pid;if (argc < 5) {printf("usage: \n"" fcntl_lock [filename] [lock_type] [start] [len] [pid]\n"" lock_type: options is 0(F_RDLCK), 1(O_WRLCK), 2(O_UNLCK)\n"" start: start is base from the start of the file head\n\n"" Remark: the lock range is [start, start + len)\n"" pid: thie param is option, to set the l_pid elem for flock struct\n");exit(-1);}str_filename = args[1];str_lock_type = args[2];str_start = args[3];str_len = args[4];str_pid = argc >= 5 ? args[5] : NULL;// parse argslock_type = atoi(str_lock_type);start = atoi(str_start);len = atoi(str_len);pid = ( str_pid != NULL ? atoi(str_pid) : 0 );// checkif (lock_type != F_RDLCK && lock_type != F_WRLCK && lock_type != F_UNLCK) {printf("locktype=%d is unalivable\n", lock_type);exit(-1);}if (start < 0) {printf("start can't set %d, must be positive integer\n", start);exit(-1);}fl.l_whence = SEEK_SET;fl.l_type = lock_type;fl.l_start = start;fl.l_len = len;fl.l_pid = 0;str_pid != NULL ? pid: getpid();if (fl.l_pid < 0) {perror("getpid with fail");exit(-1);}fd = open(str_filename, O_CREAT|O_RDWR, S_IRWXU);if (fd < 0) {perror("open test file with fail");exit(-1);}if (fcntl(fd, F_SETLKW, &fl) < 0) {perror("fcntl (F_SETLKW) with fail");exit(-1);}printf("press any key to continue");getchar();close(fd);return 0;
}
3.4.2 验证结果
//A terminal 获取[0,10)位置的读锁(获取成功)
hotice0@ubuntu:~/Documents/Unix_Program$ ./fcntl_lock test 0 0 10
press any key to continue//B terminal 获取[0, 10)位置的写锁(进入阻塞状态)
hotice0@ubuntu:~/Documents/Unix_Program$ ./fcntl_lock test 1 0 10//在A terminal 按任意键,使得A上运行的fcntl_lock正常退出。
//随后会发现 B terminal中的fcntl_lock成功的获取到了[0,10)位置的写锁。
这里,如果没有收到信号,而A一直占用读锁,则B会一直阻塞在这里。
想想之前用的windows下,csocket库,python下的一些请求库,都会有timeout参数。对于fcntl(F_SETLKW)这个函数,想实现timeout 可以查看我的另一篇博客:
Linux fcntl F_SETLKW实现超时返回
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
