提到了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#include 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):
-
F_SETLK
申请锁(读锁F_RDLCK,写锁F_WRLCK)或者释放所(F_UNLCK),但是如果kernel无法将锁授予本进程(被其他进程抢了先,占了锁),不傻等,返回error
-
F_SETLKW
和F_SETLK几乎一样,唯一的区别,这厮是个死心眼的主儿,申请不到,就傻等。
-
F_GETLK
这个接口是获取锁的相关信息: 这个接口会修改我们传入的struct flock。
如果探测了一番,发现根本就没有进程对该文件指定数据段加锁,那么了l_type会被修改成F_UNLCK
如果有进程持有了锁,那么了l_pid会返回持锁进程的PID
参考UNIX网络编程卷2 进程间通信,将这个接口封装了下,让接口变得好用些。
#include#include 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#include #include #include #include #include #include #include #include #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(<ime); fprintf(stderr,"%s I got the lock\n",ctime_r(<ime,buf)); } ret = fork(); if(ret == 0) { time(<ime); fprintf(stdout,"%s I am the son process,pid is %d,ppid = %d\n",ctime_r(<ime,buf),getpid(),getppid()); write(fd,"write by son\n",32); sleep(100); time(<ime); fprintf(stdout,"%s son exit\n",ctime_r(<ime,buf)); } else if(ret > 0) { time(<ime); fprintf(stdout,"%s I am the father process,pid is %d\n",ctime_r(<ime,buf),getpid()); write(fd,"write by father\n",32); sleep(50); close(fd); time(<ime); fprintf(stdout, "%s father exit\n",ctime_r(<ime,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下面有锁的信息:我现在分别叙述下含义:
-
POSIX FLOCK 这个比较明确,就是哪个类型的锁。flock系统调用产生的是FLOCK,fcntl调用F_SETLK,F_SETLKW产生的是POSIX类型
-
ADVISORY表明是劝告锁
-
WRITE顾名思义,是写锁,还有读锁
-
9943是持有锁的进程ID。当然对于flock这种类型的锁,会出现进程已经退出的状况。
-
08:06:2359759表示的对应磁盘文件的所在设备的主设备好,次设备号,还有文件对应的inode number。
-
0表示的是所的其实位置
-
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,欢迎大家访问:,获取代码及pdf格式的文档。
参考文献
-
深入理解linux内核
-
linux设备驱动程序(如何将锁的信息show出来,代码用了seq_file,这个又能写一篇博文,唉太多了)
- Manual