Linux锁的技术文档
第一节Unix支持的文件锁技术介绍
在多任务操作系统环境中,如果一个进程尝试对正在被其他进程读取的文件进行写操作,可能会导致正在进行读操作的进程读取到一些被破坏或者不完整的数据;如果两个进程并发对同一个文件进行写操作,可能会导致该文件遭到破坏。因此,为了避免发生这种问题,必须要采用某种机制来解决多个进程并发访问同一个文件时所面临的同步问题,由此而产生了文件加锁方面的技术。
早期的 UNIX 系统只支持对整个文件进行加锁,因此无法运行数据库之类的程序,因为此类程序需要实现记录级的加锁。在 System V Release 3 中,通过 fcntl 提供了记录级的加锁,此后发展成为 POSIX 标准的一部分。本文将基于 2.6.23 版本的内核来探讨 Linux 中文件锁的相关技术。
Linux 支持的文件锁技术主要包括劝告锁(advisory lock)和强制锁(mandatory lock)这两种。此外,Linux 中还引入了两种强制锁的变种形式:共享模式强制锁(share-mode mandatory lock)和租借锁(lease)。
1.1 读锁与写锁
Unix系统对文件加锁有两种粒度:文件锁和记录锁,文件锁用来锁定整个文件,而记录锁可以锁定文件的部分区域甚至一个字节,进程通过为文件设置多个记录锁,可以实现文件中不同区域的数据的读写同步,因此记录锁最为常见。
记录锁根据访问方式的不同,又分为读锁和写锁。
读锁允许多个进程同时进行读操作,也称共享锁。文件加了读锁就不能再设置写锁,但仍允许其他进程在同一区域再设置读锁。
写锁的主要目的是隔离文件使所写内容不被其他进程的读写干扰,以保证数据的完整性。写锁一旦加上,只有上锁的人可以操作,其他进程无论读还是写只有等待写锁释放后才能执行,故写锁又称互斥锁,写锁与任何锁都必须互斥使用。
在 Linux 中,不论进程是在使用劝告锁还是强制锁,它都可以同时使用共享锁和排他锁(又称为读锁和写锁)。
| 是否满足请求 | |
当前加上的锁 | 共享锁(读锁) | 排他锁(写锁) |
无 | 是 | 是 |
共享锁(读锁) | 是 | 否 |
排他锁(写锁) | 否 | 否 |
表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 不同系统的支持情况
另外,unlink() 系统调用并不会受到强制锁的影响,原因在于一个文件可能存在多个硬链接,此时删除文件时并不会修改文件本身的内容,而是只会改变其父目录中 dentry 的内容。
然而,在有些应用中并不适合使用强制锁,所以索引节点结构中的 i_flags 字段中定义了一个标志位MS_MANDLOCK用于有选择地允许或者不允许对一个文件使用强制锁。在 super_block 结构中,也可以将 s_flags 这个标志为设置为1或者0,用以表示整个设备上的文件是否允许使用强制锁,1允许,0不允许。
要想对一个文件采用强制锁,必须按照以下步骤执行:
使用 -o mand 选项来挂载文件系统。这样在执行 mount() 系统调用时,会传入 MS_MANDLOCK 标记,从而将 super_block 结构中的 s_flags 设置为 1,用来表示在这个文件系统上可以采用强制锁。例如:
# mount -o mand /dev/sdb7 /mnt
# mount | grep mnt
/dev/sdb7 on /mnt type ext3 (rw,mand)
1.修改要加强制锁的文件的权限:设置 SGID 位,并清除组可执行位。这种组合通常来说是毫无意义的,系统用来表示该文件被加了强制锁。例如:
# 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
2.使用 fcntl() 系统调用对该文件进行加锁或解锁操作。
共享模式锁
Linux 中还引入了两种特殊的文件锁:共享模式强制锁和租借锁。这两种文件锁可以被看成是强制锁的两种变种形式。共享模式强制锁可以用于某些私有网络文件系统,如果某个文件被加上了共享模式强制锁,那么其他进程打开该文件的时候不能与该文件的共享模式强制锁所设置的访问模式相冲突。但是由于可移植性不好,因此并不建议使用这种锁。
租借锁
采用强制锁之后,如果一个进程对某个文件拥有写锁,只要它不释放这个锁,就会导致访问该文件的其他进程全部被阻塞或不断失败重试;即使该进程只拥有读锁,也会造成后续更新该文件的进程的阻塞。为了解决这个问题,Linux 中采用了一种新型的租借锁。
当进程尝试打开一个被租借锁保护的文件时,该进程会被阻塞,同时,在一定时间内拥有该文件租借锁的进程会收到一个信号。收到信号之后,拥有该文件租借锁的进程会首先更新文件,从而保证了文件内容的一致性,接着,该进程释放这个租借锁。如果拥有租借锁的进程在一定的时间间隔内没有完成工作,内核就会自动删除这个租借锁或者将该锁进行降级,从而允许被阻塞的进程继续工作。
系统默认的这段间隔时间是 45 秒钟,定义如下:
137 int lease_break_time = 45;
这个参数可以通过修改 /proc/sys/fs/lease-break-time 进行调节(当然,/proc/sys/fs/leases-enable 必须为 1 才行)。
第二节 文件锁的总体实现和设置
2.1 内核相关的数据结构
Unix文件锁是在共享索引节点共享文件的情况下设计的,因此与锁机制相关的内核数据结构主要有虚拟索引节点(V-node)、系统打开文件表、进程打开文件表等数据表项。
其中V-node中的索引节点(i-node)中含有一个指向锁链表的首指针,该锁链表主要由对文件所加的全部的锁通过指针钩链而成。只要进程为文件或区域加锁成功,内核就创建锁结构file_lock,并根据用户设定的参数初始化后将其插入锁链表flock中。flock是锁存在的唯一标志,也是内核感知、管理及控制文件锁的主要依据,其结构和成员参见下面的struct flock。
如下所示,描述了进程为打开的文件设置文件锁的内核相关表项及各数据结构之间的关联,从中可以看出,锁同时与进程和文件相关,当进程终止或文件退出时即使是意外退出,进程对文件所加的锁将全部释放。
在 Linux 内核中,所有类型的文件锁都是由数据结构 file_lock 来描述的,file_lock 结构是在文件中定义的,如下所示:
struct file_lock {
struct file_lock *fl_next; /* singly linked list for this inode */
struct list_head fl_link; /* doubly linked list of all locks */
struct list_head fl_block; /* circular list of blocked processes */
fl_owner_t fl_owner;
unsigned int fl_pid;
wait_queue_head_t fl_wait;
struct file *fl_file;
unsigned char fl_flags;
unsigned char fl_type;
loff_t fl_start;
loff_t fl_end;
struct fasync_struct * fl_fasync; /* for lease break notifications */
unsigned long fl_break_time; /* for nonblocking lease breaks */
struct file_lock_operations *fl_ops; /* Callbacks for filesystems */
struct lock_manager_operations *fl_lmops; /* Callbacks for lockmanagers */
union {
struct nfs_lock_info nfs_fl;
struct nfs4_lock_info nfs4_fl;
struct {
struct list_head link; /* link in AFS vnode's pending_locks list */
int state; /* state of grant or error if -ve */
} afs;
} fl_u;
};
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) */
...
};
file_lock 数据结构的字段
类型 | 字段 | 字段描述 |
---|---|---|
struct file_lock* | fl_next | 与索引节点相关的锁列表中下一个元素 |
struct list_head | fl_link | 指向活跃列表或者被阻塞列表 |
struct list_head | fl_block | 指向锁等待列表 |
struct files_struct * | fl_owner | 锁拥有者的 files_struct |
unsigned int | fl_pid | 进程拥有者的 pid |
wait_queue_head_t | fl_wait | 被阻塞进程的等待队列 |
struct file * | fl_file | 指向文件对象 |
unsigned char | fl_flags | 锁标识 |
unsigned char | fl_type | 锁类型 |
loff_t | fl_start | 被锁区域的开始位移 |
loff_t | fl_end | 被锁区域的结束位移 |
struct fasync_struct * | fl_fasync | 用于租借暂停通知 |
unsigned long | fl_break_time | 租借的剩余时间 |
struct file_lock_operations * | fl_ops | 指向文件锁操作 |
struct lock_manager_operations * | fl_mops | 指向锁管理操作 |
union | fl_u | 文件系统特定信息 |
一个 file_lock 结构就是一把“锁”,结构中的 fl_file 就指向目标文件的 file 结构,而 fl_start 和 fl_end 则确定了该文件要加锁的一个区域。
当进程发出系统调用来请求对某个文件加排他锁时,如果这个文件上已经加上了共享锁,那么排他锁请求不能被立即满足,这个进程必须先要被阻塞。这样,这个进程就被放进了等待队列,file_lock 结构中的 fl_wait 字段就指向这个等待队列。指向磁盘上相同文件的所有 file_lock 结构会被链接成一个单链表 file_lock_list,索引节点结构中的 i_flock 字段会指向该单链表结构的首元素,fl_next 用于指向该链表中的下一个元素;当前系统中所有被请求,但是未被允许的锁被串成一个链表:blocked_list。fl_link 字段指向这两个列表其中一个。对于被阻塞列表(blocked_list)上的每一个锁结构来说,fl_next 字段指向与该锁产生冲突的当前正在使用的锁。所有在等待同一个锁的那些锁会被链接起来,这就需要用到字段 fl_block,新来的等待者会被加入到等待列表的尾部。 此外,fl_type 表示锁的性质,如读、写。fl_flags 是一些标志位,在 linux 2.6中,这些标志位的定义如下所示:
清单 2. 标志位的定义
773 #define FL_POSIX 1 774 #define FL_FLOCK 2 775 #define FL_ACCESS 8 /* not trying to lock, just looking */ 776 #define FL_EXISTS 16 /* when unlocking, test for existence */ 777 #define FL_LEASE 32 /* lease held on this file */ 778 #define FL_CLOSE 64 /* unlock on close */ 779 #define FL_SLEEP 128 /* A blocking lock */
FL_POSIX 锁是通过系统调用 fcntl() 创建的;而 FL_FLOCK 锁是通过系统调用 flock()创建的(详细内容请参见后文中的介绍)。FL_FLOCK 锁永远都和一个文件对象相关联,打开这个文件的进程拥有该 FL_FLOCK 锁。当一个锁被请求或者允许的时候,内核就会把这个进程在同一个文件上的锁都替换掉。FL_POSIX 锁则一直与一个进程以及一个索引节点相关联。当进程死亡或者文件描述符被关闭的时候,这个锁会被自动释放。
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:进程可以通过它来获取通过 fd 打开的那个文件的加锁信息。执行该操作时,lock 指向的结构中就保存了希望对文件加的锁(或者说要查询的锁)。如果确实存在这样一把锁,它阻止 lock 指向的 flock 结构所给出的锁描述符,则把现存的锁的信息写到 lock 指向的 flock 结构中,并将该锁拥有者的 PID 写入 l_pid 字段中,然后返回;否则,就将 lock 指向的 flock 结构中的 l_type 设置为 F_UNLCK,并保持 flock 结构中其他信息不变返回,而不会对该文件真正加锁。
- F_SETLK:进程用它来对文件的某个区域进行加锁(l_type的值为 F_RDLCK 或 F_WRLCK)或者删除锁(l_type 的值为F_UNLCK),如果有其他锁阻止该锁被建立,那么 fcntl() 就出错返回
- F_SETLKW:与 F_SETLK 类似,唯一不同的是,如果有其他锁阻止该锁被建立,则调用进程进入睡眠状态,等待该锁释放。一旦这个调用开始了等待,就只有在能够进行加锁或者收到信号时才会返回
需要注意的是,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 里面没有提到,其各自的意思如下所示:
- LOCK_SH:表示要创建一个共享锁,在任意时间内,一个文件的共享锁可以被多个进程拥有
- LOCK_EX:表示创建一个排他锁,在任意时间内,一个文件的排他锁只能被一个进程拥有
- LOCK_UN:表示删除该进程创建的锁
- LOCK_MAND:它主要是用于共享模式强制锁,它可以与 LOCK_READ 或者 LOCK_WRITE 联合起来使用,从而表示是否允许并发的读操作或者并发的写操作(尽管在 flock() 的手册页中没有介绍 LOCK_MAND,但是阅读内核源代码就会发现,这在内核中已经实现了)
通常情况下,如果加锁请求不能被立即满足,那么系统调用 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。其含义如下所示:
- F_SETLEASE:根据下面所描述的 arg 参数指定的值来建立或者删除租约:
- F_RDLCK:设置读租约。当文件被另一个进程以写的方式打开时,拥有该租约的当前进程会收到通知
- F_WRLCK:设置写租约。当文件被另一个进程以读或者写的方式打开时,拥有该租约的当前进程会收到通知
- F_UNLCK:删除以前建立的租约
- F_GETLEASE:表明调用进程拥有文件上哪种类型的锁,这需要通过返回值来确定,返回值有三种:F_RDLCK、F_WRLCK和F_UNLCK,分别表明调用进程对文件拥有读租借、写租借或者根本没有租借
某个进程可能会对文件执行其他一些系统调用(比如 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获得锁,才可打开文件。 |
http://blog.csdn.net/yebanghua/article/details/7301904
参考文献:
http://www.ibm.com/developerworks/cn/linux/l-cn-filelock/
http://www.linuxpk.com/archiver/tid-13992.html