不需要使用传统的文件 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 位,因此启用强制性锁定。此外,还启用了 set group ID 的一般语义。
请牢记锁定的以下方面:
强制性锁定只对本地文件有效。当通过 NFS 访问文件时,不支持强制性锁定。
如果一个原子事务需要多个读取或写入操作,则进程应在任何 I/O 开始之前显式锁定所有此类段。对于所有通过此方法执行的程序,建议性锁定便已足够。
任意程序都不应对使用记录锁定的文件具有无限制的访问权限。
下表中列出的文件系统同时支持建议性锁定和强制性锁定。
表 5–4 支持的文件系统
文件系统 |
说明 |
---|---|
ufs |
缺省的基于磁盘的文件系统 |
fifofs |
为进程提供对数据的公用访问的命名管道文件的伪文件系统 |
namefs |
主要由 STREAMS 用来在文件顶部动态挂载文件描述符的伪文件系统 |
specfs |
提供对特殊字符设备和块设备的访问的伪文件系统 |
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),可以通过设置结构变量来设置锁定请求的类型和起始位置。
不能使用 flock(3UCB) 锁定映射文件。但是,可以针对映射文件使用面向多线程的同步机制。可以在 POSIX 风格和 Solaris 风格下使用这些同步机制。
锁定记录时,请不要将锁定段的起始点和长度设置为零。否则,此锁定过程与文件锁定相同。
使用记录锁定的原因是存在数据争用。因此,当无法获取所有所需锁定时,应收到失败响应:
等待一定时间后,再次尝试
终止该过程,向用户发出警告
使进程进入休眠状态,直到向该进程发送已释放锁定的信号
执行上述操作的某些组合
本示例说明使用 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。
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 命令一起使用来测试进程是否在持有锁定。此接口并不返回有关锁定的位置或拥有权的信息。
(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 设置锁定的进程不会导致死锁,因为当不能被授予锁定时,这些进程并不会等待。