分类: LINUX
2012-05-14 23:00:39
Linux锁的技术文档
第一节Unix支持的文件锁技术介绍Unix系统允许多个进程同时对一个文件进行读写,虽然每一个read或write调用本身是原子的,但内核在两个读写操作之间并没有加以同步,因此当一个进程多次调用read来读文件时,其它进程有可能在两次read之间改变该文件,造成文件数据的随机性冲突。为解决此类并发进程对共享文件的访问控制问题,Unix系统设计了文件锁技术。
1.1 读锁与写锁
Unix系统对文件加锁有两种粒度:文件锁和记录锁,文件锁用来锁定整个文件,而记录锁可以锁定文件的部分区域甚至一个字节,进程通过为文件设置多个记录锁,可以实现文件中不同区域的数据的读写同步,因此记录锁最为常见。记录锁根据访问方式的不同,又分为读锁和写锁。读锁允许多个进程同时进行读操作,也称共享锁。文件加了读锁就不能再设置写锁,但仍允许其他进程在同一区域再设置读锁。写锁的主要目的是隔离文件使所写内容不被其他进程的读写干扰,以保证数据的完整性。写锁一旦加上,只有上锁的人可以操作,其他进程无论读还是写只有等待写锁释放后才能执行,故写锁又称互斥锁,写锁与任何锁都必须互斥使用。
| 是否满足请求 | |
当前加上的锁 | 共享锁(读锁) | 排他锁(写锁) |
无 | 是 | 是 |
共享锁(读锁) | 是 | 否 |
排他锁(写锁) | 否 | 否 |
1. 锁间的兼容关系
1.2 建议锁和强制锁
Unix文件锁根据实现机制的不同,又可分为建议锁和强制锁两种类型。建议锁由应用层实现,内核只为用户提供程序接口,并不参与锁的控制和协调,也不对读写操作做内部检查和强制保护,其工作原理类似于信号量机制,用户首先定义并初始化特定的锁,再根据进程间的的关系来调用相应的锁操作,只有所有进程都严格遵循锁的使用规则,才能有效防止同步错误,否则,只要有一个例外,整个锁的功能就会被破坏。
强制锁则由内核强制实施,每当有进程调用read或write时,内核都要检查读写操作是否与已加的锁冲突,如果冲突,阻塞方式下该进程将被阻塞直到锁被释放,非阻塞方式下系统将立即以错误返回。显然,使用强制锁来控制对已锁文件或文件区域的访问,是更安全可靠的同步形式,适用于网络连接、终端或串并行端口之类须独占使用的设备文件,因为对用户都可读的文件加一把强制读锁,就能使其他人不能再写该文件,从而保证了设备的独占使用。由于强制锁运行在内核空间,处理机从用户空间切换到内核空间,系统开销大,影响性能,所以应用程序很少使用。建议锁开销小,可移植性好,符合POSIX标准的文件锁实现,在数据库系统中应用广泛,特别是当多个进程交叉读写文件的不同部分时,建议锁有更好的并行性和实时性。
文件存在锁的类型 | 阻塞描述符,试图 | 非阻塞描述符,试图 | ||
read | write | read | write | |
读锁 | 允许 | 阻塞 | 允许 | EAGAIN错误 |
写锁 | 阻塞 | 阻塞 | EAGAIN错误 | EAGAIN错误 |
表2强制性锁对其他进程读、写的影响
系统 | 建议性锁 | 强制性锁 |
Linux2.4.22 | 支持 | 支持 |
Solaris 9 | 支持 | 支持 |
Mac OS X 10.3 | 支持 | 不支持 |
FreeBSD 5.2.1 | 支持 | 不支持 |
表3 不同系统的支持情况
第二节 文件锁的总体实现和设置
2.1 内核相关的数据结构
Unix文件锁是在共享索引节点共享文件的情况下设计的,因此与锁机制相关的内核数据结构主要有虚拟索引节点(V-node)、系统打开文件表、进程打开文件表等数据表项。其中V-node中的索引节点(i-node)中含有一个指向锁链表的首指针,该锁链表主要由对文件所加的全部的锁通过指针钩链而成。只要进程为文件或区域加锁成功,内核就创建锁结构file lock,并根据用户设定的参数初始化后将其插入锁链表flock中。flock是锁存在的唯一标志,也是内核感知、管理及控制文件锁的主要依据,其结构和成员参见图1中的struct flock。图1描述了进程为打开的文件设置文件锁的内核相关表项及各数据结构之间的关联,从中可以看出,锁同时与进程和文件相关,当进程终止或文件退出时即使是意外退出,进程对文件所加的锁将全部释放。
图1 文件锁相关数据结构示意
2.2 锁的使用
Unix提供了3个文件锁相关的系统调用:flock、lockf和fcntl。其中fcntl功能强大,使用灵活,移植性好,既支持建议式记录锁也支持强制式记录锁,函数原型定义为:int fcntl(int fd,int cmd,int arg),参数fd表示需要加锁的文件的描述符;参数cmd指定要进行的锁操作,如设置、解除及测试锁等;参数arg是指向锁结构flock的指针。
无论哪种类型的文件锁,使用的流程是确定的:首先以匹配的方式打开文件,然后各进程调用上锁操作同步对已锁区域的读写,读或写完成时再调用解锁操作,最后关闭文件。锁操作代码的编写方法基本类似,首先形成适当的flock结构,然后调用fcntl完成实际的锁操作。为避免每次分配flock并填充各成员的重复工作,可以预先定义专门的函数来完成频繁调用的上锁和解锁操作。下列代码就是为一个文件设置读锁的实例。
int set_read_lock(intfd, int cmd, int type, off_t start, int whence, off_t len)
{
struct flock lock;
lock.l_type=F_RDLCK;
lock.l_pid=getpid();
lock.l_whence=SEEK_SET;
lock.l_start=0;
lock.l_len=0;
return (fcntl(fd, F_SETLKW, &lock));
}
2.3 强制性锁的设置
强制性锁的设置需要两个步骤:第一,对要加锁的文件需要设置相关权限,打开set-group-ID位,关闭group-execute位。第二,让linux支持强制性锁,需要执行命令,挂载文件系统, mount /dev/sda1 /mnt –o mand。
# mount -o mand/dev/sdb7 /mnt
# mount | grep mnt
/dev/sdb7 on /mnt typeext3 (rw,mand)
# touch /mnt/testfile# ls -l /mnt/testfile -rw-r--r-- 1 root root 0 Jun 22 14:43 /mnt/testfile# chmod g+s /mnt/testfile # chmod g-x /mnt/testfile# ls -l /mnt/testfile -rw-r-Sr-- 1 root root 0 Jun 22 14:43 /mnt/testfile
附件中的测试程序(lock.c)可以测试是否支持强制性锁。
2.4 设置强制性锁注意问题
在设置强制性锁注意的问题:几乎所有的操作都需要root权限。
1. 首先,su到root权限下,输入:mount 可以看到挂载的所有内容,如果所用磁盘存在其他挂载路径,要让mand成功,必须是:所用磁盘(sda1)被挂载的方式有mand存在。
比如存在: /dev/sda1 on /boot type ext3(rw)
/dev/sda1 on /opt/327/XXX typeext3(rw, mand)
则不成功。
需要: /dev/sda1 on /boot typeext3(rw, mand)
/dev/sda1 on /opt/327/XXX type ext3(rw,mand)
2.mount /dev/sda1 /opt/327/XXX/ -o mand,把sda1磁盘挂载到/opt/327/XXX/目录下,此时,把新文件存在opt/327/XXX/下,实际是存在sda1磁盘中。
3. 此时/opt/327/XXX/的权限变成了root,只用root权限的用户才能修改这个文件夹下的内容。
第三节 三种加锁方法详细介绍和linux内部实现
3.1 Posix lock加锁的方法(fcntl):
3.1.1使用介绍:
fcntl() 函数的功能很多,可以改变已打开的文件的性质,本文中只是介绍其与获取/设置文件锁有关的功能。fcntl() 的函数原型如下所示:
int fcntl (int fd, int cmd, struct flock *lock); |
其中,参数 fd 表示文件描述符;参数 cmd 指定要进行的锁操作,由于 fcntl() 函数功能比较多,这里先介绍与文件锁相关的三个取值 F_GETLK、F_SETLK 以及 F_SETLKW。这三个值均与 flock 结构有关。flock 结构如下所示:
flock 结构:
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) */ ... }; |
在 flock 结构中,l_type 用来指明创建的是共享锁还是排他锁,其取值有三种:F_RDLCK(共享锁)、F_WRLCK(排他锁)和F_UNLCK(删除之前建立的锁);l_pid 指明了该锁的拥有者;l_whence、l_start 和l_end 这些字段指明了进程需要对文件的哪个区域进行加锁,这个区域是一个连续的字节集合。因此,进程可以对同一个文件的不同部分加不同的锁。l_whence 必须是 SEEK_SET、SEEK_CUR 或 SEEK_END 这几个值中的一个,它们分别对应着文件头、当前位置和文件尾。l_whence 定义了相对于 l_start 的偏移量,l_start 是从文件开始计算的。
可以执行的操作包括:
需要注意的是,F_GETLK 用于测试是否可以加锁,在 F_GETLK 测试可以加锁之后,F_SETLK 和 F_SETLKW 就会企图建立一把锁,但是这两者之间并不是一个原子操作,也就是说,在 F_SETLK 或者 F_SETLKW 还没有成功加锁之前,另外一个进程就有可能已经插进来加上了一把锁。而且,F_SETLKW 有可能导致程序长时间睡眠。还有,程序对某个文件拥有的各种锁会在相应的文件描述符被关闭时自动清除,程序运行结束后,其所加的各种锁也会自动清除。
fcntl() 既可以用于劝告锁,也可以用于强制锁,在默认情况下,它用于劝告锁。如果它用于强制锁,当进程对某个文件进行了读或写这样的系统调用时,系统则会检查该文件的锁的 O_NONBLOCK 标识,该标识是文件状态标识的一种,如果设置文件状态标识的时候设置了 O_NONBLOCK,则该进程会出错返回;否则,该进程被阻塞。cmd 参数的值 F_SETFL 可以用于设置文件状态标识。
3.1.2 Posix lock 内部机制:
1) 调用fcntl(fd, F_SETLK, &lock), fcntl()方法调用内核的do_fcntl()方法(linux/fs/fcntl.c)
static long do_fcntl(unsigned int fd, unsigned intcmd, unsigned long arg, struct file * filp)
{
switch (cmd) {
.....
case F_GETLK: /*Posix Lock 操作*/
err = fcntl_getlk(fd, (struct flock *) arg);
break;
case F_SETLK:
case F_SETLKW:
err = fcntl_setlk(fd, cmd, (struct flock *) arg);
break;
...
}
2) do_fcntl()里调用fcntl_setlk()(linux/fs/flock.c)
int fcntl_setlk(unsigned int fd, struct file *filp,unsigned int cmd, struct flock __user *l)
{
structfile_lock *file_lock = locks_alloc_lock();
struct flock flock;
......
//把把客户传来的flock __user l拷贝为flock
if (copy_from_user(&flock, l, sizeof(flock)))
goto out;
//把客户传来的flock的锁l变为file_lock
error =flock_to_posix_lock(filp, file_lock, &flock);
......
//阻塞的设置
if (cmd ==F_SETLKW) {
file_lock->fl_flags|= FL_SLEEP;
}
error = -EBADF;
//设置不同类型的锁信息
switch(flock.l_type) {
case F_RDLCK:
if(!(filp->f_mode & FMODE_READ))
gotoout;
break;
case F_WRLCK:
if(!(filp->f_mode & FMODE_WRITE))
gotoout;
break;
case F_UNLCK:
break;
default:
error =-EINVAL;
goto out;
//锁文件filp
error = do_lock_file_wait(filp, cmd, file_lock);
spin_lock(¤t->files->file_lock);
f = fcheck(fd);
spin_unlock(¤t->files->file_lock);
......
}
3) fcntl_setlk()调用do_lock_file_wait()
static int do_lock_file_wait(struct file *filp, unsignedint cmd, struct file_lock *fl)
{
......
error =security_file_lock(filp, fl->fl_type);
......
//关键
error =vfs_lock_file(filp, cmd, fl, NULL);
......
}
4) do_lock_file_wait()调用vfs_lock_file()
int vfs_lock_file(struct file *filp, unsigned int cmd,struct file_lock *fl, struct file_lock *conf)
{
if (filp->f_op&& filp->f_op->lock)
returnfilp->f_op->lock(filp, cmd, fl);
else
//关键
returnposix_lock_file(filp, fl, conf);
}
5) vfs_lock_file()调用posix_lock_file()
int posix_lock_file(struct file *filp, struct file_lock*fl, struct file_lock *conflock)
{
return__posix_lock_file(filp->f_path.dentry->d_inode, fl, conflock);
}
6) posix_lock_file()调用__posix_lock_file()
__posix_lock_file()是真正锁文件的函数,它具有检测冲突锁的功能。具体代码看locks.c中的__posix_lock_file()。
大致流程是这样:
遍历inode->i_flock,找是POSIX锁且有冲突的锁(用posix_locks_conflict()函数),若存在或者有阻塞标志或者死锁了,做一定处理退出;若不存在这样的锁,就可以锁文件。再次遍历inode->i_flock,找到属于自己进程的锁,如果检测到了锁并判断锁类型,处理记录相交的部分,设置锁属性,最后按照一定方式插入锁;如果未检测到锁,则插入锁。
3.1.3两种锁检测冲突:
· 建议锁(Advisory lock)
系统默认的锁,检测两锁是否冲突的函数:
posix_locks_conflict(structfile_lock *caller_fl, struct file_lock *sys_fl)函数过程:
先判断两把锁是否属于同一进程,自己不会和自己冲突。
如果它们两是不同进程的锁,判断是否有锁定区域重合。
· 强制性锁(Mandatory lock)
检测强制性锁冲突的函数,满足条件则加锁,locks_mandatory_area()函数过程:
函数里调用了posix_locks_conflict()用于判断冲突锁,调用__posix_lock_file()函数用于加锁。
3.1.3 说明一个问题:如果文件被加了强制性锁,为什么系统调用read(),write()就会受到限制?
看系统调用的read的实现方法:
1) 内核函数 sys_read() 是 read 系统调用在该层的入口点(read_write.c):
SYSCALL_DEFINE3(read, unsigned int, fd,char __user *, buf, size_t, count)
2) Sys_read()里调用vfs_read()(read_write.c)
3) vfs_read()里调用rw_verify_area(READ,file, pos, count);
4) rw_verify_area()调用了mandatory_lock()和locks_mandatory_area()(mandatory_lock在fs.h中,locks_mandatory_area在locks.c中)
5) mandatory_lock()调用了IS_MANDLOCK()和__mandatory_lock(),其中,IS_MANDLOCK()判断文件系统是否有MS_MANDLOCK,__mandatory_lock()判断文件是否setgid和去掉组执行位
6) locks_mandatory_area()检查是否有锁冲突,若有冲突,读操作出错。
结果:所以系统调用read就不能在有强制性锁的情况下读文件。Write和open是类似的道理。
3.2 flock的加锁方法(flock):
3.2.1 使用介绍:
flock() 的函数原型如下所示:
int flock(int fd, int operation); |
其中,参数 fd 表示文件描述符;参数 operation 指定要进行的锁操作,该参数的取值有如下几种:LOCK_SH, LOCK_EX, LOCK_UN 和 LOCK_MANDphost2008-07-03T00:00:00
man page 里面没有提到,其各自的意思如下所示:
通常情况下,如果加锁请求不能被立即满足,那么系统调用 flock() 会阻塞当前进程。比如,进程想要请求一个排他锁,但此时,已经由其他进程获取了这个锁,那么该进程将会被阻塞。如果想要在没有获得这个排他锁的情况下不阻塞该进程,可以将 LOCK_NB 和 LOCK_SH 或者 LOCK_EX 联合使用,那么系统就不会阻塞该进程。flock() 所加的锁会对整个文件起作用。
说明:共享模式强制锁可以用于某些私有网络文件系统,如果某个文件被加上了共享模式强制锁,那么其他进程打开该文件的时候不能与该文件的共享模式强制锁所设置的访问模式相冲突。但是由于可移植性不好,因此并不建议使用这种锁。
3.2.2 flock的内部机制:
flock和posix lock都共享一组机制,系统调用fcntl() 符合 POSIX 标准的文件锁实现,功能比flock强大,可以支持记录锁。flock() 系统调用是从 BSD 中衍生出来的,只能支持整个文件的锁。
flock_lock_file()与posix lock机制类似,详细看代码(locks.c)
3.3 lease锁的方法:
3.3.1使用说明:
系统调用 fcntl() 还可以用于租借锁,此时采用的函数原型如下:
int fcntl(int fd, int cmd, long arg); |
与租借锁相关的 cmd 参数的取值有两种:F_SETLEASE 和 F_GETLEASE。其含义如下所示:
某个进程可能会对文件执行其他一些系统调用(比如 OPEN() 或者 TRUNCATE()),如果这些系统调用与该文件上由 F_SETLEASE 所设置的租借锁相冲突,内核就会阻塞这个系统调用;同时,内核会给拥有这个租借锁的进程发信号,告知此事。拥有此租借锁的进程会对该信号进行反馈,它可能会删除这个租借锁,也可能会减短这个租借锁的租约,从而可以使得该文件可以被其他进程所访问。如果拥有租借锁的进程不能在给定时间内完成上述操作,那么系统会强制帮它完成。通过 F_SETLEASE 命令将 arg 参数指定为 F_UNLCK 就可以删除这个租借锁。不管对该租借锁减短租约或者干脆删除的操作是进程自愿的还是内核强迫的,只要被阻塞的系统调用还没有被发出该调用的进程解除阻塞,那么系统就会允许这个系统调用执行。即使被阻塞的系统调用因为某些原因被解除阻塞,但是上面对租借锁减短租约或者删除这个过程还是会执行的。
需要注意的是,租借锁也只能对整个文件生效,而无法实现记录级的加锁。
采用强制锁之后,如果一个进程对某个文件拥有写锁,只要它不释放这个锁,就会导致访问该文件的其他进程全部被阻塞或不断失败重试;即使该进程只拥有读锁,也会造成后续更新该文件的进程的阻塞。为了解决这个问题,Linux 中采用了一种新型的租借锁。
当进程尝试打开一个被租借锁保护的文件时,该进程会被阻塞,同时,在一定时间内拥有该文件租借锁的进程会收到一个信号。收到信号之后,拥有该文件租借锁的进程会首先更新文件,从而保证了文件内容的一致性,接着,该进程释放这个租借锁。如果拥有租借锁的进程在一定的时间间隔内没有完成工作,内核就会自动删除这个租借锁或者将该锁进行降级,从而允许被阻塞的进程继续工作。
系统默认的这段间隔时间是 45 秒钟,定义如下:
137 int lease_break_time = 45; |
这个参数可以通过修改 /proc/sys/fs/lease-break-time 进行调节(当然,/proc/sys/fs/leases-enable 必须为 1 才行)。
3.3.2 leaseLock内部实现:
在fs/locks.c中:
fcntl_getlease():查询目前活跃的租借锁
fcntl_setlease():为打开的文件设置一个租借锁
__break_lease():这个函数是在open和truncate的时候进行的租约判定
第四节 测试结果
4.1 JAVA锁、C锁和系统之间的关系
JAVA是用文件Channel中的tryLock()和lock()方法来对文件加锁。
下表是在建议锁的情况下的测试结果:
JAVA和JAVA之间的锁: l Test1程序先锁住一个文件file1,Test2可以检测到file1上有锁。 l 在windows下,Test1程序先锁住一个文件file1,Test2不可以直接读写文件,报异常:IOException。 l 在linux下,Test1程序先锁住一个文件file1,Test2可以直接读写文件! |
JAVA和C之间的锁: l 如果文件被加锁,相互可以检测到锁的存在,但仍然可以直接往文件写数据。 |
JAVA和系统之间的锁: l 文件被加锁,cp和vi等系统命令,仍然可以对文件进行读写。Vi很多版本用的是劝告锁。 |
下表是在强制性锁的情况下的测试结果:
JAVA和JAVA之间的锁: l Java程序Test1先锁住一个文件file1,Java程序Test2(lock)去锁file1,则Test2会阻塞,直到Test2获得锁。 l Java程序Test1先锁住一个文件file1,Java程序Test2(tryLock)去锁file1,则Test2会报异常:FileNotFoundException。 l Java程序Test1先锁住一个文件file1,Java程序Test2去读file1文件,则Test2被阻塞,直到Test2获得锁,读操作成功。 l Java程序Test1先锁住一个文件file1,Java程序Test2去写file1文件,则Test2被阻塞,直到Test2获得锁,写操作成功。 |
JAVA和C之间的锁: l 如果文件被c程序加锁,java程序读文件被阻塞,直到java程序获得锁,读取成功。 l 如果文件被c程序加锁,java程序写文件被阻塞,直到java程序获得锁,写入成功。 l 如果文件被java程序加锁,c程序读文件被阻塞,直到c程序获得锁,读取成功。 l 如果文件被java程序加锁,c程序写文件被阻塞,直到c程序获得锁,写入成功。
l 如果文件已被c程序加锁,java程序用lock()方法也去锁文件,java程序会阻塞,直到java程序获得锁。 l 如果文件已被c程序加锁,java程序用tryLock()方法也去锁文件,java程序报异常:FileNotFoundException。 l 如果文件已被java程序加锁,c程序用F_SETLK方法也去锁文件,c程序报打开错误(因为去锁文件,都需要有open获得文件描述符fd)。 l 如果文件已被java程序加锁,c程序用F_SETLKW方法也去锁文件,c程序报打开错误(因为去锁文件,都需要有open获得文件描述符fd)。 |
JAVA和系统之间的锁: l cp命令: 1. java程序锁住文件file1,系统命令cp file2 file1,返回错误: cannot create regular file `file1`: Resource temporarily unavailable 2. java程序锁住文件file1,系统命令cp file1 file2,被阻塞,直到java程序获得锁,cp方可成功。 l vi命令: java程序锁住文件file1,此时vi file1,被阻塞,直到vi获得锁,才可打开文件。 |
参考文献: