Linux 文件锁的原理、实现和应用
文件锁简介
在多数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;
}
测试结果如下:
自己进程检查到了死锁,直接返回,不再阻塞,最后父子都退出了。
Linux 文件锁的原理、实现和应用相关推荐
- [转帖]linux下的X server:linux图形界面原理
linux下的X server:linux图形界面原理 https://www.cnblogs.com/liangxiaofeng/p/5034912.html linux下的X server:lin ...
- linux下的X server:linux图形界面原理
linux下的X server:linux图形界面原理 Moblin Core是在Gnome Mobile的平台上建立.我以前玩Linux,提交的都和图像没有关系,连Xwindows都不用启动,开机后 ...
- linux系统调用的封装格式,ARM Linux系统调用的原理
ARM Linux系统调用的原理ARM Linux系统调用的原理 操作系统为在用户态运行的进程与硬件设备进行交互提供了一组接口.在应用程序和硬件之间设置一个额外层具有很多优点.首先,这使得编程更加容易 ...
- Linux 输入子系统原理理解(原创)
linux 输入子系统原理理解(原创) 以前学了单独的按键设备驱动以及鼠标驱动,实际上,在linux中实现这些设备驱动,有一种更为推荐的方法,就是input输入子系统.平常我们的按键,触摸屏,鼠 ...
- X server:linux 图形界面原理
X server: linux 图形界面原理 Moblin Core是在Gnome Mobile的平台上建立.我以前玩Linux,提交的都和图像没有关系,连Xwindows都不用启动,开机后直接进入文 ...
- 文件锁(三)——文件锁的原理
文件锁的原理 理解了文件锁的原理后,就可以理解为什么文件锁可以实现互斥与共享了. 若A进程与B进程同时打开同一个文件,他们使用同一个文件表,使用同一个V节点,V节点指向hello这个文件,里面有一个锁 ...
- Android 6.0 JNI原理分析 和 Linux系统调用(syscall)原理
JNI原理 引言:分析Android源码6.0的过程,一定离不开Java与C/C++代码直接的来回跳转,那么就很有必要掌握JNI,这是链接Java层和Native层的桥梁,本文涉及相关源码: fram ...
- 【Linux】Linux的挂载原理 |MOUNT|挂载NAS|自动挂载
目录 零.常用挂载命令 一.Linux的挂载原理 1.概念 2.举例 3.补充 4.LINUX文件结构和WINDOWS的不同 5.挂载文件系统 6.mount结构与原理 7.mount 和umount ...
- Linux 快照 (snapshot) 原理与实践(二) 快照功能实践
文章目录 0. 概要 1. 准备演示数据 2. 创建 snapshot-origin 目标 3. 创建 snapshot 目标 4. 验证 COW 操作 4.1 第一次写数据 4.2 第二次写数据 5 ...
最新文章
- swift 学习笔记之在柯里化(Currying)
- 好程序员web前端分享数组及排序、去重和随机点名
- 互联网开发(一) 并发基础知识
- 序列号生成的另一种玩法
- 矩阵的特征值和特征向量的雅克比算法C/C++实现
- import,export的支持[nodejs]
- 数百万行自研代码都捐了,华为将欧拉捐赠给开放原子开源基金会
- 构建之法阅读笔记(二)
- python学习浅谈(python2.x以及python3.x的区别、IDE)
- P4782 【模板】2-SAT 问题
- 数字图像处理 采样定理_数字图像处理(第4版)
- Oracle 数据库基本知识概念
- Exp3 免杀原理与实践 20154328 常城
- uniapp解决图片底部留白
- 游戏中的抗锯齿技术Anti-Alasing提炼总结
- 马丁富勒微服务论文学习
- 2021年金属非金属矿山井下电气考试内容及金属非金属矿山井下电气免费试题
- Python实现孤立森林(IForest)+SVR的组合预测模型
- PHP书籍推荐TOP10排行榜
- 【第32篇】SWA:平均权重导致更广泛的最优和更好的泛化