文件锁flock

flock()函数用于在单个主机上对文件进行加锁和解锁,适用于读写操作的同步。它提供共享锁(读锁)和互斥锁(写锁),但不适用于NFS文件。锁在进程结束时自动释放,不能锁定文件部分区域。在多进程写文件时,使用'w'模式fopen会清空文件,可能导致数据丢失。正确的做法是使用读/写锁配合flock避免冲突。异常情况包括非阻塞写锁尝试时,文件可能被清空。
摘要由CSDN通过智能技术生成

1 flock()函数说明

头文件

#include <sysfile.h>

函数原型

int flock(int fd, int operation)

函数说明

1、此函数是从BSD中衍生出来的,但目前在大多数UNIX系统上都能找到。
2、此函数只能处理单个主机上的文件,不能处理NFS上的文件。
3、此函数按照 operation所指定的方式对fd锁指向的文件进行加锁和解锁等相关操作。
4、此函数只能操作整个文件,不能操作此文件的部分区域。
5、此函数加的是建议性锁,当进程结束时,系统会自动释放该锁。
参数说明

参数 operation有下列四种情况:
LOCK_SH:建立共享锁定。多个进程可同时对同一个文件作共享锁定。
LOCK_EX:建立互斥锁定。一个文件同时只有一个互斥锁定
LOCK_UN:解除文件锁定状态。
LOCK_NB:无法建立锁定时,此操作不会被阻塞,将立即返回操作结果。
    1、此选项与 LOCK_SH或LOCK_EX做OR(|)组合。
       如: fblock(fd,LOCK_NB|LOCK_EX) 或 fblock(fd,LOCK_NB|LOCK_SH)
    2、单一文件无法同时建立共享锁定和互斥锁定,而当使用dup()或fork()时文件描述词不会继承此种锁定。
    3、返回值,返回0表示成功,若有错误则返回-1,错误代码存于errno。

互斥锁和共享锁的使用场景

    读写相关的问题是永远存在的,文件锁就是为了解决这个问题而做的,其实它就是个简单的信号量。读写相关性指由于同时读写文件造成文件数据的随机性冲突。为了明确知道在何时通过何种操作对更改或是读取了文件中的那些数据,有必要对操作进行序列化,原子化,同步化,使用户能确知在何时文件中有什么数据。文件锁就是其中一个工具。
    文件系统一般有两种锁,共享锁及排它锁,也可被称为读锁和写锁。
    文件系统锁的特点:一个文件打开的时候只能拥有一把锁,就是说在同时,不能给一个文件同时分配两把以上的锁。
    读写已被上锁的文件的用户可以持有这把锁,即持有这把锁的用户可以对该文件进行相应的操作,如读或写。用户可以申请持有某个文件锁,如果文件开始无锁,申请持有锁之前先由系统为该文件创建了一把锁,然后该申请者持有它。
    持有锁的规则:如果这个文件已拥有一个读(共享)锁,其它用户不能为该文件分配排它锁或只读锁,但可以持有这把锁,也就是说其它用户可以读文件,但只要该文件被锁住,就没有用户可以对其进行写入。如果该文件已有一把排它锁且已为某用户持有,则没有任何用户可以再持有这把锁,除非持有者解锁。
    有一个重要的概念要记住:对文件的操作本身与锁其实没有什么关系,无论文件是否被上锁,用户都可以随意对文件进行正常情况下的任何操作,但操作系统会检查锁,针对不同的情况给予不同的处理。比如说在无锁的情况下,任何人可以同时对某文件进行任意的读写,当然这样很有可能读写的内容会出现错误——注意只是内容出错,操作并不会出错。加锁后,某些操作在某些情况下会被拒绝。文件锁的作用并不是保护文件及数据本身,而是保证数据的同步性,因此文件锁只对持有锁的用户才是真正有效的,也只有所有用户都使用同一种完全相同的方式利用文件锁的限制对文件进行操作,文件锁才能对所有用户有效,否则,只要有一个例外,整个文件锁的功能就会被破坏。比如,所有人都遵循的开文件,加锁,操作读写,解锁,关闭文件的步骤的话,所有的人操作都不会出现问题,因为基于文件锁的分配及持有原则,文件中的数据的更新是作为原子操作存在的,是不可分的,因此也是同步的,安全的。但假如某个人不是采取此步骤,那么他在读写时就会出现问题,不是读不准就是写不进等等。
    基于以上原理,对读数据是否锁定这点就值得说说。一般来说,写数据的时候排它锁定是唯一的操作,它这时保证写到文件中的数据是正确的,文件被锁时,其它用户无法得到该锁,因此无权做任何操作。在读的时候,要视具体情况而定,大多数情况下,如果不需要特别精确或是敏感的数据,无需锁定,因为锁定要花时间和资源,一个人申请持有锁花不了时间,人一多就有问题了,最主要的是,如果该文件需要被更新的话,假如被上了只读锁,则写入无法进行,因为那些想写入的用户将得不到排它锁,如果同时申请持有只读锁的人过多的话,排它锁就有可能一直申请不到,这样表现就是文件可能很长时间内无法被写入,显得很慢。一般来说,写文件的机会相对较少,也更重要,因此主要做好排它锁定,只读锁在多数情况下并无必要。那么只读锁用在何处呢?只读锁其实只对用户本身有用,只读锁保证用户读到的数据是确实从文件中读到的真实数据,而不是被称为“dirty”的脏数据。其实,这个还是针对那些不用锁的其它用户对文件的误操作,假如文件上锁,其它用户不一定非要通过锁对文件进行读写,如果他是直接读写的话,对上了锁的文件操作不一定有效,持有读锁的用户可以肯定在他读数据的时候读出来的是从真实的文件中得到的,而不是同时已被覆盖掉的数据。
因此,在写入的时候上排它锁应该是天经地义的,可以保证这时数据的不会出错。如果你不申请共享锁,可能读出的数据有错误,但对文件本身没有任何影响,影响只是对用户的,申请共享锁后读出的数据肯定是当时读的时候文件中的真实数据,如果不是为了保证数据的精确性,共享锁可以不加,充其量就是重新读一次,如果你读它是为了写入,不如直接加排它锁,没有必要用共享锁。
    还有一点要强调的是:文件锁只对使用它的用户,而且是按规则使用它的用户才有效,否则,你用你的,我用我的,有的用,有的不用,还是会乱套的,错误还是会出现的,对同一个文件,只有大家用同一个规则用文件锁,才能保证每个用户在对该文件进行共享操作的时候不会出现读写错误。

异常场景

多进程写文件时 fopen 需要注意

fopen 有三类打开模式,读,写,追加。
在这里插入图片描述

其中 写入模式w 是会重置文件的,如果文件存在,就会将其重置为 0。

当多个进程需要写入同一个文件时,为了避免写入冲突,需要加文件锁,有一种做法是通过 flock 来实现,但 flock 操作的是文件句柄,所以先要打开文件句柄,打开句柄的时候,如果用w或者w+,如果 A 进程拿到了锁,正在写文件,此时 B 进程通过 fopen 打开了文件,就会清空 A 进程写入的内容,虽然 B 进程拿不到写锁只能退出,但此时文件内容已经被重置了。实测,用r+就OK。,只要时w的都会清空文件,包括w,w+,wb,wt+,实测!

r      以只读方式打开文件,该文件必须存在。
r+    以可读写方式打开文件,该文件必须存在。
rb+  读写打开一个二进制文件,只允许读写数据。
rt+   读写打开一个文本文件,允许读和写。
w      打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。
w+    打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。
a       以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留)
a+     以附加方式打开可读写的文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾后,即文件原先的内容会被保留。 (原来的EOF符不保留)
wb     只写打开或新建一个二进制文件;只允许写数据。
wb+   读写打开或建立一个二进制文件,允许读和写。
wt+    读写打开或着建立一个文本文件;允许读写。  t表示文本,text
at+     读写打开一个文本文件,允许读或在文本末追加数据。
ab+     读写打开一个二进制文件,允许读或在文件末追加数据。
fp = fopen(file, "w+");
if (fp == NULL)
{
    goto err_label;
}
if (0 == flock (fileno (fp), LOCK_EX | LOCK_NB))
{
    // write file here
}
else if (EWOULDBLOCK == errno)
{
    /* write lock failed, another process is writing, try wait and read */
}
else
{
    // error
}

举例说明:
现在有两个进程A和B,都会读取文件cfg的内容,修改后再写入到文件中。

fp = fopen(cfg, "w+");
if (fp == NULL)
{
    goto err_label;
}
if (flock (fileno (fp), LOCK_EX | LOCK_NB)<0)
{
    return;
}

异常情况:
A进程在读打开文件,并加上文件锁,然后进行读操作,B进程以写open文件,发送文件被锁了,如果是非阻塞的,则返回,这时候由于使用的是open(cfg,“w”),这个操作会把文件清空,这时文件内容就改变了,这时候A才把文件fget到内存中,获取的内容就是空的。

在这里插入图片描述

fclose

在定位以上问题时,考虑过是不是文件缓存没有存入磁盘导致的问题。

结论:c库缓冲-----fflush---------〉内核缓冲--------fsync-----〉磁盘(存储设备)
fclose()会调用fflush,仅刷新C库提供的用户空间缓冲区。为了确保将数据物理存储在磁盘上,内核缓冲区也必须进行刷新,例如,使用sync(2)或fsync(2)。

拓展内容:
涉及多层缓冲区/高速缓存。

CPU缓存。

数据逐字节组合在一起,并存储在CPU缓存中。如果CPU缓存已满,并且一段时间未访问数据,则包含我们数据的块可能会写入主内存。在大多数情况下,这些对应用程序程序员都是隐藏的。

进程内缓冲区。

在收集数据的过程中预留了一些内存,因此我们需要向OS发出尽可能少的请求,因为这相对昂贵。该过程将数据复制到这些缓冲区,这些缓冲区又可以由CPU缓存支持,因此不能保证将数据复制到主存储器。应用程序需要显式刷新这些缓冲区,例如使用fclose(3)或fsync(3)。在进程终止之前,exit(3)函数也会执行此操作,而_exit(2)函数不会执行此操作,这就是为什么手册页中有一个大警告,要求该函数仅在您知道自己是什么的情况下才能调用它在做。

内核缓冲区

然后,操作系统将保留自己的缓存,以最大程度地减少发送到磁盘的请求数量。该高速缓存尤其不属于任何进程,因此其中的数据可能属于已经完成的进程,并且由于所有访问都通过此处,因此如果下一个程序到达此处,则下一个程序将看到该数据。内核将在有时间或明确要求时将这些数据写入磁盘。

驱动器缓存

磁盘驱动器本身也保留了高速缓存以加快访问速度。这些文件的写入速度非常快,并且有一个命令可将剩余数据写入高速缓存中,并在完成时进行报告,操作系统在关机时会使用该命令来确保在断电之前不会遗漏任何数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值