记录锁

简介:

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 }
复制代码

程序执行结果如下:

相关文章
|
6天前
|
NoSQL Cloud Native Redis
Redis核心开发者的新征程:阿里云与Valkey社区的技术融合与创新
阿里云瑶池数据库团队后续将持续参与Valkey社区,如过往在Redis社区一样耕耘,为开源社区作出持续贡献。
Redis核心开发者的新征程:阿里云与Valkey社区的技术融合与创新
|
6天前
|
关系型数据库 分布式数据库 数据库
PolarDB闪电助攻,《香肠派对》百亿好友关系实现毫秒级查询
PolarDB分布式版助力《香肠派对》实现百亿好友关系20万QPS的毫秒级查询。
PolarDB闪电助攻,《香肠派对》百亿好友关系实现毫秒级查询
|
7天前
|
消息中间件 Cloud Native Serverless
RocketMQ 事件驱动:云时代的事件驱动有啥不同?
本文深入探讨了云时代 EDA 的新内涵及它在云时代再次流行的主要驱动力,包括技术驱动力和商业驱动力,随后重点介绍了 RocketMQ 5.0 推出的子产品 EventBridge,并通过几个云时代事件驱动的典型案例,进一步叙述了云时代事件驱动的常见场景和最佳实践。
115029 1
|
8天前
|
弹性计算 安全 API
访问控制(RAM)|云上安全使用AccessKey的最佳实践
集中管控AK/SK的生命周期,可以极大降低AK/SK管理和使用成本,同时通过加密和轮转的方式,保证AK/SK的安全使用,本次分享为您介绍产品原理,以及具体的使用步骤。
101801 1
|
7天前
|
自然语言处理 Cloud Native Serverless
通义灵码牵手阿里云函数计算 FC ,打造智能编码新体验
近日,通义灵码正式进驻函数计算 FC WebIDE,让使用函数计算产品的开发者在其熟悉的云端集成开发环境中,无需再次登录即可使用通义灵码的智能编程能力,实现开发效率与代码质量的双重提升。
95384 2
Doodle Jump — 使用Flutter&Flame开发游戏真不错!
用Flutter&Flame开发游戏是一种什么体验?最近网上冲浪的时候,我偶然发现了一个国外的游戏网站,类似于国内的4399。在浏览时,我遇到了一款经典的小游戏:Doodle Jump...
112727 12
|
12天前
|
SQL 存储 JSON
Flink+Paimon+Hologres 构建实时湖仓数据分析
本文整理自阿里云高级专家喻良,在 Flink Forward Asia 2023 主会场的分享。
71312 1
Flink+Paimon+Hologres 构建实时湖仓数据分析
|
15天前
|
弹性计算 运维 安全
访问控制(RAM)|云上程序使用临时凭证的最佳实践
STS临时访问凭证是阿里云提供的一种临时访问权限管理服务,通过STS获取可以自定义时效和访问权限的临时身份凭证,减少长期访问密钥(AccessKey)泄露的风险。本文将为您介绍产品原理,以及具体的使用步骤。
151041 4
|
14天前
|
监控 负载均衡 Java
深入探究Java微服务架构:Spring Cloud概论
**摘要:** 本文深入探讨了Java微服务架构中的Spring Cloud,解释了微服务架构如何解决传统单体架构的局限性,如松耦合、独立部署、可伸缩性和容错性。Spring Cloud作为一个基于Spring Boot的开源框架,提供了服务注册与发现、负载均衡、断路器、配置中心、API网关等组件,简化了微服务的开发、部署和管理。文章详细介绍了Spring Cloud的核心模块,如Eureka、Ribbon、Hystrix、Config、Zuul和Sleuth,并通过一个电商微服务系统的实战案例展示了如何使用Spring Cloud构建微服务应用。
103517 9
|
15天前
|
Java 数据处理 调度
更高效准确的数据库内部任务调度实践,阿里云数据库SelectDB 内核 Apache Doris 内置 Job Scheduler 的实现与应用
Apache Doris 2.1 引入了内置的 Job Scheduler,旨在解决依赖外部调度系统的问题,提供秒级精确的定时任务管理。