IO Lock
版权声明:本文为 cdeveloper 原创文章,可以随意转载,但必须在明确位置注明出处!
文件锁简介
锁是保护共享资源的一种方法。在许多 UNIX 系统上,如果一个文件同时被多个进程编辑,该文件的最后状态取决于写该文件的最后一个进程。但是对于一些特殊的程序,如数据库有时需要独占一个文件,这时就不能让这个文件被多个进程同时操作了。于是 UNIX 系统提供了文件锁来实现这种独占文件的功能。
文件锁:当一个进程正在读或者或者修改文件的某个部分时,使用锁可以阻止其他进程修改同一个文件区域,文件锁可以锁定整个文件或者文件中的一个区域。
文件锁 API
早期的 BSD 只支持 flock
函数,该函数只能对整个文件加锁,不能对文件中的一部分加锁。但是 POSIX 标准的加锁使用 fcntl
,这个函数不仅能够对整个文件加锁,也能对文件区域加锁,还能实现其他的文件控制操作。
这个 fcntl
函数也非常重要,来认真学习下这个函数,你可以通过 man fcntl
来查看帮助手册。
fcntl 函数
函数声明如下:
#include <unistd.h>
#include <fcntl.h>
/*
* fd: 文件描述符
* cmd:F_GETLK,F_SETLK,F_SETLKW
* arg:一个取决于 cmd 的可选参数
* return:成功返回 0,失败返回 -1
*/
int fcntl(int fd, int cmd, ... /* arg */ );
其中 cmd
和 arg
参数比较重要,我们详细介绍这两个参数。
arg 参数
这个参数是一个可选参数,指定文件加锁的详细信息,在加锁文件时我们都会指定这个参数。这个参数是一个指向 struct flock
结构的指针:
struct flock {
...
short l_type;
short l_whence;
off_t l_start;
off_t l_len;
pid_t l_pid;
...
};
其中 5 个参数比较重要:
l_type
:希望的锁的类型,F_RDLCK
(共享读锁),F_WRLCK
(独占性写锁),F_UNLCK
(解锁)l_whence
:与 lseek 的参数相同,指定 l_start 从文件的何处开始偏移,SEEK_SET,SEEK_CUR, SEEK_ENDl_start
:加锁或者解锁的字节偏移量,与 l_whence 配合使用l_len
:要加锁或者解锁的字节长度l_pid
:进程 ID 为 l_pid 的进程能够阻塞加锁操作。
cmd 参数
这个参数有 3 种取值方式:
F_GETLK
:判断当前文件是否被加锁,如果有锁则将现有锁的信息复制到struct flock
中,否则设置l_type = F_UNLCK
F_SETLK
:设置由arg
所描述的锁F_SETLKW
:如果请求的锁不能被授予,那么调用进程会被置为休眠
总体来讲:用 F_GETLK
来测试能够获得一把锁,然后用 F_SETLK
或者 F_SETLKW
尝试加上一把锁。
文件锁分类
按照锁的类型和系统中锁的属性可以分为下面的 2 类。
读锁(L_RDLCK)和写锁(L_WRLCK)
这两个锁的区别是:任意多个进程在一个给定的字节上可以有一把共享的读锁,但是在一个给定的字节上只能有一个进程有一把独占写锁。
如下表所示:
table
但是这个规则只适用不同进程之间的提出的锁请求。在单个进程下,如果在已有的锁上加新锁,则新锁会替换旧锁,也不管锁是何种类型。例如:读锁可以替换写锁,写锁也可以替换读锁。
强制锁和建议锁
- 强制锁:内核管理的底层锁,一个进程锁定后其他用户不能打开
- 建议锁:用户管理的上层锁,一个进程锁定后其他用户可以打开
Linux 默认加的是建议锁,如果需要加强制锁,则需要在文件系统上用 mount
命令的 -o mand
选项来打开强制锁机制,参考 man fcntl
。
实例:fcntl 锁定文件
为了更好的理解锁的机制,我们来写一个实际的加锁程序来锁定一个文件,然后测试是否加锁成功。
1. 对文件加建议锁
/*
* file_lock.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
// 打开文件
int fd = open("./hello.txt", O_RDWR | O_CREAT, 0666) ;
if(fd < 0) {
printf("file open fail.\n");
exit(1);
}
// 建立一个独占性写锁
struct flock lock;
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
lock.l_pid = getpid();
// 对 hello.txt 加锁
if (fcntl(fd, F_SETLK, &lock) < 0)
printf("fcntl error.\n");
printf("Process %d has lock.\n", lock.l_pid);
while(1);
}
编译运行:
# 编译
gcc file_lock.c -o file_lock
# 运行
./file_lock
Process 11080 has lock.
# Ctrl - C 结束程序
然后我们启动另一个终端,尝试向 hello.txt
中写入数据,观察是否会阻塞:
echo hello_world >> hello.txt
cat hello.txt
# 写入成功了
hello_world
我们发现竟然写入成功了,我们不是已经加了锁了吗?其实这是因为默认加的是建议锁,建议锁可以被其他进程直接打开,因此可以写入成功。但是我们在程序中一般都是先用 F_GETLCK
主动判断要打开的文件是否有锁,例如下面这个例子:
2. 测试文件是否加锁成功
/*
* lock_test.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = open("./hello.txt", O_RDWR | O_CREAT, 0666) ;
if(fd < 0) {
printf("file open fail.\n");
exit(1);
}
// 如果发现有进程已经对这个文件加锁
// 那么现有锁的信息将重写下面的结构
struct flock lock;
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
lock.l_pid = getpid();
// 判断 hello.txt 上是否已经加锁
if (fcntl(fd, F_GETLK, &lock) < 0)
printf("fcntl error.\n");
// 如果没有进程对文件加锁,只将 l_type 设置为 F_UNLCK,其他信息不变
if (lock.l_type == F_UNLCK)
printf("No process has the lock.\n");
else
printf("Process %d has the lock.\n", lock.l_pid);
return 0;
}
我们编译运行:
gcc lock_test.c -o lock_test
./lock_test
No process has the lock.
发现当前 hello.txt
文件上没有锁,我们运行上一个 ./file_lock
程序:
./file_lock
Process 11080 has lock.
# while (1) 阻塞
这时进程 11080
已经对 hello.txt
加上了建议的独占性写锁,我们再次用 ./lock_test
测试这个文件是否被加锁:
./lock_test
Process 11080 has the lock.
可以看到这个文件已经被 11080
进程加锁了,我们之后就不要操作这个文件了。
记住在使用 fcntl
时,一定主要主动判断文件是否被加锁。
结语
文件锁是一个非常重要的话题,这篇博客主要介绍了一个重要的 fcntl
函数和文件锁的相关概念,一定要掌握这个函数的基本用法,其他重要的锁话题,例如死锁等,我们后面再议。