锁区域
创建锁文件对于资源的排他访问,例如串口,是相当合适的,但是对于访问大的共享文件就是太好了。假如我们拥有一个由一个程序写入的大文件,但是是由许多不同的程序进行持续更新的。当一个程序正在记录一些在较长的时间内所得到的数据,并且正在为其他的一些程序进行处理时就会出现这样的情况。这些正在处理的程序并不会等待日志程序结束--他们是连续运行的--所以他们需要一些合作的方法从而可以提供对于同一个文件的同时访问。
我们可以通过锁住文件的一个区域来到达这种结果,这样只是文件的某一个区域被锁住,但是其他程序可以访问程序的其他部分。这称之为文件段(file-segment),或是文件区域(file-region)。Linux有两种方法可以做到这一点:使用fcntl系统调用与使用lockf调用。我们会主要了解fcntl接口,因为这是最经常用到的接口。lockf是相对较为简单的,并且在Linux上只是fcntl的替换接口用法。然而,fcntl与lockf锁机制不可以同时工作:他们使用不同的底层实现,所以我们不能混用这两种调用;只使用这一种或是另一种。
我们在第3章介绍了fcntl调用。其定义如下:
#include <fcntl.h>
int fcntl(int fildes, int command, ...);
fcntl在文件描述符上进行操作,并且依据command参数可以执行不同的任务。而我们所感兴趣的有关文件锁的三个:
❑ F_GETLK
❑ F_SETLK
❑ F_SETLKW
当我们使用这些命令时,第三个参数必须是一个指向struct flock的指针,所以实际上的原型形式如下:
int fcntl(int fildes, int command, struct flock *flock_structure);
flock结构是依赖于实现的,但是他至少包含下面的成员:
❑ short l_type;
❑ short l_whence;
❑ off_t l_start;
❑ off_t l_len;
❑ pid_t l_pid;
l_type成员可以是几个值中的一个,这些值通常定义在fcntl.h中。如下表所示:
值 描述
F_RDLCK 共享锁(或读锁)。多个进程可以在文件的相同区域(或重叠)具有一个共享锁。如果任何进程在文件的某一部分具有一个共享锁,其他的进程就不可以在相同的区域获得排他锁。为了获得一个共享锁,文件必须使用读或是读写访问模式打开。
F_UNLCK 解锁;用于清除锁。
F_WRLCK 排他锁(或写锁)。在文件的某一个特定区域只可以有一个进程获得排他锁。一旦有一个进程具有一个这样的锁,其他的进程就不可以在此区域上获得任何锁类型。要获得一个排他锁,文件必须以写或是读写模式打开。
l_whence成员定义了一个文件中的区域--一个连续的字节集合。l_whence的值必须是SEEK_SET,SEEK_CUR,SEEK_END中的一个(定义在unistd.h)中。他们分别对应于开始位置,当前位置以及文件结尾。l_whence定义了相对于l_start的偏移,l_start为区域的第一个字节。通常,这个值为SEEK_SET,所以l_start通常由文件的开始处算起。l_len参数定义了区域中的字节数。
l_pid参数用于报告存放锁的进程。由后面的F_GETLK更详细的描述了这一点。
文件中的第一个字节在任意时刻只能具有一种锁类型,或者是共享锁,或者是排他锁,或者是解锁。
fcntl调用的命令与选项有几种组合,下面我们会依次进行讨论。
F_GETLK命令
第一个命令是F_GETLK。他会获得打开的文件fildes的锁信息。他并不会尝试为文件加锁。这个调用进程传递他希望创建的锁的类型信息,并且使用F_GETLK命令的fcntl调用会返回阻止加锁的信息。
flock结构所使用的值如下表所示:
值 描述
l_type 或者是共享锁F_RDLCK,或者是排他锁F_WRLCK
l_whence SEEK_SET,SEEK_CUR,SEEK_END其中的一个
l_start 感兴趣文件区域的起始字节
l_len 感兴趣的文件区域中的字节数
l_pid 带有锁的进程的标识符
一个进程可以使用F_GETLK来确定一个文件区域的加锁状态。他应该设置flock结构来标识他所需要的锁类型并且定义所感兴趣的文件区域。如果fcntl调用成功则会返回一个非-1的值。如果文件已经具有会阻止所请求执行锁的锁时,他就会使用相应的信息来覆盖flock结构。如果请求锁成功,flock结构则不会发生变化。如果F_GETLK调用不能获取相应的信息,他就会返回-1来标识失败。
如果F_GETLK调用成功(例如,他返回一个非-1的值),调用程序必须检测flock结构的内容以确定他是否被修改。因为l_pid的值会被设置为锁进程的值(如果查找成功),这是一个确定flock结构是否发生变化的合理区域。
F_SETLK命令
这个集合会尝试加锁或是解锁fildes所指向的文件区域。flock结构中会用到的值(与F_GETLK所用到的值不同)如下表所示:
值 描述
l_type l_type可以只读或是共享的F_RDLCK或是F_WRLCK。或者是只读或是共享的F_RDLCK;或者是排他或是写入的F_WRLCK;或者是解锁的F_UNLCK。
l_pid 不使用
如果加锁成功,fcntl会返回一个非-1的值;如果失败,则会返回-1。函数调用会立刻返回。
F_SETLKW命令
F_SETLKW命令与上面的F_SETLK命令相似,所不同的是如果他不能获得锁,他就会等待,直到可以获得锁为止。一旦这个调用开始等待,他就只会在可以获得锁或是有信号发生时才返回。我们会在第11章讨论信号
程序在一个文件上加的所有锁都会在相应的文件描述符关闭时进行原子清除。当程序结束时也会自动执行这些动作。
使用锁进行读写操作
当我们在一个文件区域上使用锁时,使用底层的read与write调用来访问文件中的数据是相当重要的,而不是高层的fread与fwrite。这是必须的,因为fread与fwrite会在库在执行数据的读写缓冲,所以执行一个fread调用来读取一个文件的前100个字节也许(事实上,通常会这样)会读取多于100个字节,并且在库中缓冲其余的数据。如果程序然后使用fread来读取接下来的100个字节,他实际上会读取缓冲在库中的数据,并且不允许底层的read调用由文件中读取更多的数据。
要了解为什么这是一个问题,考虑两个程序要更新同一个文件。假设这个文件由200个字节的全0数据组成。第一个程序首先运行,并且在文件的前100个字节上获得了一个写锁。然后他使用fread来在这个100个字节中进行读取。然而,正如我们在前面的章节所看到的,fread会一次读取直到BUFSIZ字节的数据,所以实际上他会将所有文件读取到内存中,但只会将前100个字节传递回程序。
然后第二个程序启动。他在程序的后100个字节上获得一个写锁。这也会成功,因为第一个程序只锁住了前100个字节。第二个程序在100到199字节上写入2,然后关闭文件,解锁,退出。第一个程序然后锁住文件的后100个字节,并且调用fread来读取。因为数据进行了缓冲,程序实际上看到的是100个0,而不是文件中实际存在的100个2。当我们使用read与write时就不会出这样的问题。
上面所描述的加锁也许会有些复杂,但是使用起来并没有描述的那样困难。
试验--使用fcntl锁住文件
下面让我们来看一下文件锁是如何工作的:lock3.c。要试验文件锁,我们需要两个文件:一个用于锁住文件,一个用于测试。第一个程序实现锁功能。
1 程序代码以必要的文件包含和变量声明开始:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
const char *test_file = “/tmp/test_lock”;
int main()
{
int file_desc;
int byte_count;
char *byte_to_write = “A”;
struct flock region_1;
struct flock region_2;
int res;
2 打开一个文件描述符:
file_desc = open(test_file, O_RDWR | O_CREAT, 0666);
if (!file_desc) {
fprintf(stderr, “Unable to open %s for read/write/n”, test_file);
exit(EXIT_FAILURE);
}
3 在文件中写入一些数据:
for(byte_count = 0; byte_count < 100; byte_count++) {
(void)write(file_desc, byte_to_write, 1);
}
4 使用共享锁设置区域1,由10到30字节:
region_1.l_type = F_RDLCK;
region_1.l_whence = SEEK_SET;
region_1.l_start = 10;
region_1.l_len = 20;
5 使用排他锁设置区域2,由40到50字节:
region_2.l_type = F_WRLCK;
region_2.l_whence = SEEK_SET;
region_2.l_start = 40;
region_2.l_len = 10;
6 现在锁住文件:
printf(“Process %d locking file/n”, getpid());
res = fcntl(file_desc, F_SETLK, ®ion_1);
if (res == -1) fprintf(stderr, “Failed to lock region 1/n”);
res = fcntl(file_desc, F_SETLK, ®ion_2);
if (res == -1) fprintf(stderr, “Failed to lock region 2/n”);
7 然后等待一会:
sleep(60);
printf(“Process %d closing file/n”, getpid());
close(file_desc);
exit(EXIT_SUCCESS);
}
工作原理
这个程序首先创建了一个文件,以读写的方式打开,并向其中填充一些数据。然后他设置两个区域:第一个由10到30字节,使用共享锁,而第二个由40到50字节,使用排他锁。然后程序调用fcntl来锁住两个区域,并且在程序关闭文件退出之前等待一会。
就程序本身而言,程序并没有多大用处。我们需要另一个文件来测试文件锁,lock4.c。
试验--在文件上测试文件锁
下面我们来编写一个程序来测试文件上不同区域的锁类型。
1 如平常一样,我们的程序代码以必要的文件包含和变量声明开始:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
const char *test_file = “/tmp/test_lock”;
#define SIZE_TO_TRY 5
void show_lock_info(struct flock *to_show);
int main()
{
int file_desc;
int res;
struct flock region_to_test;
int start_byte;
2 打开一个文件描述符:
file_desc = open(test_file, O_RDWR | O_CREAT, 0666);
if (!file_desc) {
fprintf(stderr, “Unable to open %s for read/write”, test_file);
exit(EXIT_FAILURE);
}
for (start_byte = 0; start_byte < 99; start_byte += SIZE_TO_TRY) {
3 设置我们希望测试的文件区域:
region_to_test.l_type = F_WRLCK;
region_to_test.l_whence = SEEK_SET;
region_to_test.l_start = start_byte;
region_to_test.l_len = SIZE_TO_TRY;
region_to_test.l_pid = -1;
printf(“Testing F_WRLCK on region from %d to %d/n”,
start_byte, start_byte + SIZE_TO_TRY);
4 测试文件锁:
res = fcntl(file_desc, F_GETLK, ®ion_to_test);
if (res == -1) {
fprintf(stderr, “F_GETLK failed/n”);
exit(EXIT_FAILURE);
}
if (region_to_test.l_pid != -1) {
printf(“Lock would fail. F_GETLK returned:/n”);
show_lock_info(®ion_to_test);
}
else {
printf(“F_WRLCK - Lock would succeed/n”);
}
5 使用共享锁重复此操作。再次设置我们希望测试的文件区域:
region_to_test.l_type = F_RDLCK;
region_to_test.l_whence = SEEK_SET;
region_to_test.l_start = start_byte;
region_to_test.l_len = SIZE_TO_TRY;
region_to_test.l_pid = -1;
printf(“Testing F_RDLCK on region from %d to %d/n”,
start_byte, start_byte + SIZE_TO_TRY);
6 再次测试文件锁:
res = fcntl(file_desc, F_GETLK, ®ion_to_test);
if (res == -1) {
fprintf(stderr, “F_GETLK failed/n”);
exit(EXIT_FAILURE);
}
if (region_to_test.l_pid != -1) {
printf(“Lock would fail. F_GETLK returned:/n”);
show_lock_info(®ion_to_test);
}
else {
printf(“F_RDLCK - Lock would succeed/n”);
}
}
close(file_desc);
exit(EXIT_SUCCESS);
}
void show_lock_info(struct flock *to_show) {
printf(“/tl_type %d, “, to_show->l_type);
printf(“l_whence %d, “, to_show->l_whence);
printf(“l_start %d, “, (int)to_show->l_start);
printf(“l_len %d, “, (int)to_show->l_len);
printf(“l_pid %d/n”, to_show->l_pid);
}
要测试文件锁,我们首先需要运行lock3程序;然后我们运行lock4程序来测试锁文件。我们可以用下面的命令来使得lock3程序在后台运行:
$ ./lock3 &
$ process 1534 locking file
返回命令提示符是因为lock3在后台运行,然后我们用下面的命令来运行lock4程序:
$ ./lock4
我们得到的程序输出如下所示:
Testing F_WRLOCK on region from 0 to 5
F_WRLCK - Lock would succeed
Testing F_RDLOCK on region from 0 to 5
F_RDLCK - Lock would succeed
...
Testing F_WRLOCK on region from 10 to 15
Lock would fail. F_GETLK returned:
l_type 0, l_whence 0, l_start 10, l_len 20, l_pid 1534
Testing F_RDLOCK on region from 10 to 15
F_RDLCK - Lock would succeed
Testing F_WRLOCK on region from 15 to 20
Lock would fail. F_GETLK returned:
l_type 0, l_whence 0, l_start 10, l_len 20, l_pid 1534
Testing F_RDLOCK on region from 15 to 20
F_RDLCK - Lock would succeed
...
Testing F_WRLOCK on region from 25 to 30
Lock would fail. F_GETLK returned:
l_type 0, l_whence 0, l_start 10, l_len 20, l_pid 1534
Testing F_RDLOCK on region from 25 to 30
F_RDLCK - Lock would succeed
...
Testing F_WRLOCK on region from 40 to 45
Lock would fail. F_GETLK returned:
l_type 1, l_whence 0, l_start 40, l_len 10, l_pid 1534
Testing F_RDLOCK on region from 40 to 45
Lock would fail. F_GETLK returned:
l_type 1, l_whence 0, l_start 40, l_len 10, l_pid 1534
...
Testing F_RDLOCK on region from 95 to 100
F_RDLCK - Lock would succeed
工作原理
对于文件中的每五个字节组,lock4设置一个区域结构来测试文件锁,然后程序使用这个区域结构进行测试以确定其为读锁还是写锁。返回的显示了区域字节数,相对于零字节的偏移量,这会使得锁请求失败。因为返回结构的l_pid部分包含当前使得文件锁住的程序的进程标识,程序将其设置为-1,然后当fcntl调用返回时,程序会检测其是否发生了改变。如果所测试的区域并没有被加锁,l_pid就不会发生改变。
要理解程序的输出,我们需要查看所包含的fcntl.h文件,由此我们可以了解l_type的值为1是由F_WRLCK的值为1时得来的,而l_type的值为0是由F_RDLCK的值为0得来的。所以,l_type的值为1告诉我们由于排他的写锁而加锁失败,而l_type的值0是由已经存在读锁而引起的。在文件中这块区域lock3程序并没有加锁,所以共享锁与排他锁都是成功的。
由10到30字节,我们可以看到他可能会获得一个共享锁,因为lock3程序此处存在的是共享锁,而不是排他锁。在40到50字节区域,两种类型的锁都会失败,因为lock3程序在此处区域加了排他锁。