文件锁原理,不同方式问文件锁

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

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

  26.文件锁_it


多进程共享读写同一个文件时,如果数据很重要的话,为了防止数据相互修改,应该满足如下读写条件:
(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 flock *flockptr */ );

  第三个参数是...,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 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的成员设置为特定的值,就可以将文件锁设置为你想要的锁。

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

26.文件锁_it_02

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

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

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

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

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


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

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

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


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

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:解锁


1)用于多进程
代码:mul_prcs_flock.c

#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(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, "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;
}