Fork me on GitHub

高级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
View Code

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     
View Code

 

文件锁原理

 多个进程读——图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
View Code

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 }
View Code

文件锁的实现——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     
View Code

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 }
View Code

用于多线程时与用于多进程一样,各线程必须使用各自open所返回的文件描述符才能加锁。

posted @ 2018-08-09 21:35  克拉默与矩阵  阅读(802)  评论(0编辑  收藏  举报