linux I/O-记录锁(record lock)

        记录锁(record lock)也称字节范围锁、文件范围锁、文件段锁,是一种在文件的某个字节、某个区域进行加锁的机制,记录锁总是和进程、文件相关。本篇博客介绍的是建议性记录锁。

1 记录锁的函数原型:

  #include <unistd.h>
       #include <fcntl.h>

       int fcntl(int fd, int cmd, ... /* arg */ );

这个函数除了对文件记录锁进行操作外,还可以对文件其他属性进行操作。这里只讨论它的记录锁功能。使用记录锁时,此函数有三个参数,int fd,int cmd,struct flock *lock;第一个参数是打开的文件描述符,第二个参数是命令集合,主要有3个,第三个参数是指向记录锁结构的指针。记录锁结构如下所示:

  struct flock {
               ...
               short l_type;    /* Type of lock: F_RDLCK,
                                   F_WRLCK, F_UNLCK */
               short l_whence;  /* How to interpret l_start:
                                   SEEK_SET, SEEK_CUR, SEEK_END */
               off_t l_start;   /* Starting offset for lock */
               off_t l_len;     /* Number of bytes to lock */
               pid_t l_pid;     /* PID of process blocking our lock
                                   (set by F_GETLK and F_OFD_GETLK) */
               ...
           };

(1)l_type是指记录锁的的类型,有三种:F_RDLCK、F_WRLCK、 F_UNLCK,是共享读锁、独占写锁、解锁。加 读锁要求描述符要为读而打开,加写锁要求描述符为写而打开,最后一个是解锁。解锁的方法有三个:使用F_UNLCK、关闭文件描述符、进程终止。

(2)l_whence 是要加锁的文件位置,SEEK_SET代表从文件开始处,SEEK_CUR是从文件当前位置处、SEEK_END是代表从文件结尾处。

(3)l_start 锁的起始偏移值,在l_whence 值确定后,确定相对于l_whence 的起始偏移值,对于SEEK_CUR和SEEK_END,l_start可为负值。

(4)l_len 从起始偏移值开始的文件长度。如果l_len=0,代表从l_whence、l_start 确定的位置一直到文件末尾,无论向文件中添加多少数据。

(5)l_pid是进程pid值,只有cmd 为F_GETLK时,才使用此参数,代表加锁的进程ID。

2 第二个参数的三种情况

(1)F_GETLCK。测试一个指定的锁(即第三个参数lock)能否加上。如果能加上,则lock锁并不实际加锁到指定的位置,而是只是将lock.l_type设置未F_UNLCK,lock结构的其他参数保持不变。如果测试不能加上,则lock返回的是文件当前锁的l_type、l_whence 、l_start 、l_len和l_pid。锁的互斥性如下表所示。

当前区域状态请求读锁请求写锁
无锁允许允许
共享读锁允许拒绝
独占写锁拒绝拒绝

 注意此函数的返回第三个参数并非一直是文件当前锁的信息,而是只有当尝试加锁失败时,lock才返回当前已加锁的锁信息。也就是测试锁的状态会有以下几种结果

(a)当前无锁,用读锁和写锁同时测试,则所得结果均为F_UNLCK

  (b)当前为读锁,用读锁测试结果为F_UNLCK,用写锁测试结果为F_WRLCK  

(c)当前为写锁,用读锁和写锁测试,结果均为F_WRLCK;

另外对于进程来说,

(a)一个进程对文件的一个位置加了锁,如果后续此进程在同一位置再加锁,则新锁将代替旧锁。

   (b)一个进程解锁只能解自己加的锁,而不能解锁其它进程加的锁。

   (c)利用fork产生的子进程,子进程不能继承父进程已有的锁,它需要重新获得自己的锁才行。

   (d)进程不能测试自己的锁,因为进程对自己加的锁可以更换任何锁,尝试加锁总是成功的,所以进程测试自己的锁,得到的一直是F_UNLCK。

源程序locktest1.c(编程环境gcc 8.3.1,内核版本linu-4.18.0)

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <string.h>
#include <unistd.h>

int lock_reg(int fd,int cmd,int type,off_t offset,int whence,off_t len)
{
struct flock lock;
lock.l_type=type;
lock.l_start=offset;
lock.l_whence=whence;
lock.l_len=len;
return(fcntl(fd,cmd,&lock));
}

define read_lock(fd, offset, whence, len) \
            lock_reg((fd), F_SETLK, F_RDLCK, (offset), (whence), (len))
#define readw_lock(fd, offset, whence, len) \
            lock_reg((fd), F_SETLKW, F_RDLCK, (offset), (whence), (len))
#define write_lock(fd, offset, whence, len) \
            lock_reg((fd), F_SETLK, F_WRLCK, (offset), (whence), (len))
#define writew_lock(fd, offset, whence, len) \
            lock_reg((fd), F_SETLKW, F_WRLCK, (offset), (whence), (len))
#define un_lock(fd, offset, whence, len) \
            lock_reg((fd), F_SETLK, F_UNLCK, (offset), (whence), (len))
pid_t mylock_test(int fd,int type,off_t offset,int whence,off_t len,char *s)
{

        struct flock lock;
        lock.l_type=type;
        lock.l_start=offset;
        lock.l_whence=whence;
        lock.l_len=len;

        if(fcntl(fd,F_GETLK,&lock)<0)
        {
                perror("fcntl error");
                exit(1);
        }
        if(lock.l_type==F_UNLCK)
                return (0);
        if(lock.l_type==F_RDLCK)
                strcpy(s,"F_RDLCK");
        else if(lock.l_type==F_WRLCK)
                strcpy(s,"F_WRLCK");
        return(lock.l_pid);
}
int main(void)
{
int fd,result;
char lockname[]="F_UNLCK";
int n;
pid_t pid;

if((fd=open("templock",O_RDWR|O_CREAT|O_TRUNC,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH))<0)
{
        perror("open error");
        exit(1);
}
if((n=write(fd,"abcdefg",7))!=7)
{
        perror("wrtie error");
        exit(1);
}
// 0字节不加锁,1字节加读锁
if((result=read_lock(fd,1,SEEK_SET,1))<0)
{
        perror("read_lock error");
        exit(1);
}
//2字节加写锁
if((result=write_lock(fd,2,SEEK_SET,1))<0)
{
        perror("write_lock error");
        exit(1);
}

if((pid=fork())<0)
{
        perror("fork error");
        exit(1);

}
else if(pid==0){
        //子进程测试父进程加的锁
        if(mylock_test(fd,F_WRLCK,0,SEEK_SET,1,lockname)!=0) //用写锁测试0字节
                printf("test write lock,child found %s at 0th byte\n",lockname);
        else printf("test write lock,child found no lock at 0th byte\n");

        if(mylock_test(fd,F_RDLCK,1,SEEK_SET,1,lockname)!=0) //用读锁测试1字节
                printf("test read lock,child found %s  at 1st byte\n",lockname);
        else printf("test read lock,child found no lock at 1st byte\n");

        if(mylock_test(fd,F_WRLCK,1,SEEK_SET,1,lockname)!=0) //用写锁测试1字节
                printf("test write lock,child found %s at 1st byte\n",lockname);
        else printf("test write lock,child found no lock at 1st byte\n");

        if(mylock_test(fd,F_RDLCK,2,SEEK_SET,1,lockname)!=0) //用读锁测试2字节
                printf("test read lock,child found %s at 2nd byte\n",lockname);
        else printf("test read lock,child found no lock at 2nd byte\n");

        if(mylock_test(fd,F_WRLCK,2,SEEK_SET,1,lockname)!=0) //用写锁测试2字节
                printf("test write lock,child found %s at 2nd byte\n",lockname);
        else printf("test write lock,child found no lock at 2nd byte\n");

}
else {
        //父进程测试自己的锁,在0字节、1字节、2字节上均用写锁测试
        if(mylock_test(fd,F_WRLCK,0,SEEK_SET,1,lockname)!=0)
                printf("test write lock,parent found %s at 0th byte\n",lockname);
        else printf("test write lock,parent found no lock at 0th byte\n");

        if(mylock_test(fd,F_WRLCK,1,SEEK_SET,1,lockname)!=0)
                printf("test write lock,parent found %s  at 1st byte\n",lockname);
        else printf("test write lock,parent found no lock at 1st byte\n");

        if(mylock_test(fd,F_WRLCK,2,SEEK_SET,1,lockname)!=0)
                printf("test write lock,parent found %s at 2nd byte\n",lockname);
        else printf("test write lock,parent found no lock at 2nd byte\n");

        if(result=waitpid(pid,NULL,0)!=pid)
        {
                perror("waitpid error");
                exit(1);
        }
}
exit(0);
}

输出结果如下:

如上图所示,父进程测试自己的任何锁,得到的结果均为F_UNLCK;第一字节加的读锁,子进程用读锁测试得到的是F_UNLCK,用写锁测试得到的是F_RDLCK;第二字节加的写锁,子进程用读锁和写锁测试得到的结果均为F_WRLCK,所以用写锁测试能得到当前锁的真实状态。

(2)F_SETLK。对指定位置的区域进行加锁(l_type是F_RDLCK或F_WRLCK)或解锁(l_type是F_UNLCK)操作。如果根据锁的互斥性原则,导致加锁失败,则函数立即返回-1,并赋值errno为EACCES、EAGAIN。个人认为一般情况下,一个进程对一个描述符的特定位置解锁总是 成功的,因为默认情况下,进程对特定位置是不加锁的,同时它又不能解锁其他进程的锁,所以一般来说加解锁总是成功的。

(3)F_SETLKW,是加锁或解锁的阻塞版本,也就是当加锁失败时,函数将阻塞休眠,W代表wait;当下列条件发生时,进程被唤醒

(a)已具备加锁条件

(b)进程休眠期间,捕捉到一个信号并从信号处理程序中返回,则函数返回-1,并赋值errno为EINTR。

在此要说的是进程具备条件后,被唤醒的时间。源程序locktest2.c(只写主函数,其他参考locktest1.c)

int main(void)
{
        int fd,result;
        pid_t pid;
        int n;
        char lockname[]="F_UNLCK";

        if((fd=open("templock2",O_RDWR|O_CREAT|O_TRUNC,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH))<0)
        {
                perror("open error");
                exit(1);
        }
        if((n=write(fd,"abcdefg",7))!=7)
        {
                perror("wrtie error");
                exit(1);
        }
// 0字节加写锁
if((result=write_lock(fd,0,SEEK_SET,1))<0)
{
        perror("write_lock error");
        exit(1);
}

if((pid=fork())<0)
        {
        perror("fork error");
        exit(1);
        }
else if(pid==0)
        {
        if(mylock_test(fd,F_WRLCK,0,SEEK_SET,1,lockname)!=0) //用写锁测试0字节
                printf("test write lock,child found %s at 0th byte\n",lockname);
        else printf("test write lock,child found no lock at 0th byte\n");

        if(writew_lock(fd,0,SEEK_SET,1)<0)//阻塞获得写锁
                 {
                        perror("child writew_lock error");
                        exit(1);
                 }
        else
                printf("child add write lock\n");
        close(fd);
        }
else {
        sleep(2);// 父进程休眠2秒钟,让子进程先执行,并阻塞
        if(un_lock(fd,0,SEEK_SET,1)<0)
                 {
                perror("parent un_lock error");
                exit(1);
                 }
        printf("parent unlock 0th bytes\n");
        if(result=waitpid(pid,NULL,0)!=pid)
        {
                perror("waitpid error");
                exit(1);
        }
close(fd);
}
exit(0);
}

程序执行结果如下:

从程序输出结果来看,父进程解锁0字节后,子进程被唤醒,并打印“child add write lock”,然后父进程继续执行并打印"parent unlock 0th bytes"。

3 其他说明

(1)函数返回值。刚才说了,根据第二个参数的不同,函数有不同的返回值,正常情况下返回0,出错情况下会返回-1,并赋值errno。常见的错误码如下:

(a)EACCESS或EAGAIN。加锁失败

(b)EBADF。文件描述符尚未打开或文件描述符打开的模式不对,操作无法执行

(c)EINTR:F_SETLK或F_SETLKW操作时,被信号中断

(d)EINVAL。第二个参数无法识别或参数无效

(2)如果进程关闭指向一个文件的任何一个描述符,则此进程加在文件的所有锁都会失效,不管进程是在哪个描述符上获得的。也就是说使用dup、open操作的使同一个文件,关闭这个文件上打开的任何一个描述符,锁均被释放。

(3) 进程的所有线程共享进程的锁。

修改locktest2.c为循环的情况,本程序locktest3.c

int main(void)
{
        int fd,result;
        pid_t pid;
        int n;
        char lockname[]="F_UNLCK";

        if((fd=open("templock2",O_RDWR|O_CREAT|O_TRUNC,S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH))<0)
        {
                perror("open error");
                exit(1);
        }
        if((n=write(fd,"abcdefg",7))!=7)
        {
                perror("wrtie error");
                exit(1);
        }
// 0字节加写锁
if((result=write_lock(fd,0,SEEK_SET,1))<0)
{
        perror("write_lock error");
        exit(1);
}

if((pid=fork())<0)
        {
        perror("fork error");
        exit(1);
        }
else if(pid==0)
        {
        if(mylock_test(fd,F_WRLCK,0,SEEK_SET,1,lockname)!=0) //用写锁测试0字节
                printf("test write lock,child found %s at 0th byte\n",lockname);
        else printf("test write lock,child found no lock at 0th byte\n");

        for(int i=0;i<5;i++){
                if(writew_lock(fd,0,SEEK_SET,1)<0)//阻塞获得写锁
                 {
                        perror("child writew_lock error");
                        exit(1);
                 }
                else
                printf("child add write lock\n");
                if(un_lock(fd,0,SEEK_SET,1)<0) //释放获得的锁
                 {
                        perror("child unlock error");
                        exit(1);
                 }


}
        close(fd);
        }
else {
        sleep(2);// 父进程休眠2秒钟,让子进程先执行,并阻塞
        for(int i=0;i<5;i++){
                if(un_lock(fd,0,SEEK_SET,1)<0)
                        {
                        perror("parent un_lock error");
                        exit(1);
                        }
                printf("parent unlock 0th bytes\n");
                if(readw_lock(fd,0,SEEK_SET,1)<0) //阻塞获得读锁
                 {
                        perror("parent readw_lock error");
                        exit(1);
                 }

                }
                if(result=waitpid(pid,NULL,0)!=pid)
                {
                        perror("waitpid error");
                        exit(1);
                }
        close(fd);
}
exit(0);
}

输出结果如下:

从结果看出,程序不是交替输出,也就是子进程 获得写锁并释放后,父进程没有立即执行,而是由子进程进入下一个循环。子进程循环结束后,锁被释放,父进程才又开始执行。

 

 

 

 

 

 

参考资料:

1 https://man7.org/linux/man-pages/man2/fcntl.2.html

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值