本文的目的是想帮助读者理清 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 所示。
表 1. 锁间的兼容关系是否满足请求 | ||
当前加上的锁 | 共享锁 | 排他锁 |
无 | 是 | 是 |
共享锁 | 是 | 否 |
排他锁 | 否 | 否 |
劝告锁是一种协同工作的锁。对于这一种锁来说,内核只提供加锁以及检测文件是否已经加锁的手段,但是内核并不参与锁的控制和协调。也就是说,如果有 进程不遵守“游戏规则”,不检查目标文件是否已经由别的进程加了锁就往其中写入数据,那么内核是不会加以阻拦的。因此,劝告锁并不能阻止进程对文件的访 问,而只能依靠各个进程在访问文件之前检查该文件是否已经被其他进程加锁来实现并发控制。进程需要事先对锁的状态做一个约定,并根据锁的当前状态和相互关 系来确定其他进程是否能对文件执行指定的操作。从这点上来说,劝告锁的工作方式与使用信号量保护临界区的方式非常类似。
劝告锁可以对文件的任意一个部分进行加锁,也可以对整个文件进行加锁,甚至可以对文件将来增大的部分也进行加锁。由于进程可以选择对文件的某个部分进行加锁,所以一个进程可以获得关于某个文件不同部分的多个锁。
与劝告锁不同,强制锁是一种内核强制采用的文件锁,它是从 System V Release 3 开始引入的。每当有系统调用 open()、read() 以及write() 发生的时候,内核都要检查并确保这些系统调用不会违反在所访问文件上加的强制锁约束。也就是说,如果有进程不遵守游戏规则,硬要往加了锁的文件中写入内 容,内核就会加以阻拦:
如果一个文件已经被加上了读锁或者共享锁,那么其他进程再对这个文件进行写操作就会被内核阻止;
如果一个文件已经被加上了写锁或者排他锁,那么其他进程再对这个文件进行读取或者写操作就会被内核阻止。
如果其他进程试图访问一个已经加有强制锁的文件,进程行为取决于所执行的操作模式和文件锁的类型,归纳如表 2 所示:
表 2. 进行对已加强制锁的文件进行操作时的行为当前锁类型 | 阻塞读 | 阻塞写 | 非阻塞读 | 非阻塞写 |
读锁 | 正常读取数据 | 阻塞 | 正常读取数据 | 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) |
# 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 |
Linux 中还引入了两种特殊的文件锁:共享模式强制锁和租借锁。这两种文件锁可以被看成是强制锁的两种变种形式。共享模式强制锁可以用于某些私有网络文件系统,如 果某个文件被加上了共享模式强制锁,那么其他进程打开该文件的时候不能与该文件的共享模式强制锁所设置的访问模式相冲突。但是由于可移植性不好,因此并不 建议使用这种锁。
采用强制锁之后,如果一个进程对某个文件拥有写锁,只要它不释放这个锁,就会导致访问该文件的其他进程全部被阻塞或不断失败重试;即使该进程只拥有读锁,也会造成后续更新该文件的进程的阻塞。为了解决这个问题,Linux 中采用了一种新型的租借锁。
当进程尝试打开一个被租借锁保护的文件时,该进程会被阻塞,同时,在一定时间内拥有该文件租借锁的进程会收到一个信号。收到信号之后,拥有该文件租 借锁的进程会首先更新文件,从而保证了文件内容的一致性,接着,该进程释放这个租借锁。如果拥有租借锁的进程在一定的时间间隔内没有完成工作,内核就会自 动删除这个租借锁或者将该锁进行降级,从而允许被阻塞的进程继续工作。
系统默认的这段间隔时间是 45 秒钟,定义如下:
137 int lease_break_time = 45; |
Linux 内核中关于文件锁的实现
在 Linux 内核中,所有类型的文件锁都是由数据结构 file_lock 来描述的,file_lock 结构是在 文件中定义的,内容如下所示:
清单 1. 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 | 文件系统特定信息 |
清单 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 */ |
对于强制锁来说,在 Linux 中,内核提供了 inline 函数 locks_verify_locked() 用于检测目标文件或者目标文件所在的设备是否允许使用强制锁,并且检查该设备是否已经加上了锁,相关函数如下所示:
清单 3. 与强制锁相关的函数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 lock
1049 * @inode: the file to check
1050 *
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 set
1370 * 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 } |
Linux 中关于文件锁的系统调用
这里介绍在 Linux 中与文件锁关系密切的两个系统调用:flock() 和 fcntl()。劝告锁既可以通过系统调用 flock() 来实现,也可以通过系统调用 fcntl() 来实现。flock() 系统调用是从 BSD 中衍生出来的,在传统的类 UNIX 操作系统中,系统调用flock() 只适用于劝告锁。但是,Linux 2.6内核利用系统调用 flock() 实现了我们前面提到的特殊的强制锁:共享模式强制锁。另外,flock() 只能实现对整个文件进行加锁,而不能实现记录级的加锁。系统调用fcntl() 符合 POSIX 标准的文件锁实现,它也是非常强大的文件锁,fcntl() 可以实现对纪录进行加锁。
flock() 的函数原型如下所示:
int flock(int fd, int operation); |
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); |
清单 4. 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) */
...
}; |
可以执行的操作包括:
- 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); |
- 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 就可以删除这个租借锁。不管对该租借锁减短租约或者干脆删除的操作是进程自愿的还是内核强迫的,只要被阻塞的系统调用还没有被发出该调用的进程解除阻塞, 那么系统就会允许这个系统调用执行。即使被阻塞的系统调用因为某些原因被解除阻塞,但是上面对租借锁减短租约或者删除这个过程还是会执行的。
需要注意的是,租借锁也只能对整个文件生效,而无法实现记录级的加锁。
文件锁的使用样例为了使读者更深入理解本文中介绍的内容,下面我们给出了一个例子来详细介绍文件锁的具体用法。这个例子可以用来检测所使用的文件是否支持强制锁,其源代码如下所示:
清单 5. 锁的使用方法具体示例# cat -n mandlock.c
1 #include <errno.h>
2 #include <stdio.h>
3 #include <fcntl.h>
4 #include <sys/wait.h>
5 #include <sys/stat.h>
6
7 int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)
8 {
9 struct flock lock;
10
11 lock.l_type = type; /* F_RDLCK, F_WRLCK, F_UNLCK */
12 lock.l_start = offset; /* byte offset, relative to l_whence */
13 lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
14 lock.l_len = len; /* #bytes (0 means to EOF) */
15
16 return( fcntl(fd, cmd, &lock) );
17 }
18
19 #define read_lock(fd, offset, whence, len) \
20 lock_reg(fd, F_SETLK, F_RDLCK, offset, whence, len)
21 #define write_lock(fd, offset, whence, len) \
22 lock_reg(fd, F_SETLK, F_WRLCK, offset, whence, len)
23
24 #define err_sys(x) { perror(x); exit(1); }
25
26 int main(int argc, char *argv[])
27 {
28 int fd, val;
29 pid_t pid;
30 char buf[5];
31 struct stat statbuf;
32 if (argc != 2) {
33 fprintf(stderr, "usage: %s filename\n", argv[0]);
34 exit(1);
35 }
36 if ((fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC )) < 0)
37 err_sys("open error");
38 if (write(fd, "hello world", 11) != 11)
39 err_sys("write error");
40
41 /* turn on set-group-ID and turn off group-execute */
42 if (fstat(fd, &statbuf) < 0)
43 err_sys("fstat error");
44 if (fchmod(fd, (statbuf.st_mode & ~S_IXGRP) | S_ISGID) < 0)
45 err_sys("fchmod error");
46
47 sleep(2);
48
49 if ((pid = fork()) < 0) {
50 err_sys("fork error");
51 } else if (pid > 0) { /* parent */
52 /* write lock entire file */
53 if (write_lock(fd, 0, SEEK_SET, 0) < 0)
54 err_sys("write_lock error");
55
56 sleep(20); /* wait for child to set lock and read data */
57
58 if (waitpid(pid, NULL, 0) < 0)
59 err_sys("waitpid error");
60
61 } else { /* child */
62 sleep(10); /* wait for parent to set lock */
63
64 if ( (val = fcntl(fd, F_GETFL, 0)) < 0)
65 err_sys("fcntl F_GETFL error");
66
67 val |= O_NONBLOCK; /* turn on O_NONBLOCK flag */
68
69 if (fcntl(fd, F_SETFL, val) < 0)
70 err_sys("fcntl F_SETFL error");
71
72 /* first let's see what error we get if region is locked */
73 if (read_lock(fd, 0, SEEK_SET, 0) != -1) /* no wait */
74 err_sys("child: read_lock succeeded");
75
76 printf("read_lock of already-locked region returns %d: %s\n", errno, strerror(errno));
77
78 /* now try to read the mandatory locked file */
79 if (lseek(fd, 0, SEEK_SET) == -1)
80 err_sys("lseek error");
81 if (read(fd, buf, 5) < 0)
82 printf("read failed (mandatory locking works)\n");
83 else
84 printf("read OK (no mandatory locking), buf = %5.5s\n", buf);
85 }
86 exit(0);
87 }
88 |
该程序在测试系统的执行结果如下所示:
# mount | grep mnt
/dev/sdb7 on /mnt type ext3 (rw,mand)
/dev/sdb6 on /tmp/mnt type ext3 (rw)
# ./mandlock /mnt/testfile
read_lock of already-locked region returns 11: Resource temporarily unavailable
read failed (mandatory locking works)
# ./mandlock /tmp/mnt/testfile
read_lock of already-locked region returns 11: Resource temporarily unavailable
read OK (no mandatory locking), buf = hello |
总结
Linux 的文件锁在以共享索引节点共享文件的情况下设计的,文件锁的实现可以使得不同用户同时读写同一文件的并发问题得以解决。本文描述了 Linux 中各类文件锁的概念,使用场景,内核中描述文件锁的数据结构以及与文件锁密切相关的系统调用等内容,至于与文件锁相关的索引节点数据结构,以及在对文件进 行加锁时遇到的死锁问题等其他知识,这里没有做详尽介绍,感兴趣的读者可以自行参考内核源代码。
本文的目的是想帮助读者理清 Linux 2.6中文件锁的概念以及 Linux 2.6 都提供了何种数据结构以及关键的系统调用来实现文件锁,从而可以帮助读者更好地使用文件锁来解决多个进程读取同一个文件的互斥