引入
有两个进程写操作同一个文件,如果不对这个文件上锁,一定会出现进程安全问题。如下
seqno
1
main.cpp
#include <string.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define SEQFILE "seqno" /* filename */
#define MAXLINE 4096
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
void my_lock(int), my_unlock(int);
int main(int argc, char **argv)
{
int fd;
long i, seqno;
pid_t pid;
ssize_t n;
char line[MAXLINE + 1];
pid = getpid();
fd = open(SEQFILE, O_RDWR, FILE_MODE);
for (i = 0; i < 20; i++) {
my_lock(fd);
lseek(fd, 0L, SEEK_SET); /* rewind before read */
n = read(fd, line, MAXLINE);
line[n] = '\0'; /* null terminate for sscanf */
n = sscanf(line, "%ld\n", &seqno);
printf("%s: pid = %ld, seq# = %ld\n", argv[0], (long) pid, seqno);
seqno++; /* increment sequence number */
snprintf(line, sizeof(line), "%ld\n", seqno);
lseek(fd, 0L, SEEK_SET); /* rewind before write */
write(fd, line, strlen(line));
my_unlock(fd);
}
exit(0);
}
void my_lock(int fd)
{
return; // 什么也不干
}
void my_unlock(int fd) // 什么也不干
{
return;
}
每次允许的结果都不一样(最终结果应该是40(多允许几次/多几个进程同时运行看看)—》线程安全只保证结果不保证过程):
在多个进程同时操作同一份文件的过程中,很容易导致文件中的数据混乱,需要锁操作来保证数据的完整性,这里介绍的专门针对文件的锁,称之为“文件锁”-flock
尽管可以使用(比如说)信号量来完成所需的同步,但通常文件锁更好一些,因为内核能够自动将锁与文件关联起来
文件锁的分类
根据内核行为来分,锁可以分成建议性锁与强制性锁两大类:
- 建议性锁: 要求每个使用上锁文件的进程都要检查是否有锁存在,并且尊重已有的锁。在一般情况下,内核和系统都不使用建议性锁,它们依靠程序员遵守这个规定。
- 强制性锁: 是由内核执行的锁,当一个文件被上锁进行写入操作的时候,内核将阻止其他任何文件对其进行读写操作。采用强制性锁对性能的影响很大,每次读写操作都必须检查是否有锁存在。
- 使用强制性锁之后,内核检查每个read和write请求,以验证其操作不会干扰某个进程持有的某个锁
- 对于阻塞式描述符,与某个强制性锁冲突的read或者write将会把调用进程投入睡眠,直到该锁释放为止
- 对于非阻塞式描述符,与某个强制性锁冲突的read或者write将会导致返回一个EAGAIN错误
- 使用强制性锁之后,内核检查每个read和write请求,以验证其操作不会干扰某个进程持有的某个锁
默认情况下,文件锁是劝告锁,这表示一个进程可以简单的忽略另一个进程在文件上放置的锁。要使得劝告性加锁模型能够正常工作,所有访问文件的进程都必须相互配合,即在使用文件IO之前首先需要在文件上放置一把锁。与之对应的是,强制式加锁系统会强制一个进程在执行IO时需要遵从其他进程持有的锁
API
一般来讲,有两组不同的给文件加锁的 API。
- flock()对整个文件加锁
- fcntl()对一个文件区域加锁
使用 flock()和 fcntl()的常规方法如下。
- 给文件加锁
- 执行文件 I/O
- 解锁文件使得其他进程能够给文件加锁
尽管文件加锁通常会与文件IO一起使用,但是也可以将其作为一项更通用的同步技术来使用。协作进程可以约定一个进程对整个文化或者一个文件区域进行加锁表示对一些共享资源而非文件本身访问
混用使用锁与stdio函数
由于stdio库会在用户空间进行缓冲,因此在混合使用stdio函数与文件锁是需要特别小心。这里的问题是一个输入缓冲器在被加锁之前可能会被填满会在一个输出缓冲区在锁被删除之后可能会被刷新。要避免这些问题则可以采用下面这些方法
- 使用 read()和 write()(以及相关的系统调用)取代 stdio 库来执行文件 I/O。
- 在对文件加锁之后立即刷新 stdio 流,并且在释放锁之前立即再次刷新这个流。
- 使用 setbuf()(或类似的函数)来禁用 stdio 缓冲,当然这可能会牺牲一些效率
flock
经过fcntl()提供的功能涵盖了flock()的功能,但这里仍需要对其进行介绍,因为在一些应用程序中仍然使用flock()并且其在继承和锁释放方面的一些语义与fcntl不同
NAME
flock -在打开的文件上应用或删除劝告锁
SYNOPSIS
#include <sys/file.h>
int flock(int fd, int operation);
flock()系统调用在整个文件上放置一个锁。待加锁的文件是通过传入fd的一个打开的文件描述符指定的。flock()中 operation 参数的可取值如下:
值 | 描述 |
---|---|
LOCK_SH | 在 fd 引用的文件上放置一把共享锁 |
LOCK_EX | 在 fd 引用的文件上放置一把互斥锁 |
LOCK_UN | 解锁 fd 引用的文件 |
LOCK_NB | 发起一个非阻塞锁请求 |
默认情况下,如果另一个进程已经持有了文件上的一个不兼容的锁,那么flock()会阻塞。如果需要防止出现这种情况,那么可以在 operation 参数中对这些值取 OR(|)。在这种情况下,如果另一个进程已经持有了文件上的一个不兼容的锁,那么 flock()就不会阻塞,相反它会返回−1 并将 errno 设置成 EWOULDBLOCK。
任意数量的进程可以同时持有一个文件上的共享锁,但是在同一个时刻只有一个进程能够持有一个文件上的互斥锁(即互斥锁会拒绝其他进程的互斥和共享锁请求)。下表对 flock()锁的兼容规则进行了总结。这里假设进程 A 首先放置了锁,表中给出了进程 B 是否能够放置一把锁
不管一个进程在文件上的访问模式是什么(读、写、或读写),它都可以在文件上放置一把共享锁或者互斥锁
通过再次调用flock()并在operation参数中指定恰当的值可以将既有共享锁转换成一个互斥锁锁(反之亦然)。将一个共享锁转换成一个互斥锁,在另一个进程持有了文件上的共享锁时会阻塞,除非同时指定了LOCL_NB标记
锁转换的过程不一定是原子的。在转换过程中首先会删除既有锁,然后创建一个新锁。在这两步之间另一个进程对一个不兼容锁的未决请求可能会满足。如果发生了这种情况,那么转换过程会被阻塞,或者在指定了LOCK_NB的情况下转换过程会失败并且进程丢失其原先持有的锁
下面程序演示了如何使用flock()。这个程序首先对一个文件加锁,睡眠指定的秒数,然后对文件解锁。程序接收三个命令行参数,其中第一个参数是待加锁的文件,第二个参数指定了锁的类型(共享或互斥)以及是否包含 LOCK_NB(非阻塞)标记,第三个参数指定了在获取和释放锁之间睡眠的秒数,并且这个参数是可选的,其默认值是 10 秒
#include <sys/file.h>
#include <fcntl.h>
#include <time.h>
#define BUF_SIZE 1000
char * currTime(const char *format)
{
static char buf[BUF_SIZE]; /* Nonreentrant */
time_t t;
size_t s;
struct tm *tm;
t = time(NULL);
tm = localtime(&t);
if (tm == NULL)
return NULL;
s = strftime(buf, BUF_SIZE, (format != NULL) ? format : "%c", tm);
return (s == 0) ? NULL : buf;
}
int main(int argc, char *argv[])
{
int fd, lock;
const char *lname;
if (argc < 3 || strcmp(argv[1], "--help") == 0 || strchr("sx", argv[2][0]) == NULL)
{
printf("%s file lock [sleep-time]\n"
" 'lock' is 's' (shared) or 'x' (exclusive)\n"
" optionally followed by 'n' (nonblocking)\n"
" 'sleep-time' specifies time to hold lock\n", argv[0]);
exit(EXIT_FAILURE);
}
lock = (argv[2][0] == 's') ? LOCK_SH : LOCK_EX;
if (argv[2][1] == 'n')
lock |= LOCK_NB;
fd = open(argv[1], O_RDONLY); /* Open file to be locked */
if (fd == -1){
perror("open");
exit(EXIT_FAILURE);
}
lname = (lock & LOCK_SH) ? "LOCK_SH" : "LOCK_EX";
printf("PID %ld: requesting %s at %s\n", (long) getpid(), lname,
currTime("%T"));
if (flock(fd, lock) == -1) {
if (errno == EWOULDBLOCK)
{
printf("PID %ld: already locked - bye!", (long) getpid());
exit(EXIT_FAILURE);
}
else{
printf("flock (PID=%ld)", (long) getpid());
exit(EXIT_FAILURE);
}
}
printf("PID %ld: granted %s at %s\n", (long) getpid(), lname,
currTime("%T"));
sleep((argc > 3) ? atoi(argv[3]) : 10);
printf("PID %ld: releasing %s at %s\n", (long) getpid(), lname,
currTime("%T"));
if (flock(fd, LOCK_UN) == -1){
perror("flock");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
使用效果:下面首先创建了一个文件,然后在后台启动一个程序并持有一个共享锁60s
接着启动另一个能够成功请求一个共享锁的程序实例,然后释放这个共享锁
但当启用另一个程序来非阻塞的请求一个互斥锁就会历史失败
当启动另一个程序实例来阻塞地请求一个互斥锁时程序就会阻塞。当原来持有共享锁的后台进程在 60 秒后释放这个锁之后,被阻塞的请求就会得到满足。
锁继承与释放的语义
上面提到,通过flock()调用并将operation参数指定为LOCK_UN可以释放一个文件锁。另外,锁会在相应的文件描述符被关闭之后自动被释放。但问题其实更加复杂,通过flock()获取的文件锁是与打开的文件描述而不是文件描述符会在文件本身相关联的,这意味着当一个文件描述符被复制是(通过 dup()、dup2()或一个 fcntl() F_DUPFD 操作),新文件描述符会引用同一个文件锁。比如,如果获取了fd所引用的文件上的一个锁,那么下面的代码就会释放这个锁
flock(fd, LOCK_EX);
newfd = dup(fd);
flock(fd, LOCK_UN);
如果已经通过一个特定的文件描述符获取了一个锁并创建了该文件描述符的一个或者多个副本,那么------如果不显示的执行一个解锁操作,那么只有当所有描述符副本都被关闭之后所才会被释放
如果使用open()获取第二个引用同一个文件的文件描述符,那么flock()就会将第二个文件描述符当成是一个不同的描述符。比如下面代码会在第二个flock()调用上阻塞
fd1 = open("a.txt", O_RDWR);
fd2 = open("a.txt", O_RDWR);
flock(fd1, LOCK_EX);
flock(fd2, LOCK_EX); // 阻塞,因为fd1已经获取到了锁
当使用fork()创建一个子进程时,这些子进程会复制其父进程的文件描述符,并且与使用dup()调用之类的函数复制的描述符一样,这些描述符会引用同一个打开的文件描述,进而会引用同一个锁。比如,下面代码会导致子进程删除父进程的锁
flock(fd, LOCK_EX); // 父进程获取锁
if(fock() == 0){
flock(fd, LOCK_UN); // release lock shared with parent
}
有时候可以利用这些语义来将一个文件锁从父进程(原子地)传输到子进程:在 fork()之后,父进程关闭其文件描述符,然后锁就只在子进程的控制之下了。读者稍后就会看到使用 fcntl()返回的记录锁是无法取得这种效果的。
通过 flock()创建的锁在 exec()中会得到保留(除非在文件描述符上设置了 close-on-exec标记并且该文件描述符是最后一个引用底层的打开的文件描述的描述符)
flock()的限制
- 只能对整个文件加锁。这种粗粒度的加锁会限制协作进程之间的并发性。例如,假设存在多个进程,其中各个进程都想要同时访问同一个文件的不同部分,那么通过 flock()加锁会不必要地阻止这些进程并发完成这些操作
- 通过 flock()只能放置劝告式锁
- 很多 NFS 实现不识别 flock()放置的锁。
fcntl
使用fcntl()能够在一个文件的任意部分上放置一把锁,这个文件部分既可以是一个字节,也可以是整个文件。这种形式的文件锁被称为记录加锁,但这种称谓是不恰当的,因为Unixit上的文件是一个字节序列,并不存在记录边界的概念,文件记录的概念只存在应用程序中。
下面显示了如何使用记录锁来同步两个进程对一个文件中的同一块区域的访问。(在这幅图中假设所有的锁请求都会阻塞,这样它们在锁被另一个进程持有时就会等待。)
NAME
fcntl - manipulate file descriptor
SYNOPSIS
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
DESCRIPTION
参数fd:表示文件描述符,引用了待加锁的文件
flock结构定义了待获取或者删除的锁:
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) */
...
};
(1)l_type 字段表示需放置的锁的类型,其取值为表 55-3 中列出的值中的一个。
锁 的 类 型 | 描 述 |
---|---|
F_RDLCK | 放置一把读锁 |
F_WRLCK | 放置一把写锁 |
F_UNLCK | 删除一把既有锁 |
- 从语义上来讲,读(F_RDLCK)和写(F_WRLCK)锁对应于 flock()施加的共享锁和互斥锁,并且它们遵循着同样的兼容性规则:任何数量的进程能够持有一块文件区域上的读锁,但只有一个进程能够持有一把写锁,并且这把锁会将其他进程的读锁和写锁排除在外。
- 为了在一个文件上放置一把读锁就必须要打开文件以允许读取。类似地,要放置一把写锁就必须要打开文件以允许写入。要放置两种锁就必须要打开文件以允许读写(O_RDWR)。试图在文件上放置一把与文件访问模式不兼容的锁将会导致一个 EBADF错误。
(2)l_whence、l_start 以及 l_len 字段一起指定了待加锁的字节范围
- l_whence指定相对起点,l_start 字段指定了文件中的一个偏移量
SEEK_SET
: l_start相对于文件的开头解释SEEK_CUR
: l_start相对于文件的当前字节读写指针位置解释;l_start 可以是一个负数,只要最终得到的文件位置不会小于文件的起始位置(字节 0)即可。SEEK_END
:l_start相对于文件的开头末尾;l_start 可以是一个负数,只要最终得到的文件位置不会小于文件的起始位置(字节 0)即可。
- l_len指定从该偏移开始的连续字节数(指定待加锁的字节数),其起始位置由 l_whence 和 l_start 定义。
- 长度为0表示从字节偏移开始到文件偏移的最大可能值
- 对文件结尾之后并不存在的字节进行加锁是可以的,但无法对在文件起始位置之前的字节进行加锁
- 锁住整个文件的两种方式:
- 指定
l_whence
=SEEK_SET
,l_start=0, l_len=0
(常用) - 使用
lseek
将读写指针定位到文件头,然后指定l_whence
=SEEK_CUR
,l_start=0, l_len=0
- 指定
一般来讲,应用程序应该支队所需的最小字节范围加锁,这样其他进程就能够同时对同一个文件的不同区域进行加锁,进而取得更到的并发性。
在某些情况下需要对术语最小范围进行限定。在比如NFS和CIFS之类的网络网际系统上混合使用及连锁和mmap()调用会导致不期望的结果。之所以会发生这种问题就是因为mmap()映射文件的单位是系统分页的大小。如果一个文件锁是分页对齐的,那么所有的一切都会正常工作,因为锁会覆盖与其一脏分页对应的整个区域。但如果锁没有分页对齐,那么就会存在一种竞争条件——当映射分页的任意部分发生变更之后内核可能就会写入未被锁覆盖的区域
(3) cmd参数用于记录上锁的命令,一共有三个值:
F_SETLK
:- 获取(
l_type
成员为F_RDLCK/F_WRLCK
)或者释放(l_type
成员为F_UNLCK
)由arg
指向的flock
解锁锁描述的值。 - 如果另一个进程持有了一把待加锁的区域中任意部分上的不兼容的锁时,fcntl()就会失败并返回 EAGAIN 错误。在一些 UNIX 实现上 fcntl()在碰到这种情况时会失败并返回EACCES 错误。SUSv3 允许实现采用其中任意一种处理方式,因此可移植的应用程序应该对
这两个值都进行测试。
- 获取(
F_SETLKW
:- 与
F_SETLK
类似,唯一不同的是如果无法获取该锁授予调用进程,该调用进程将阻塞直到能够获取:内核会将调用进程拖入睡眠,直到该锁可用,然后唤醒它 - 如果正在处理一个信号并且没有指定 SA_RESTART,那么 F_SETLKW 操作就可能会被中断(即失败并返回 EINTR 错误)。开发人员可以利用这种行为来使用 alarm()或 setitimer()为一个加锁请求设置一个超时时间。
- 与
F_GETLK
:
- 确定是否可以在一个给定的区域上放置一把锁,但实际不获取这把锁。l_type 字段的值必须为 F_RDLCK 或 F_WRLCK
- lock结果是一个值-结果参数,在返回时它包含了有关释放能够放置指定的锁的信息。
- 如果允许加锁(即在指定的文件区域上不存在不兼容的锁),那么在 l_type 字段中会返回 F_UNLCK,并且剩余的字段会保持不变。
- 如果在区域上存在一个或多个不兼容的锁,那么 flockstr 会返回与那些锁中其中一把锁(无法确定是哪把锁)相关的信息,包括其类型(l_type)、字节范围(l_start 和 l_len;l_whence 总是返回为 SEEK_SET)以及持有这把锁的进程的进程 ID(l_pid)
注意,在使用 F_GETLK 之后接着使用 F_SETLK 或 F_SETLKW 的话就可能会出现竞争条件,因为在执行后面一个操作时,F_GETLK 返回的信息可能已经过时了,因此 F_GETLK的实际作用比其一开始看起来的作用要小很多。即使 F_GETLK 表示可以放置一把锁,仍然需要为 F_SETLK 返回一个错误或 F_SETLKW 阻塞做好准备
锁获取和释放的细节
有关获取和释放由 fcntl()创建的锁方面需要注意以下几点。
- 解锁一块文件区域总是会立即成功。即使当前并不持有一块区域上的锁,对这块区域解锁也不是一个错误
- 在任意时刻,一个进程只能持有一个文件的某个特定区域上的一种锁。在之前已经锁住的区域上放置一把新锁会不发生任何事情(新锁的类型与既有锁的类型是一样的)或原子地将既有锁转换成新模式。在后一种情况中,当一个读锁转换成写锁时需要为调用返回一个错误(F_SETLK)或阻塞(F_SETLKW)做好准备。
- 一个进程永远无法将自己锁在一个文件区域之外,即使通过多个引用同一文件的文件描述符放置锁也是如此。
- 在已经持有的锁中间放置一把模式不同的锁会产生三把锁:在新锁的两端会创建两个模式为之前模式更小一点的锁,如下图所示。
锁获取和释放的语义
fcntl()记录锁继承和释放的语义与使用 flock()创建的锁的继承和释放的语义是不同的,以下几点需要注意
- 锁不能通过fork子进程继承:因为锁和进程ID紧密关联,而父子进程有不同的进程ID。这与flock()不同,flock()时,子进程会继承一个引用同一把锁的引用并且能够释放这把锁,从而导致父进程也会失去这把锁。
- 记录锁在 exec()中会得到保留。(但需要注意下面描述的 close-on-exec 标记的作用)。
- 一个进程中的所有线程会共享同一组记录锁。
- 记录锁同时与一个进程和一个i-node关联。从这种关联关系可以得出结果是当一个进程终止之后,其所有记录锁会被释放。另一个推论是当一个进程关闭了一个描述符之后,进程持有的对应文件上的锁会被释放,不管这些锁是通过哪个文件描述符获得的。比如下面代码中,colse(fd2)调用会释放调用进程持有的testfile文件上的锁,尽管这把锁是通过文件描述符fd1获得的?????待测试
struct flock f1;
f1.l_type = F_WRLCK;
fl.l_whence = SEEK_SET;
fl.l_start = 0;
fl.l_len = 0;
fd1 = open("testfile", O_RDWD);
fd2 = open("testfile", O_RDWD);
if(fcntl(fd1, cmd, &f1) == -1){
perror("testfile");
exit(EXIT_FAILURE);
}
close("fd2");
dup()、dup2()以及 fcntl()都可以用来获取一个打开着的文件描述符的副本。除了执行一个显式的 close()之外,一个描述符在设置了 close-on-exec标记时会被一个 exec()调用关闭,或者也可以通过一个 dup2()调用来关闭其第二个文件描述符参数,当然前提是该描述符已经被打开了
在使用 flock()时,一把锁只会与一个打开的文件描述关联,并且会持续发挥作用直到持有这把锁的引用的任意进程显式地释放这把锁或所有引用这个打开着的文件描述的文件描述符被关闭之后为止
-
记录上锁不应同标准IO库一起使用,因为该函数库会执行内部缓存。当某个文件需要上锁时,应该使用
read/write
-
fcntl
是劝告性上锁(当然也可以强制性上锁,但是不建议这么用)
锁定饿死和排队加锁请求的优先级
Linux上的规则如下所述
- 排队的锁请求被准予的顺序是不确定的。如果多个进程正在等待加锁,那么它们被满足的顺序取决于进程的调度。
- 写者并不比读者拥有更高的优先权,反之亦然。
在其他系统上这些论断可能就是不正确的了。在一些 UNIX 实现上,锁请求的服务是按照 FIFO 的顺序来完成的,并且读者比写者拥有更高的优先权
死锁
在使用F_SETLKW时要避免小心死锁。如上图,如果内核不对这种情况进行抑制,那么会导致两个进程永远阻塞。为避免这种情况,内核会对F_SETLKW发起的每个新锁请求进行检测以判断是否会导致死锁。如果会,那么内核就会选中其中一个被阻塞的进程使其fcntl()调用接触阻塞并返回EDEADLK
实现
通过fcntl
void my_lock(int fd)
{
struct flock lock;
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0; /* write lock entire file */
fcntl(fd, F_SETLKW, &lock);
}
void my_unlock(int fd)
{
struct flock lock;
lock.l_type = F_UNLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0; /* unlock entire file */
fcntl(fd, F_SETLK, &lock);
}
我们可以进一步的封装它
#include <stdio.h>
#include <fcntl.h>
#include <zconf.h>
int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)
{
struct flock lock;
lock.l_type = type; /* F_RDLCK, F_WRLCK, F_UNLCK */
lock.l_start = offset; /* byte offset, relative to l_whence */
lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
lock.l_len = len; /* #bytes (0 means to EOF) */
return( fcntl(fd, cmd, &lock) ); /* -1 upon error */
}
/* end lock_reg */
void Lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)
{
if (lock_reg(fd, cmd, type, offset, whence, len) == -1)
printf("lock_reg error");
}
#define read_lock(fd, offset, whence, len) \
lock_reg(fd, F_SETLK, F_RDLCK, offset, whence, len)
#define readw_lock(fd, offset, whence, len) \
lock_reg(fd, F_SETLKW, F_RDLCK, offset, whence, len)
#define write_lock(fd, offset, whence, len) \
lock_reg(fd, F_SETLK, F_WRLCK, offset, whence, len)
#define writew_lock(fd, offset, whence, len) \
lock_reg(fd, F_SETLKW, F_WRLCK, offset, whence, len)
#define un_lock(fd, offset, whence, len) \
lock_reg(fd, F_SETLK, F_UNLCK, offset, whence, len)
#define is_read_lockable(fd, offset, whence, len) \
lock_test(fd, F_RDLCK, offset, whence, len)
#define is_write_lockable(fd, offset, whence, len) \
lock_test(fd, F_WRLCK, offset, whence, len)
#define Read_lock(fd, offset, whence, len) \
Lock_reg(fd, F_SETLK, F_RDLCK, offset, whence, len)
#define Readw_lock(fd, offset, whence, len) \
Lock_reg(fd, F_SETLKW, F_RDLCK, offset, whence, len)
#define Write_lock(fd, offset, whence, len) \
Lock_reg(fd, F_SETLK, F_WRLCK, offset, whence, len)
#define Writew_lock(fd, offset, whence, len) \
Lock_reg(fd, F_SETLKW, F_WRLCK, offset, whence, len)
#define Un_lock(fd, offset, whence, len) \
Lock_reg(fd, F_SETLK, F_UNLCK, offset, whence, len)
#define Is_read_lockable(fd, offset, whence, len) \
Lock_test(fd, F_RDLCK, offset, whence, len)
#define Is_write_lockable(fd, offset, whence, len) \
Lock_test(fd, F_WRLCK, offset, whence, len)
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
验证用法
问:怎么证明fcntl是劝告性上锁。
同时运行一份上锁的,部分不上锁的文件
问:验证文件锁允许多个读者
#include <time.h>
char *gf_time(void)
{
struct timeval tv;
static char str[30];
char *ptr;
if (gettimeofday(&tv, NULL) < 0){
printf("gettimeofday error");
exit(0);
}
ptr = ctime(&tv.tv_sec);
strcpy(str, &ptr[11]);
/* Fri Sep 13 00:00:00 1986\n\0 */
/* 0123456789012345678901234 5 */
snprintf(str+8, sizeof(str)-8, ".%06ld", tv.tv_usec);
return(str);
}
char *Gf_time(void)
{
return(gf_time());
}
int main(int argc, char **argv)
{
int fd;
fd = open("test1.data", O_RDWR | O_CREAT, (mode_t) FILE_MODE);
Read_lock(fd, 0, SEEK_SET, 0); /* parent read locks entire file */
printf("%s: parent has read lock\n", Gf_time());
if (fork() == 0) {
/* 4child */
Read_lock(fd, 0, SEEK_SET, 0); /* this should work */
printf("%s: child has read lock\n", Gf_time());
sleep(2);
Un_lock(fd, 0, SEEK_SET, 0);
printf("%s: child releases read lock\n", Gf_time());
(fd, 0, SEEK_SET, 0); /* this should work */
printf("%s: child has read lock\n", Gf_time());
sleep(2);
_exit(0);
}
/* parent */
sleep(4);
Un_lock(fd, 0, SEEK_SET, 0);
printf("%s: parent releases read lock\n", Gf_time());
_exit(0);
}
- 当一个读锁已经上锁,其他还是可以有读锁继续上锁的
问: 验证文件锁
int main(int argc, char **argv)
{
int fd;
fd = open("test1.data", O_RDWR | O_CREAT, FILE_MODE);
Read_lock(fd, 0, SEEK_SET, 0); /* parent read locks entire file */
printf("%s: parent has read lock\n", Gf_time());
if (fork() == 0) {
/* 4first child */
sleep(1);
printf("%s: first child tries to obtain write lock\n", Gf_time());
Writew_lock(fd, 0, SEEK_SET, 0); /* this should block */
printf("%s: first child obtains write lock\n", Gf_time());
sleep(2);
Un_lock(fd, 0, SEEK_SET, 0);
printf("%s: first child releases write lock\n", Gf_time());
_exit(0);
}
if (fork() == 0) {
/* 4second child */
sleep(3);
printf("%s: second child tries to obtain read lock\n", Gf_time());
Readw_lock(fd, 0, SEEK_SET, 0);
printf("%s: second child obtains read lock\n", Gf_time());
sleep(4);
Un_lock(fd, 0, SEEK_SET, 0);
printf("%s: second child releases read lock\n", Gf_time());
_exit(0);
}
/* 4parent */
sleep(5);
Un_lock(fd, 0, SEEK_SET, 0);
printf("%s: parent releases read lock\n", Gf_time());
_exit(0);
}
- 当有读锁时,可以继续有读锁上锁,但是写锁不能上
- 当有读锁时,即使有写锁在等待,还是可以增加读者(这个可能导致一个问题:只要不断的发出读出锁请求,写入者可能会获取不到写入锁而”挨饿“)
- 当有读锁时,写锁会被阻塞,其他读锁不会被阻塞,而是立即就可以获取到
问:验证文件锁
int main(int argc, char **argv)
{
int fd;
fd = open("test1.data", O_RDWR | O_CREAT, FILE_MODE);
Write_lock(fd, 0, SEEK_SET, 0); /* parent write locks entire file */
printf("%s: parent has write lock\n", Gf_time());
if (fork() == 0) {
/* 4first child */
sleep(1);
printf("%s: first child tries to obtain write lock\n", Gf_time());
Writew_lock(fd, 0, SEEK_SET, 0); /* this should block */
printf("%s: first child obtains write lock\n", Gf_time());
sleep(2);
Un_lock(fd, 0, SEEK_SET, 0);
printf("%s: first child releases write lock\n", Gf_time());
exit(0);
}
if (fork() == 0) {
/* 4second child */
sleep(3);
printf("%s: second child tries to obtain read lock\n", Gf_time());
Readw_lock(fd, 0, SEEK_SET, 0);
printf("%s: second child obtains read lock\n", Gf_time());
sleep(4);
Un_lock(fd, 0, SEEK_SET, 0);
printf("%s: second child releases read lock\n", Gf_time());
exit(0);
}
/* parent */
sleep(5);
Un_lock(fd, 0, SEEK_SET, 0);
printf("%s: parent releases write lock\n", Gf_time());
exit(0);
}
- 当有写锁上锁时,后面的读锁/写锁都会被阻塞
- 当有写锁上锁时,现有写锁在阻塞,然后有读锁在阻塞:当写锁被释放时,写锁先获得锁
int main(int argc, char **argv)
{
int fd;
fd = open("test1.data", O_RDWR | O_CREAT, FILE_MODE);
Write_lock(fd, 0, SEEK_SET, 0); /* parent write locks entire file */
printf("%s: parent has write lock\n", Gf_time());
if (fork() == 0) {
/* 4first child */
sleep(3);
printf("%s: first child tries to obtain write lock\n", Gf_time());
Writew_lock(fd, 0, SEEK_SET, 0); /* this should block */
printf("%s: first child obtains write lock\n", Gf_time());
sleep(2);
Un_lock(fd, 0, SEEK_SET, 0);
printf("%s: first child releases write lock\n", Gf_time());
exit(0);
}
if (fork() == 0) {
/* 4second child */
sleep(1);
printf("%s: second child tries to obtain read lock\n", Gf_time());
Readw_lock(fd, 0, SEEK_SET, 0);
printf("%s: second child obtains read lock\n", Gf_time());
sleep(4);
Un_lock(fd, 0, SEEK_SET, 0);
printf("%s: second child releases read lock\n", Gf_time());
exit(0);
}
/* parent */
sleep(5);
Un_lock(fd, 0, SEEK_SET, 0);
printf("%s: parent releases write lock\n", Gf_time());
exit(0);
}
- 当有写锁上锁时,后面的读锁/写锁都会被阻塞
- 当有写锁上锁时,现有读锁在阻塞,然后有写锁在阻塞:当写锁被释放时,写锁先获得锁
从上面两个例子可以看出:等待着的写入者必等待着的读出者优先
问:文件锁还有什么其他用途吗?
用途:启动一个守护进程的唯一副本
记录上锁的一个常见用途是确保某个程序(比如守护程序)在任何时刻只有一个副本在运行
#define PATH_PIDFILE "pidfile"
int main(int argc, char **argv)
{
int pidfd;
char line[MAXLINE];
/* 打开 PID 文件, 如果不存在则创建它 */
pidfd = open(PATH_PIDFILE, O_RDWR | O_CREAT, FILE_MODE);
/* 获取一个写锁:锁定整个文件 */
if (write_lock(pidfd, 0, SEEK_SET, 0) < 0) {
if (errno == EACCES || errno == EAGAIN) {// 如果没有获取到,说明该程序有另一个副本运行
printf("unable to lock %s, is %s already running?", PATH_PIDFILE, argv[0]); // 输出错误信息,然后运行
exit(0);
}else{
printf("unable to lock %s", PATH_PIDFILE);
exit(0);
}
}
snprintf(line, sizeof(line), "%ld\n", (long) getpid());
ftruncate(pidfd, 0); // 清空文件
write(pidfd, line, strlen(line)); // 将当前进程的pid写入文件
/* then do whatever the daemon does ... */
pause();
}
测试:
[oceanstar@localhost build]$ ./onedaemon &
[1] 15067
[oceanstar@localhost build]$ cat pidfile
15067
[oceanstar@localhost build]$ ./onedaemon
unable to lock pidfile, is ./onedaemon already running?
其他实现
Posix.1保证:如果以O_CREAT
(文件不存在则创建)和O_EXCL
(独占打开)调用open函数,那么一旦该文件已经存在,该函数就返回一个错误。这样就能够保证任何时候只能有一个进程能够创建这样的文件(即获取锁),释放这样的锁只需要unlink
(删除)文件
#define LOCKFILE "/tmp/seqno.lock"
void my_lock(int fd)
{
int tempfd;
while ( (tempfd = open(LOCKFILE, O_RDWR|O_CREAT|O_EXCL, FILE_MODE)) < 0) {
if (errno != EEXIST)
printf("open error for lock file");
/* someone else has the lock, loop around and try again */
}
close(tempfd); /* opened the file, we have the lock */
}
void my_unlock(int fd)
{
unlink(LOCKFILE); /* release lock by removing file */
}
#include <fcntl.h>
#include <errno.h>
#include <zconf.h>
#include <stdio.h>
#define LOCKFILE "/tmp/seqno.lock"
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
void my_lock(int fd)
{
int tempfd;
while ( (tempfd = open(LOCKFILE, O_RDWR|O_CREAT|O_EXCL, FILE_MODE)) < 0) {
if (errno != EEXIST)
printf("open error for lock file");
/* someone else has the lock, loop around and try again */
}
close(tempfd); /* opened the file, we have the lock */
}
void my_unlock(int fd)
{
unlink(LOCKFILE); /* release lock by removing file */
}
#include <errno.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define LOCKFILE "/tmp/seqno.lock"
#define PREFIX "/tmp" /* prefix for temp pathname */
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
void my_lock(int fd)
{
char *ptr;
ptr = tempnam(PREFIX, NULL);
/* create/open and then close */
close(open(ptr, O_CREAT | O_RDWR | O_TRUNC, FILE_MODE));
while (link(ptr, LOCKFILE) < 0) {
if (errno != EEXIST){
printf("link error for lock file");
_exit(0);
}
/* someone else has the lock, loop around and try again */
}
unlink(ptr); /* linked the file, we have the lock */
free(ptr); /* tempnam() calls malloc() */
}
void my_unlock(int fd)
{
unlink(LOCKFILE); /* release lock by removing file */
}
但是这样的锁有问题:
- 如果持有当前该锁的进程没有释放就终止,那么文件没有被删除,其他进程也不能获取锁了
- 如果某个进程已经打开了所文件,那么当前进程是在一个无限循环中一次又一次的调用
open
,浪费CPU - 调用
open
和unlink
创建和删除一个文件设计文件系统的访问,比调用fctnl
两次所花时间要长的多
这种锁只是存在与fcntl
还没有的时候才用
/proc/locks 文件
通过检查 Linux 特有的/proc/locks 文件中的内容能够查看系统中当前存在的锁
- 锁在该文件上所有锁中的序号
- 锁的类型。其中 FLOCK 表示 flock()创建的锁,POSIX 表示 fcntl()创建的锁
- 锁的模式,其值是 ADVISORY 或 MANDATORY。
- 锁的类型,其值是 READ 或 WRITE(对应于 fcntl()的共享锁和互斥锁)
- 持有锁的进程的进程 ID
- 三个用冒号分隔的数字,它们标识出了锁所属的文件。这些数字是文件所处的文件系统的主要和次要设备号,后面跟着文件的 i-node 号
- 锁的起始字节。对于 flock()锁来讲,其值永远是 0。
- 锁的结尾字节。其中 EOF 表示锁延伸到文件的结尾(即对于 fcntl()创建的锁来讲是将l_len 指定为 0)。对于 flock()锁来讲,这一列的值永远是 EOF。
通过/proc/locks 还能够获取被阻塞的锁请求的相关信息,如下面的输出所示
其中锁号后面随即跟着->字符的行表示被相应锁号阻塞的锁请求。因此从上面的输出可以看出一个请求被阻塞在锁 1 上,两个请求被阻塞在锁 2 上(使用 fcntl()创建的一把锁),一个请求被阻塞在锁 3 上(使用 flock()创建的一把锁)
仅运行一个程序的单个实例
一些程序——特别是很多 daemon——需要确保同一时刻只有一个程序实例在系统中运行。完成这项任务的一个常见方法是让daemon在一个标准目录中创建一个文件并在该文件上放置一把写锁。daemon在其执行期间一直持有这个文件上并在即将终止之前删除这个文件。如果启用了daemon的另一个实例,那么它在获取该文件上的写锁时就会失败,其结果是它会意识到 daemon 的另一个实例肯定正在运行,然后终止
很多网络服务器采用了另一种常规做法,即当服务器绑定的众所周知的 socket 端口号已经被使用时就认为该服务器实例已经处于运行状态了
/var/run 目录通常是存放此类锁文件的位置。或者也可以在 daemon 的配置文件中加一行来指定文件的位置。
通常,daemon会将其进程ID写入锁文件,因此这个文件在命名时通常将.pid作为扩展名(如syslogd 会创建文件/var/run/syslogd.pid)
static int lockReg(int fd, int cmd, int type, int whence, int start, off_t len)
{
struct flock fl;
fl.l_type = type;
fl.l_whence = whence;
fl.l_start = start;
fl.l_len = len;
return fcntl(fd, cmd, &fl);
}
int lockRegion(int fd, int type, int whence, int start, int len) /* Lock a file region using nonblocking F_SETLK */
{
return lockReg(fd, F_SETLK, type, whence, start, len);
}
// 创建一个 PID 锁文件以确保只有一个程序实例被启动了
int createPidFile(const char *progName, const char *pidFile, int flags)
{
int fd;
char buf[BUF_SIZE];
fd = open(pidFile, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1){
printf("Could not open PID file %s", pidFile);
exit(EXIT_FAILURE);
}
if (flags & CPF_CLOEXEC) {
/* Set the close-on-exec file descriptor flag */
/* Instead of the following steps, we could (on Linux) have opened the
file with O_CLOEXEC flag. However, not all systems support open()
O_CLOEXEC (which was standardized only in SUSv4), so instead we use
fcntl() to set the close-on-exec flag after opening the file */
flags = fcntl(fd, F_GETFD); /* Fetch flags */
if (flags == -1){
printf("Could not get flags for PID file %s", pidFile);
exit(EXIT_FAILURE);
}
flags |= FD_CLOEXEC; /* Turn on FD_CLOEXEC */
if (fcntl(fd, F_SETFD, flags) == -1) /* Update flags */
{
printf("Could not set flags for PID file %s", pidFile);
exit(EXIT_FAILURE);
}
}
if (lockRegion(fd, F_WRLCK, SEEK_SET, 0, 0) == -1) {
if (errno == EAGAIN || errno == EACCES)
{
printf("PID file '%s' is locked; probably "
"'%s' is already running", pidFile, progName);
exit(EXIT_FAILURE);
}
else
{
printf("Unable to lock PID file '%s'", pidFile);
exit(EXIT_FAILURE);
}
}
if (ftruncate(fd, 0) == -1)
{
printf("Could not truncate PID file '%s'", pidFile);
exit(EXIT_FAILUER);
}
snprintf(buf, BUF_SIZE, "%ld\n", (long) getpid());
if (write(fd, buf, strlen(buf)) != strlen(buf))
{
printf("Writing to PID file '%s'", pidFile);
exit(EXIT_FAILUER);
}
return fd;
}
int /* Lock a file region using blocking F_SETLKW */
lockRegionWait(int fd, int type, int whence, int start, int len)
{
return lockReg(fd, F_SETLKW, type, whence, start, len);
}
/* Test if a file region is lockable. Return 0 if lockable, or
PID of process holding incompatible lock, or -1 on error. */
pid_t regionIsLocked(int fd, int type, int whence, int start, int len)
{
struct flock fl;
fl.l_type = type;
fl.l_whence = whence;
fl.l_start = start;
fl.l_len = len;
if (fcntl(fd, F_GETLK, &fl) == -1)
return -1;
return (fl.l_type == F_UNLCK) ? 0 : fl.l_pid;
}
使用:
if(createPidFile("mydaemon", "/var/run/mydaemon.pid", -1) == -1){
exit(EXIT_FAILUER);
}
createPidFile()函数中的一个精妙之处是使用 ftruncate()来清除锁文件中之前存在的所有字符串。之所以要这样做是因为 daemon 的上一个实例在删除文件时可能因系统崩溃而失败。在这种情况下,如果新 daemon 实例的进程 ID 较小,那么可能就无法完全覆盖之前文件中的内容。例如,如果进程 ID 是 789,那么就只会向文件写入 789\n,但之前的 daemon 实例可能已经向文件写入了 12345\n,这时如果不截断文件的话得到的内容就会是 789\n5\n。从严格意义上来讲,清除所有既有字符串并不是必需的,但这样做显得更加简洁并且能排除产生混淆的可能
在 flags 参数中可以指定常量 CPF_CLOEXEC 将会导致 createPidFile()为文件描述符设置close-on-exec 标记。这对于通过调用 exec()重启自己的服务器来讲是比较有用的。如果在 exec()时文件描述符没有被关闭,那么重新启动的服务器会认为服务器的另一个实例正处于运行状态。