记录锁(record lock)也称字节范围锁、文件范围锁、文件段锁,是一种在文件的某个字节、某个区域进行加锁的机制,记录锁总是和进程、文件相关。本篇博客介绍的是建议性记录锁。
1 记录锁的函数原型:
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
这个函数除了对文件记录锁进行操作外,还可以对文件其他属性进行操作。这里只讨论它的记录锁功能。使用记录锁时,此函数有三个参数,int fd,int cmd,struct flock *lock;第一个参数是打开的文件描述符,第二个参数是命令集合,主要有3个,第三个参数是指向记录锁结构的指针。记录锁结构如下所示:
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) */
...
};
(1)l_type是指记录锁的的类型,有三种:F_RDLCK、F_WRLCK、 F_UNLCK,是共享读锁、独占写锁、解锁。加 读锁要求描述符要为读而打开,加写锁要求描述符为写而打开,最后一个是解锁。解锁的方法有三个:使用F_UNLCK、关闭文件描述符、进程终止。
(2)l_whence 是要加锁的文件位置,SEEK_SET代表从文件开始处,SEEK_CUR是从文件当前位置处、SEEK_END是代表从文件结尾处。
(3)l_start 锁的起始偏移值,在l_whence 值确定后,确定相对于l_whence 的起始偏移值,对于SEEK_CUR和SEEK_END,l_start可为负值。
(4)l_len 从起始偏移值开始的文件长度。如果l_len=0,代表从l_whence、l_start 确定的位置一直到文件末尾,无论向文件中添加多少数据。
(5)l_pid是进程pid值,只有cmd 为F_GETLK时,才使用此参数,代表加锁的进程ID。
2 第二个参数的三种情况
(1)F_GETLCK。测试一个指定的锁(即第三个参数lock)能否加上。如果能加上,则lock锁并不实际加锁到指定的位置,而是只是将lock.l_type设置未F_UNLCK,lock结构的其他参数保持不变。如果测试不能加上,则lock返回的是文件当前锁的l_type、l_whence 、l_start 、l_len和l_pid。锁的互斥性如下表所示。
当前区域状态 | 请求读锁 | 请求写锁 |
无锁 | 允许 | 允许 |
共享读锁 | 允许 | 拒绝 |
独占写锁 | 拒绝 | 拒绝 |
注意此函数的返回第三个参数并非一直是文件当前锁的信息,而是只有当尝试加锁失败时,lock才返回当前已加锁的锁信息。也就是测试锁的状态会有以下几种结果
(a)当前无锁,用读锁和写锁同时测试,则所得结果均为F_UNLCK
(b)当前为读锁,用读锁测试结果为F_UNLCK,用写锁测试结果为F_WRLCK
(c)当前为写锁,用读锁和写锁测试,结果均为F_WRLCK;
另外对于进程来说,
(a)一个进程对文件的一个位置加了锁,如果后续此进程在同一位置再加锁,则新锁将代替旧锁。
(b)一个进程解锁只能解自己加的锁,而不能解锁其它进程加的锁。
(c)利用fork产生的子进程,子进程不能继承父进程已有的锁,它需要重新获得自己的锁才行。
(d)进程不能测试自己的锁,因为进程对自己加的锁可以更换任何锁,尝试加锁总是成功的,所以进程测试自己的锁,得到的一直是F_UNLCK。
源程序locktest1.c(编程环境gcc 8.3.1,内核版本linu-4.18.0)
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <string.h>
#include <unistd.h>
int lock_reg(int fd,int cmd,int type,off_t offset,int whence,off_t len)
{
struct flock lock;
lock.l_type=type;
lock.l_start=offset;
lock.l_whence=whence;
lock.l_len=len;
return(fcntl(fd,cmd,&lock));
}
define read_lock(fd, offset, whence, len) \
lock_reg((fd), F_SETLK, F_RDLCK, (offset), (whence), (len))
#define readw_lock(fd, offset, whence, len) \
lock_reg((fd), F_SETLKW, F_RDLCK, (offset), (whence), (len))
#define write_lock(fd, offset, whence, len) \
lock_reg((fd), F_SETLK, F_WRLCK, (offset), (whence), (len))
#define writew_lock(fd, offset, whence, len) \
lock_reg((fd), F_SETLKW, F_WRLCK, (offset), (whence), (len))
#define un_lock(fd, offset, whence, len) \
lock_reg((fd), F_SETLK, F_UNLCK, (offset), (whence), (len))
pid_t mylock_test(int fd,int type,off_t offset,int whence,off_t len,char *s)
{
struct flock lock;
lock.l_type=type;
lock.l_start=offset;
lock.l_whence=whence;
lock.l_len=len;
if(fcntl(fd,F_GETLK,&lock)<0)
{
perror("fcntl error");
exit(1);
}
if(lock.l_type==F_UNLCK)
return (0);
if(lock.l_type==F_RDLCK)
strcpy(s,"F_RDLCK");
else if(lock.l_type==F_WRLCK)
strcpy(s,"F_WRLCK");
return(lock.l_pid);
}
int main(void)
{
int fd,result;
char lockname[]="F_UNLCK";
int n;
pid_t pid;
if((fd=open("templock",O_RDWR|O_CREAT|O_TRUNC,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH))<0)
{
perror("open error");
exit(1);
}
if((n=write(fd,"abcdefg",7))!=7)
{
perror("wrtie error");
exit(1);
}
// 0字节不加锁,1字节加读锁
if((result=read_lock(fd,1,SEEK_SET,1))<0)
{
perror("read_lock error");
exit(1);
}
//2字节加写锁
if((result=write_lock(fd,2,SEEK_SET,1))<0)
{
perror("write_lock error");
exit(1);
}
if((pid=fork())<0)
{
perror("fork error");
exit(1);
}
else if(pid==0){
//子进程测试父进程加的锁
if(mylock_test(fd,F_WRLCK,0,SEEK_SET,1,lockname)!=0) //用写锁测试0字节
printf("test write lock,child found %s at 0th byte\n",lockname);
else printf("test write lock,child found no lock at 0th byte\n");
if(mylock_test(fd,F_RDLCK,1,SEEK_SET,1,lockname)!=0) //用读锁测试1字节
printf("test read lock,child found %s at 1st byte\n",lockname);
else printf("test read lock,child found no lock at 1st byte\n");
if(mylock_test(fd,F_WRLCK,1,SEEK_SET,1,lockname)!=0) //用写锁测试1字节
printf("test write lock,child found %s at 1st byte\n",lockname);
else printf("test write lock,child found no lock at 1st byte\n");
if(mylock_test(fd,F_RDLCK,2,SEEK_SET,1,lockname)!=0) //用读锁测试2字节
printf("test read lock,child found %s at 2nd byte\n",lockname);
else printf("test read lock,child found no lock at 2nd byte\n");
if(mylock_test(fd,F_WRLCK,2,SEEK_SET,1,lockname)!=0) //用写锁测试2字节
printf("test write lock,child found %s at 2nd byte\n",lockname);
else printf("test write lock,child found no lock at 2nd byte\n");
}
else {
//父进程测试自己的锁,在0字节、1字节、2字节上均用写锁测试
if(mylock_test(fd,F_WRLCK,0,SEEK_SET,1,lockname)!=0)
printf("test write lock,parent found %s at 0th byte\n",lockname);
else printf("test write lock,parent found no lock at 0th byte\n");
if(mylock_test(fd,F_WRLCK,1,SEEK_SET,1,lockname)!=0)
printf("test write lock,parent found %s at 1st byte\n",lockname);
else printf("test write lock,parent found no lock at 1st byte\n");
if(mylock_test(fd,F_WRLCK,2,SEEK_SET,1,lockname)!=0)
printf("test write lock,parent found %s at 2nd byte\n",lockname);
else printf("test write lock,parent found no lock at 2nd byte\n");
if(result=waitpid(pid,NULL,0)!=pid)
{
perror("waitpid error");
exit(1);
}
}
exit(0);
}
输出结果如下:
如上图所示,父进程测试自己的任何锁,得到的结果均为F_UNLCK;第一字节加的读锁,子进程用读锁测试得到的是F_UNLCK,用写锁测试得到的是F_RDLCK;第二字节加的写锁,子进程用读锁和写锁测试得到的结果均为F_WRLCK,所以用写锁测试能得到当前锁的真实状态。
(2)F_SETLK。对指定位置的区域进行加锁(l_type是F_RDLCK或F_WRLCK)或解锁(l_type是F_UNLCK)操作。如果根据锁的互斥性原则,导致加锁失败,则函数立即返回-1,并赋值errno为EACCES、EAGAIN。个人认为一般情况下,一个进程对一个描述符的特定位置解锁总是 成功的,因为默认情况下,进程对特定位置是不加锁的,同时它又不能解锁其他进程的锁,所以一般来说加解锁总是成功的。
(3)F_SETLKW,是加锁或解锁的阻塞版本,也就是当加锁失败时,函数将阻塞休眠,W代表wait;当下列条件发生时,进程被唤醒
(a)已具备加锁条件
(b)进程休眠期间,捕捉到一个信号并从信号处理程序中返回,则函数返回-1,并赋值errno为EINTR。
在此要说的是进程具备条件后,被唤醒的时间。源程序locktest2.c(只写主函数,其他参考locktest1.c)
int main(void)
{
int fd,result;
pid_t pid;
int n;
char lockname[]="F_UNLCK";
if((fd=open("templock2",O_RDWR|O_CREAT|O_TRUNC,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH))<0)
{
perror("open error");
exit(1);
}
if((n=write(fd,"abcdefg",7))!=7)
{
perror("wrtie error");
exit(1);
}
// 0字节加写锁
if((result=write_lock(fd,0,SEEK_SET,1))<0)
{
perror("write_lock error");
exit(1);
}
if((pid=fork())<0)
{
perror("fork error");
exit(1);
}
else if(pid==0)
{
if(mylock_test(fd,F_WRLCK,0,SEEK_SET,1,lockname)!=0) //用写锁测试0字节
printf("test write lock,child found %s at 0th byte\n",lockname);
else printf("test write lock,child found no lock at 0th byte\n");
if(writew_lock(fd,0,SEEK_SET,1)<0)//阻塞获得写锁
{
perror("child writew_lock error");
exit(1);
}
else
printf("child add write lock\n");
close(fd);
}
else {
sleep(2);// 父进程休眠2秒钟,让子进程先执行,并阻塞
if(un_lock(fd,0,SEEK_SET,1)<0)
{
perror("parent un_lock error");
exit(1);
}
printf("parent unlock 0th bytes\n");
if(result=waitpid(pid,NULL,0)!=pid)
{
perror("waitpid error");
exit(1);
}
close(fd);
}
exit(0);
}
程序执行结果如下:
从程序输出结果来看,父进程解锁0字节后,子进程被唤醒,并打印“child add write lock”,然后父进程继续执行并打印"parent unlock 0th bytes"。
3 其他说明
(1)函数返回值。刚才说了,根据第二个参数的不同,函数有不同的返回值,正常情况下返回0,出错情况下会返回-1,并赋值errno。常见的错误码如下:
(a)EACCESS或EAGAIN。加锁失败
(b)EBADF。文件描述符尚未打开或文件描述符打开的模式不对,操作无法执行
(c)EINTR:F_SETLK或F_SETLKW操作时,被信号中断
(d)EINVAL。第二个参数无法识别或参数无效
(2)如果进程关闭指向一个文件的任何一个描述符,则此进程加在文件的所有锁都会失效,不管进程是在哪个描述符上获得的。也就是说使用dup、open操作的使同一个文件,关闭这个文件上打开的任何一个描述符,锁均被释放。
(3) 进程的所有线程共享进程的锁。
修改locktest2.c为循环的情况,本程序locktest3.c
int main(void)
{
int fd,result;
pid_t pid;
int n;
char lockname[]="F_UNLCK";
if((fd=open("templock2",O_RDWR|O_CREAT|O_TRUNC,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH))<0)
{
perror("open error");
exit(1);
}
if((n=write(fd,"abcdefg",7))!=7)
{
perror("wrtie error");
exit(1);
}
// 0字节加写锁
if((result=write_lock(fd,0,SEEK_SET,1))<0)
{
perror("write_lock error");
exit(1);
}
if((pid=fork())<0)
{
perror("fork error");
exit(1);
}
else if(pid==0)
{
if(mylock_test(fd,F_WRLCK,0,SEEK_SET,1,lockname)!=0) //用写锁测试0字节
printf("test write lock,child found %s at 0th byte\n",lockname);
else printf("test write lock,child found no lock at 0th byte\n");
for(int i=0;i<5;i++){
if(writew_lock(fd,0,SEEK_SET,1)<0)//阻塞获得写锁
{
perror("child writew_lock error");
exit(1);
}
else
printf("child add write lock\n");
if(un_lock(fd,0,SEEK_SET,1)<0) //释放获得的锁
{
perror("child unlock error");
exit(1);
}
}
close(fd);
}
else {
sleep(2);// 父进程休眠2秒钟,让子进程先执行,并阻塞
for(int i=0;i<5;i++){
if(un_lock(fd,0,SEEK_SET,1)<0)
{
perror("parent un_lock error");
exit(1);
}
printf("parent unlock 0th bytes\n");
if(readw_lock(fd,0,SEEK_SET,1)<0) //阻塞获得读锁
{
perror("parent readw_lock error");
exit(1);
}
}
if(result=waitpid(pid,NULL,0)!=pid)
{
perror("waitpid error");
exit(1);
}
close(fd);
}
exit(0);
}
输出结果如下:
从结果看出,程序不是交替输出,也就是子进程 获得写锁并释放后,父进程没有立即执行,而是由子进程进入下一个循环。子进程循环结束后,锁被释放,父进程才又开始执行。
参考资料:
1 https://man7.org/linux/man-pages/man2/fcntl.2.html