文件锁也被称为记录锁。
如果深层次涉及到文件锁的话,最起码要区分建议锁和强制锁。
我们这里先进行初步的说明。主要是为了对比学习各种的加锁机制,比如进程有进程信号量加锁机制,线程有线程互斥锁、线程信号量等加锁机制,学习文件锁有助于我们对比理解。
顾名思义,文件锁就是用来保护文件数据。
当多个进程共享读写同一个文件时,为了不让进程们各自读写数据时相互干扰,我们可以使用进程信号量来互斥实现,除了可以使用进程信号量以外,还可以使用我们这里要说明的“文件锁”来实现,而且功能更丰富,使用起来相对还更容易些。
多进程共享读写同一个文件时,如果数据很重要的话,为了防止数据相互修改,应该满足如下读写条件:
当某个进程正在写文件,而且在数据没有写完时,其它进程不能写,否者会相互打乱对方写的数据。
分两种情况:
某个进程正在写数据,而且在数据没有写完时,其它进程不能读数据因为别人在没有写完之前,读到的数据是不完整的,所以读和写时互斥的。
某个进程正在读数据,在数据没有读完之前,其它进程不能写数据因为可能会扰乱别人读到的数据。
某个进程在读数时,就算数据没有读完,其它进程也可以共享读数据,并不需要互斥等别人读完后才能读。因为读文件是不会修改文件的内容,所以不用担心数据相互干扰的问题。
总结起来就是,多进程读写文件时,如果你想进行资源保护的话,完美的资源保护应该满足如下这样的。
1)写与写之间互斥
2)读与写之间互斥
3)读与读之间共享
如何实现以上读写要求?
如果使用信号量来实现保护的话,只能是一律互斥,包括读与读都是互斥的,不能够像上面图解描述的,既能互斥又能共享,但是文件锁可以做到。
文件锁的读锁与写锁
对文件加锁时可以加两种锁,分别是“读文件锁”和“写文件锁”,我们这里简称为读锁和写锁。
可以重复加读锁,别人加了读锁在没有解锁之前,我依然可以加读锁,这就是共享。
别人加了读锁没有解锁前,加写锁会失败,反过来也是如此。
加锁失败后两种处理方式,
别人加了写锁在没有解锁前,不能加写锁,加写锁会失败。
加锁失败后两种处理方式,
我们常用的是阻塞加锁,至于如何实现阻塞和非阻塞,后面详细说明。
使用文件锁对文件进行保护
读文件时加读锁,写文件时就加写锁,然后就可以很容易的实现符合如下要求的资源保护。
1)写与写之间互斥
2)读与写之间互斥
3)读与读之间共享
当你对整个文件加锁时,如果文件的长度因为写入新数据或者截短而发生了变化,加锁内容的长度会自动变化,保证对内容变化着的整个文件加锁。
不过一般来说是,对多少内容加锁,就对多少内容解锁,如果你是对整个文件加锁,就将整个文件解锁。
但是实际上加锁和实际解锁的长度可以不相同,比如我对1000个字节的内容加了锁,但是可以只对其中的100字节解锁,不过这种情况用的少,知道有这么回事就行。
实现文件锁时,我们还是需要使用fcntl函数。
#include
#include
int fcntl(int fd, int cmd, .../*struct flock *flockptr */ );
第三个参数是…,表示fcntl函数是一个变参函数,第三个参数用不到时就不写。
fcntl函数有多种功能,我们这里主要介绍实现文件锁的功能,当cmd被设置的是与文件锁相关的宏时, fcntl就是用于实现文件锁。
成功返回0,失败则返回-1,并且errno被设置。
1)fd:文件描述符,指向需要被加锁的文件。
2)cmd:实现文件锁时,cmd有三种设置,F_GETLK、F_SETLK和F_SETLKW含义如下:
(a)F_GETLK
从内核获取文件锁的信息,将其保存到第三个参数,此时第三个参数为struct flock *flockptr。
我们这里是要设置文件锁,而不是获取已有文件锁的信息,我们这里用不到这个宏。
(b)F_SETLK
设置第三个参数所代表的文件锁,而且设置的是非阻塞文件锁,也就是如果加锁失败不会阻塞。
也就是说加锁失败后如果不想阻塞的话,就是由F_SETLK宏来决定的。此时需要用到第三个参数,struct flock *flockptr。
使用举例:
· 第一步:定义一个struct flock flockptr结构体变量(这个结构体变量就是文件锁)。
· 第二步:设置flockptr的成员,表示你想设置什么样的文件锁。
· 第三步:通过第三个参数,将设置好的flockptr的地址传递给fcntl,设置你要的文件锁。
(c)F_SETLKW
与F_SETLK一样,只不过设置的是阻塞文件锁,也就说加锁不成功的话就阻塞,是由F_SETLKW宏来决定的。
int fcntl(int fd, int cmd, …/*struct flock *flockptr */ );
3)第三个参数
第三个参数设置为什么视情况而定,如果fcntl用于实现文件锁的话,第三个参数为struct flock *flockptr,flockptr代表的就是文件锁。
对flockptr的成员设置为特定的值,就可以将文件锁设置为你想要的锁。
在linux平台查看结构体fcntl中的结构体:
使用命令:man fcntl
建议记录锁:
(a)结构体原型
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)
}
成员说明:
· l_type:锁类型
- F_RDLCK:读锁(或称共享锁)
- F_WRLCK:写锁
- F_UNLCK:解锁
· l_whence:加锁位置粗定位,设置同lseek的whence
l_whence这个与lseek函数的whence是一个含义,
off_t lseek(int fd, off_t offset, int whence);
文件IO我们就详细的讲过lseek函数。
· l_start:精定位,相对l_whence的偏移,与lseek的offset的含义完全一致通过l_whence和l_start的值,就可以用来指定从文件的什么位置开始加锁,不过一般来说,我们会将l_whence指定为SEEK_SET,l_start指定为0,表示从整个文件头上开始加锁。
· l_len:从l_whence和l_start所指定的起始地点算起,对文件多长的内容加锁。
如果l_len被设置0,表示一直加锁到文件的末尾,如果文件长度是变化的,将自动调整
加锁的末尾位置。
将l_whence和l_start设置为SEEK_SET和0,然后再将l_len设置为0,就表示从文件头加锁到文件末尾,其实就是对整个文件加锁。
flockptr.l_whence=SEEK_SET;
flockptr.l_start=0;
flockptr.l_len=0;
就表示对整个文件加锁。
如果只是对文件中间的某段加锁,这只是区域加锁,加区域锁时可以给文件n多个的独立区域加锁。
· l_pid:当前正加着锁的那个进程的PID 只有当我们获取一个已存在锁的信息时,才会使用这个成员,这个成员的值不是我们设置的,是由文件锁自己设置的,我们只是获取以查看当前那个进程正加着锁。对于我们目前设置文件锁来说,这个成员用不到。
使用文件锁的互斥操作,解决父子进程向同一文件写“hello ”,“world\n”时,hello hello world相连的问题。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//非阻塞设置写锁
#define SET_WRFLCK(fd, l_whence, l_offset, l_len)\
set_filelock(fd, F_SETLK, F_WRLCK, l_whence, l_offset, l_len)
//阻塞设置写锁
#define SET_WRFLCK_W(fd, l_whence, l_offset, l_len)\
set_filelock(fd, F_SETLKW, F_WRLCK, l_whence, l_offset, l_len)
//非阻塞设置读锁
#define SET_RDFLCK(fd, l_whence, l_offset, l_len)\
set_filelock(fd, F_SETLK, F_RDLCK, l_whence, l_offset, l_len)
//阻塞设置读锁
#define SET_RDFLCK_W(fd, l_whence, l_offset, l_len)\
set_filelock(fd, F_SETLKW, F_RDLCK, l_whence, l_offset, l_len)
//解锁
#define UNLCK(fd, l_whence, l_offset, l_len)\
set_filelock(fd, F_SETLK, F_UNLCK, l_whence, l_offset, l_len)
/* 调用这个函数,即可实现阻塞加读锁/阻塞加写锁, 非阻塞加读锁/非阻塞加写锁/解锁 */
static void set_filelock(int fd, int ifwait, int l_type, int l_whence, int l_offset, int l_len)
{
int ret = 0;
struct flock flck;
flck.l_type = l_type;
flck.l_whence = l_whence;
flck.l_start = l_offset;
flck.l_len = l_len;
ret = fcntl(fd, ifwait, &flck);
if (ret == -1)
{
perror("fcntl fail");
exit(-1);
}
}
void print_err(char *str, int line, int err_no)
{
printf("%d, %s: %s\n", line, str, strerror(err_no));
exit(-1);
}
int main(void)
{
int fd = 0;
int ret = 0;
fd = open("./file", O_RDWR|O_CREAT|O_TRUNC, 0664);
if(fd == -1) print_err("./file", __LINE__, errno);
ret = fork();
if(ret > 0)
{
while(1)
{
SET_WRFLCK_W(fd, SEEK_SET, 0, 0);
write(fd, "hello ", 6);
write(fd, "world\n", 6);
UNLCK(fd, SEEK_SET, 0, 0);
}
}
else if(ret == 0)
{
while(1)
{
SET_WRFLCK_W(fd, SEEK_SET, 0, 0);
write(fd, "hello ", 6);
write(fd, "world\n", 6);
UNLCK(fd, SEEK_SET, 0, 0);
}
}
return 0;
}
我们可以看到写入文件的内容里面不会出现hello hello world的情况。
我们理解了文件锁的原理后,我们就可以理解为什么文件锁可以实现互斥与共享了。
图解说明:
所有的进程共享操作同一个文件的时候,只有一个V节点,共享同一个文件,相当于就共享同一个锁链表。实现互斥和共享就是通过实现共享锁链表来实现的。
链表上节点代表是一把锁(读锁和写锁),节点存在时表示没有解锁,如果解锁了锁节点就不存在了。
锁节点记录了锁的基本信息。
· 锁类型
· 加锁的起始位置(l_whence、l_start)
· 加锁的长度(l_len)
· 当前正在加着锁的那个进程的PID
加锁时,进程会检查共享的文件锁链表。
1)如果链表上只有读锁节点
所有目前其它进程对该文件只加了读锁,由于读锁时共享的,所以不管链表上有几个读锁节点,当前进程都能成功加读锁。
链表上可不可以存在n多个读锁节点?
答:可以,因为读锁是共享的,不管别的进程有没有解读锁,所有的进程都可以加读锁,每加一个读锁,链表上就多一个读锁节点,只有当解锁时节点才被删除。
2)如果链表上有一个写锁节点表明目前有进程对文件加了写锁,锁节点还存在,表示人家目前还没有解锁,读锁和写锁是互斥的,所以当前不能加读锁,别人解锁后才能加读锁,加锁后链表上就插入一个读锁节点。
链表上能不能同时存在多个写锁节点?
答:不可能,因为写锁是互斥的,目前只能有一个进程在给文件加写锁,在解锁之前,别的进程不能加写锁。所以链表上不可能有>一个的写锁节点,否者就不能实现互斥了。
链表上会不会同时存在读锁节点和写锁节点?
读锁节点和写锁节点也是互斥的,链表上有读锁节点就不可能存在写锁节点,反过来有写锁节点就不可能有读锁节点。
1)如果链表上有读锁节点,别人还没有解锁,读锁与写锁互斥,不能加写锁。
2) 如果链表上有写锁节点,别人还没有解锁,写锁与写锁互斥,多以当前进程不能加写锁。
1)进程信号量:进程间共享信号量集合,通过检查集合中信号量的值,从而知道自己能不能操作。
2)文件锁:进程共享文件锁链表,通过检查链表上的锁节点,从而知道自己能不能操作。
(a)在同一进程中,如果多个文件描述符指向同一文件,只要关闭其中任何一个文件描述符,那么该进程加在文件上的文件锁将会被删除,也就是该进程在“文件锁链表”上的“读锁写锁”节点会被删除。
进程终止时会关闭所有打开的文件描述符,所以进程结束时会自动删除所有加的文件锁。
(b)父进程所加的文件锁,子进程不会继承,我们在讲进程控制时就说过加锁是进程各自私人事情,不能继承,就好比你老爸有抽烟的嗜好,难道这也需要继承吗,肯定不是的。
多线程间能不能使用fcntl实现的文件锁呢?
可以,但是线程不能使用同一个open返回的文件描述符,线程必须使用自己open所得到的文件描述符才有效。
代码演示:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//非阻塞设置写锁
#define SET_WRFLCK(fd, l_whence, l_offset, l_len)\
set_filelock(fd, F_SETLK, F_WRLCK, l_whence, l_offset, l_len)
//阻塞设置写锁
#define SET_WRFLCK_W(fd, l_whence, l_offset, l_len)\
set_filelock(fd, F_SETLKW, F_WRLCK, l_whence, l_offset, l_len)
//非阻塞设置读锁
#define SET_RDFLCK(fd, l_whence, l_offset, l_len)\
set_filelock(fd, F_SETLK, F_RDLCK, l_whence, l_offset, l_len)
//阻塞设置读锁
#define SET_RDFLCK_W(fd, l_whence, l_offset, l_len)\
set_filelock(fd, F_SETLKW, F_RDLCK, l_whence, l_offset, l_len)
//解锁
#define UNLCK(fd, l_whence, l_offset, l_len)\
set_filelock(fd, F_SETLK, F_UNLCK, l_whence, l_offset, l_len)
/* 调用这个函数,即可实现阻塞加读锁/阻塞加写锁, 非阻塞加读锁/非阻塞加写锁/解锁 */
static void set_filelock(int fd, int ifwait, int l_type, int l_whence, int l_offset, int l_len)
{
int ret = 0;
struct flock flck;
flck.l_type = l_type;
flck.l_whence = l_whence;
flck.l_start = l_offset;
flck.l_len = l_len;
ret = fcntl(fd, ifwait, &flck);
if (ret == -1)
{
perror("fcntl fail");
exit(-1);
}
}
void print_err(char *str, int line, int err_no)
{
printf("%d, %s: %s\n", line, str, strerror(err_no));
exit(-1);
}
void *pth_fun(void *pth_arg)
{
int fd = 0;
fd = open("./file", O_RDWR|O_CREAT|O_TRUNC, 0664);
if(fd == -1) print_err("open ./file fail", __LINE__, errno);
while(1)
{
SET_WRFLCK_W(fd, SEEK_SET, 0, 0);
write(fd, "hello ", 6);
write(fd, "world\n", 6);
UNLCK(fd, SEEK_SET, 0, 0);
}
return NULL;
}
int main(void)
{
int fd = -1;
int ret = -1;
pthread_t tid;
fd = open("./file", O_RDWR|O_CREAT|O_TRUNC, 0664);
if(fd == -1) print_err("open ./file fail", __LINE__, errno);
ret = pthread_create(&tid, NULL, pth_fun, NULL);
if(ret == -1) print_err("pthread_create fail", __LINE__, ret);
while(1)
{
SET_WRFLCK_W(fd, SEEK_SET, 0, 0);
write(fd, "hello ", 6);
write(fd, "world\n", 6);
UNLCK(fd, SEEK_SET, 0, 0);
}
return 0;
}
运行结果为:
我们就可以看到主线程和次线程的加锁效果。
flock与fcntl所实现的文件锁一样,既能够用在多进程上,也能用在多线程上,而且使用起来比fcntl的实现方式更方便,只是使用这个函数时,需要注意一些小细节。
#include
int flock(int fd, int operation);
按照operation的要求,对fd所指向的文件加对应的文件锁。加锁不成功时会阻塞。
成功返回0,失败返回-1,errno被设置
(a)fd:指向需要被加锁的文件
(b)operation
· LOCK_SH:加共享锁
· LOCK_EX:加互斥锁
· LOCK_UN:解锁
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
void print_err(char *str, int line, int err_no)
{
printf("%d, %s: %s\n", line, str, strerror(err_no));
exit(-1);
}
int main(void)
{
int fd = 0;
int ret = 0;
ret = fork();
if(ret > 0)
{
fd = open("./file", O_RDWR|O_CREAT|O_TRUNC|O_APPEND, 0664);
if(fd == -1) print_err("./file", __LINE__, errno);
while(1)
{
flock(fd, LOCK_EX);
write(fd, "hello ", 6);
write(fd, "world\n", 6);
flock(fd, LOCK_UN);
}
}
else if(ret == 0)
{
fd = open("./file", O_RDWR|O_CREAT|O_TRUNC|O_APPEND, 0664);
if(fd == -1) print_err("./file", __LINE__, errno);
while(1)
{
flock(fd, LOCK_EX);
write(fd, "hello ", 6);
write(fd, "world\n", 6);
flock(fd, LOCK_UN);
}
}
return 0;
}
们可以看到加入互斥锁之后的,不会出现hello hello 的情况。
我们加入共享锁进行查看:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
void print_err(char *str, int line, int err_no)
{
printf("%d, %s: %s\n", line, str, strerror(err_no));
exit(-1);
}
int main(void)
{
int fd = 0;
int ret = 0;
ret = fork();
if(ret > 0)
{
fd = open("./file", O_RDWR|O_CREAT|O_TRUNC|O_APPEND, 0664);
if(fd == -1) print_err("./file", __LINE__, errno);
while(1)
{
flock(fd, LOCK_SH);
write(fd, "hello ", 6);
write(fd, "world\n", 6);
flock(fd, LOCK_UN);
}
}
else if(ret == 0)
{
fd = open("./file", O_RDWR|O_CREAT|O_TRUNC|O_APPEND, 0664);
if(fd == -1) print_err("./file", __LINE__, errno);
while(1)
{
flock(fd, LOCK_SH);
write(fd, "hello ", 6);
write(fd, "world\n", 6);
flock(fd, LOCK_UN);
}
}
return 0;
}
运行结果为:
我们可以看到加入共享锁之后的,出现hello hello 的情况。
flock用于多进程时,各进程必须独立open打开文件,对于非亲缘进程来说,打开文件时肯定是各自独立调用open打开的。
需要你注意的是亲缘进程(父子进程),子进程不能使用从父进程继承而来的文件描述符,父子进程flock时必须使用独自open所返回的文件描述符。
这一点与fcntl实现的文件锁不一样,父子进程可以使用各自open返回的文件描述符加锁,但是同时子进程也可以使用从父进程继承而来的文件描述符加锁。
· 共享锁与互斥锁之间互斥
· 互斥锁与互斥锁之间互斥
· 共享锁与共享锁之间共享
用于多线程
代码演示:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
void print_err(char *str, int line, int err_no)
{
printf("%d, %s: %s\n", line, str, strerror(err_no));
exit(-1);
}
void *pth_fun(void *pth_arg)
{
int fd = 0;
fd = open("./file", O_RDWR|O_CREAT|O_TRUNC, 0664);
if(fd == -1) print_err("open ./file fail", __LINE__, errno);
while(1)
{
flock(fd, LOCK_EX);
write(fd, "hello ", 6);
write(fd, "world\n", 6);
flock(fd, LOCK_UN);
}
return NULL;
}
int main(void)
{
int fd = -1;
int ret = -1;
pthread_t tid;
fd = open("./file", O_RDWR|O_CREAT|O_TRUNC, 0664);
if(fd == -1) print_err("open ./file fail", __LINE__, errno);
ret = pthread_create(&tid, NULL, pth_fun, NULL);
if(ret == -1) print_err("pthread_create fail", __LINE__, ret);
while(1)
{
flock(fd, LOCK_EX);
write(fd, "hello ", 6);
write(fd, "world\n", 6);
flock(fd, LOCK_UN);
}
return 0;
}
运行结果:
用于多线程时与用于多进程一样,各线程必须使用各自open所返回的文件描述符才能实现加锁。
flock 和fcntl 都可以使用在多进程和多线程。
fcntl 用于多进程的时候,对于亲缘父进程和子进程来说子进程可以使用从父进程进程而来的文件描述符进行加锁。如果不是亲缘父子进程那么每个进程都需要独立的open得到文件文件描述符进行加锁。
使用fcntl对于多线程进行加锁的时候,不同的线程必须独立的open打开文件描述符,然后使用文件描述符进行加锁。
flock函数的使用比较fcntl简单,不管用在多进程还是多线程上,必须各自独立的调用open打开文件,返回文件描述符,然后才能加锁成功。