对于进程间对同一文件访问的同步问题。fcntl提供了一种记录锁的方法
int fcntl(int filedes,int cmd,.../*arg*/)
对于记录锁,cmd是F_GETLK ,F_SETLK和 F_SETLKW。第三个参数是一个指向flock结构的指针
flock依赖于具体的实现,但它至少包含以下成员
struct flock{
short l_type; /* F_RDLCK F_WRLCK F_UNLCK 中的一个*/
off_t l_start; /*需要加锁或解锁的第一个位置的相对位置(相对于 l_whence的位置)*/
short l_whence; /*SEEK_SET(文件头) ,SEEK_CUR(当前文件位置),SEEK_END(文件尾) 中的一个*/
off_t l_len; /*要加锁或解锁的长度*/
pid_t l_pid; /*持有锁的进程(仅有F_GETLK返回,后面会有解释)*/
}
对于flock结构我们需要做一些说明。
l_type成员的取值:
F_RDLCK: 共享锁(或读锁),可以有多个进程拥有文件上同一区域的共享锁,只要任意进程拥有一把共享锁,那么就
没有进程可以在获得该区域上的独占锁。为了获得共享锁,文件需以读或读写方式打开
F_WRLCK: 独占锁(写锁),只有一个进程可以在文件的某个区域获得一把独占锁。一旦一个进程拥有了一个区域的独占锁。
任何其他进程都无法在改区域上再获得任何锁,为了获得一把独占锁,文件需以写或读写方式打开
F_UNLCK: 解锁,用来清除锁。
以上为不同锁之间的兼容性。
注意:F_UNLCK 他只能接触自己加的锁,无法解除别的进程在某个区域加的锁。
l_whence和l_start的取值:
这两个成员共同决定欲加锁或解锁的区域的起始位置。比如l_whence=SEEK_SET l_start=5
那么加锁区域从距文件头五个位置开始。
l_len的取值:
l_len决定了加锁或解锁区域的长度,需要注意的是 如l_len=0.则加锁区域从加锁起始位置一直到文件尾,并且以后文件中添加的数据都在锁的范围内
现在我们说明三个命令的作用
F_GETLK:
它用于获取filedes(第一个参数)打开的文件的锁的信息,调用进程吧自己想创建的锁的类型信息写到flock中再传给fcntl函数,则使用F_GETLK命令
的fcntl就会返回阻止获取锁的任何信息(返回信息会覆盖flock参数中的值),如果没有阻止则只将flock中的l_type设置为F_UNLCK,其他值不变(注意它不会尝试去锁定文件,相当于是测试的意思)。
举个例子,比如我想查看当前某个区域的锁状态,我把读锁写入flock参数再传入fcntl函数调用(即测试这个区域还能不能加读锁,但只是测试,不会真的加读锁)
如果这个区域本来有写锁存在根据上面介绍的兼容性,则不能再加读锁(即阻止调用加读锁),那么fcntl中的参数flock的值就会被阻止这个读锁的锁的信息覆盖(这里就是写锁的信息会被写入flock,覆盖掉原来读锁的信息)
从上面的信息我们就可以有两种测试方法。上面我们说过l_pid仅有F_GETLCK命令返回。那么比如我现在是测试在这个区域上能不能加读锁
那么第一种方法:我把l_pid赋值为 -1(没有进程的pid为-1).然后fcntl返回后我在查看flock中的l_pid是不是等于-1.不是就说明flock被覆盖了
就说明当前区域不能加读锁
第二中方法: 测试l_type 如果是F_UNLCK就说明测试读锁成功。如果是其他值,就说明flock被覆盖了。
说明当前区域不能加读锁。
下面的测试代码。我们将用第一种方法。
F_SETLK:
设置有flcok所描述的锁。如果该区域不能加期望的锁(对照上面说的兼容性)。那么fcntl出错返回。
若l_type为F_UNLCK则清楚本进程在该区域所加的锁。
注意 改命令比不使用 l_pid成员
F_SETLKW:
这是F_SETLK命令的阻塞版本。当改区域不能加期望的锁时。则该进程进入休眠。当请求的锁被允许添加或者休眠由信号中断。则进程被唤醒
下面我们来写几个测试程序来理解上面的几个命令的作用
我们用a 程序创建一个文件然后写入五十个数据。然后在10-20区域上设置读锁 40-50区域上设置写锁。
用 b 进程 每10个字节( 五十个字节分五次测试)做一次能否加 读锁 和能否加写锁的测试。
a进程代码
1 #include
2 #include
3 #include
4 #include
5 int main(void){
6 int fd;
/*创建文件,并以读写打开*/
7 fd=open("test",O_RDWR | O_CREAT,0666);
8 if(fd==-1){
9 perror("open error");
10 exit(1);
11 }
12
13 int i;
/*写入50个数据到文件中*/
14 for(i=0;i<50;i++){
15 if(1!=write(fd,"a",1)){
16 perror("write error");
17 exit(1);
18 }
19 }
20
21 struct flock flock1,flock2;
22 /*锁 1 用来在10-20区域设置读锁*/
23 flock1.l_type=F_RDLCK;
24 flock1.l_start=10;
25 flock1.l_whence=SEEK_SET;
26 flock1.l_len=10;
27
28 /*锁 2 用来在40-50区域上设置写锁*/
29 flock2.l_type=F_WRLCK;
30 flock2.l_start=40;
31 flock2.l_whence=SEEK_SET;
32 flock2.l_len=10;
33 /*调用fcntl 来设置锁*/
34 if(fcntl(fd,F_SETLK,&flock1)==-1 || fcntl(fd,F_SETLK,&flock2)==-1){
35 perror("fcntl error");
36 exit(1);
37 }
38
39 sleep(10);
40 exit(0);
41
42 }
注意: 在a进程中 我们要让a 退出前 睡眠一会。这样好让b进程可以有时间运行。如果a进程在b进程运行之前结束了。
那么a 进程中的打开文件就会关闭,自然所加的锁也就被释放了,那么会导致测试任何加锁都会成功。
b进程代码:
1 #include
2 #include
3 #include
4 #include
5
6 #define FILE_SIZE 50
7
8 int main(void){
9 int fd;
10 struct flock test_flock;
11
12 int start_address=0;
13 int region_size=10;
14
15 fd=open("test",O_RDWR);
16 /* 下面的循环从文件头开始 每10个字节测试一次 能否加读锁和写锁*/
17 while(start_address+region_size<=FILE_SIZE){
18 /*测试能否加读锁*/
test_flock.l_type=F_RDLCK;
19 test_flock.l_start=start_address;
20 test_flock.l_whence=SEEK_SET;
21 test_flock.l_len=region_size;
22 test_flock.l_pid=-1;
23
24 fcntl(fd,F_GETLK,&test_flock);
25 /*如果l_pid改变,说明test_flock被覆盖了。即不能加锁测试的锁*/
if(test_flock.l_pid!=-1){
26 printf("can't get read_lock form %d to %d\n",
27 test_flock.l_start,
28 test_flock.l_start+test_flock.l_len);
29 }else{
30 printf("test read_lock from %d to %d succeed\n",
31 test_flock.l_start,
32 test_flock.l_start+test_flock.l_len);
33 }
34
35 /*测试能否写锁*/
36 test_flock.l_type=F_WRLCK;
37 test_flock.l_start=start_address;
38 test_flock.l_whence=SEEK_SET;
39 test_flock.l_len=region_size;
40 test_flock.l_pid=-1;
41
42 fcntl(fd,F_GETLK,&test_flock);
43 if(test_flock.l_pid!=-1){
44 printf("can't get write_lock from %d to %d\n",
45 test_flock.l_start,
46 test_flock.l_start+test_flock.l_len);
47 }else{
48 printf("test write_lock form %d to %d succeed\n",
49 test_flock.l_start,
50 test_flock.l_start+test_flock.l_len);
51 }
52 /*测试完一个区域后 开始测试下一个区域*/
53 start_address += region_size;
54 }
55
56 exit(0);
57
58 }
让a进程后台运行,然后运行b进程输出如下:
feng@ubuntu:~/learn_linux_c_second/chapter_14$ ./a &
[1] 4906
feng@ubuntu:~/learn_linux_c_second/chapter_14$ ./b
test read_lock from 0 to 10 succeed
test write_lock form 0 to 10 succeed
test read_lock from 10 to 20 succeed
can't get write_lock from 10 to 20
test read_lock from 20 to 30 succeed
test write_lock form 20 to 30 succeed
test read_lock from 30 to 40 succeed
test write_lock form 30 to 40 succeed
can't get read_lock form 40 to 50
can't get write_lock from 40 to 50
从输出我们不难看出没加锁的区域,测试读锁,写锁都成成功。但是10-20区域被a 加了读锁。所以b测试读锁成功了
测试写锁出错。
40-50区域被a添加了写锁,锁以b进程测试读锁写锁都失败。
这也 验证了我们上面所说的锁的兼容性。
那么说了那么多读写锁有什么用呢,他可以使两个进程协作运行,就是说比如a进程现在正在写一个文件的某个区域,那么a进程没写完
之前就不能有另一个进程去读,不然会造成数据错乱,同样一个进程没完读某个区域是就不能有别的进程去写这个区域,不然也会造成错乱
那么我们就可以用记录锁来协同这两个进程。
a进程想 写某个区域的时候就先加写锁,然后写完以后再解锁。在未解锁之前如果别的进程想读或写这个区域,那么就要先加读锁或写锁
如果加锁不能成功就说明有别的进程在该区域持有另一把互斥锁。
最后需要注意的一点是这里说的记录所是建议锁,就是说只用这个锁的前提的两个(或多个)进程都是合作进程,就是他们
都遵循规则,在写或读之前,先加锁。看能不能成功,能加锁说明现在能执行他要求的操作,加锁失败则说明不能执行他现在要求的操作
也就是说。比如 a进程现在在某区域加了一个写锁,b进程并不调用fcntl()执行加锁,而是直接去改或读这个区域的数据,那么些事可以读写成功的。(只是可能会造成数据错乱而已)
阅读(1851) | 评论(0) | 转发(1) |