文件锁定是多用户、多任务操作系统中一个非常重要的组成部分。程序经常需要共享数据,而这通常是通过文件来实现的。因此,对于这些程序来说,建立某种控制文件的方式就非常重要了。只有这样,文件才可以通过一种安全的方式更新,或者说,当一个程序正在对文件进行写操作时,文件就会进入一个暂时状态,在这个状态下,如果另外一个程序尝试读这个文件,它就会自动停下来等待这个状态的结束。
Linux提供了多种特性来实现文件锁定。其中最简单的方法就是以原子操作的方式创建锁文件,所谓“原子操作”就是在创建锁文件时,系统将不允许任何其他的事情发生。这就给程序提供了一种方式来确保它所创建的文件是唯一的,而且这个文件不可能被其他程序在同一时刻创建。
第二种方法更高级一些,它允许程序锁定文件的一部分,从而可以独享对这一部分内容的访问。有两种不同的方式可以实现第二种形式的文件锁定。我们将只对其中的一种做详细介绍,因为两种方式非常相似——第二种方式只不过是程序接口稍微不同而已。
创建锁文件
许多应用程序只需要能够针对某个资源创建一个锁文件即可。然后,其他程序就可以通过检查这个文件来判断它们自己是否被允许访问这个资源。
这些锁文件通常都被放置在一个特定位置,并带有一个与被控制资源相关的文件名。例如,当一个调制解调器正在被使用时,Linux通常会在/var/spool目录下创建一个锁文件。
注意,锁文件仅仅只是充当一个指示器的角色,程序间需要通过相互协作来使用它们。用术语来说,锁文件只是建议锁,而不是强制锁,在后者中,系统将强制锁的行为。
为了创建一个用作锁指示器的文件,你可以使用在fcntl.h头文件中定义的open系统调用,并带上O_CREAT和O_EXCL标志。这样能够以一个原子操作同时完成两项工作:确认文件不存在,然后创建它。
/* lock1.c */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
int main()
{
int file_desc;
int save_errno;
file_desc = open("LCK.test",O_RDWR|O_CREAT|O_EXCL,0444);
if (file_desc == -1)
{
save_errno = errno;
perror( "error : ");
printf( "open failed with error %d\n", save_errno);
}
else
{
printf( " Open succeededin");
}
exit(EXIT_SUCCESS);
}
这个程序调用带有O_CREAT和O_EXCL标志的open来创建文件LCK.test。第一次运行程序时,由于文件并不存在,所以open调用成功。但对程序的后续调用失败了,因为文件已经存在了。如果想让程序再次执行成功,你必须删除那个锁文件。
至少在Linux系统中,错误号17代表的是EEXIST,这个错误用来表示一个文件已存在。错误号定义在头文件errno.h或(更常见的)它所包含的头文件中。在本例中,这个错误号实际定义在头文件/usr/include/ asm-generic/errno-base.h中:
#define EEXIST 17 /*File exists*/
这是一个适合于表示open(O_CREAT| O_EXCL)失败的错误号。
如果一个程序在它执行时,只需独占某个资源一段很短的时间——这用术语来说,通常被称为临界区,它就需要在进入临界区之前使用open系统调用创建锁文件,然后在退出临界区时用urlink系统调用删除该锁文件。
你可以通过编写一个示例程序并同时运行它的两份副本,来演示程序是如何利用这个锁机制来协调工作的。
/* lock2.c */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
const char *lock_file = "LCK.test2";
int main()
{
fork();
int file_desc;
int tries = 10;
while (tries--)
{
file_desc = open (lock_file, O_RDWR| O_CREAT|O_EXCL, 0444);
if (file_desc == -1)
{
printf("%d - Lock already present\n", getpid());
sleep(3);
}
else
{
printf("%d-I have exclusive access\n", getpid());
sleep(1);
close(file_desc);
unlink(lock_file);
sleep(2);
}
}
exit(EXIT_SUCCESS);
}
出于演示目的,你使用while语句让程序循环10次。这个程序然后通过创建一个唯一的锁件LCK.test2来访问临界资源。如果因为文件已存在而失败,程序将等候一小段时间后再次尝试。如果成功,它就可以开始访问资源。在标记为“临界区”的部分,你可以执行任何需要独占式访问的处理。
因为这只是一个演示程序,所以你只等待了一小段时间。程序使用完资源后,它将通过删除锁文件来释放锁。然后它可以在重新申请锁之前执行一些其他的处理(本例中只是调用sleep函数)。这里锁文件扮演了类似二进制信号量的角色,就问题“我可以使用这个资源吗?”给每个程序一个“是”或“否”的答案。
这是一个进程间协调性的安排,你必须正确地编写代码以使其正常工作,意识到这一点是非常重要的。当程序创建锁文件失败时,它不能通过删除文件并重新尝试的方法来解决此问题。或许这样做可以让它创建锁文件,但另一个创建锁文件的程序将无法得知它已经不再拥有对这个资源的独占式访问权了。
区域锁定
用创建锁文件的方法来控制对诸如串行口或不经常访问的文件之类的资源的独占式访问,是一个不错的选择,但它并不适用于访问大型的共享文件。假设你有一个大文件,它由一个程序写入数据,但却由许多不同的程序同时对这个文件进行更新。当一个程序负责记录长期以来连续收集到的数据,而其他一些程序负责对记录的数据进行处理时,这种情况就可能发生。处理程序不能等待记录程序结束,因为记录程序将一直不停地运行,所以它们需要一些协调方法来提供对同一个文件的并发访问。
你可以通过锁定文件区域的方法来解决这个问题,文件中的某个特定部分被锁定了,但其他程序可以访问这个文件中的其他部分。这被称为文件段锁定或文件区域锁定。Linux提供了至少两种方式来实现这一功能:使用fcntl系统调用和使用lockf调用。我们将主要介绍fcntl接口,因为它是最常使用的接口。lockf和fcntl非常相似,在Linux中,它一般作为fcntl的备选接口。但是,fcnt1和lockf的锁定机制不能同时工作:它们使用不同的底层实现,因此决不要混合使用这两种类型的调用,而应坚持使用其中的一种。
#include <fcntl.h>
int fcntl (int fi1des, int command, ..- ):
fcntl对一个打开的文件描述符进行操作,并能根据command参数的设置完成不同的任务。它为我们提供了3个用于文件锁定的命令选项:
F_GETLK
F_SETLK
F_SETLKW
当使用这些命令选项时,fcntl的第三个参数必须是一个指向flock结构的指针,所以实际的函数原型应为:
int fcntl(int fildes, int command,struct flock *flock_structure);
flock(文件锁)结构依赖具体的实现,但它至少包含下述成员:
short l_type
short l_whence
off_t l_start
o