高级IO——文件锁
文件锁也被称为记录所,文件锁如果深讲的话,内容不少(比如文件锁最起码分为了建议锁和强制性锁,暂时挖坑,后面填)。
文件锁作用
顾名思义,就是用来保护文件数据的。当多个进程共享读写同一个文件时,为了不让进程们各自读写数据时相互干扰,我们可以使用进程信号量来互斥实现,除了可以使用进程信号量以外,还可以使用我们本小节要讲的“文件锁”来实现,而且功能更丰富,使用起来相对还更容易些。
多进程读写文件
多进程共享读写同一个文件时,如果数据很重要的话,为了防止数据相互修改,应该满足如下读写条件:
①写与写应该互斥
当某个进程正在写文件,而且在数据没有写完时,其它进程不能写,否者会相互打乱对方写的数据。
②读与写也应该是互斥的
分两种情况:
1)某个进程正在写数据,而且在数据没有写完时,其它进程不能读数据。因为别人在没有写完之前,读到的数据是不完整的,所以读和写时互斥的。
2)某个进程正在读数据,在数据没有读完之前,其它进程不能写数据。因为可能会扰乱别人读到的数据。
③读与读共享
某个进程在读数时,就算数据没有读完,其它进程也可以共享读数据,并不需要互斥等别人读完后才能读。因为读文件是不会修改文件的内容,所以不用担心数据相互干扰的问题。
总结起来就是,多进程读写文件时,如果你想进行资源保护的话,完美的资源保护应该满足如下这样的。
1)写与写之间互斥
2)读与写之间互斥
3)读与读之间共享
如何实现以上读写要求?
如果使用信号量来实现保护的话,只能是一律互斥,包括读与读都是互斥的,不能够向上面描述的,既能互斥又能共享,但是文件锁可以做到。
文件锁的读锁与写锁
对文件加锁时可以加两种锁,分别是“读文件锁”和“写文件锁”,我们这里简称为读锁和写锁。
读锁、写锁之间关系
①读锁和读锁共享:可以重复加读锁,别人加了读锁在没有解锁之前,我依然可以加读锁,这就是共享。
②读锁与写锁互斥:别人加了读锁没有解锁前,加写锁会失败,反过来也是如此。
加锁失败后两种处理方式,
(1)阻塞,直到别人解锁然后加锁成功为止
(2)出错返回,不阻塞
③写锁与写锁互斥:别人加了写锁在没有解锁前,不能加写锁,加写锁会失败。
加锁失败后两种处理方式,
(1)阻塞,直到别人解锁然后加锁成功为止
(2)出错返回,不阻塞
使用文件锁对文件进行保护
读文件时加读锁,写文件时就加写锁,然后就可以很容易的实现符合如下要求的资源保护。
1)写与写之间互斥
2)读与写之间互斥
3)读与读之间共享
文件锁的加锁方式
(1)对整个文件内容加锁
对整个文件加锁是最常用的文件锁的加锁方式。当你对整个文件加锁时,如果文件的长度因为写入新数据或者截短而发生了变化,加锁内容的长度会自动变化,保证对内容变化着的整个文件加锁。
(2)对文件某部分内容加锁
不过一般来说是,对多少内容加锁,就对多少内容解锁,如果你是对整个文件加锁,就将整个文件解锁。但是实际上加锁和实际解锁的长度可以不相同,比如我对1000个字节的内容加了锁,但是可以只对其中的100字节解锁,不过这种情况用的少,知道有这么回事即可。
文件锁的实现——fcntl
实现文件锁时,我们还是需要使用fcntl函数。
再看看fcntl的函数
原型
#include <unistd.h> #include <fcntl.h> int fcntl(int fd, int cmd, .../*struct flock *flockptr */ );
功能
fcntl函数有多种功能,我们这里主要介绍实现文件锁的功能,当cmd被设置的是与文件锁相关的宏时,fcntl就是用于实现文件锁。
返回值
成功返回0,失败则返回-1,并且errno被设置。
参数
fd:文件描述符,指向需要被加锁的文件。
cmd:实现文件锁时,cmd有三种设置,F_GETLK、F_SETLK和F_SETLKW含义如下:
F_GETLK:
从内核获取文件锁的信息,将其保存到第三个参数,此时第三个参数为struct flock *flockptr。我们这里是要设置文件锁,而不是获取已有文件锁的信息,我们这里用不到这个宏。
F_SETLK:
设置第三个参数所代表的文件锁,而且设置的是非阻塞文件锁,也就是如果加锁失败不会阻塞。也就是说加锁失败后如果不想阻塞的话,就是由F_SETLK宏来决定的。
此时需要用到第三个参数,struct flock *flockptr。
使用举例:
第一步:定义一个struct flock flockptr结构体变量(这个结构体变量就是文件锁)。
第二步:设置flockptr的成员,表示你想设置什么样的文件锁。
第三步:通过第三个参数,将设置好的flockptr的地址传递给fcntl,设置你要的文件锁
F_SETLKW:
与F_SETLK一样,只不过设置的是阻塞文件锁,也就说加锁不成功的话就阻塞,是由F_SETLKW宏来决定的。
第三个参数:
第三个参数设置为什么视情况而定,如果fcntl用于实现文件锁的话,第三个参数为struct flock *flockptr,flockptr代表的就是文件锁。对flockptr的成员设置为特定的值,就可以将文件锁设置为你想要的锁。
struct 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) }
成员说明
l_type:锁类型
F_RDLCK:读锁(或称共享锁)
F_WRLCK:写锁
F_UNLCK:解锁
l_whence:加锁位置粗定位,设置同lseek的whence
SEEK_SET:文件开始处
SEEK_CUR:文件当前位置处
SEEK_END:文件末尾位置处
l_whence这个与lseek函数的whence是一个含义,off_t lseek(int fd, off_t offset, int whence);
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相连的问题。
file_lock.h
1 #ifndef H_FILELOCK_H 2 #define H_FILELOCK_H 3 4 #include <unistd.h> 5 #include <fcntl.h> 6 7 //非阻塞设置写锁 8 #define SET_WRFLCK(fd, l_whence, l_offset, l_len)\ 9 set_filelock(fd, F_SETLK, F_WRLCK, l_whence, l_offset, l_len) 10 //阻塞设置写锁 11 #define SET_WRFLCK_W(fd, l_whence, l_offset, l_len)\ 12 set_filelock(fd, F_SETLKW, F_WRLCK, l_whence, l_offset, l_len) 13 14 //非阻塞设置读锁 15 #define SET_RDFLCK(fd, l_whence, l_offset, l_len)\ 16 set_filelock(fd, F_SETLK, F_RDLCK, l_whence, l_offset, l_len) 17 //阻塞设置读锁 18 #define SET_RDFLCK_W(fd, l_whence, l_offset, l_len)\ 19 set_filelock(fd, F_SETLKW, F_RDLCK, l_whence, l_offset, l_len) 20 21 //解锁 22 #define UNLCK(fd, l_whence, l_offset, l_len)\ 23 set_filelock(fd, F_SETLK, F_UNLCK, l_whence, l_offset, l_len) 24 25 /* 调用这个函数,即可实现阻塞加读锁/阻塞加写锁, 非阻塞加读锁/非阻塞加写锁/解锁 */ 26 static void set_filelock(int fd, int ifwait, int l_type, int l_whence, int l_offset, int l_len) 27 { 28 int ret = 0; 29 struct flock flck; 30 31 flck.l_type = l_type; 32 flck.l_whence = l_whence; 33 flck.l_start = l_offset; 34 flck.l_len = l_len; 35 36 ret = fcntl(fd, ifwait, &flck); 37 if(ret == -1) 38 { 39 perror("fcntl fail"); 40 exit(-1); 41 } 42 } 43 44 45 46 #endif
main.c
1 #include <stdio.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <unistd.h> 5 #include <fcntl.h> 6 #include <string.h> 7 #include <sys/types.h> 8 #include <sys/stat.h> 9 #include <fcntl.h> 10 #include <errno.h> 11 #include "file_lock.h" 12 13 14 void print_err(char *str, int line, int err_no) 15 { 16 printf("%d, %s: %s\n", line, str, strerror(err_no)); 17 exit(-1); 18 } 19 20 int main(void) 21 { 22 int fd = 0; 23 int ret = 0; 24 25 fd = open("./file", O_RDWR|O_CREAT|O_TRUNC, 0664); 26 if(fd == -1) print_err("./file", __LINE__, errno); 27 28 ret = fork(); 29 if(ret > 0) 30 { 31 32 while(1) 33 { 34 SET_WRFLCK_W(fd, SEEK_SET, 0, 0); 35 write(fd, "hello ", 6); 36 write(fd, "world\n", 6); 37 UNLCK(fd, SEEK_SET, 0, 0); 38 } 39 } 40 else if(ret == 0) 41 { 42 while(1) 43 { 44 SET_WRFLCK_W(fd, SEEK_SET, 0, 0); 45 write(fd, "hello ", 6); 46 write(fd, "world\n", 6); 47 UNLCK(fd, SEEK_SET, 0, 0); 48 } 49 } 50 51 return 0; 52 } 53 54
文件锁原理
多个进程读——图A
仔细看上面图的话,很多人可能会感觉“画错了吧,怎么锁链表上全是A进程呢?”
假如前提条件是:父进程创建fd,子进程继承这个fd。加锁只涉及加读锁。加锁只对整个文件加锁
那么上面图,如果锁是共享锁,没画错。如果是互斥锁,画错了。
假如前提条件是:父进程创建fd,子进程继承这个fd。加锁只涉及加读锁。锁是互斥锁。但是加锁都是对文件部分片段加锁,各个片段不重复。
上面那幅图也是对的
还需要注意一点,上面那个锁链表只是一个示意图,内核实现不一定是这样的。一个fd也就是一个文件,后面对这个文件的加锁,无论是互斥锁,共享锁... 锁定的范围无论是文件局部,还是全部。都可以串成一个锁链表。也可以向进程调度队列那样,根据不同加锁原因设置不同锁链表。这些我都是内核实现问题,都是有可能的。上图只是个逻辑画法。
只允许一个进程写(写写互斥,写读互斥)——图B
链表上节点代表是一把锁(读锁和写锁),节点存在时表示没有解锁,如果解锁了锁节点就不存在了。锁节点记录了锁的基本信息。
①锁类型
②加锁的起始位置(l_whence、l_start)
③加锁的长度(l_len)
④当前正在加着锁的那个进程的PID
加锁时,进程会检查共享的文件锁链表。
进程想加读锁
1)如果链表上只有读锁节点
所有目前其它进程对该文件只加了读锁,由于读锁时共享的,所以不管链表上有几个读锁节点,当前进程都能成功加读锁。见上面图A
提问:链表上可不可以存在n多个读锁节点?
答:可以,因为读锁是共享的,不管别的进程有没有解读锁,所有的进程都可以加读锁,每加一个读锁,链表上就多一个读锁节点,只有当解锁时节点才被删除。
2)如果链表上有一个写锁节点
表明目前有进程对文件加了写锁,锁节点还存在,表示人家目前还没有解锁,读锁和写锁是互斥的,所以当前不能加读锁,别人解锁后才能加读锁,加锁后链表上就插入一个读锁节点。
提问:链表上能不能同时存在多个写锁节点?
答:不可能,因为写锁是互斥的,目前只能有一个进程在给文件加写锁,在解锁之前,别的进程不能加写锁。所以链表上不可能有大于一个的写锁节点,否者就不能实现互斥了。
提问:链表上会不会同时存在读锁节点和写锁节点?
答:读锁节点和写锁节点也是互斥的,链表上有读锁节点就不可能存在写锁节点,反过来有写锁节点就不可能有读锁节点。
进程想加写锁
1)如果链表上有读锁节点,别人还没有解锁,读锁与写锁互斥,不能加写锁。
2)如果链表上有写锁节点,别人还没有解锁,写锁与写锁互斥,多以当前进程不能加写锁
对比进程信号量
1)进程信号量:进程间共享信号量集合,通过检查集合中信号量的值,从而知道自己能不能操作
2)文件锁:进程共享文件锁链表,通过检查链表上的锁节点,从而知道自己能不能操作
文件锁其它值得注意的地方
(a)在同一进程中,如果多个文件描述符指向同一文件,只要关闭其中任何一个文件描述符,那么该进程加在文件上的文件锁将会被删除,也就是该进程在“文件锁链表”上的“读锁写锁”节点会被删除。进程终止时会关闭所有打开的文件描述符,所以进程结束时会自动删除所有加的文件锁。
(b)父进程所加的文件锁,子进程不会继承,我们在讲进程控制时就说过。加锁是进程各自私人事情,不能继承,就好比你老爸有抽烟的嗜好,难道这也需要继承吗,肯定不是的。
(c)多线程间能不能使用fcntl实现的文件锁呢?
可以,但是线程不能使用同一个open返回的文件描述符,线程必须使用自己open所得到的文件描述符才有效。
代码演示
file_lock.h
1 #ifndef H_FILELOCK_H 2 #define H_FILELOCK_H 3 4 #include <unistd.h> 5 #include <fcntl.h> 6 7 //非阻塞设置写锁 8 #define SET_WRFLCK(fd, l_whence, l_offset, l_len)\ 9 set_filelock(fd, F_SETLK, F_WRLCK, l_whence, l_offset, l_len) 10 //阻塞设置写锁 11 #define SET_WRFLCK_W(fd, l_whence, l_offset, l_len)\ 12 set_filelock(fd, F_SETLKW, F_WRLCK, l_whence, l_offset, l_len) 13 14 //非阻塞设置读锁 15 #define SET_RDFLCK(fd, l_whence, l_offset, l_len)\ 16 set_filelock(fd, F_SETLK, F_RDLCK, l_whence, l_offset, l_len) 17 //阻塞设置读锁 18 #define SET_RDFLCK_W(fd, l_whence, l_offset, l_len)\ 19 set_filelock(fd, F_SETLKW, F_RDLCK, l_whence, l_offset, l_len) 20 21 //解锁 22 #define UNLCK(fd, l_whence, l_offset, l_len)\ 23 set_filelock(fd, F_SETLK, F_UNLCK, l_whence, l_offset, l_len) 24 25 /* 调用这个函数,即可实现阻塞加读锁/阻塞加写锁, 非阻塞加读锁/非阻塞加写锁/解锁 */ 26 static void set_filelock(int fd, int ifwait, int l_type, int l_whence, int l_offset, int l_len) 27 { 28 int ret = 0; 29 struct flock flck; 30 31 flck.l_type = l_type; 32 flck.l_whence = l_whence; 33 flck.l_start = l_offset; 34 flck.l_len = l_len; 35 36 ret = fcntl(fd, ifwait, &flck); 37 if(ret == -1) 38 { 39 perror("fcntl fail"); 40 exit(-1); 41 } 42 } 43 44 45 46 #endif
main.c
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <pthread.h> 5 #include <sys/types.h> 6 #include <sys/stat.h> 7 #include <fcntl.h> 8 #include <string.h> 9 #include <errno.h> 10 #include "file_lock.h" 11 #include <sys/file.h> 12 13 14 15 void print_err(char *str, int line, int err_no) 16 { 17 printf("%d, %s: %s\n", line, str, strerror(err_no)); 18 exit(-1); 19 } 20 21 void *pth_fun(void *pth_arg) 22 { 23 int fd = 0; 24 25 fd = open("./file", O_RDWR|O_CREAT|O_TRUNC, 0664); 26 if(fd == -1) print_err("open ./file fail", __LINE__, errno); 27 while(1) 28 { 29 SET_WRFLCK_W(fd, SEEK_SET, 0, 0); 30 write(fd, "hello ", 6); 31 write(fd, "world\n", 6); 32 UNLCK(fd, SEEK_SET, 0, 0); 33 } 34 35 return NULL; 36 } 37 38 int main(void) 39 { 40 int fd = -1; 41 int ret = -1; 42 pthread_t tid; 43 44 fd = open("./file", O_RDWR|O_CREAT|O_TRUNC, 0664); 45 if(fd == -1) print_err("open ./file fail", __LINE__, errno); 46 47 48 ret = pthread_create(&tid, NULL, pth_fun, NULL); 49 if(ret == -1) print_err("pthread_create fail", __LINE__, ret); 50 51 52 while(1) 53 { 54 SET_WRFLCK_W(fd, SEEK_SET, 0, 0); 55 write(fd, "hello ", 6); 56 write(fd, "world\n", 6); 57 UNLCK(fd, SEEK_SET, 0, 0); 58 } 59 60 61 return 0; 62 }
文件锁的实现——flock
lock与fcntl所实现的文件锁一样,既能够用在多进程上,也能用在多线程上,而且使用起来比fcntl的实现方式更方便,只是使用这个函数时,需要注意一些小细节。
flock函数
原型
#include<sys/file.h> int flock(int fd, int operation);
功能
按照operation的要求,对fd所指向的文件加对应的文件锁。加锁不成功时会阻塞。
参数
fd:指向需要被加锁的文件
operation:
LOCK_SH:加共享锁
LOCK_EX:加互斥锁
LOCK_UN:解锁
返回值
成功返回0,失败返回-1,errno被设置
代码演示
用于多进程
1 #include <stdio.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <unistd.h> 5 #include <fcntl.h> 6 #include <string.h> 7 #include <sys/types.h> 8 #include <sys/stat.h> 9 #include <fcntl.h> 10 #include <errno.h> 11 #include <sys/file.h> 12 13 14 void print_err(char *str, int line, int err_no) 15 { 16 printf("%d, %s: %s\n", line, str, strerror(err_no)); 17 exit(-1); 18 } 19 20 int main(void) 21 { 22 int fd = 0; 23 int ret = 0; 24 25 26 ret = fork(); 27 if(ret > 0) 28 { 29 //使用flock必须在每个进程独立打开文件才行,为了避免笔尖相互覆盖还要加O_APPEND 30 fd = open("./file", O_RDWR|O_CREAT|O_TRUNC|O_APPEND, 0664); 31 if(fd == -1) print_err("./file", __LINE__, errno); 32 33 while(1) 34 { 35 flock(fd, LOCK_SH); 36 write(fd, "hello ", 6); 37 write(fd, "world\n", 6); 38 flock(fd, LOCK_UN); 39 } 40 } 41 else if(ret == 0) 42 { 43 fd = open("./file", O_RDWR|O_CREAT|O_TRUNC|O_APPEND, 0664); 44 if(fd == -1) print_err("./file", __LINE__, errno); 45 46 while(1) 47 { 48 flock(fd, LOCK_SH); 49 write(fd, "hello ", 6); 50 write(fd, "world\n", 6); 51 flock(fd, LOCK_UN); 52 } 53 } 54 55 return 0; 56 } 57 58
flock用于多进程时,各进程必须独立open打开文件,对于非亲缘进程来说,不用说打开文件时肯定是各自独立调用open打开的。需要你注意的是亲缘进程(父子进程),子进程不能使用从父进程继承而来的文件描述符,父子进程flock时必须使用独自open所返回的文件描述符。
这一点与fcntl实现的文件锁不一样,父子进程可以使用各自open返回的文件描述符加锁,但是同时子进程也可以使用从父进程继承而来的文件描述符加锁。
用于多线程
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <pthread.h> 5 #include <sys/types.h> 6 #include <sys/stat.h> 7 #include <fcntl.h> 8 #include <string.h> 9 #include <errno.h> 10 #include <sys/file.h> 11 12 13 14 void print_err(char *str, int line, int err_no) 15 { 16 printf("%d, %s: %s\n", line, str, strerror(err_no)); 17 exit(-1); 18 } 19 20 void *pth_fun(void *pth_arg) 21 { 22 int fd = 0; 23 24 fd = open("./file", O_RDWR|O_CREAT|O_TRUNC, 0664); 25 if(fd == -1) print_err("open ./file fail", __LINE__, errno); 26 while(1) 27 { 28 flock(fd, LOCK_EX); 29 write(fd, "hello ", 6); 30 write(fd, "world\n", 6); 31 flock(fd, LOCK_UN); 32 } 33 34 return NULL; 35 } 36 37 int main(void) 38 { 39 int fd = -1; 40 int ret = -1; 41 pthread_t tid; 42 43 fd = open("./file", O_RDWR|O_CREAT|O_TRUNC, 0664); 44 if(fd == -1) print_err("open ./file fail", __LINE__, errno); 45 46 47 ret = pthread_create(&tid, NULL, pth_fun, NULL); 48 if(ret == -1) print_err("pthread_create fail", __LINE__, ret); 49 50 51 while(1) 52 { 53 flock(fd, LOCK_EX); 54 write(fd, "hello ", 6); 55 write(fd, "world\n", 6); 56 flock(fd, LOCK_UN); 57 } 58 59 60 return 0; 61 }
用于多线程时与用于多进程一样,各线程必须使用各自open所返回的文件描述符才能加锁。