一、概述
应用程序的一个常见的需求是从一个文件中读取数据,修改这些数据,然后将这些数据写回文件,当同一时刻只有一个进程使用这个文件,这么做不会出现问题,但是当多个进程同时更新一个文件时,就会出现资源竞争的问题。
文件锁是一种文件读写机制,在任何特定的时间只允许一个进程访问一个文件。利用这种机制能够使读写单个文件的过程变得更安全。
主要内容
- 文件锁的分类与原理
- 文件锁的使用
- 结束语
二、文件锁的介绍
文件锁主要分为两种:一种是劝告式的,一种是强制式的锁;但是在默认的情况下,文件锁是劝告式的。
劝告式文件锁
这种锁不会介入write和read的操作,是一种协议式的锁,在对一个文件进行读写的时候,采用如下的过程
graph LR
加锁-->读写操作
读写操作-->解锁
但是劝告式文件锁不会从系统层上强制应用程序遵守这个协议,其他的程序可以不检测文件的锁的状态而直接操作文件.
强制式文件锁
在程序中可以使用chmod()和fchmod()函数对文件强制加锁,强制式的锁会使read/write函数阻塞或失败.
三、文件锁的使用
文件锁有多种实现,这里将的均是劝告锁,强制锁在应用中的价值不高,并且存在很大的弊端(容易锁死).
在使用文件锁时,读写文件的接口必须使用read和write,不能使用库函数的IO操作函数,因为IO操作函数会带有缓冲,影响加锁的结果.
(好像标准库函数的IO操作函数可以选择没有缓冲,但是依旧慎重)
flock
- 功能:为一个已经打开的文件 添加或者移除一把劝告锁(advisory lock)
- 原型:int flock(int fd, int operation);
参数:
- fd 文件id
- operation 操作数
operation可以使下列的三个值
- LOCK_SH 放置一个共享锁
- LOCK_EX 放置一个排他锁
LOCK_UN 解锁
此外,LOCK_NB可以与前两个共用,通过’|’,意思是非阻塞的锁.当使用flock(fd,LOCK_SH|LOCK_NB)时,如果其他进程持有锁,则不会阻塞,而是返回错误,并记录一个错误码到error.
进程A | 进程B LOCK_SH | 进程B LOCK_EX |
---|---|---|
LOCK_SH | 是 | 否 |
LOCK_EX | 否 | 否 |
一个进程在一个文件上只能拥有一种类型的锁,每次调用flock会改变锁的模式.锁会在文件描述符关闭的时候自动释放.
- 不足 :
- 粒度大,只能锁整个文件
- 劝告锁,无法真正的锁定文件
- 有些NFS实现不支持flock
fcntl
这种锁通常称为”记录加锁”,fcntl可以对文件的任意部分加锁,而不影响其他部分的使用,记录加锁是针对应用程序说的,记录边界是应用程序定义的一个字节范围
- 原型:int fcntl(int fd, int cmd, … /* arg */ );
- 参数:
- fd 文件描述符
- cmd :
- F_SET_LK 设置锁状态,失败返回错误
- F_SET_LKW 设置锁状态,失败阻塞
- F_GET_LK 获取锁状态
- arg :struct flock 结构指针
fcntl的锁操作,通过下面这个结构体进行设置
struct flock
{
short int l_type; /* Type of lock: F_RDLCK, F_WRLCK, or F_UNLCK. */
short int l_whence; /* Where `l_start' is relative to (like `lseek'). */
#ifndef __USE_FILE_OFFSET64
__off_t l_start; /* Offset where the lock begins. */
__off_t l_len; /* Size of the locked area; zero means until EOF. */
#else
__off64_t l_start; /* Offset where the lock begins. */
__off64_t l_len; /* Size of the locked area; zero means until EOF. */
#endif
__pid_t l_pid; /* Process holding the lock. */
};
锁有三种状态,分别是读锁/写锁和无锁.
一个用于设置锁状态的函数
int pidf_set_lock(int fd,int lock_type)
{
struct flock flockstr;
//default
if(lock_type!=F_RDLCK&&lock_type!=F_WRLCK&&lock_type!=F_UNLCK)
return -1;
flockstr.l_type =lock_type;
flockstr.l_whence =SEEK_SET;
flockstr.l_start =0;
flockstr.l_len =0;
if(fcntl(fd,F_SETLK,&flockstr)==0)
{
if(flockstr.l_type==F_RDLCK)
pr_pidf_debug("set a F_RDLCK,thd pid = %d; \n",getpid());
if(flockstr.l_type==F_WRLCK)
pr_pidf_debug("set a F_WRLCK,thd pid = %d; \n",getpid());
if(flockstr.l_type==F_UNLCK)
pr_pidf_debug("set a F_UNLCK,thd pid = %d; \n",getpid());
return flockstr.l_type;
}
return -1;
}
获取锁状态的函数
int pidf_get_lock(int fd,pid_t *pid)
{
struct flock flockstr;
flockstr.l_type =F_WRLCK;
flockstr.l_whence =SEEK_SET;
flockstr.l_start =0;
flockstr.l_len =0;
if(fcntl(fd,F_GETLK,&flockstr)==0)
{
if(pid!=NULL)
*pid =flockstr.l_pid;
return flockstr.l_type;
}
perror("fcntl F_GETLK");
return -1;
}
继承和释放
- fork产生的子进程不会继承记录锁(fcntl),但是会继承flock
- 记录锁在exec()中会得到保留
- 记录锁同时和一个进程,一个inode关联,所以进程结束或者进程对文件的链接关闭,都会导致其所持有的记录锁被释放
四、结束语
参考
《Linux/UNIX系统编程手册(下)》