不需要使用传统的文件 I/O 来锁定文件元素。应针对映射文件使用《多线程编程指南》中介绍的较轻量级同步机制。
锁定文件可防止当多个用户同时尝试更新一个文件时可能会发生的错误。可以锁定文件的一部分。
锁定文件后,将阻止对整个文件进行访问。锁定记录后,将阻止对文件中指定的段进行访问。在 SunOS 中,所有文件都是一系列的数据字节:“记录”这一概念是指使用该文件的程序。
强制性锁定将暂停进程,直到所请求的文件段被释放。建议性锁定返回一个表明是否成功获得锁定的结果。进程可以忽略建议性锁定的结果。对于同一文件,不能同时使用强制性和建议性文件锁定。打开文件时所用的模式决定了对文件的锁定是强制性锁定还是建议性锁定。
在两种基本锁定调用中,fcntl(2) 比 lockf(3C) 更容易移植、功能更强大,但更不易用。fcntl(2) 是在 POSIX 1003.1 标准中指定的。提供 lockf(3C) 是为了与较早版本的应用程序兼容。
对于强制性锁定,文件必须是设置了 set-group-ID 位而未设置组执行权限的常规文件。如果不符合其中任一条件,则所有记录锁定均为建议性锁定。
可按如下方式设置强制性锁定。
#include <sys/types.h> #include <sys/stat.h> int mode; struct stat buf; ... if (stat(filename, &buf) < 0) { perror("program"); exit (2); } /* get currently set mode */ mode = buf.st_mode; /* remove group execute permission from mode */ mode &= ~(S_IEXEC>>3); /* set 'set group id bit' in mode */ mode |= S_ISGID; if (chmod(filename, mode) < 0) { perror("program"); exit(2); } ...
当系统在执行文件时,操作系统会忽略记录锁定。所有带有记录锁定的文件都不应设置执行权限。
chmod(1) 命令还可用于将文件设置为允许强制性锁定。
$ chmod +l file
此命令在文件模式中设置 O20n0 权限位,用来指示对文件进行强制性锁定。如果 n 是偶数,则此位将会解释为启用强制性锁定。如果 n 是奇数,则此位将会解释为“执行时设置组 ID”。
当使用 -l 选项请求长列表格式时,ls(1) 命令显示以下设置:
$ ls -l file
此命令显示以下信息:
-rw---l--- 1 user group size mod_time file
权限中的字母 "l" 指示已设置 set-group-ID 位。由于已设置 set-group-ID 位,因此启用强制性锁定。此外,还启用了设置组 ID (set group ID) 的一般语义。
请牢记锁定的以下方面:
强制性锁定只对本地文件有效。当通过 NFS 访问文件时,不支持强制性锁定。
如果一个原子事务需要多个读取或写入操作,则进程应在任何 I/O 开始之前显式锁定所有此类段。对于所有通过此方法执行的程序,建议性锁定便已足够。
任意程序都不应对使用记录锁定的文件具有无限制的访问权限。
下表列出的文件系统既支持建议性锁定又支持强制性锁定。
表 6-4 支持的文件系统
|
NFS 仅支持建议性文件锁定。proc 和 fd 文件系统不支持文件锁定。
只能使用有效的打开描述符来请求对文件进行锁定。对于读取锁定,文件必须至少是使用读取权限打开的。对于写入锁定,文件也必须是使用写入权限打开的。在以下示例中,将打开文件以进行读取和写入访问。
... filename = argv[1]; fd = open (filename, O_RDWR); if (fd < 0) { perror(filename); exit(2); } ...
要锁定整个文件,请将偏移量设置为零,并将大小设置为零。
可以通过多种方法设置文件锁定。选择哪种方法取决于该锁定与程序其余部分如何交互以及性能和可移植性。本示例使用与 POSIX 标准兼容的 fcntl(2) 接口。此接口尝试锁定文件,直到发生以下情况之一:
成功设置文件锁定。
出现错误。
超过 MAX_TRY,程序停止尝试锁定文件。
#include <fcntl.h> ... struct flock lck; ... lck.l_type = F_WRLCK; /* setting a write lock */ lck.l_whence = 0; /* offset l_start from beginning of file */ lck.l_start = (off_t)0; lck.l_len = (off_t)0; /* until the end of the file */ if (fcntl(fd, F_SETLK, &lck) <0) { if (errno == EAGAIN || errno == EACCES) { (void) fprintf(stderr, "File busy try again later!\n"); return; } perror("fcntl"); exit (2); } ...
使用 fcntl(2),可通过设置结构变量来设置锁定请求的类型和起始位置。
锁定记录时,请不要将锁定段的起始点和长度设置为零。否则,此锁定过程与文件锁定相同。
使用记录锁定的原因是存在数据争用。因此,当无法获取所有所需锁定时,应采取相应的失败响应措施:
等待一定时间后,再次尝试
中止该过程,向用户发出警告
使进程进入休眠状态,直到向该进程发送已释放锁定的信号
执行上述操作的某些组合
本示例显示了一个使用 fcntl(2) 锁定的记录。
{ struct flock lck; ... lck.l_type = F_WRLCK; /* setting a write lock */ lck.l_whence = 0; /* offset l_start from beginning of file */ lck.l_start = here; lck.l_len = sizeof(struct record); /* lock "this" with write lock */ lck.l_start = this; if (fcntl(fd, F_SETLKW, &lck) < 0) { /* "this" lock failed. */ return (-1); ... }
下一示例显示了 lockf(3C) 接口。
#include <unistd.h> { ... /* lock "this" */ (void) lseek(fd, this, SEEK_SET); if (lockf(fd, F_LOCK, sizeof(struct record)) < 0) { /* Lock on "this" failed. Clear lock on "here". */ (void) lseek(fd, here, 0); (void) lockf(fd, F_ULOCK, sizeof(struct record)); return (-1); }
可使用与设置锁定相同的方法来删除锁定。只是锁定类型有所不同 (F_ULOCK)。解除锁定不受其他进程的阻止,并且只影响调用进程所设置的锁定。解除锁定只影响在先前锁定调用中指定的文件段。
可以确定哪个进程在持有锁定。可以按照前面示例所示设置锁定,并在 fcntl(2) 中使用 F_GETLK。
示例 6-2 输出文件的锁定段
struct flock lck; lck.l_whence = 0; lck.l_start = 0L; lck.l_len = 0L; do { lck.l_type = F_WRLCK; (void) fcntl(fd, F_GETLK, &lck); if (lck.l_type != F_UNLCK) { (void) printf("%d %d %c %8ld %8ld\n", lck.l_sysid, lck.l_pid, (lck.l_type == F_WRLCK) ? 'W' : 'R', lck.l_start, lck.l_len); /* If this lock goes to the end of the address space, no * need to look further, so break out. */ if (lck.l_len == 0) { /* else, look for new lock after the one just found. */ lck.l_start += lck.l_len; } } } while (lck.l_type != F_UNLCK);
fcntl(2) 与 F_GETLK 命令可以在等待服务器响应时处于休眠状态。如果客户机或服务器出现资源不足的情况,则此命令可能会失败,同时返回 ENOLCK。
将 lockf(3C) 与 F_TEST 命令一起使用来测试进程是否在持有锁定。此接口并不返回有关锁定的位置或拥有权的信息。
示例 6-3 使用 lockf 测试进程
(void) lseek(fd, 0, 0L); /* set the size of the test region to zero (0). to test until the end of the file address space. */ if (lockf(fd, (off_t)0, SEEK_SET) < 0) { switch (errno) { case EACCES: case EAGAIN: (void) printf("file is locked by another process\n"); break; case EBADF: /* bad argument passed to lockf */ perror("lockf"); break; default: (void) printf("lockf: unexpected error <%d>\n", errno); break; } }
进程派生时,子进程会收到父进程打开的文件描述符的副本。子进程不继承锁定,因为锁定由特定进程拥有。父进程和子进程共享每个文件的公用文件指针。这两个进程都可能会尝试对同一文件中的相同位置设置锁定。使用 lockf(3C) 和 fcntl(2) 都会出现此问题。如果持有记录锁定的程序进行派生,则子进程应该关闭此文件。关闭此文件之后,子进程应该重新打开此文件以设置新的独立文件指针。
UNIX 锁定功能可以检测和避免死锁。仅当系统准备将记录锁定接口置于休眠状态时才会发生死锁。执行搜索来确定两个进程是否处于死锁状态。如果检测到潜在的死锁,则锁定接口将失败,并设置 errno 以指示死锁。使用 F_SETLK 设置锁定的进程不会导致死锁,因为当不能被授予锁定时,这些进程并不会等待。