Linux 支持的文件锁技术主要包括建议锁(advisory lock)和强制锁(mandatory lock)这两种。此外,Linux 中还引入了两种强制锁的变种形式:共享模式强制锁(share-mode mandatory lock)和租借锁(lease)。在这里,主要讨论建议锁(advisory lock)。
建议锁并不由内核强制实行,也就是说如果有进程不遵守“游戏规则”,不检查目标文件是否已经由别的进程加了锁就往其中写入数据,那么内核是不会加以阻拦的。因此,建议锁并不能阻止进程对文件的访问,而只能依靠各个进程在访问文件之前检查该文件是否已经被其他进程加锁来实现并发控制。进程需要事先对锁的状态做一个约定,并根据锁的当前状态和相互关系来确定其他进程是否能对文件执行指定的操作。而强制锁是由内核强制采用的文件锁——由于内核对每个read()和write()操作都会检查相应的锁,所以会降低系统性能。
对于建议锁,Linux提供两种实现方式:锁文件(lock files)和记录锁( record locking)。
(1)锁文件(lock files)
锁文件是最简单的对文件加锁的方法,每个需要加锁的数据文件都有一个锁文件(lock file)。当锁文件存在时,就认为该数据文件已经被加锁,别的进程不应该访问(但是你非要访问,Linux也不会阻止)。当锁不存在,进程就可以创建一个锁文件,然后访问相应的数据文件。只要创建锁的过程是原子的,就能保证某一时刻只有一个进程拥有该锁,这种方法保证某一时刻只有一个进程访问文件。
这种想法很简单,当一个进程想访问文件时,可以按如下方式对文件加锁:
fd = open("somefile.lck", O_RDONLY, 0644);
if (fd >= 0) {
close(fd);
printf("the file is already locked");
return 1;
} else {
/* the lock file does not exist, we can lock it and access it */
fd = open("somefile.lck", O_CREAT | O_WRONLY, 0644");
if (fd < 0) {
perror("error creating lock file");
return 1;
}
/* we could write our pid to the file */
close(fd);
}
当一个进程处理完文件后,就可以调用unlink("somefile.lck")释放锁了——本质上是删除somefile.lck文件。
上面这段代码实际上存在竞争情况,原因在于if语句块不是原子性的,进入if语句块,内核可能调度别的进程运行。更好的方式如下:
fd = open("somefile.lck", O_WRONLY | O_CREAT | O_EXCL, 0644);
if (fd < 0 && errno == EEXIST) {
printf("the file is already locked");
return 1;
} else if (fd < 0) {
perror("unexpected error checking lock");
return 1;
}
/* we could write our pid to the file */
close(fd);
O_EXCL标志保证open()创建锁文件的过程是原子性的。
注意以下几点:
1、任何时刻只有一个进程可以拥有锁。
2、O_EXCL标志只对本志文件系统是可靠的,对于网络文件系统并不能很好的支持。
3、锁仅仅只是建议性的。
4、如果一个持有锁的进程不正常结束,锁文件仍然存在。如果加锁进程的pid存储在锁文件中,其它进程可以检查锁进程是否存在,当它结束时就可以删除锁。但是,在检查的时候,如果pid被其它进程使用了,此时就无能为力了。
(2)记录锁(Record Locking)
为了克服锁文件的缺点,System V和BSD4.3引入了记录锁,相应的系统调用为lockf()和flock()。而POSIX对于记录锁提供了另外一种机制,其系统调用为fcntl()。Linux提供三种接口,在这里仅讨论POSIX的接口。
记录锁和锁文件有两个很重要的区别:首先,记录锁可以对文件的任何一部分加锁——这对于DBMS这样的应用程序,有极大的帮助,SQLite当然没有放过这样的好处。其次,记录锁的另一个优点就是它由内核持有,而不是文件系统持有。当进程结束时,所有的锁也随之释放。
和锁文件一样,POSIX锁也是建议性的。记录锁有两种锁:读锁(read locks)和写锁(write locks)。读锁也就是共享锁(shared lock),写锁也就是排它锁(exclusive lock)。对于一个记录,只能有一个进程持有写锁,读锁不能存在。
对于一个进程本身而言,多个锁绝不会冲突。如果一个进程对文件的200-250字节持有读锁,然后对200-225字节数据加写锁,是会成功的。此时,200-225为写锁,而226-250字节数据为读锁,该规则主要是防止进程本身发生死锁(尽管多进程之间仍然可能发生死锁)。
POSIX锁通过fcntl()系统来实现,如下:
#include <fcntl.h>
int fcntl(int fd, int command, long arg);
arg为指向flock结构体的指针:
#include <fcntl.h>
struct flock {
short l_type;
short l_whence;
off_t l_start;
off_t l_len;
pid_t l_pid;
};
Windows中的锁都是强制锁(mandatory locks),Windows中的共享文件通过以下几个机制来管理:
(1) 通过共享访问控制方式,应用程序可以指定整个文件进行共享读,写或者删除。
(2) 通过字节范围锁(byte range locks)可以对文件的某一部分进行读写访问。
(3) Windows文件系统不允许正在执行的文件被打开用来进行写或删除操作。
文件的共享方式由WIN32 API中的打开文件函数CreateFile()中的sharing mode参数确定:
HANDLE CreateFile(
LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);
dwShareMode的取值通常为:
FILE_SHARE_DELETE:
Enables subsequent open operations on an object to request delete access.
Otherwise, other processes cannot open the object if they request delete access.
If this flag is not specified, but the object has been opened for delete access, the function fails.
FILE_SHARE_READ:
Enables subsequent open operations on an object to request read access.
Otherwise, other processes cannot open the object if they request read access.
If this flag is not specified, but the object has been opened for read access, the function fails.
FILE_SHARE_WRITE:
Enables subsequent open operations on an object to request write access.
Otherwise, other processes cannot open the object if they request write access.
If this flag is not specified, but the object has been opened for write access, the function fails.