fcntl函数原型:
#include <unsitd.h> #include <fcntl.h> int fcntl(int fd,int cmd, ...);
fcntl函数用于改变已打开文件性质,函数参数是可变参数:
第一个参数 fd 表示打开文件的文件描述符,
第二个参数是对应的功能指令,不同指令决定不同的后续参数。
fcntl 基本用途包括以下五点:
1.复制一个现有的描述符(cmd = F_DUPFD)。
2.获得/设置文件描述符标记(cmd = F_GETFD或F_SETFL)。
3.获得/设置文件状态标志(cmd = F_GETOWN或F_SETOWN)。
4.获得/设置异步I/O所有权(cmd = F_GETOWN或F_SETOWN)。
5.获得/设置记录锁(cmd = F_GETLK、F_SETLK或F_SETLKW)。
本博客讲述的是第五种功能,即获得/设置记录锁:
I.第三个参数:
凡是与记录锁有关时,必定会有fcntl.h 文件下的一个重要结构体:
struct flock{ short l_type; // F_RDLCK读锁,F_WRLCK写锁,F_UNLCK解锁 off_t l_start; // 锁偏移的字节 short l_whence;// 锁开始的位置,SEEK_SET,SEEK_CURorSEEK_END off_t l_len; // 锁的范围,为0表示从起始到文件末 pid_t l_pid; // 放入具有能阻塞当前进程的锁(仅由F_GETLK返回) };
关于该结构体搭配函数,有以下几点需要强调:
1. l_start 是相对 l_whence 的偏移量,合起来表示锁起始处,单位是字节。可以是文件尾端处或更后,但不可是文件初始之前。
2. l_len 单位是字节,当是具体数字时是正整数,0时表示从起始位置到文件末尾。0时就表示之后在解锁前加入的内容都在锁区域。
3. 通常对通篇文件上锁的参数是 l_start = 0; l_whence = SEEK_SET ; l_len = 0;
1 //给文件整体上锁 2 int lockfile(int fd) 3 { 4 struct flock fl; 5 fl.l_type = F_WRLCK; 6 fl.l_start = 0; 7 fl.l_whence = SEEK_SET; 8 fl.l_len = 0; 9 return (fcntl(fd,F_SETLK,&fl)); 10 }
4.不同类型锁之间的兼容性不同。
我们可以很自然联想到POSIX里的读写锁。
读写锁的规则时:读时可共享,写时必独占。读锁处于读锁住模式时遇到写锁加锁请求时,系统会阻塞后续的读模式上锁请求。
记录锁下不同锁的规律:
可以发现 记录锁 和 读写锁 还是有一定差异的,其实两者本就是两种东西,因为读写锁是一个信号量,二记录锁中的读锁和写锁是两个事物。
当一个给定字节上已经有一把或多把读锁时,此时有一个其他一个进程想加个写锁,是不会被允许的。
但是必须声明:这个图中的兼容性只适用于不同进程提出的锁请求,但是并不适用于单个进程提出的多个请求。
比如,一个进程已经对一段给定字节上了一把锁,如果再在之后同样区域上锁,记住!新锁将代替老锁。
还需谨记:加读锁时,该描述符必须为读打开;加写锁时,该描述符必须是写打开。
II. 各自意义:
F_GETLK——
判断对应文件中的结构体中给定区间中是否有对应结构体中的锁。
比如我将结构体置为struct flock _flock; > _flock.l_type = F_WRLCK, _flock.l_start = 0, _flock.l_whence = SEEK_SET, _flock.l_len = 0;
即表示我在请求获取fd文件描述符对应的文件是否全区域有加写锁。
如果写锁存在,就会阻止我fcntl函数进行,并且会替换我给的结构体指针中的结构体内容。返回当前锁的信息,并且在l_pid 处返回锁的持有者。
如果写锁不存在,则将 _flock.l_type改为F_UNLCK ,其余不变。
这里写一个用例:
这里编写一个函数,用于测试一把锁,可以很好体现F_GETLK的作用。
1 //测试一把锁 2 pid_t lock_test(int fd,int type,off_t offset,int whence,off_t len) 3 { 4 struct flock lock; 5 lock.l_type = type; /*F_RDLCK or F_WRLCK*/ 6 lock.l_start = offset; 7 lock.l_whence = whence; /*SEEK_SET,SEEK_CUR or SEEK_END*/ 8 lock.l_len = len; 9 10 if(fcntl(fd,F_GETLK,&lock) < 0) 11 err_sys("fcntl error"); 12 13 if(lock.l_type == F_UNLCK) /*false*/ 14 return(0); 15 return (lock.l_pid); /*true*/ 16 }
F_SETLK
F_SETLK相对于F_GETLK就要常用很多。其目的顾名思义就是对文件指定区域加锁/解锁。
记住这一切都要满足记录锁的兼容性规则,当你回想起记录锁的兼容性规则时一定要记得,它适用于不同进程,而不适用于同一进程。
当加锁不成功时,即其上面本来就有规则不允许的条件的锁时,就会出错返回,errno会设置为EACCES或EAGAIN。
EACCES:无存取权限。
EAGAIN:非阻塞下调用阻塞操作。(非阻塞socket编程会遇到。)
F_SETLKW
F_SETLKW即F_SETLK的阻塞等待版,W即wait。
当fcntl 的请求不能满足时,就会进入休眠,当请求创建的锁可用时会被信号中断休眠,进程就会被唤醒。
这里再看一个实例:
编写一个函数来请求/释放一个锁,并生产对应的宏。
1 #define read_lock(fd,offset,whence,len) 2 lock_reg((fd),F_SETLK,F_RDLCK,(offset),(whence),(len)) 3 #define readw_lock(fd,offset,whence,len) 4 lock_reg((fd),F_SETLKW,F_RDLCK,(offset),(whence),(len)) 5 #define write_lock(fd,offset,whence,len) 6 lock_reg((fd),F_SETLK,F_WRLCK,(offset),(whence),(len)) 7 #define writew_lock(fd,offset,whence,len) 8 lock_reg((fd),F_SETLKW,F_WRLCK,(offset),(whence),(len)) 9 #define un_lock(fd,offset,whence,len) 10 lock_reg((fd),F_SETLK,F_UNLCK,(offset),(whence),(len)) 11 12 //请求和释放一把锁 13 int lock_reg(int fd,int cmd,int type,off_t offset,int whence,off_t len) 14 { 15 struct flock lock; 16 lock.l_type = type; 17 lock.l_start = offset; 18 lock.l_whence = whence; 19 lock.l_len = len; 20 21 return (fcntl(fd,cmd,&lock)); 22 }
关于获取/设置/释放锁有以下两点需要注意:
1.F_GETLK 和 F_SETLK(或F_SETLKW)并非一个原子操作,所以要视情况而写代码。
2.当你在一块区域中上锁后,如果要释放这块区域中的中间一块区域(不含边界),那么系统会自动将一个锁分成两个锁。
如果再将刚刚释放的区域重新加上本来的锁,系统就会将三个区域合并。
III.锁无法继承,锁的释放。
锁的继承:
让我们再回顾一下,子进程生成的时候发生了什么?(fork()为例)
1.父进程给子进程分配pid和PCB。
2.复制父进程的环境。
3.给子进程分配地址空间和资源。(vfork() 不执行3,4两步。不建议用,函数不成熟)
4.复制父进程的地址信息。
那么子进程会获得什么?
1.父进程的持有者和允许使用者以及其属性。
2.父进程的环境。
3.父进程分配给其的进程上下文。(比如堆栈信息)。
4.自己独立的一份内存。
5.nice值和进程调度级别,其中进程调度级别在 多级反馈队列中尤为体现。(这是windows和linux下的进程调度置换算法)。
6.根目录和工作目录。
7.打开的文件描述符。(父子进程间沟通关键)。
等....
那么父进程不会给子进程什么?(三点)
1.父进程的父进程ID
2.父进程的阻塞信号和计时器。
3.父进程的文件锁。
OK,那么接下来我们就可以得到结论了:
子进程是无法继承父进程所设置的锁的。
做个很简单的例子,当我父进程给一个文件上写锁后,执行fork(),如果子进程也能继承文件锁,那其也可以在文件中肆意写,这与记录锁的兼容性规则相悖。
锁的释放:
我们都知道,进程通信中IPC对象的持续性分为三种:随进程持续性,随内核持续性,随文件持续性。
记录锁是随进程持续性的IPC对象,锁与进程和文件两方面有关。
即一:当你的进程结束了,其对文件上的所有锁也就都被释放了。
二:当你的文件描述符被关闭了,其上面的锁也就都会被销毁。
这个有点不好理解,并不是文件被关闭,才会销毁锁,即当文件描述符的引用计数在变少时,其上的锁就会被销毁。(前提是这些锁是被执行关闭文件描述符操作的进程上的)
看下列代码:
1 fd1 = open("file1",...); 2 read_lock(fd1,...);//上读锁 3 fd2 = dup(fd1); 4 close(fd2);//锁会被释放 5 6 7 ////////////////////////// 8 9 fd1 = open("file1",...); 10 read_lock(fd1,...);//上读锁 11 fd2 = open("file2",...); 12 close(fd2);//锁会被释放
IV.需要独立出来讲的文件尾端加锁
看下列代码:
1 writew_lock(fd,0,SEEK_END,0);//从内容末到文件末阻塞上读锁 2 write(fd,buf,1); 3 un_lock(fd,0,SEEK_END,0);//从内容末到文件末解锁 4 write(fd,buf,1);
看起来并没有什么问题,但其实上述中的两个内容末并不是同一位置,我们在上锁后还进行了写入操作导致内容后移,所以当我们上文件末锁操作后还有解锁需求时,要记得相对偏移量。
X.关于强制性锁和建议性锁。
建议性锁:
它的规则有点类似读写锁,它可以限制访问数据库的函数,但对于对数据库拥有写权限的任何其它进程对数据库文件提出的写请求无法驳回。
强制性锁:
强制性锁又叫强迫方式锁。
当一个进程视图读、写一个强制性锁起作用的文件,且读、写部分又被别的进程加了读或写锁。之后的结果要综合三种情况:
1.操作类型(读or写)2.其他进程设置锁的类型(读锁or写锁)3.文件描述符类型(阻塞or非阻塞)
参考文献:《UNIX环境高级编程》
https://blog.csdn.net/qingkongyeyue/article/details/53914885
https://www.cnblogs.com/fengkang1008/p/4725514.html (写的挺好)