概述
在多数unix
系统中,当多个进程/线程同时编辑一个文件时,该文件的最后状态取决于最后一个写该文件的进程。
对于有些应用程序,如数据库,各个进程需要保证它正在单独地写一个文件。这时就要用到文件锁。
文件锁(也叫记录锁)的作用是,当一个进程读写文件的某部分时,其他进程就无法修改同一文件区域。
能够实现文件锁的函数主要有2个:flock
和fcntl
。
早期的伯克利版本只支持flock,该函数只能对整个文件加锁,不能对文件的一部分加锁。
lockf
是在fcntl
基础上构造的函数,它提供了一个简化的接口。它们允许对文件中任意字节区域加锁,短至一个字节,长至整个文件。
fcntl函数
#include <fcntl.h>
int fcntl(int fd, int cmd, .../*struct flock *flockptr*/);
#返回值:若成功,返回值依赖于cmd,失败返回-1
cmd
是F_GETLK, F_SETLK, F_SETLKW
中的一个。第三个参数是指向flock
结构的指针,flock
结构如下:
struct flock {
short l_type;/* one of F_RDLCK, F_WRLCK, F_UNLCK */
short l_whence;/* SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start;/* offset in bytes, relative to l_whence */
off_t l_end;/* length, in bytes, 0 means lock to EOF */
off_t l_pid;/* returned with F_GETLK */
};
其中,
- 锁类型:共享读锁
F_RDLCK
,独占性写锁F_WRLCK
,解锁F_UNLCK
- 加锁或解锁区域的起始字节偏移量(
l_start, l_whence
) - 区域字节长度(
L_len
) - 进程的id持有的锁能阻塞当前进程,仅由
F_GETLK
返回 - 锁可以在文件尾处开始或者越过尾端开始,但是不能在文件起始位置之前开始
- 若
l_len=0
, 表示锁的范围可以扩大到最大可能偏移量,这意味着不管向文件中追加多少数据,它们都可以处于锁的范围内,而且起始位置可以任意 - 设置
l_start
和l_whence
指向文件的起始位置,并且指定l_len=0
,以实现对整个文件加锁(一般l_start=0, l_whence=SEEK_SET
)
锁的使用
使用锁的基本规则:
- 任意多个进程在一个给定的字节上可以有一把共享的读锁(
F_RDLCK
),但是在一个给定的字节上只能有一个进程有一把独占性写锁(F_WRLCK
) - 如果在一个给定字节上已经有一把或多把读锁,则不能在该字节上再加写锁,如果在一个字节上已经有一把独占性写锁,则不能再对它加任何读锁
- 对于单个进程而言,如果进程对某个文件区域已经有了一把锁,然后又试图在相同区域再加一把锁,则新锁会替换旧锁
- 加读锁时,该描述符必须是读打开,加写锁时,该描述符必须是写打开
fcntl三种cmd的使用:
-
F_GETLK
:判断由flockptr
所描述的锁是否会被另一把锁所排斥(阻塞),如果存在一把锁阻止创建由flockptr
所描述的锁,由该现有锁的信息将重写flockptr
指向的信息。如果不存在这种情况,则除了将l_type
设置为F_UNLCK
之处,flockptr
所指向结构中的其他信息保持不变 -
F_SETLK
:设置由flockptr
所描述的锁,如果程序试图获得一把锁,而系统阻止程序获得该锁,则fcntl
会立即返回错误,errno
设置为EACCES或EAGAIN
。当l_type=F_UNLCK
时,此命令用来清除指定的锁 -
F_SETLKW:F_SETLK
的阻塞版本(wait
)。如果程序尝试获得的锁无法被授予,调用进程会进入休眠直到进程获得锁或者信号中断
注意:用F_GETLK 测试能否创建一把锁,然后用F_SETLK尝试建立锁之间并非原子操作,也就是说两次调用之间有可能另一进程插入并创建了相同的锁。如果不希望在等待锁变为可用时产生阻塞,就必须处理由F_SETLK返回的可能出错值
下面是测试一把锁的例子:
#include <stdio.h>
#include <errno.h>
#include <pthread.h>
#include <fcntl.h>
pid_t lock_test(int fd, 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;
l ock.l_len = len;
if (fcntl(fd, F_GETLK, &lock) < 0) {
printf("fcntl error: %s\n", strerror(errno));
return 1;
}
if (lock.l_type == F_UNLCK) {
return 0;
}
return lock.l_pid;
}
锁的继承与释放
锁的继承和释放有以下三条原则:
- 锁与进程和文件两者相关联。即当一个进程终止时,它所建立的所有锁均释放,对于描述符而言,无论它何时关闭,进程通过它引用的文件上的任何一把锁也都会释放
- 由
fork
产生的子进程不继承父进程所设置的锁 - 执行
exec
后,新程序可以继承原程序的锁。注意,如果对一个文件描述符设置了执行时关闭标志,那么当作为exec的一部分关闭该文件描述符时,将释放相应文件的所有锁
避免死锁
如果两个进程互相等待对方持有并且不释放的资源时,这两个进程就会进入死锁状态。
如果一个进程已经控制了文件中的一个加锁区域,然后它又试图对另一个进程控制的区域加锁,那么它就会进入睡眠,并有可能发生死锁。
检测到死锁时,内核必须选择一个进程接收错误返回。
总结
在多进程或多线程环境中,当多个应用需要读写同一个文件时,需要考虑对文件加锁,以保证对文件修改的一致性。
在使用文件锁时,应明确应用模式,防止死锁。
更多关于文件锁的使用细节,请参考《UNIX环境高级编程》。
网友评论