Linux 文件锁与记录锁
基本概念
记录锁
记录上锁(record locking)是读写锁(readers-writer lock,简称rw lock)的一种扩展类型,可用于亲缘进程或无亲缘进程之间共享某个文件的读和写,常简称为记录锁。读写锁可参见这篇文章:Linux 自旋锁,互斥量(互斥锁),读写锁。
记录锁锁定的文件通过文件描述符访问,调用fcntl执行上锁和解锁操作。记录锁的维护通常在内核中,属主是由属主进程ID标识。也就是说,记录锁用于不同进程间的上锁,而不是用于同一进程内不同线程间的上锁。
文件锁
记录锁可以指定文件中待上锁或解锁部分的字节范围(byte range)。当记录锁锁定的范围是整个文件时,就称为文件上锁(file locking,简称文件锁)。可以说,文件锁是一种特殊的记录锁。
粒度
粒度(granularity)用于标记能被锁住的对象的大小。对于Posix记录锁,粒度是单个字节。对于文件锁,粒度是整个文件。
记录锁与读写锁
记录锁(record lock)和读写锁(rw lock)的上锁方式都分为:读出锁、写入锁。读出锁可以同时有多个,是共享的;写入锁同时只能有一个,是互斥的。
读写锁与记录锁的区别:
-
多线程,多进程应用环境
读写锁锁定的对象是内存中的锁变量,可用于多进程,也可以用于多线程;记录锁锁定的对象是记录(文件的某些字节范围),而文件描述符表是以进程为单位由操作系统给分配的,只能用于多进程。
具体地,读写锁是pthread_rwlock_t类型的变量在内存中分配的。当读写锁是在单个进程内的各个线程间共享时(默认属性),分配的变量可以在单个进程内;当读写锁在共享内存区时,各进程共享读写锁变量。 -
优先级顺序
读出锁和写入锁有一个优先级关系,在写入锁待处理期间,会优先选择优先级高的锁。
读写锁有两种优先级策略:读出锁优先,写入锁优先。取决于具体实现。
记录锁有多种种实现,Solaris 2.6, Digital Unix 4.0B以FIFO顺序处理;BSD/OS 3.1优先考虑读出锁。
Posix记录上锁
函数接口fcntl
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg: struct flock* */);
参数
- fd 已打开文件的文件描述符
- cmd 有3个取值,代表3个命令:F_SETLK, F_SETLKW, F_GETLK。
- arg flock结构对象
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) */
...
};
cmd3个命令:
取值 | 描述 |
---|---|
F_SETLK | 获取(l_type = F_RDLCK或F_WRLCK)或释放(l_type=F_UNLCK)由arg指向的flock结构所描述的锁。 如果无法将该锁授予调用进程,该函数就立即返回一个EACCESS或EAGAIN错误而不阻塞。 |
F_SETLKW | 与F_SETLK类似,区别在于调用线程如果无法取得锁,调用线程将阻塞到该锁能取得为止。名字最后的W是“wait”的意思。 |
F_GETLK | 检查是否有线程/进程已获得锁。如果当前没有上锁,由arg指向的flock.l_type会被置为F_UNLCK;否则,由arg指向的flock对象就会包含持有该锁的进程ID。 |
返回值
成功取决于cmd;出错,返回-1。
既然返回值已经能表示获得锁的结果,为何还需要cmd=F_GETLK命令?
因为当执行fcntl + F_SETLK/F_SETLKW命令返回一个错误时,导致该错误的某个锁的信息可以由F_GETLK命令返回,从而允许确定是哪个进程锁住了所请求的文件区,及上锁方式(读出锁或写入锁)。当然,当文件区解锁时,F_GETLK命令也可能返回文件区解锁的信息。
flock结构锁住文件区
上锁类型
flock对文件区的上锁类型由l_type成员决定:
- F_RDLCK 读出方式上锁
- F_WRLCK 写入方式上锁
- F_UNLCK 解锁(不论之前以读出,还是写入方式上锁)
锁定范围
flock对文件区的锁定范围取决于3个成员值l_whence + l_start + l_len
l_whence用来解释l_start起始位置,粗略定位起始位置;l_start(相对偏移)是在l_whence基础上的偏移;l_len指定从l_whence和l_start决定的偏移开始的连续字节数。
下面根据l_whence 取值来解释文件区锁定范围:
- SEEK_SET: l_start相对于文件的开头解释,锁定范围为[l_start, l_len);
- SEEK_CUR: l_start相对于文件的当前字节偏移(即文件当前读写指针位置)解释,锁定范围为[cur_pos + l_start, cur_pos + l_len)(假设当前文件读写指针位于cur_pos(相对于文件起始位置而言的偏移));
- SEEK_END: l_start相对于文件的末尾解释,锁定范围为[file_size+ l_start, file_size+ l_len)(假设当前文件字节数为file_size);
锁住整个文件
锁住整个文件的方式有2种:
1)设置flock结构成员l_whence = SEEK_SET,l_start = 0, l_len = 0(0表示不限制字节数);
2)调用lseek将读写指针定位到文件开头,然后设置flock结构成员l_whence = SEEK_CUR,,l_start = 0, l_len = 0(0表示不限制字节数);
最后,调用fcntl F_SETLK/F_SETLKW获得锁;
常用第1)种方式。
记录锁的释放
1)通过设置flock.l_type = F_UNLCK,调用fcntl F_SETLK/F_SETLKW来解锁;
2)通过关闭与文件关联的进程,会关闭所有文件描述符,删除锁;
问题:记录锁能应用于多线程吗?
先不着急下结论,而是做个实验:创建2个线程tid1和tid2,观察tid1获得记录锁期间,tid2是否也能同时获得锁。
如果tid1获得记录锁的时候,tid2也能获得记录锁,说明记录锁不能用于多线程;否则,说明记录锁可以用于多线程。
void *thread_test_flock1(void *arg)
{
int id = (int)arg;
printf("hello, thread %d\n", id);
writew_lock(file_fd);
printf("thread %d get write flock\n", id);
sleep(5);
printf("thread %d work\n", id);
un_lock(file_fd);
printf("thread %d release write flock\n", id);
}
void *thread_test_flock2(void *arg)
{
int id = (int)arg;
printf("hello, thread %d\n", id);
sleep(2);
printf("thread %d get write flock\n", id);
writew_lock(file_fd);
printf("thread %d work\n", id);
un_lock(file_fd);
printf("thread %d release write flock\n", id);
}
这是获得写入锁和解锁的操作:
int writew_lock(int fd)
{
struct flock lock;
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
lock.l_pid = getpid();
if (fcntl(fd, F_SETLKW, &lock) < 0) {
perror("fcntl error");
exit(1);
}
return 0;
}
int un_lock(int fd)
{
struct flock lock;
lock.l_type = F_UNLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
lock.l_pid = getpid();
if (fcntl(fd, F_SETLKW, &lock) < 0) {
perror("fcntl error");
exit(1);
}
return 0;
}
file_fd定义及创建线程的main:
int file_fd = -1;
int main()
{
pthread_t tid1, tid2;
if ((file_fd = open("test_flock.txt", O_CREAT | O_WRONLY)) < 0) {
perror("open error");
exit(1);
}
pthread_create(&tid1, NULL, thread_test_flock1, (void *)1);
pthread_create(&tid2, NULL, thread_test_flock2, (void *)2);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
运行结果:
表明在线程tid1获得写入锁时,tid2也获得了写入锁。说明记录锁不能应用于多线程环境。
hello, thread 1
thread 1 get write flock
hello, thread 2
thread 2 get write flock
thread 2 work
thread 2 release write flock
thread 1 work
thread 1 release write flock
问题:记录锁能用于多进程吗?
虽然我们已经知道是可以,不过,这里还是实验验证下,跟前面多线程环境做对比。
获得写入锁、解锁操作,跟前面一样,保持不变。main函数见下:
int main()
{
pid_t pid1, pid2;
int fd = open("test_flock.txt", O_CREAT | O_WRONLY, 0774);
if (fd < 0) {
perror("open error");
exit(1);
}
if ((pid1 = fork()) < 0) { // error
perror("fork1 error");
exit(1);
}
else if (pid1 == 0) { // child - pid1
if ((pid2 = fork()) < 0) {// error
perror("fork2 error");
exit(1);
}
else if (pid2 == 0){// child - pid2
sleep(1);
printf("hello, pid2\n");
writew_lock(fd);
printf("pid2 get write lock\n");
un_lock(fd);
printf("pid2 unlock\n");
}
// pid1
printf("hello, pid1\n");
writew_lock(fd);
printf("pid1 get write lock\n");
sleep(5);
un_lock(fd);
printf("pid1 unlock\n");
}
int status;
while (waitpid(-1, &status, 0) > 0) {
if (errno == ECHILD) {
break;
}
}
return 0;
}
运行结果:
可以看出,在进程pid1释放记录锁前,pid2无法获得锁。
hello, pid1
pid1 get write lock
hello, pid2
pid1 unlock
pid2 get write lock
pid2 unlock
hello, pid1
pid1 get write lock
pid1 unlock
文件上锁
文件上锁通常使用flock()函数进行,相比较记录上锁的调用fcntl,传入flock结构更简便。
flock函数接口
#include <sys/file.h>
int flock(int fd, int operation);
参数
- fd 已打开文件描述符。
- operation 上锁操作可以是下面几个取值:
operation值 | 描述 |
---|---|
LOCK_SH | Place a shared lock. More than one process may hold a shared lock for a given file at a given time. |
LOCK_EX | Place an exclusive lock. Only one process may hold an exclusive lock for a given file at a given time. |
LOCK_UN | Remove an existing lock held by this process. |
可以看出,LOCK_SH用于共享锁,对应记录上锁的读锁;LOCK_EX用于独占锁,对应记录上锁的写入锁;LOCK_UN用于解锁。
返回值
成功,返回0;出错,返回-1.
问题:文件上锁flock能应用于多线程环境?
答案是不行的。验证方式类似前面的,将获得写入锁writew_lock,和解锁un_lock,都换成flock函数。
#include <sys/file.h>
void *thread_test_flock1(void *arg)
{
int id = (int)arg;
printf("hello, thread %d\n", id);
// writew_lock(file_fd);
flock(file_fd, LOCK_EX); // 取得独占锁
printf("thread %d get write flock\n", id);
sleep(5);
printf("thread %d work\n", id);
// un_lock(file_fd);
flock(file_fd, LOCK_UN); // 释放锁
printf("thread %d release write flock\n", id);
}
void *thread_test_flock2(void *arg)
{
int id = (int)arg;
printf("hello, thread %d\n", id);
sleep(2);
printf("thread %d get write flock\n", id);
// writew_lock(file_fd);
flock(file_fd, LOCK_EX); // 取得独占锁
printf("thread %d work\n", id);
// un_lock(file_fd);
flock(file_fd, LOCK_UN); // 释放锁
printf("thread %d release write flock\n", id);
}
运行结果:
可以看到,在线程1释放独占锁之前,线程2也获得了独占锁。
hello, thread 1
thread 1 get write flock
hello, thread 2
thread 2 get write flock
thread 2 work
thread 2 release write flock
thread 1 work
thread 1 release write flock
参考
UNP 卷2