“文件锁”

文件锁也被称为记录锁,文件锁如果深讲的话,内容不少 (比如文件锁最起码分为了建议锁和强制性锁) 这里不准备深纠,深纠没有任何意义

主要是为了对比学习各种的加锁机制,比如进程有进程信号量加锁机制,线程有线程互斥锁、线程信号量等 加锁机制,学习文件锁有助于我们对比理解,对于我们后续理解驱动课程中的内核锁,c++、java等库所提供的 资源保护的锁机制,都是很有意义的。 当然还有另一个目的,那就是练习fcntl函数的使用,因为文件锁也需要用到fcntl函数。 由于文件锁的知识点是刚才所讲的这样一种存在,所以对于文件锁内容的学习,理解>实际使用。 4.1 文件锁的作用 顾名思义,就是用来保护文件数据的。 当多个进程共享读写同一个文件时,为了不让进程们各自读写数据时相互干扰,我们可以使用进程信号量来 互斥实现,除了可以使用进程信号量以外,还可以使用我们这里的的“文件锁”来实现,而且功能更丰富, 使用起来相对还更容易些。 4.2 多进程读写文件 file_lock.png

多进程共享读写同一个文件时,如果数据很重要的话,为了防止数据相互修改,应该满足如下读写条件: (1) 写与写应该互斥 当某个进程正在写文件,而且在数据没有写完时,其它进程不能写,否者会相互打乱对方写的数据 (2) 读与写也应该是互斥的 分两种情况:

  1. 某个进程正在写数据,而且在数据没有写完时,其它进程不能读数据因为别人在没有写完之前,读到的数据是不完整的,所以读和写时互斥的
  2. 某个进程正在读数据,在数据没有读完之前,其它进程不能写数据因为可能会扰乱别人读到的数据

(3) 读与读共享 某个进程在读数时,就算数据没有读完,其它进程也可以共享读数据,并不需要互斥等别人读完后才能读。 因为读文件是不会修改文件的内容,所以不用担心数据相互干扰的问题

总结起来就是,多进程读写文件时,如果你想进行资源保护的话,完美的资源保护应该满足如下这样的。

  1. 写与写之间互斥
  2. 读与写之间互斥
  3. 读与读之间共享

如何实现以上读写要求? 如果使用信号量来实现保护的话,只能是一律互斥,包括读与读都是互斥的,不能够向上面描述的,能互斥又能共享,但是文件锁可以做到 4.3 文件锁 4.3.1 文件锁的读锁与写锁 对文件加锁时可以加两种锁,分别是“读文件锁”和“写文件锁”,我们这里简称为读锁和写锁。

读锁、写锁之间关系 (1) 读锁和读锁共享: 可以重复加读锁,别人加了读锁在没有解锁之前,我依然可以加读锁,这就是共享。

(2) 读锁与写锁互斥: 别人加了读锁没有解锁前,加写锁会失败,反过来也是如此。 加锁失败后两种处理方式,

  • 阻塞,直到别人解锁然后加锁成功为止
  • 出错返回,不阻塞 (3) 写锁与写锁互斥: 别人加了写锁在没有解锁前,不能加写锁,加写锁会失败。 加锁失败后两种处理方式,
  • 阻塞,直到别人解锁然后加锁成功为止
  • 出错返回,不阻塞 常用的是阻塞加锁,至于如何实现阻塞和非阻塞,后面详细讲

4.3.2 使用文件锁对文件进行保护 读文件时加读锁,写文件时就加写锁,然后就可以很容易的实现符合如下要求的资源保护。

  1. 写与写之间互斥
  2. 读与写之间互斥
  3. 读与读之间共享 4.4 文件锁的加锁方式 (1) 对整个文件内容加锁 对整个文件加锁是最常用的文件锁的加锁方式。 当你对整个文件加锁时,如果文件的长度因为写入新数据或者截短而发生了变化,加锁内容的长度会 自动变化,保证对内容变化着的整个文件加锁。 (2) 对文件某部分内容加锁 不过一般来说是,对多少内容加锁,就对多少内容解锁,如果你是对整个文件加锁,就将整个文件解锁。 但是实际上加锁和实际解锁的长度可以不相同,比如我对1000个字节的内容加了锁,但是可以只对其中的 100字节解锁,不过这种情况用的少,知道有这么回事即可 4.5 文件锁的实现 实现文件锁时,我们还是需要使用fcntl函数

4.5.1 再看看fcntl的函数原型

include <unistd.h>

include <fcntl.h>

int fcntl(int fd, int cmd, …/struct flockflockptr */ );

第三个参数是…,fcntl函数是一个变参函数,第三个参数用不到时就不写

(1) 功能 fcntl函数有多种功能,里主要介绍实现文件锁的功能,当cmd被设置的是与文件锁相关的宏时, fcntl就是用于实现文件锁

(2) 返回值: 成功返回0,失败则返回-1,并且errno被设置。

(3) 参数

  1. fd: 文件描述符,指向需要被加锁的文件。
  2. cmd: 实现文件锁时,cmd有三种设置,F_GETLK、F_SETLK和F_SETLKW含义如下: (a) F_GETLK 从内核获取文件锁的信息,将其保存到第三个参数,此时第三个参数为struct flock flockptr。 这里是要设置文件锁,而不是获取已有文件锁的信息,这里用不到这个宏。 (b) F_SETLK 设置第三个参数所代表的文件锁,而且设置的是非阻塞文件锁,也就是如果加锁失败不会阻塞。 也就是说加锁失败后如果不想阻塞的话,就是由F_SETLK宏来决定的 此时需要用到第三个参数,struct flockflockptr。

使用举例: · 第一步: 定义一个struct flock flockptr结构体变量 (这个结构体变量就是文件锁) · 第二步: 设置flockptr的成员,表示你想设置什么样的文件锁 · 第三步: 通过第三个参数,将设置好的flockptr的地址传递给fcntl,设置你要的文件锁

(c) F_SETLKW 与F_SETLK一样,只不过设置的是阻塞文件锁,也就说加锁不成功的话就阻塞,是由F_SETLKW 宏来决定的。

int fcntl(int fd, int cmd, …/struct flockflockptr / ); 3) 第三个参数 第三个参数设置为什么视情况而定,如果fcntl用于实现文件锁的话,第三个参数为 struct flockflockptr,flockptr代表的就是文件锁 对flockptr的成员设置为特定的值,就可以将文件锁设置为你想要的锁。

struct flock结构体如下: (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
  • 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

只有当我们获取一个已存在锁的信息时,才会使用这个成员,这个成员的值不是我们 设置的,是由文件锁自己设置的,我们只是获取以查看当前那个进程正加着锁。

对于我们目前设置文件锁来说,这个成员用不到

4.5.2 代码

file_lock.c ,file_lock.h

file_lock.h

复制代码

ifndef H_FILELOCK_H

define H_FILELOCK_H

include <unistd.h>

include <fcntl.h>

//非阻塞设置写锁

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);
}

}

endif

复制代码 file_lock.c

复制代码

include <unistd.h>

include <stdio.h>

include <stdlib.h>

include <sys/types.h>

include <sys/stat.h>

include <fcntl.h>

include <string.h>

include <errno.h>

include “file_lock.h”

void print_err(char *str,int line,int err_no){ printf("%d,%s:%s\n”,line,str,strerror(err_no) ); exit(-1); }

int main(int argc, char const *argv[]) { int fd =-1; int ret= 0;

fd =open("./file",O_RDWR|O_CREAT|O_TRUNC,0664); 
if(fd == -1) print_err("open fail",__LINE__,errno);

ret =fork();
if(ret > 0){
    while(1){
        SET_WRFLCK_W(fd,SEEK_SET,0,0);
        write(fd,"php",3);
        write(fd,"java\n",5);
        UNLCK(fd,SEEK_SET,0,0);
    }
}else if(ret == 0){
    while(1){
        SET_WRFLCK_W(fd,SEEK_SET,0,0);
        write(fd,"js",2);
        write(fd,"css\n",3);
        UNLCK(fd,SEEK_SET,0,0);
    }
}
return 0;

} 复制代码 4.5.3 文件锁的原理 理解了文件锁的原理后,就可以理解为什么文件锁可以实现互斥与共享了

file_lock_table_all.png

链表上节点代表是一把锁 (读锁和写锁) ,节点存在时表示没有解锁,如果解锁了锁节点就不存在了。 锁节点记录了锁的基本信息。 · 锁类型 · 加锁的起始位置 (l_whence、l_start) · 加锁的长度 (l_len) · 当前正在加着锁的那个进程的PID

加锁时,进程会检查共享的文件锁链表。

(1) 进程想加读锁

  1. 如果链表上只有读锁节点 所有目前其它进程对该文件只加了读锁,由于读锁时共享的,所以不管链表上有几个读锁节点,当前进程都能成功加读锁

链表上可不可以存在n多个读锁节点? 可以,因为读锁是共享的,不管别的进程有没有解读锁,所有的进程都可以加读锁,每加一个读锁,链表上就多一个读锁节点,只有当解锁时节点才被删除。

  1. 如果链表上有一个写锁节点 表明目前有进程对文件加了写锁,锁节点还存在,表示人家目前还没有解锁,读锁和写锁是互斥的,所以当前不能加读锁,别人解锁后才能加读锁,加锁后链表上就插入一个读锁节点。

提问: 链表上能不能同时存在多个写锁节点

答: 不可能,因为写锁是互斥的,目前只能有一个进程在给文件加写锁,在解锁之前,别的进程不能加写锁。 所以链表上不可能有>一个的写锁节点,否者就不能实现互斥了

链表上会不会同时存在读锁节点和写锁节点? 读锁节点和写锁节点也是互斥的,链表上有读锁节点就不可能存在写锁节点,反过来有写锁节点就不可能有读锁节点。

(2) 你想加写锁

  1. 如果链表上有读锁节点,别人还没有解锁,读锁与写锁互斥,不能加写锁
  2. 如果链表上有写锁节点,别人还没有解锁,写锁与写锁互斥,多以当前进程不能加写锁

(3) 对比进程信号量

  1. 进程信号量: 进程间共享信号量集合,通过检查集合中信号量的值,从而知道自己能不能操作
  2. 文件锁: 进程共享文件锁链表,通过检查链表上的锁节点,从而知道自己能不能操作

4.5.4 文件锁其它值得注意的地方 (a) 在同一进程中,如果多个文件描述符指向同一文件,只要关闭其中任何一个文件描述符,那么该进程加在文件上的文件锁将会被删除,也就是该进程在“文件锁链表”上的“读锁写锁”节点会被删除。

进程终止时会关闭所有打开的文件描述符,所以进程结束时会自动删除所有加的文件锁。

(b) 父进程所加的文件锁,子进程不会继承,我们在讲进程控制时就说过加锁是进程各自私人事情,不能继承。

4.5.5 提一个问题: 多线程间能不能使用fcntl实现的文件锁呢? 可以,但是线程不能使用同一个open返回的文件描述符,线程必须使用自己open所得到的文件描述符才有效。

代码: pth_write_hello_world.c

复制代码

include <stdio.h>

include <stdlib.h>

include <unistd.h>

include <pthread.h>

include <sys/types.h>

include <sys/stat.h>

include <fcntl.h>

include <string.h>

include <errno.h>

include “file_lock.h”

include <sys/file.h>

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(voidpth_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;

} 复制代码 4.6 使用flock函数来实现文件锁

flock与fcntl所实现的文件锁一样,既能够用在多进程上,也能用在多线程上,而且使用起来比fcntl的实现方式更方便,只是使用这个函数时,需要注意一些小细节。 4.6.2 flock函数 (1) 函数原型

include<sys/file.h>

int flock(int fd, int operation);

  1. 功能 按照operation的要求,对fd所指向的文件加对应的文件锁 加锁不成功时会阻塞

  2. 返回值: 成功返回0,失败返回-1,errno被设置

  3. 参数 (a) fd: 指向需要被加锁的文件 (b) operation · LOCK_SH: 加共享锁 · LOCK_EX: 加互斥锁 · LOCK_UN: 解锁

  4. 用于多进程 代码: mul_prcs_flock.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/file.h>

// 多进程中使用flock文件锁

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, "php ", 3);
            write(fd, "java\n", 5);
            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, "js ", 2);
            write(fd, "css\n", 3);
            flock(fd, LOCK_UN);
        }
    }    
    
    return 0;
}   

flock用于多进程时,各进程必须独立open打开文件,对于非亲缘进程来说,不用说打开文件时肯定是各自独立调用open打开的。

需要你注意的是亲缘进程 (父子进程) ,子进程不能使用从父进程继承而来的文件描述符,父子进程flock时必须使用独自open所返回的文件描述符。

这一点与fcntl实现的文件锁不一样,父子进程可以使用各自open返回的文件描述符加锁,但是同时子进程也可以使用从父进程继承而来的文件描述符加锁 · 共享锁与互斥锁之间互斥 · 互斥锁与互斥锁之间互斥 · 共享锁与共享锁之间共享 2) 用于多线程

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

复制代码

include <stdio.h>

include <stdlib.h>

include <unistd.h>

include <pthread.h>

include <sys/types.h>

include <sys/stat.h>

include <fcntl.h>

include <string.h>

include <errno.h>

include <sys/file.h>

// 多线程中使用flock实现的文件锁 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(voidpth_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, "php",3);    
    write(fd, "java\n", 5);
    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, "js ", 2);    
    write(fd, "css\n", 4);
    flock(fd, LOCK_UN);
}

    
return 0;

}