简介: 本文的目的是想帮助读者理清 Linux 2.6中文件锁的概念以及 Linux 2.6 都提供了何种数据结构以及关键的系统调用来实现文件锁,从而可以帮助读者更好地使用文件锁来解决多个进程读取同一个文件的互斥问题。本文主要描述了 Linux 中各类文件锁的概念,使用场景,内核中描述文件锁的数据结构以及与文件锁密切相关的系统调用等内容。
在多任务操作系统环境中,如果一个进程尝试对正在被其他进程读取的文件进行写操作,可能会导致正在进行读操作的进程读取到一些被破坏或者不完整的数据;如果两个进程并发对同一个文件进行写操作,可能会导致该文件遭到破坏。因此,为了避免发生这种问题,必须要采用某种机制来解决多个进程并发访问同一个文件时所面临的同步问题,由此而产生了文件加锁方面的技术。
早期的 UNIX 系统只支持对整个文件进行加锁,因此无法运行数据库之类的程序,因为此类程序需要实现记录级的加锁。在 System V Release 3 中,通过 fcntl 提供了记录级的加锁,此后发展成为 POSIX 标准的一部分。本文将基于 2.6.23 版本的内核来探讨 Linux 中文件锁的相关技术。
Linux 支持的文件锁技术主要包括劝告锁(advisory lock)和强制锁(mandatory lock)这两种。此外,Linux 中还引入了两种强制锁的变种形式:共享模式强制锁(share-mode mandatory lock)和租借锁(lease)。
在 Linux 中,不论进程是在使用劝告锁还是强制锁,它都可以同时使用共享锁和排他锁(又称为读锁和写锁)。多个共享锁之间不会相互干扰,多个进程在同一时刻可以对同一个文件加共享锁。但是,如果一个进程对该文件加了排他锁,那么其他进程则无权再对该文件加共享锁或者排他锁,直到该排他锁被释放。所以,对于同一个文件来说,它可以同时拥有很多读者,但是在某一特定时刻,它只能拥有一个写者,它们之间的兼容关系如表 1 所示。
是否满足请求 | ||
当前加上的锁 | 共享锁 | 排他锁 |
无 | 是 | 是 |
共享锁 | 是 | 否 |
排他锁 | 否 | 否 |
劝告锁是一种协同工作的锁。对于这一种锁来说,内核只提供加锁以及检测文件是否已经加锁的手段,但是内核并不参与锁的控制和协调。也就是说,如果有进程不遵守“游戏规则”,不检查目标文件是否已经由别的进程加了锁就往其中写入数据,那么内核是不会加以阻拦的。因此,劝告锁并不能阻止进程对文件的访问,而只能依靠各个进程在访问文件之前检查该文件是否已经被其他进程加锁来实现并发控制。进程需要事先对锁的状态做一个约定,并根据锁的当前状态和相互关系来确定其他进程是否能对文件执行指定的操作。从这点上来说,劝告锁的工作方式与使用信号量保护临界区的方式非常类似。
劝告锁可以对文件的任意一个部分进行加锁,也可以对整个文件进行加锁,甚至可以对文件将来增大的部分也进行加锁。由于进程可以选择对文件的某个部分进行加锁,所以一个进程可以获得关于某个文件不同部分的多个锁。
与劝告锁不同,强制锁是一种内核强制采用的文件锁,它是从 System V Release 3 开始引入的。每当有系统调用 open()、read() 以及write() 发生的时候,内核都要检查并确保这些系统调用不会违反在所访问文件上加的强制锁约束。也就是说,如果有进程不遵守游戏规则,硬要往加了锁的文件中写入内容,内核就会加以阻拦:
如果一个文件已经被加上了读锁或者共享锁,那么其他进程再对这个文件进行写操作就会被内核阻止;
如果一个文件已经被加上了写锁或者排他锁,那么其他进程再对这个文件进行读取或者写操作就会被内核阻止。
如果其他进程试图访问一个已经加有强制锁的文件,进程行为取决于所执行的操作模式和文件锁的类型,归纳如表 2 所示:
当前锁类型 | 阻塞读 | 阻塞写 | 非阻塞读 | 非阻塞写 |
读锁 | 正常读取数据 | 阻塞 | 正常读取数据 | EAGAIN |
写锁 | 阻塞 | 阻塞 | EAGAIN | EAGAIN |
需要注意的是,如果要访问的文件的锁类型与要执行的操作存在冲突,那么采用阻塞读/写操作的进程会被阻塞,而采用非阻塞读/写操作的进程则不会阻塞,而是立即返回 EAGAIN。
另外,unlink() 系统调用并不会受到强制锁的影响,原因在于一个文件可能存在多个硬链接,此时删除文件时并不会修改文件本身的内容,而是只会改变其父目录中 dentry 的内容。
然而,在有些应用中并不适合使用强制锁,所以索引节点结构中的 i_flags 字段中定义了一个标志位MS_MANDLOCK用于有选择地允许或者不允许对一个文件使用强制锁。在 super_block 结构中,也可以将 s_flags 这个标志为设置为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 才行)。
在 Linux 内核中,所有类型的文件锁都是由数据结构 file_lock 来描述的,file_lock 结构是在 文件中定义的,内容如下所示:
811 struct file_lock { 812 struct file_lock *fl_next; /* singly linked list for this inode */ 813 struct list_head fl_link; /* doubly linked list of all locks */ 814 struct list_head fl_block; /* circular list of blocked processes */ 815 fl_owner_t fl_owner; 816 unsigned int fl_pid; 817 wait_queue_head_t fl_wait; 818 struct file *fl_file; 819 unsigned char fl_flags; 820 unsigned char fl_type; 821 loff_t fl_start; 822 loff_t fl_end; 823 824 struct fasync_struct * fl_fasync; /* for lease break notifications */ 825 unsigned long fl_break_time; /* for nonblocking lease breaks */ 826 827 struct file_lock_operations *fl_ops; /* Callbacks for filesystems */ 828 struct lock_manager_operations *fl_lmops; /* Callbacks for lockmanagers */ 829 union { 830 struct nfs_lock_info nfs_fl; 831 struct nfs4_lock_info nfs4_fl; 832 struct { 833 struct list_head link; /* link in AFS vnode's pending_locks list */ 834 int state; /* state of grant or error if -ve */ 835 } afs; 836 } fl_u; 837 }; |
表 3 简单描述了 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中,这些标志位的定义如下所示:
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 锁则一直与一个进程以及一个索引节点相关联。当进程死亡或者文件描述符被关闭的时候,这个锁会被自动释放。
对于强制锁来说,在 Linux 中,内核提供了 inline 函数 locks_verify_locked() 用于检测目标文件或者目标文件所在的设备是否允许使用强制锁,并且检查该设备是否已经加上了锁,相关函数如下所示:
166 #define __IS_FLG(inode,flg) ((inode)->i_sb->s_flags & (flg))173 #define IS_MANDLOCK(inode) __IS_FLG(inode, MS_MANDLOCK)1047 /**1048 * locks_mandatory_locked - Check for an active lock1049 * @inode: the file to check1050 *1051 * Searches the inode's list of locks to find any POSIX locks which conflict.1052 * This function is called from locks_verify_locked() only.1053 */1054 int locks_mandatory_locked(struct inode *inode)1055 {1056 fl_owner_t owner = current->files;1057 struct file_lock *fl;1058 1059 /*1060 * Search the lock list for this inode for any POSIX locks.1061 */1062 lock_kernel();1063 for (fl = inode->i_flock; fl != NULL; fl = fl->fl_next) {1064 if (!IS_POSIX(fl)) 1065 continue; 1066 if (fl->fl_owner != owner)1067 break;1068 } 1069 unlock_kernel();1070 return fl ? -EAGAIN : 0;1071 } 1368 /*1369 * Candidates for mandatory locking have the setgid bit set1370 * but no group execute bit - an otherwise meaningless combination.1371 */1372 #define MANDATORY_LOCK(inode) \1373 (IS_MANDLOCK(inode) && ((inode)->i_mode & (S_ISGID | S_IXGRP)) == S_ISGID)1374 1375 static inline int locks_verify_locked(struct inode *inode)1376 {1377 if (MANDATORY_LOCK(inode))1378 return locks_mandatory_locked(inode);1379 return 0;1380 } |
这里,函数 locks_verify_locked() 利用宏 MANDATORY_LOCK 来检测目标文件是否允许加锁,条件包括:该文件所在的设备的 super_block 结构中的 s_flags 必须被置为 1,该文件的 SGID 被置为 1 而且同组可执行位被清 0。如果允许,则调用函数locks_mandatory_locked(),该函数从索引节点的锁列表中查找是否存在有与其相冲突的锁,即是否已经加上了锁。
这里介绍在 Linux 中与文件锁关系密切的两个系统调用:flock() 和 fcntl()。劝告锁既可以通过系统调用 flock() 来实现,也可以通过系统调用 fcntl() 来实现。flock() 系统调用是从 BSD 中衍生出来的,在传统的类 UNIX 操作系统中,系统调用flock() 只适用于劝告锁。但是,Linux 2.6内核利用系统调用 flock() 实现了我们前面提到的特殊的强制锁:共享模式强制锁。另外,flock() 只能实现对整个文件进行加锁,而不能实现记录级的加锁。系统调用fcntl() 符合 POSIX 标准的文件锁实现,它也是非常强大的文件锁,fcntl() 可以实现对纪录进行加锁。
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() 所加的锁会对整个文件起作用。
fcntl() 函数的功能很多,可以改变已打开的文件的性质,本文中只是介绍其与获取/设置文件锁有关的功能。fcntl() 的函数原型如下所示:
int fcntl (int fd, int cmd, struct flock *lock); |
其中,参数 fd 表示文件描述符;参数 cmd 指定要进行的锁操作,由于 fcntl() 函数功能比较多,这里先介绍与文件锁相关的三个取值 F_GETLK、F_SETLK 以及 F_SETLKW。这三个值均与 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 可以用于设置文件状态标识。
此外,系统调用 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 就可以删除这个租借锁。不管对该租借锁减短租约或者干脆删除的操作是进程自愿的还是内核强迫的,只要被阻塞的系统调用还没有被发出该调用的进程解除阻塞,那么系统就会允许这个系统调用执行。即使被阻塞的系统调用因为某些原因被解除阻塞,但是上面对租借锁减短租约或者删除这个过程还是会执行的。
需要注意的是,租借锁也只能对整个文件生效,而无法实现记录级的加锁。