文件锁 flock及fcntl flock

10 篇文章 0 订阅
9 篇文章 0 订阅

原文地址:http://blog.chinaunix.net/uid-24774106-id-3488649.html


 提到了flock,不提fcntl这个锁有点不想话,毕竟fcntl这个锁才是更常见的一把锁。咱也不能拈轻怕重,逮着软柿子可劲捏,今天我们比较下这两种类型锁的异同,并从kernel实现的层面,来讲讲为啥表现不同,准备好了没,LET GO!


    上一篇博文讲到了flock系统调用那把锁是FL_FLOCK类型的锁,而fcntl创建的锁是遵循POSIX标准的,所以称为FL_POSIX类型的锁。上一篇博文做了一个实验,进程A首先申请FL_FLOCK类型的锁一把,然后fork出来子进程B,此时在启动同一个可执行程序,启动进程C,C也会首先申请FL_FLOCK锁,当然了,都是对同一个文件加排他锁。我们发现,在A进程推出后,C进程依然申请不到这把锁,直到B 进程推出,C进程才持有了这把锁。我们得到结论,fork出来的子进程,不但拷贝所有父进程的所有打开的文件(当然了同一个struct file,struct file引用计数+1), 同时也持有了父进程申请的FL_FLOCK类型的锁。这就是上篇博文的结论,当然我们没有从代码层面分析这种锁的继承性的缘由。没关系,这是我们这篇博文涉及的东西。


应用层fcntl

    首先说,我不太喜欢fcntl这个函数,因为这个函数有点瑞士军刀的意思,方便是方便了,但是这厮干的事儿有点多,不符合一个接口只干一件事,并把事情干好的UNIX哲学。不喜欢归不喜欢,但是咱也得从了。西游记说,世界尚不完美,经书怎能苛求完美。是啊,世界尚不完美,我们也没办法苛求太多。


    flock系统调用本质是给文件上锁,它比较死心眼,一锁就是整个文件,要求flock系统调用给某文件前40个字节上锁,不好意思,flock他老人家太老了,这么细的活儿干不了。但是fcntl不同了,它属于江湖晚辈,做的就比较细致了,他能够精确打击,让它给文件的某一个字节加锁,他都能办得到。OK ,闲言少叙看接口。

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

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

       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
                               (F_GETLK only) */
           ...
       }; 

    文件记录加锁相关的cmd 分三种(fcntl这厮还有其他于加锁无关的cmd):

  1. F_SETLK

    申请锁(读锁F_RDLCK,写锁F_WRLCK)或者释放所(F_UNLCK),但是如果kernel无法将锁授予本进程(被其他进程抢了先,占了锁),不傻等,返回error

  2. F_SETLKW

    和F_SETLK几乎一样,唯一的区别,这厮是个死心眼的主儿,申请不到,就傻等。

  3. F_GETLK

    这个接口是获取锁的相关信息: 这个接口会修改我们传入的struct flock。

    如果探测了一番,发现根本就没有进程对该文件指定数据段加锁,那么了l_type会被修改成F_UNLCK

    如果有进程持有了锁,那么了l_pid会返回持锁进程的PID 

参考UNIX网络编程卷2 进程间通信,将这个接口封装了下,让接口变得好用些。

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

            static 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));
            }

            static pid_t lock_test(int fd,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;

                if(fcntl(fd,F_GETLK,&lock) == -1)
                {
                    return -1;
                }
                if(lock.l_type = F_UNLCK)
                    return 0;
                return lock.l_pid;
            }

            int read_lock(int fd,off_t offset,int whence,off_t len)
            {
                return lock_reg(fd,F_SETLKW,F_RDLCK,offset,whence,len);
            }

            int read_lock_try(int fd,off_t offset,int whence,off_t len)
            {
                return lock_reg(fd,F_SETLK,F_RDLCK,offset,whence,len);
            }

            int write_lock(int fd,off_t offset,int whence,off_t len)
            {
                return lock_reg(fd,F_SETLKW,F_WRLCK,offset,whence,len);
            }

            int write_lock_try(int fd,off_t offset,int whence,off_t len)
            {
                return lock_reg(fd,F_SETLK,F_WRLCK,offset,whence,len);
            }

            int unlock(int fd,off_t offset, int whence,off_t len)
            {
                return lock_reg(fd,F_SETLK,F_UNLCK,offset,whence,len);
            }

            int is_read_lockable(int fd, off_t offset,int whence,off_t len)
            {
                return !lock_test(fd,F_RDLCK,offset,whence,len);
            }

            int is_write_lockable(int fd, off_t offset,int whence,off_t len)
            {
                return !lock_test(fd,F_WRLCK,offset,whence,len);
            } 

下面是头文件rwlock.h

    #ifndef __RWLOCK_H__
    #define __RWLOCK_H__

    int read_lock(int fd,off_t offset,int whence,off_t len);
    int read_lock_try(int fd,off_t offset,int whence,off_t len);
    int write_lock(int fd,off_t offset,int whence,off_t len);
    int write_lock_try(int fd,off_t offset,int whence,off_t len);
    int unlock(int fd,off_t offset, int whence,off_t len);
    int is_read_lockable(int fd, off_t offset,int whence,off_t len);
    int is_write_lockable(int fd, off_t offset,int whence,off_t len);

    #endif 

现在万事具备了,我们可以写我们的测试程序了。实验内容同flock系统调用一样,A进程申请锁,然后fork出B 进程,然后C进程申请锁。过一会A进程死去,B仍然活着,看下C能否申请到锁。


FL_POSIX锁父子进程继承性实验

测试程序和上一篇一样,只不过使用我们上面提到的write_lock,而不是flock函数。

            #include<stdio.h>
            #include <stdlib.h>
            #include <sys/types.h>
            #include <unistd.h>
            #include <sys/file.h>
            #include <errno.h>
            #include <string.h> 
            #include <time.h>
            #include <fcntl.h>
            #include "rwlock.h"

            int main()
            {
                char buf[128];
                time_t ltime;
                int fd = open("./tmp.txt",O_RDWR|O_APPEND);
                if(fd < 0)
                {
                    fprintf(stderr,"open failed %s\n",strerror(errno));
                    return -1;
                }

                int ret = write_lock(fd,0,SEEK_SET,0);
                if(ret)
                {
                    fprintf(stderr,"fcntl  failed for father\n");
                    return -2;
                }
                else
                {
                    time(&ltime);
                    fprintf(stderr,"%s    I got the lock\n",ctime_r(&ltime,buf));
                }

                ret = fork();
                if(ret == 0)
                {
                    time(&ltime);  
                    fprintf(stdout,"%s  I am the son process,pid is %d,ppid = %d\n",ctime_r(&ltime,buf),getpid(),getppid());
                    write(fd,"write by son\n",32);
                    sleep(100);
                    time(&ltime);
                    fprintf(stdout,"%s    son exit\n",ctime_r(&ltime,buf));
                }
                else if(ret > 0)
                {
                    time(&ltime);
                    fprintf(stdout,"%s    I am the father process,pid is %d\n",ctime_r(&ltime,buf),getpid());
                    write(fd,"write by father\n",32);
                    sleep(50);
                    close(fd);
                    time(&ltime);
                    fprintf(stdout, "%s    father exit\n",ctime_r(&ltime,buf));
                    return 0;
                }
                else
                {
                    fprintf(stderr, "error happened in fork\n");
                    return -3;
                }

            } 

A进程持有锁后,持续50秒,B进程作为子进程持续100s,C进程在A推出前创建,我们观察A死去后,C能否立刻获取FL_POSIX类型的锁 如果可以,表明锁没有继承性,子进程B并不持有锁。 如果不可以,非要等到B死去后才能申请到,那么说明父进程的锁,被继承到了子进程。


其实细心的筒子看到struct flock的l_pid大概就能猜到,锁记录了进程ID,精确归某进程所有,就不会被继承到子进程,我们验证之。

 pid_t l_pid;     /* PID of process blocking our lock
                                 (F_GETLK only) */ 

看下输出结果:

 root@manu:~/code/c/self/flock# ./fcntl_test 
    Sun Feb 10 16:14:45 2013
        I got the lock
    Sun Feb 10 16:14:45 2013
        I am the father process,pid is 6475
    Sun Feb 10 16:14:45 2013
      I am the son process,pid is 6476,ppid = 6475
    Sun Feb 10 16:15:35 2013
        father exit
    root@manu:~/code/c/self/flock# Sun Feb 10 16:16:25 2013
        son exit

    root@manu:~/code/c/self/flock# 
    root@manu:~/code/c/self/flock# ./fcntl_test 
    Sun Feb 10 16:15:35 2013
        I got the lock
    Sun Feb 10 16:15:35 2013
        I am the father process,pid is 6477
    Sun Feb 10 16:15:35 2013
      I am the son process,pid is 6482,ppid = 6477
    Sun Feb 10 16:16:25 2013
        father exit
    root@manu:~/code/c/self/flock# 

结论: 父进程A退出后,进程C就获取到了FL_POSIX锁,所以子进程不会继承FL_POSIX类型的锁。这和FL_FLOCK类型的锁是不同的。 WHY!!!


kernel分析原因

实验到了这个份上,我们就需要从内核代码分析原因了。所有的代码都在fs/locks.c,大家感兴趣可以细细參详,我只讲继承性差异的原因,为啥FL_FLOCK锁可以被继承,但是FL_POSIX只精确的属于某进程,不会被子进程继承。


注意了我们都没有主动UN_LOCK,flock我们没有调用LOCK_UN,fcntl没有调用F_UNLCK,锁的释放在close的时候去释放。 先说flock:flock在内核调用locks_delete_flock来释放锁,同时唤醒沉睡在这把锁上的其他进程。 close--->filp_close--------->fput 注意fput:

            void fput(struct file *file)
            {
                if (atomic_long_dec_and_test(&file->f_count)) {
                    struct task_struct *task = current;
                    file_sb_list_del(file);
                    if (unlikely(in_interrupt() || task->flags & PF_KTHREAD)) {
                        unsigned long flags;
                        spin_lock_irqsave(&delayed_fput_lock, flags);
                        list_add(&file->f_u.fu_list, &delayed_fput_list);
                        schedule_work(&delayed_fput_work);
                        spin_unlock_irqrestore(&delayed_fput_lock, flags);
                        return;
                    }
                    init_task_work(&file->f_u.fu_rcuhead, ____fput);
                    task_work_add(task, &file->f_u.fu_rcuhead, true);
                }
            } 

注意了,条件atomic_long_dec_and_test(&file->f_count),由于父子进程,那么父进程退出引用计数减1,仍然不会调用到里面的内容,而我们释放FL_FLOCK类型锁是在____fput,脉络如下:

                 ____fput-----> __fput----->locks_remove_flock---------->locks_delete_flock 

那么大家也就明白了,正是因为引用计数并没有减少到1,所以父进程的退出,并不会调用locks_delete_flock来唤醒等待这把锁的进程。

对于fcntl实现的FL_POSIX类型的锁,则不同,最终的释放会走到__posix_lock_file,当然了,调用F_UNLCK最终也会调到此处。当进程推出,尝试关闭进程打开的文件的时候,遵循这样的脉络


    close----->filp_close----->locks_remove_posix---->vfs_lock_file----->posix_lock_file----->__posix_lock_file 

当然走的是解锁的分支。这条路径上,没有什么条件阻止走到真正解锁的地方,所以,当进程推出的时候,FL_POSIX类型的锁就被释放了。


观察tool

我们如何观测文件锁的状况呢?比如,我们知道某文件被锁,如何知道是那个进程锁的这个文件呢?procfs提供了信息:

            root@manu:~/code/c/self/flock# ./test 
            Sun Feb 10 20:51:06 2013
                I got the lock
            Sun Feb 10 20:51:06 2013
                I am the father process,pid is 9941
            Sun Feb 10 20:51:06 2013
              I am the son process,pid is 9942,ppid = 9941

            root@manu:~/code/c/self/flock# ./fcntl_test 
            Sun Feb 10 20:51:14 2013
                I got the lock
            Sun Feb 10 20:51:14 2013
                I am the father process,pid is 9943
            Sun Feb 10 20:51:14 2013
              I am the son process,pid is 9944,ppid = 9943

              root@manu:~/code/c/classical/linux-3.6.7/fs# cat /proc/locks 
            1: POSIX  ADVISORY  WRITE 9943 08:06:2359759 0 EOF
            2: FLOCK  ADVISORY  WRITE 9941 08:06:2359759 0 EOF 

我们可以看到/proc/locks下面有锁的信息:我现在分别叙述下含义:

  1. POSIX FLOCK 这个比较明确,就是哪个类型的锁。flock系统调用产生的是FLOCK,fcntl调用F_SETLK,F_SETLKW产生的是POSIX类型

  2. ADVISORY表明是劝告锁

  3. WRITE顾名思义,是写锁,还有读锁

  4. 9943是持有锁的进程ID。当然对于flock这种类型的锁,会出现进程已经退出的状况。

  5. 08:06:2359759表示的对应磁盘文件的所在设备的主设备好,次设备号,还有文件对应的inode number。

  6. 0表示的是所的其实位置

  7. EOF表示的是结束位置。 这两个字段对fcntl类型比较有用,对flock来是总是0 和EOF。

看下/home所在的分区主设备号就是8,次设备号就是6,而我们操作的文件的inode,就是2359759

            /dev/sda6      77993572 47528652 26558672   65% /home

               8        6   78125000 sda6

            root@manu:~/code/c/self/flock# ls -li tmp.txt 
            2359759 -rw-r--r-- 1 manu root 2689  2月 10 20:51 tmp.txt 
 
 
 


本文做实验都是采用的fork产生子进程,另外system系统调用也会产生子进程,首先产生sh 子进程,sh又调起了system入参那个命令,对于system,flock会传递到子进程,fcntl产生的劝告锁不会传递到子进程,有兴趣的筒子可以自己实验。


相关代码和pdf类型的文档,已经上传到了github,欢迎大家访问:https://github.com/manuscola/rwlock,获取代码及pdf格式的文档。


参考文献

  1. 深入理解linux内核

  2. linux设备驱动程序(如何将锁的信息show出来,代码用了seq_file,这个又能写一篇博文,唉太多了)

  3. Manual

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值