本文的目的是想帮助读者理清 Linux 2.6中文件锁的概念以及 Linux 2.6 都提供了何种数据结构以及关键的系统调用来实现文件锁,从而可以帮助读者更好地使用文件锁来解决多个进程读取同一个文件的互斥问题。本文主要描述了 Linux 中各类文件锁的概念,使用场景,以及与文件锁密切相关的系统调用等内容。
在多任务操作系统环境中,如果一个进程尝试对正在被其他进程读取的文件进行写操作,可能会导致正在进行读操作的进程读取到一些被破坏或者不完整的数 据;如果两个进程并发对同一个文件进行写操作,可能会导致该文件遭到破坏。因此,为了避免发生这种问题,必须要采用某种机制来解决多个进程并发访问同一个 文件时所面临的同步问题,由此而产生了文件加锁方面的技术。
早期的 UNIX 系统只支持对整个文件进行加锁(通过flock系统调用),因此无法运行数据库之类的程序,因为此类程序需要实现记录级的加锁。在 System V Release 3 中,通过 fcntl 提供了记录级的加锁,此后发展成为 POSIX 标准的一部分。本文将基于 2.6.23 版本的内核来探讨 Linux 中文件锁的相关技术。
一、Linux中的文件锁
Linux 支持的文件锁技术主要包括劝告锁(advisory lock)和强制锁(mandatory lock)这两种。此外,Linux 中还引入了两种强制锁的变种形式:共享模式强制锁(share-mode mandatory lock)和租借锁(lease),本文不对这两种变形形式加以讨论。
在 Linux 中,不论是劝告锁还是强制锁,它们都包含两种锁:共享锁和排他锁(又称为读锁和写锁)。多个共享锁之间不会相互干扰,多个进程在同一时刻可以对同 一个文件加共享锁。但是,如果一个进程对该文件加了排他锁,那么其他进程则无权再对该文件加共享锁或者排他锁,直到该排他锁被释放。所以,对于同一个文件 来说,它可以同时拥有很多读者,但是在某一特定时刻,它只能拥有一个写者,它们之间的兼容关系如表 1 所示。
表1 锁间的兼容关系
当前 加上的锁 | 共享锁 | 排他锁 |
无 | 是 | 是 |
共享锁 | 是 | 否 |
排他锁 | 是 | 否 |
二、劝告性锁
Posix记录锁默认为劝告性锁。劝告锁是一种协同工作的锁。对于这一种锁来说,内核只提供加锁以及检测文件是否已经加锁的手段,但是内核并不参与锁的控制和协调。也就是说,它不能阻止一个进程写已由另一个进程读锁定的某个文件,类似地,它也不能防止一个进程读已由另一个进程写锁定的某个文件。因此,劝告锁并不能阻止进程对文件的访问,而只 能依靠各个进程在访问文件之前检查该文件是否已经被其他进程加锁来实现并发控制。从这点上来说,劝告锁的工作方式与使用信号量保护临界区的方式非常类似。
三、强制性锁
与劝告锁不同,强制锁是一种内核强制采用的文件锁,它是从 System V Release 3 开始引入的。使用强制性锁后,每当有系统调用read() 以及write() 发生的时候,内核都要检查并确保这些系统调用不会违反在所访问文件上加的强制锁约束。对于通常的阻塞式描述符,与某个强制性锁冲突的read或write将把调用进程投入睡眠,直到该锁释放为止。对于非阻塞式描述符,与某个强制性锁冲突的read或write将导致它们返回一个EAGAIN错误。归纳如表2所示。
表2 对已加强制锁的文件进行操作时的行为
当前锁类型 | 阻塞读 | 阻塞写 | 非阻塞读 | 非阻塞写 |
读锁 | 正常读取数据 | 阻塞 | 正常读取数据 | EAGAIN |
写锁 | 阻塞 | 阻塞 | EAGAIN | EAGAIN |
需要注意的是,如果要访问的文件的锁类型与要执行的操作存在冲突,那么采用阻塞读/写操作的进程会被阻塞,而采用非阻塞读/写操作的进程则不会阻塞,而是立即返回 EAGAIN。另外,unlink() 系统调用并不会受到强制锁的影响,原因在于一个文件可能存在多个硬链接,此时删除文件时并不会修改文件本身的内容,而是只会改变其父目录中 dentry 的内容。
四、如何对文件加强制性锁
要想对一个文件采用强制锁,必须按照以下步骤执行:
使用 -o mand 选项来挂载文件系统。这样在执行 mount() 系统调用时,会传入 MS_MANDLOCK 标记,从而将 super_block 结构中的 s_flags 设置为 1,用来表示在这个文件系统上可以采用强制锁。例如:
# mount -o mand /dev/sdb7 /mnt
# mount | grep mnt
/dev/sdb7 on /mnt type ext3 (rw,mand)
1.修改要加强制锁的文件的权限:设置 SGID 位,并清除组可执行位。这种组合通常来说是毫无意义的,系统用来表示该文件被加了强制锁。以这种方式加上的强制性锁不会影响任何现有的用户软件。强制性锁不需要新的系统调用,仍使用fcntl()。例如:
# touch /mnt/testfile
# ls -l /mnt/testfile
-rw-r--r-- 1 root root 0 Jun 22 14:43 /mnt/testfile
# chmod g+s /mnt/testfile
# chmod g-x /mnt/testfile
# ls -l /mnt/testfile -rw-r-Sr-- 1 root root 0 Jun 22 14:43 /mnt/testfil
2.使用 fcntl() 系统调用对该文件进行加锁或解锁操作。
五、Linux中关于文件锁的系统调用
这里介绍在 Linux 中与文件锁关系密切的系统调用:fcntl()。系统调用fcntl() 符合 POSIX 标准的文件锁实现,fcntl() 可以实现对记录进行加锁,fctnl既可用于劝告性锁也可用于强制性锁。另外,还有一个早期的系统调用:flock(),它只能实现对整个文件进行加锁,而不能实现记录级的加锁,在此不对其加以讨论。
fcntl() 函数的功能很多,可以改变已打开的文件的性质,本文中只是介绍其与获取/设置文件锁有关的功能。fcntl() 的函数原型如下所示:
int fcntl (int fd, int cmd, struct flock *lock);
其中,参数 fd 表示文件描述符;参数 cmd 指定要进行的锁操作,由于 fcntl() 函数功能比较多,这里先介绍与文件锁相关的三个取值 F_GETLK、F_SETLK 以及 F_SETLKW。这三个值均与 flock 结构有关。flock 结构如下所示:
struct flock{
short l_type; /*F_RDLCK,F_WRLCK,F_UNLCK*/
off_t l_start; /*offset in bytes,relative to l_whence*/
short l_whence; /*SEEK_SET,SEEK_CUR,SEEK_END*/
off_t l_len; /*length in bytes,0 means lock to EOF*/
off_t l_pid; /*returned with F_GETLK*/
};
对flock结构说明如下:
l_type指明锁类型:F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)、F_UNLCK(解锁一个区域);l_start和l_whence两者共同决定要加锁或解锁的起始字节偏移量;l_len指定区域的字节长度,若l_len为0,则表示锁的区域从其起点开始直至最大可能偏移量为止;具有能阻塞当前进程的锁,其持有进程的ID存放在l_pid中(仅有F_GETLK返回)。
对fcntl的三种命令说明如下:
F_GETLK:判断由flockptr所描述的锁是否会被另外一把锁所排斥。如果存在一把锁,它阻止创建由flockptr所描述的锁,则把现存锁的信息写到flockptr指向的结构中,如果不存在这种情况,则除了将l_type设置为F_UNLCK外,flockptr所指向结构中的其他信息保持不变。
F_SETLK:设置由flockptr所描述的锁。如果试图建立一把读锁或写锁,而按兼容性规则不能允许,则fcntl立即返回,此时errno设置为EAGAIN。
F_SETLKW:这是F_SETLK的阻塞版本。如果因为当前在所请求区间的某个部分另一个进程已经有一把锁,因而按兼容性规则由flockptr所请求的锁不能被创建,则使调用进程休眠。如果请求创建的锁已经可用,或者休眠由信号中断,则该进程被唤醒。
需要注意的是,F_GETLK 用于测试是否可以加锁,在 F_GETLK 测试可以加锁之后,F_SETLK 和 F_SETLKW 就会企图建立一把锁,但是这两者之间并不是一个原子操作,也就是说,在 F_SETLK 或者 F_SETLKW 还没有成功加锁之前,另外一个进程就有可能已经插进来加上了一把锁。而且,F_SETLKW 有可能导致程序长时间睡眠。还有,程序对某个文件拥有的各种锁会在相应的文件描述符被关闭时自动清除,程序运行结束后,其所加的各种锁也会自动清除。
六、文件锁使用举例
为了使读者更深入理解本文中介绍的内容,下面我们给出了一个例子来详细介绍文件锁的具体用法。这个例子可以用来检测所使用的文件是否支持强制锁,其源代码如下所示:
程序1 锁使用方法示例
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/stat.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));
}
#define read_lock(fd,offset,whence,len) /
lock_reg(fd,F_SETLK,F_RDLCK,offset,whence,len)
#define write_lock(fd,offset,whence,len) /
lock_reg(fd,F_SETLK,F_WRLCK,offset,whence,len)
#define err_sys(x) {perror(x);exit(1);}
int main(int argc,char *argv[ ])
{
int fd,val;
pid_t pid;
char buf[5];
struct stat statbuf;
if(argc!=2){
fprintf(stderr,"usage:%s filename/n",argv[0]);
exit(1);
}
if((fd=open(argv[1],O_RDWR | O_CREAT | O_TRUNC))<0)
err_sys("open error");
if(write(fd,"hello world",11)!=11)
err_sys("write error");
/*turn on set-group-ID and turn off group-execute*/
if(fstat(fd,&statbuf)<0)
err_sys("fstat error");
if(fchmod(fd,(statbuf.st_mode & ~S_IXGRP) | S_ISGID)<0)
err_sys("fchmod error");
sleep(2);
if((pid=fork())<0){
err_sys("fork error");
}else if(pid>0){ /*parent*/
/*write lock entire file*/
if(write_lock(fd,0,SEEK_SET,0)<0)
err_sys("write_lock error");
sleep(20); /*wait for child to set lock and read data*/
if(waitpid(pid,NULL,0)<0)
err_sys("waitpid error");
}else{ /*child*/
sleep(10); /*wait for parent to set lock*/
if((val=fcntl(fd,F_GETFL,0))<0)
err_sys("fcntl F_GETFL error");
val |= O_NONBLOCK; /*turn on O_NONBLOCK flag*/
if(fcntl(fd,F_SETFL,val)<0)
err_sys("fcntl F_SETFL error");
/*first let's see what error we get if region is locked*/
if(read_lock(fd,0,SEEK_SET,0)!=-1) /*no wait*/
err_sys("child:read_lock succeed");
printf("read_lock of already-locked region returns %d:%s/n",errno,strerror(errno));
/*now try to read mandatory locked file*/
if(lseek(fd,0,SEEK_SET)==-1)
err_sys("lseek error");
if(read(fd,buf,5)<0)
printf("read failed (mandatory locking works)/n");
else
printf("read OK (no mandatory locking),buf=%5.5s/n",buf);
}
exit(0);
}
样例代码中所采用的技术在前文中大都已经介绍过了,其基本思想是在首先在父进程中对文件加上写锁;然后在子进程中将文件描述符设置为非阻塞模式(第 69 行),然后对文件加读锁,并尝试读取该文件中的内容。如果系统支持强制锁,则子进程中的 read() 系统调用(代码中的第 81 行)会立即返回 EAGAIN;否则,等父进程完成写文件操作之后,子进程中的 read() 系统调用就会返回父进程刚刚写入的前 5 个字节的数据。代码中的几个 sleep() 是为了协调父进程与子进程之间的同步而使用的。
该程序在测试系统的执行结果如下所示:
# mount | grep mnt
/dev/sdb7 on /mnt type ext3 (rw,mand)
/dev/sdb6 on /tmp/mnt type ext3 (rw)
# ./mandlock /mnt/testfile
read_lock of already-locked region returns 11: Resource temporarily unavailable
read failed (mandatory locking works)
# ./mandlock /tmp/mnt/testfile
read_lock of already-locked region returns 11: Resource temporarily unavailable
read OK (no mandatory locking), buf = hello
我们可以看到,/dev/sdb7 使用 –o mand 选项挂载到了 /mnt 目录中,而 /dev/sdb6 则么有使用这个选项挂载到了 /tmp/mnt 目录中。由于在程序中我们完成了对测试文件 SGID 和同组可执行位的设置(第 44 行),因此 /mnt/testfile 可以支持强制锁,而 /tmp/mnt/testfile 则不能。这也正是为什么前者的 read() 系统调用会失败返回而后者则可以成功读取到 hello 的原因。
参考资料:
(1)APUE 2 Edition Section 14.3
(2)UNP 2 Edition Section 9.4,9.5
(3)http://www.ibm.com/developerworks/cn/linux/l-cn-filelock/