记录锁
1、概述
记录锁是读写锁的一种扩展类型,可用于亲缘关系或无亲缘关系的进程之间共享某个文件的读与写。被锁住的文件通过文件描述符进行访问,执行上锁的操作函数是fcntl,这种类型的锁通常在内核中维护。
记录锁的功能是:一个进程正在读或修改文件的某个部分时,可以阻止其他进程修改同一文件区,即其锁定的是文件的一个区域或整个文件。记录锁有两种类型:共享读锁,独占写锁。基本规则是:多个进程在一个给定的字节上可以有一把共享的读锁,但在一个给定字节上的写锁只能有一个进程独用。即:如果在一个给定的字节上已经有一把读或多把读锁,则不能在该字节上再加写锁;如果在一个字节上已经有一把独占性的写锁,则不能再对它加任何读锁。
采用Uinx打印假脱机处理系统举例说明记录锁的作用。打印假脱机处理系统使用技巧是给每台打印机准备一个文件,它只是一个单行执行的ASCII文本文件,其中还有待用的下一个序列号,需要给某个打印作业赋一个序列号的每个进程都得经历以下三个步骤:
(1)读序列号文件
(2)使用其中的序列号
(3)给序列号加1并写回文件中
存在问题:当某个进程在执行这个三个步骤时,另一个进程可能在执行同样的三个步骤,这将导致结果混乱。
解决办法:一个进程能够设置某个锁,以宣称没有其他进程能够访问相应的文件,直到第一个进程完成访问为止。
先来看看一下不上锁的影响,程序如下:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <unistd.h>
5 #include <fcntl.h>
6 #include <errno.h>
7
8 #define SEQFILE "seqno"
9 #define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
10 #define MAXLINE 1024
11 void my_lock(int fd);
12 void my_unlock(int fd);
13
14 int main(int argc,char *argv[])
15 {
16 int fd;
17 long i;
18 ssize_t n;
19 char line[MAXLINE+1];
20 fd = open(SEQFILE,O_RDWR,FILE_MODE);
21 //执行两万次
22 for(i=0;i<20000;++i)
23 {
24 my_lock(fd);
25 lseek(fd,0L,SEEK_SET);
26 n = read(fd,line,MAXLINE);
27 line[n] = '\0';
28 seqno++;
29 snprintf(line,sizeof(line),"%ld\n",seqno);
30 lseek(fd,0L,SEEK_SET);
31 write(fd,line,strlen(line));
32 my_unlock(fd);
33 }
34 exit(0);
35 }
36 //根本不上锁函数
37 void my_lock(int fd)
38 {
39 return;
40 }
41
42 void my_unlock(int fd)
43 {
44 return;
45 }
先建立一个文件,文件中初始化序号为1,启动4个进程执行,如果正确的话结果应该是80001。实际执行结果如下:
实际结果不等于80001,每次执行结果不一致,说明四个进程都在访问文件,导致结果混乱。
2、Posix fcntl记录上锁
fcntl函数原型如下:int fcntl(int fd, int cmd, ... /* struct flock *arg */); 包含在<fcntl.h>中。
第三个参数是指向flock类型的指针:
struct flock
{
short l_type; /* F_RDLCK, F_WRLCK, F_UNLCK */
short l_whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* relative starting offset in bytes */
off_t l_len; /* #bytes; 0 means until end-of-file */
pid_t l_pid; /* PID return by F_GETLK */
};
cmd参数有三个值:
F_SETLK:获取或释放由arg指向的flock结构所描述的锁,若无法获取锁,则立刻返回一个EACCES或EAGAIN错误而不阻塞。
F_SETLKW :与F_SETLK不同的是,如果无法获取的锁,将阻塞直到获取锁为止,W即wait
F_GETLK:检查由arg指向的锁以确定是否有某个已存在的锁。若不存在锁在将arg指向的l_type成员设置为F_UNLCK,若存在锁,则返回arg所指向的flock结构信息。
flock结构描述锁的类型(读出锁或写入锁)以及待锁住的字节范围。锁定整个文件的两种方法:
(1)指定l_whence成员为SEEK_SET,l_start成员为0,l_len成员为0。(最常用方法)
(2)使用lseek把读写指针定位到文件,然后指定l_whence成员为SEEK_CUR、l_start成员为0,l_len成员为0。
注意:对于一个打开着某个文件的给定进程来说,当它关闭该文件的所有描述符或它本身终止时,与该文件关联的所有锁都被删除。锁不能通过fork由子进程继承。
现将上面的例子采用记录锁进行实现,程序如下:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <unistd.h>
5 #include <fcntl.h>
6 #include <errno.h>
7
8 #define SEQFILE "seqno"
9 #define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
10 #define MAXLINE 1024
11 void my_lock(int fd);
12 void my_unlock(int fd);
13
14 int main(int argc,char *argv[])
15 {
16 int fd;
17 long i,seqno;
18 pid_t pid;
19 ssize_t n;
20 char line[MAXLINE+1];
21
22 pid = getpid();
23 fd = open(SEQFILE,O_RDWR,FILE_MODE);
24 for(i=0;i<20000;++i)
25 {
26 my_lock(fd);
27 lseek(fd,0L,SEEK_SET);
28 n = read(fd,line,MAXLINE);
29 line[n] = '\0';
30 n = sscanf(line,"%ld\n",&seqno);
31 // printf("%s: pid = %ld,seq# = %ld\n",argv[0],(long) pid,seqno);
32 seqno++;
33 snprintf(line,sizeof(line),"%ld\n",seqno);
34 lseek(fd,0L,SEEK_SET);
35 write(fd,line,strlen(line));
36 my_unlock(fd);
37 }
38 exit(0);
39 }
40
41 void my_lock(int fd)
42 {
43 struct flock lock;
44 lock.l_type = F_WRLCK; //写入锁
45 lock.l_whence = SEEK_SET; //从文件头开始
46 lock.l_start = 0; //锁住整个文件
47 lock.l_len = 0;
48 fcntl(fd,F_SETLKW,&lock);
49 }
50
51 void my_unlock(int fd)
52 {
53 struct flock lock;
54 lock.l_type = F_UNLCK; //释放锁
55 lock.l_whence = SEEK_SET; //从文件头开始
56 lock.l_start = 0; //释放整个个文件
57 lock.l_len = 0;
58 fcntl(fd,F_SETLK,&lock);
59 }
程序执行结果如下:
从结果可以看出,四个进程执行完后,最终序号是80001,得到正确的结果。
为了简化fcntl函数的调用,可以简化用宏如下所示:
1 #define read_lock(fd,offset,whence,len) lock_reg(fd,F_SETLK,F_RDLCK,offset,whence,len)
2 #define readw_lock(fd,offset,whence,len) lock_reg(fd,F_SETLKW,F_RDLCK,offset,whence,len)
3 #define write_lock(fd,offset,whence,len) lock_reg(fd,F_SETLK,F_WRLCK,offset,whence,len)
4 #define writew_lock(fd,offset,whence,len) lock_reg(fd,F_SETLKW,F_WRLCK,offset,whence,len)
5 #define un_lock(fd,offset,whence,len) lock_reg(fd,F_SETLK,F_UNLCK,offset,whence,len)
6 #define is_read_lockable(fd,offset,whence,len) !lock_test(fd,F_RDLCK,offset,whence,len)
7 #define is_write_lockable(fd,offset,whence,len) !lock_test(fd,F_WRLCK,offset,whence,len)
8
9 int lock_reg(int fd,int cmd,int type,off_t offset,int whence,off_t len)
10 {
11 struct flock lock;
12 lock.l_type = type;
13 lock.l_start = offset;
14 lock.l_whence = whence;
15 lock.l_len = len;
16 return (fcntl(fd,cmd,&lock));
17 }
18
19 pid_t lock_test(int fd,int type,off_t offset,int whence,off_t len)
20 {
21 struct flock lock;
22 lock.l_type = type;
23 lock.l_start = offset;
24 lock.l_whence = whence;
25 lock.l_len = len;
26 if(fcntl(fd,F_GETLK,&lock) == -1)
27 {
28 return (-1); //error
29 }
//region is not locked
30 if(lock.l_type == F_UNLCK)
31 return (0);
32 return (lock.l_pid);
33 }
3、劝告性上锁
Posix记录上锁称为劝告性上锁,内核维护着已由各个进程上锁的所有文件的正确信息。针对于协作进程是足够了,但是对于非协作进程,结果是不可预料的。
4、强制性上锁
有些系统提供了强制性锁(mandatory locking)。使用强制性锁后,内核将检查每个read和write请求,以验证其操作不会干扰由某个进程持有的某个锁。对于通常的阻塞式描述字,与某个强制性锁冲突的read或write将把调用进程投入睡眠,直到该锁释放为止。对于非阻塞式描述字,与某个强制性锁冲突的read或write将导致它们返回一个EAGAIN错误。对某个特定文件施行强制性锁,应满足:
- 组成员执行位必须关闭;
- SGID位必须打开。
强制性锁不需要新的系统调用。虽然强制性上锁有一定作用,但多个进程在更新同一个文件时,仍然会导致混乱。进程之间还是需要某种上锁形式的协作。
5、读出者和写入者的优先级
例子1:某个写入锁待处理期间的额外读出锁
问题:如果某个资源已经读锁定,并有一个写入者请求在等待处理,那么是否允许另一个读出锁?
测试程序如下:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <unistd.h>
5 #include <fcntl.h>
6 #include <errno.h>
7 #include <time.h>
8 #include "public.h" //上面定义的记录锁宏文件
9
10 #define SEQFILE "seqno"
11 #define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
12 #define MAXLINE 1024
13
14 char* gf_time()
15 {
16 time_t tm;
17 struct tm *ptm;
18 char *buf = malloc(100);
19 tm = time(&tm);//获取时间
20 ptm = gmtime(&tm);
21 strftime(buf,100,"%T",ptm); //时间格式为"H:M:S"
22 return buf;
23 }
24
25 int main(int argc,char *argv[])
26 {
27 int fd;
28 fd = open("test1.data",O_RDWR | O_CREAT,FILE_MODE);
29 read_lock(fd,0,SEEK_SET,0); //父进程获得读出锁
30 printf("%s: parent has read lock.\n",gf_time());
31 if(fork() == 0) //子进程1
32 {
33 sleep(1);
34 printf("%s: first child tries to obtain write lock.\n",gf_time());
35 writew_lock(fd,0,SEEK_SET,0); //申请写入锁
36 printf("%s: first child obtains write lock.\n",gf_time());
37 sleep(2);
38 un_lock(fd,0,SEEK_SET,0);
39 printf("%s: first child release write lock.\n",gf_time());
40 exit(0);
41 }
42 if(fork() == 0) //子进程2
43 {
44 sleep(3);
45 printf("%s: second child tries to obtain read lock.\n",gf_time());
46 readw_lock(fd,0,SEEK_SET,0); //申请读出锁
47 printf("%s: second child obtains read lock.\n",gf_time());
48 sleep(4);
49 un_lock(fd,0,SEEK_SET,0);
50 printf("%s: second child releases read lock.\n",gf_time());
51 exit(0);
52 }
53 sleep(5);
54 un_lock(fd,0,SEEK_SET,0);
55 printf("%s: parent releases read lock.\n",gf_time());
56 exit(0);
57 }
程序执行结果如下:
从程序结果可以看出:在父进程持有读写锁时,子进程1比子进程2提前申请写入锁,但是子进程2申请读出锁立刻成功,子进程1等父进程和子进程2释放读出锁后才获取写入锁。这样一来,只要连续不断的发出读出锁请求,写入者就可能因获取不了写入锁而“挨饿”。
例子2:等待着的写入者是否比等待着的读出者优先
问题: 等待着的写入者是否比等待着的读出者优先?测试程序代码如下:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <unistd.h>
5 #include <fcntl.h>
6 #include <errno.h>
7 #include <time.h>
8 #include "public.h"
9
10 #define SEQFILE "seqno"
11 #define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
12 #define MAXLINE 1024
13
14 char* gf_time()
15 {
16 time_t tm;
17 struct tm *ptm;
18 char *buf = malloc(100);
19 tm = time(&tm);
20 ptm = gmtime(&tm);
21 strftime(buf,100,"%T",ptm);
22 return buf;
23 }
24
25 int main(int argc,char *argv[])
26 {
27 int fd;
28 fd = open("test1.data",O_RDWR | O_CREAT,FILE_MODE);
29 write_lock(fd,0,SEEK_SET,0); //父进程获取写入锁
30 printf("%s: parent has write lock.\n",gf_time());
31 if(fork() == 0)
32 {
33 sleep(1);
34 printf("%s: first child tries to obtain write lock.\n",gf_time());
35 writew_lock(fd,0,SEEK_SET,0); //申请写入锁
36 printf("%s: first child obtains write lock.\n",gf_time());
37 sleep(2);
38 un_lock(fd,0,SEEK_SET,0);
39 printf("%s: first child release write lock.\n",gf_time());
40 exit(0);
41 }
42 if(fork() == 0)
43 {
44 sleep(3);
45 printf("%s: second child tries to obtain read lock.\n",gf_time());
46 readw_lock(fd,0,SEEK_SET,0); //申请读出锁
47 printf("%s: second child obtains read lock.\n",gf_time());
48 sleep(4);
49 un_lock(fd,0,SEEK_SET,0);
50 printf("%s: second child releases read lock.\n",gf_time());
51 exit(0);
52 }
53 sleep(5);
54 un_lock(fd,0,SEEK_SET,0);
55 printf("%s: parent releases read lock.\n",gf_time());
56 exit(0);
57 }
程序执行结果如下:
从结果可以看出:子进程1比子进程2先获得锁,说明内核是以FIFO顺序准予上锁请求,而不管上锁请求的类型。
6、启动一个守护进程的唯一副本
记录上锁的一个常见用途是确保某个程序(守护程序)在任何时刻只有一个副本在运行。如下程序所示:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <unistd.h>
5 #include <fcntl.h>
6 #include <errno.h>
7 #include <time.h>
8 #include "public.h"
9
10 #define PATH_PIDFILE "pidfile"
11 #define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
12 #define MAXLINE 1024
13
14 int main(int argc,char *argv[])
15 {
16 int pidfd;
17 char line[MAXLINE];
18 pidfd = open(PATH_PIDFILE,O_RDWR | O_CREAT,FILE_MODE);
19 //上锁过程中进行判断
20 if(write_lock(pidfd,0,SEEK_SET,0) < 0)
21 {
22 if(errno == EACCES || errno == EAGAIN)
23 {
24 printf("unable to lock %s,is %s already running?\n",PATH_PIDFILE,argv[0]);
25 exit(-1);
26 }
27 else
28 {
29 printf("unable to lock %s",PATH_PIDFILE);
30 exit(0);
31 }
32 }
33 snprintf(line,sizeof(line),"%ld\n",(long) getpid());
34 ftruncate(pidfd,0);//clear file content
35 write(pidfd,line,strlen(line));
36 pause();
37 }
程序执行结果如下: