记录锁(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);}elseprintf("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);}elseprintf("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

linux I/O-记录锁(record lock)相关推荐

  1. Mysql 死锁过程及案例详解之记录锁与间隔锁Record Lock Gap Lock

    记录锁Record Lock与间隔锁GAP Lock 记录锁Record Lock 记录锁Record Locks又称为行锁,它同时包含索引和间隔锁.记录锁可以是共享锁也可能是排他锁.可以通过perf ...

  2. MySQL锁 —— 记录锁、间隙锁、临键锁、自增锁

    MySQL的事务隔离级别有:未提交读.已提交读.可重复读.可串行化,除了串行化,在不同的隔离级别下,都会存在不同的并发问题,MySQL默认隔离级别是可重复读,在这种隔离级别下,存在幻读的问题,MySQ ...

  3. MySQL怎么运行的系列(十)Innodb中的锁:记录锁、临键锁、间隙锁、意向锁

    本系列文章目录 展开/收起 MySQL怎么运行的系列(一)mysql体系结构和存储引擎 MySQL怎么运行的系列(二)Innodb缓冲池 buffer pool 和 改良版LRU算法 Mysql怎么运 ...

  4. mysql主键查询gap锁失效,mysql记录锁(record lock),间隙锁(gap lock),Next-key锁(Next-key lock)...

    1. 什么是幻读? 幻读是在可重复读的事务隔离级别下会出现的一种问题,简单来说,可重复读保证了当前事务不会读取到其他事务已提交的 UPDATE 操作.但同时,也会导致当前事务无法感知到来自其他事务中的 ...

  5. Mysql INNODB引擎行锁的3种算法 Record Lock Next-Key Lock Grap Lock

    Mysql INNODB引擎行锁的3种算法 InnoDB存储引擎有3种行锁的算法,其分别是: □ Record Lock:单个行记录上的锁 Record Lock总是会去锁住索引记录,如果InnoDB ...

  6. mysql 共享锁和排他锁 意向锁 记录锁 Gap Locks Next-Key Locks 插入意向锁介绍

    文章目录 前言: 共享锁和排它锁 LOCK TABLES 和 UNLOCK TABLES 语句 意向锁 记录锁Record Locks 间隙锁 Gap Locks 下一键锁定 next-key 插入意 ...

  7. mysql临键锁_详解 MySql InnoDB 中的三种行锁(记录锁、间隙锁与临键锁)

    详解 MySql InnoDB 中的三种行锁(记录锁.间隙锁与临键锁) 前言 InnoDB 通过 MVCC 和 NEXT-KEY Locks,解决了在可重复读的事务隔离级别下出现幻读的问题.MVCC  ...

  8. 记录锁、间隙锁和临键锁

    记录锁.间隙锁和临键锁 Record Lock A record lock is a lock on an index record. For example, SELECT c1 FROM t WH ...

  9. MySQL 锁全集(共享锁/排它锁、记录锁/间隙锁/临键锁)

    提升工作效率利器: ‎Mac App Store 上的"Whale - 任务管理.时间.卡片.高效率" 简介:锁是计算机协调多个进程或线程并发访问某一资源变得有序的机制. 一.锁分 ...

  10. MySQL的锁机制 - 记录锁、间隙锁、临键锁

    记录锁(Record Locks) 记录锁是 封锁记录,记录锁也叫行锁,例如: SELECT * FROM `test` WHERE `id`=1 FOR UPDATE; 它会在 id=1 的记录上加 ...

最新文章

  1. Openstack_通用模块_Oslo_vmware 创建 vSS PortGroup
  2. 广药谋定中国农民丰收节交易会-万祥军:谋定乡村振兴基金
  3. 函数的参数-列表使用+=本质上是调用extend方法
  4. 网站攻击软件_如何防止网站建设中出现安全问题?
  5. python语法学习—打印九九乘法表
  6. vue-seamless-scroll在小米手机上显示不正常 显示出两行的问题
  7. 基于JAVA+SpringMVC+Mybatis+MYSQL的培训中心管理系统
  8. 地表径流分布数据/水文站点分布/降雨量分布/辐射分布数据
  9. Netty自带的心跳机制——IdleStateHandler
  10. 2021暑假Leetcode刷题——Two Pointers(1)
  11. Android studio Intent
  12. 外汇EA真的有用吗?外汇EA如何设置
  13. Unity制作UFO小游戏
  14. bootstrap table表格点击行checkbox勾选或取消勾选
  15. 计算机考研408真题(全国统考2009--2020)、985高校计算机考研资料(清北+北理+北邮+武大+华科+浙大+复旦+哈工大+西安交大+华南理工)、王道四件套、天勤四件套---百度网盘免费下载
  16. 计算机专业秃顶图片,大学被叫惨的三大专业,计算机秃顶是常事,医学专业这个就惨了!...
  17. 盘点中国顶级黑客Top10,雷军也名列其中!
  18. LINODE优惠码与服务器搭建
  19. UG如何把语言改成中文,UG如何把界面语言改成中文
  20. Linux下U盘变成只读

热门文章

  1. 旋转数组(右旋转,js实现,unshift,splicec实现)
  2. android 6.0 点亮屏幕,android6.0 otg连接设备 点亮屏幕(案例)
  3. php和xml区别,php和XML
  4. json为全局变量 vue_Vue-cli开发笔记二----------接口调用、配置全局变量
  5. lambda qt 参数 槽函数_C++中的lambda表达式用法
  6. java多线程【线程安全问题】
  7. 搜狗浏览器收藏夹在哪_安卓Edge浏览器最新版42.0.2轻体验,整体优良但无特别惊喜...
  8. 软件测试常见笔试面试题(一)
  9. linux 查看hadoop命令大全,linux下hadoop集群常用命令
  10. php retoken,laravel 5.5 关闭token的3种实现方式