0%

Linux文件锁

Linux文件锁

  • 对于有些应用程序,进程有时需要确保只有它自己能够对某一文件进行 I/O 操作,在这段时间内不允许 其它进程对该文件进行 I/O 操作。为了向进程提供这种功能,Linux 系统提供了文件锁机制。
  • 譬如进程对文件进行 I/O 操作时,首先对文件进行上锁,将其锁住,然后再进行读写操作;只要进程没有对 文件进行解锁,那么其它的进程将无法对其进行操作;这样就可以保证,文件被锁住期间,只有它(该进程) 可以对其进行读写操作。

文件锁的分类

建议性锁

  • 建议性锁本质上是一种协议,程序访问文件之前,先对文件上锁,上锁成功之后再访问文件,这是建议 性锁的一种用法;但是如果你的程序不管三七二十一,在没有对文件上锁的情况下直接访问文件,也是可以 访问的,并非无法访问文件;如果是这样,那么建议性锁就没有起到任何作用,如果要使得建议性锁起作用, 那么大家就要遵守协议,访问文件之前先对文件上锁。这就好比交通信号灯,规定红灯不能通行,绿灯才可 以通行,但如果你非要在红灯的时候通行,谁也拦不住你,那么后果将会导致发生交通事故;所以必须要大 家共同遵守交通规则,交通信号灯才能起到作用。

强制性锁

  • 强制性锁比较好理解,它是一种强制性的要求,如果进程对文件上了强制性锁,其它的进程在没有获取 到文件锁的情况下是无法对文件进行访问的。其本质原因在于,强制性锁会让内核检查每一个 I/O 操作(譬 如 read()、write()),验证调用进程是否是该文件锁的拥有者,如果不是将无法访问文件。当一个文件被上 锁进行写入操作的时候,内核将阻止其它进程对其进行读写操作。采取强制性锁对性能的影响很大,每次进 行读写操作都必须检查文件锁。

flock()函数加锁

  • 先来学习系统调用 flock(),使用该函数可以对文件加锁或者解锁,但是 flock()函数只能产生建议性锁
#include <sys/file.h>
int flock(int fd, int operation);
  • fd:参数 fd 为文件描述符,指定需要加锁的文件。

  • operation:参数 operation 指定了操作方式,可以设置为以下值的其中一个:

    • LOCK_SH:在 fd 引用的文件上放置一把共享锁。所谓共享,指的便是多个进程可以拥有对同一 个文件的共享锁,该共享锁可被多个进程同时拥有。
    • LOCK_EX:在 fd 引用的文件上放置一把排它锁(或叫互斥锁)。所谓互斥,指的便是互斥锁只 能同时被一个进程所拥有。
    • LOCK_UN:解除文件锁定状态,解锁、释放锁。
    • LOCK_NB:表示以非阻塞方式获取锁。默认情况下,调用 flock()无法获取到文件锁时会阻塞、直 到其它进程释放锁为止,如果不想让程序被阻塞,可以指定 LOCK_NB 标志,如果无法获取到锁 应立刻返回(错误返回,并将 errno 设置为 EWOULDBLOCK),通常与 LOCK_SH 或 LOCK_EX 一起使用,通过位或运算符组合在一起。
  • 注意,虽然一个程序对文件加锁之后,另一个程序企图加锁文件会失败,但是另一个程序同样可以打开并且编辑这个文件。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <signal.h>
static int fd = -1; //文件描述符
/* 信号处理函数 */
static void sigint_handler(int sig)
{
if (SIGINT != sig)
return;
/* 解锁 */
flock(fd, LOCK_UN);
close(fd);
printf("进程 1: 文件已解锁!\n");
}
int main(int argc, char *argv[])
{
if (2 != argc)
{
fprintf(stderr, "usage: %s <file>\n", argv[0]);
exit(-1);
}
/* 打开文件 */
fd = open(argv[1], O_WRONLY);
if (-1 == fd)
{
perror("open error");
exit(-1);
}
/* 以非阻塞方式对文件加锁(排它锁) */
if (-1 == flock(fd, LOCK_EX | LOCK_NB))
{
perror("进程 1: 文件加锁失败");
exit(-1);
}
printf("进程 1: 文件加锁成功!\n");
/* 为 SIGINT 信号注册处理函数 */
signal(SIGINT, sigint_handler);
for (;;)
sleep(1);
}
  • 另一个试图读写文件的程序
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <string.h>
int main(int argc, char *argv[])
{
char buf[100] = "Hello World!";
int fd;
int len;
if (2 != argc)
{
fprintf(stderr, "usage: %s <file>\n", argv[0]);
exit(-1);
}
/* 打开文件 */
fd = open(argv[1], O_RDWR);
if (-1 == fd)
{
perror("open error");
exit(-1);
}
/* 以非阻塞方式对文件加锁(排它锁) */
if (-1 == flock(fd, LOCK_EX | LOCK_NB))
perror("进程 2: 文件加锁失败");
else
printf("进程 2: 文件加锁成功!\n");
/* 写文件 */
len = strlen(buf);
if (0 > write(fd, buf, len))
{
perror("write error");
exit(-1);
}
printf("进程 2: 写入到文件的字符串<%s>\n", buf);
/* 将文件读写位置移动到文件头 */
if (0 > lseek(fd, 0x0, SEEK_SET))
{
perror("lseek error");
exit(-1);
}
/* 读文件 */
memset(buf, 0x0, sizeof(buf)); //清理 buf
if (0 > read(fd, buf, len))
{
perror("read error");
exit(-1);
}
printf("进程 2: 从文件读取的字符串<%s>\n", buf);
/* 解锁、退出 */
flock(fd, LOCK_UN);
close(fd);
exit(0);
}
  • 使用 kill 命令向 testApp1 进程发送编号为 2 的信号,也就是 SIGIO 信号,testApp1 接收到信号之后, 对 infile 文件进行解锁、然后退出;接着再次执行 testApp2 程序,从打印信息可知,这次能够成功对 infile 文件加锁了,读写也是没有问题的。
  • 关于 flock()的几条规则
    • 同一进程对文件多次加锁不会导致死锁。当进程调用 flock()对文件加锁成功,再次调用 flock()对 文件(同一文件描述符)加锁,这样不会导致死锁,新加的锁会替换旧的锁。譬如调用 flock()对文 件加共享锁,再次调用 flock()对文件加排它锁,最终文件锁会由共享锁替换为排它锁。
    • 文件关闭的时候,会自动解锁。进程调用 flock()对文件加锁,如果在未解锁之前将文件关闭,则会 导致文件锁自动解锁,也就是说,文件锁会在相应的文件描述符被关闭之后自动释放。同理,当一 个进程终止时,它所建立的锁将全部释放。
    • 一个进程不可以对另一个进程持有的文件锁进行解锁。
    • 由 fork()创建的子进程不会继承父进程所创建的锁。这意味着,若一个进程对文件加锁成功,然后 该进程调用 fork()创建了子进程,那么对父进程创建的锁而言,子进程被视为另一个进程,虽然子 进程从父进程继承了其文件描述符,但不能继承文件锁。这个约束是有道理的,因为锁的作用就是 阻止多个进程同时写同一个文件,如果子进程通过 fork()继承了父进程的锁,则 父进程和子进程就 可以同时写同一个文件了
    • 除此之外,当一个文件描述符被复制时(譬如使用 dup()、dup2()或 fcntl()F_DUPFD 操作),这些通过 复制得到的文件描述符和源文件描述符都会引用同一个文件锁,使用这些文件描述符中的任何一个进行解 锁都可以

fcntl()函数加锁

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ );
  • 与锁相关的 cmd 为 F_SETLK、F_SETLKW、F_GETLK,第三个参数 flockptr 是一个 struct flock 结构体 指针。使用 fcntl()实现文件锁功能与 flock()有两个比较大的区别:

    • flock()仅支持对整个文件进行加锁/解锁;而 fcntl()可以对文件的某个区域(某部分内容)进行加锁 /解锁,可以精确到某一个字节数据
    • flock()仅支持建议性锁类型;而 fcntl()可支持建议性锁和强制性锁两种类型。
  • 结构体参数如下

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(set by F_GETLK and F_OFD_GETLK) */
...
};
  • 具体说明

    • l_type:所希望的锁类型,可以设置为 F_RDLCK、F_WRLCK 和 F_UNLCK 三种类型之一,F_RDLCK 表示共享性质的读锁,F_WRLCK 表示独占性质的写锁,F_UNLCK 表示解锁一个区域。
    • l_whence 和 l_start:这两个变量用于指定要加锁或解锁区域的起始字节偏移量,与 2.7 小节所学 的 lseek()函数中的 offset 和 whence 参数相同,这里不再重述,如果忘记了,可以回到 2.7 小节再 看看。
    • l_len:需要加锁或解锁区域的字节长度。
    • l_pid:一个 pid,指向一个进程,表示该进程持有的锁能阻塞当前进程,当 cmd=F_GETLK 时有效。
  • 几条规则

    • 锁区域可以在当前文件末尾处开始或者越过末尾处开始,但是不能在文件起始位置之前开始。
    • 若参数 l_len 设置为 0,表示将锁区域扩大到最大范围,也就是说从锁区域的起始位置开始,到文 件的最大偏移量处(也就是文件末尾)都处于锁区域范围内。而且是动态的,这意味着不管向该文 件追加写了多少数据,它们都处于锁区域范围,起始位置可以是文件的任意位置。
    • 如果我们需要对整个文件加锁,可以将 l_whence 和 l_start 设置为指向文件的起始位置,并且指定 参数 l_len 等于 0。
  • 锁的类型

  • 上面我们提到了两种类型的锁,分别为共享性读锁(F_RDLCK)和独占性写锁(F_WRLCK)。基本的 规则与 12.5 小节所介绍的线程同步读写锁很相似,任意多个进程在一个给定的字节上可以有一把共享的读 锁,但是在一个给定的字节上只能有一个进程有一把独占写锁,进一步而言,如果在一个给定的字节上已经 有一把或多把读锁,则不能在该字节上加写锁;如果在一个字节上已经有一把独占性写锁,则不能再对它加 任何锁(包括读锁和写锁)

image-20220126132857197

  • 如果一个进程对文件的某个区域已经上了一把锁,后来该进程又试图在该区域再加一把锁,那么通常新 加的锁将替换旧的锁。譬如,若某一进程在文件的 100200 字节区间有一把写锁,然后又试图在 100200 字 节区间再加一把读锁,那么该请求将会成功执行,原来的写锁会替换为读锁。

  • 当对文件的某一区域加读锁时,调用进程必须对该文件有读权限,譬如 open() 时 flags 参数指定了 O_RDONLY 或 O_RDWR;当对文件的某一区域加写锁时,调用进程必须对该文件有写 权限,譬如 open()时 flags 参数指定了 O_WRONLY 或 O_RDWR。

  • F_SETLK、F_SETLKW 和 F_GETLK

    • F_GETLK:这种用法一般用于测试,测试调用进程对文件加一把由参数 flockptr 指向的 struct flock 对象所描述的锁是否会加锁成功。如果加锁不成功,意味着该文件的这部分区域已经存在一把锁, 并且由另一进程所持有,并且调用进程加的锁与现有锁之间存在排斥关系,现有锁会阻止调用进程 想要加的锁,并且现有锁的信息将会重写参数 flockptr 指向的对象信息。如果不存在这种情况,也 就是说 flockptr 指向的 struct flock 对象所描述的锁会加锁成功,则除了将 struct flock 对象的 l_type 修改为 F_UNLCK 之外,结构体中的其它信息保持不变。
    • F_SETLK:对文件添加由 flockptr 指向的 struct flock 对象所描述的锁。譬如试图对文件的某一区 域加读锁(l_type 等于 F_RDLCK)或写锁(l_type 等于 F_WRLCK),如果加锁失败,那么 fcntl() 将立即出错返回,此时将 errno 设置为 EACCES 或 EAGAIN。也可用于清除由 flockptr 指向的 struct flock 对象所描述的锁(l_type 等于 F_UNLCK)。
    • F_SETLKW:此命令是 F_SETLK 的阻塞版本(命令名中的 W 表示等待 wait),如果所请求的读 锁或写锁因另一个进程当前已经对所请求区域的某部分进行了加锁,而导致请求失败,那么调用进 程将会进入阻塞状态。只有当请求的锁可用时,进程才会被唤醒。
  • 规则

    • 文件关闭的时候,会自动解锁。
    • 一个进程不可以对另一个进程持有的文件锁进行解锁。
    • 由 fork()创建的子进程不会继承父进程所创建的锁。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char *argv[])
{
struct flock lock = {0};
int fd = -1;
char buf[] = "Hello World!";
/* 校验传参 */
if (2 != argc)
{
fprintf(stderr, "usage: %s <file>\n", argv[0]);
exit(-1);
}
/* 打开文件 */
fd = open(argv[1], O_WRONLY);
if (-1 == fd)
{
perror("open error");
exit(-1);
}
/* 对文件加锁 */
lock.l_type = F_WRLCK; //独占性写锁
lock.l_whence = SEEK_SET; //文件头部
lock.l_start = 0; //偏移量为 0
lock.l_len = 0;
if (-1 == fcntl(fd, F_SETLK, &lock))
{
perror("加锁失败");
exit(-1);
}
printf("对文件加锁成功!\n");
/* 对文件进行写操作 */
if (0 > write(fd, buf, strlen(buf)))
{
perror("write error");
exit(-1);
}
/* 解锁 */
lock.l_type = F_UNLCK; //解锁
fcntl(fd, F_SETLK, &lock);
/* 退出 */
close(fd);
exit(0);
}

  • 不同区域加锁
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
struct flock wr_lock = {0};
struct flock rd_lock = {0};
int fd = -1;
/* 校验传参 */
if (2 != argc)
{
fprintf(stderr, "usage: %s <file>\n", argv[0]);
exit(-1);
}
/* 打开文件 */
fd = open(argv[1], O_RDWR);
if (-1 == fd)
{
perror("open error");
exit(-1);
}
/* 将文件大小截断为 1024 字节 */
ftruncate(fd, 1024);
/* 对 100~200 字节区间加写锁 */
wr_lock.l_type = F_WRLCK;
wr_lock.l_whence = SEEK_SET;
wr_lock.l_start = 100;
wr_lock.l_len = 100;
if (-1 == fcntl(fd, F_SETLK, &wr_lock))
{
perror("加写锁失败");
exit(-1);
}
printf("加写锁成功!\n");
/* 对 400~500 字节区间加读锁 */
rd_lock.l_type = F_RDLCK;
rd_lock.l_whence = SEEK_SET;
rd_lock.l_start = 400;
rd_lock.l_len = 100;
if (-1 == fcntl(fd, F_SETLK, &rd_lock))
{
perror("加读锁失败");
exit(-1);
}
printf("加读锁成功!\n");
/* 对文件进行 I/O 操作 */
// ......
// ......
/* 解锁 */
wr_lock.l_type = F_UNLCK; //写锁解锁
fcntl(fd, F_SETLK, &wr_lock);
rd_lock.l_type = F_UNLCK; //读锁解锁
fcntl(fd, F_SETLK, &rd_lock);
/* 退出 */
close(fd);
exit(0);
}

  • 多个进程对同一文件的相同区域都可以加读锁,说明读锁是共享性的。由于程序 是放置在后台运行的,测试完毕之后,可以使用 kill 命令将这些进程杀死,或者直接关闭当前终端,重新启 动新的终端。
  • 第一次启动的进程对文件加写锁之后,后面再启动进程对同一文件的相同区域加写 锁发现都会失败,所以由此可知,写锁是独占性的。

锁的规则同上面的flock()函数

强制性锁会直接影响read()write()函数的操作(失败会报错),在此处略