7.10. 避免竞争条件

“竞争条件”可以定义为“由于对事件相对时序的意外关键依赖性而产生的异常行为”[FOLDOC]。竞争条件通常涉及一个或多个进程访问共享资源(例如文件或变量),而这种多重访问没有得到适当的控制。

一般来说,进程不会原子地执行;另一个进程可能在任何两个指令之间中断它。如果安全程序的进程没有为这些中断做好准备,则另一个进程可能能够干扰安全程序的进程。如果安全程序中任意两个操作之间执行了另一个进程的任意数量的代码,则这两个操作仍然必须正确工作。

竞争条件问题可以概念性地分为两类

7.10.1. 排序(非原子)问题

一般来说,您必须检查您的代码中是否存在任何一对操作,如果在它们之间执行任意代码,则可能会失败。

请注意,加载和保存共享变量通常作为单独的操作实现,而不是原子的。这意味着“递增变量”操作通常转换为加载、递增和保存操作,因此如果变量内存是共享的,则另一个进程可能会干扰递增。

安全程序必须确定是否应批准请求,如果应批准,则对该请求采取行动。在程序对其采取行动之前,不信任的用户绝不能更改用于此确定的任何内容。这种竞争条件有时被称为“检查时间 - 使用时间”(TOCTOU)竞争条件。

7.10.1.1. 文件系统中的原子操作

未能重复执行原子操作的问题在文件系统中经常出现。一般来说,文件系统是许多程序使用的共享资源,一些程序可能会干扰其他程序对其的使用。安全程序通常应避免使用 access(2) 来确定是否应批准请求,然后再使用 open(2),因为用户可能能够在这些调用之间移动文件,可能创建他们自己选择的符号链接或文件。安全程序应改为设置其有效 id 或文件系统 id,然后直接进行 open 调用。安全地使用 access(2) 是可能的,但前提是用户无法影响文件或从文件系统根目录到该文件路径上的任何目录。

创建文件时,您应该使用 O_CREAT | O_EXCL 模式打开它,并仅授予非常狭窄的权限(仅限当前用户);您还需要为 open 失败做好准备。如果您需要能够打开文件(例如,为了防止拒绝服务),您需要重复执行以下操作:(1)创建一个“随机”文件名,(2)如上所述打开文件,以及(3)当 open 成功时停止重复。

如果普通程序没有正确创建文件,则可能成为安全漏洞。例如,“joe”文本编辑器有一个名为“DEADJOE”符号链接漏洞的弱点。当 joe 以非标准方式退出时(例如系统崩溃、关闭 xterm 或网络连接中断),joe 会无条件地将其打开的缓冲区附加到文件“DEADJOE”。这可以通过在 root 通常使用 joe 的目录中创建 DEADJOE 符号链接来利用。通过这种方式,joe 可以用于将垃圾附加到潜在敏感文件中,从而导致拒绝服务和/或意外访问。

再举一个例子,当对文件的元信息执行一系列操作时(例如更改其所有者、stat 文件或更改其权限位),首先打开文件,然后对打开的文件使用操作。这意味着使用 fchown()、fstat() 或 fchmod() 系统调用,而不是使用文件名的函数,例如 chown()、chgrp() 和 chmod()。这样做将防止文件在您的程序运行时被替换(可能存在竞争条件)。例如,如果您关闭一个文件,然后使用 chmod() 更改其权限,攻击者可能能够在这两个步骤之间移动或删除该文件,并创建一个指向另一个文件(例如 /etc/passwd)的符号链接。其他有趣的文件包括 /dev/zero,它可以为程序提供无限长的数据输入流;如果攻击者可以“切换”文件,则结果可能是危险的。

但即使这样也很复杂 - 创建文件时,您必须为它们提供尽可能小的权限集,然后在您希望时将权限更改为更广泛的权限。一般来说,这意味着您需要使用 umask 和/或 open 的参数来限制对用户和用户组的初始访问。例如,如果您创建一个最初是世界可读的文件,然后尝试关闭“世界可读”位,攻击者可能会尝试在权限位表示这是允许的情况下打开该文件。在大多数类 Unix 系统上,权限仅在 open 时检查,因此这将导致攻击者拥有超出预期的权限。

一般来说,如果多个用户可以写入类 Unix 系统中的目录,您最好在该目录上设置“粘滞”位,并且最好实现粘滞目录。然而,最好完全避免这个问题,并创建只有受信任的特殊进程才能访问的目录(然后仔细实现它)。传统的 Unix 临时目录(/tmp 和 /var/tmp)通常实现为“粘滞”目录,并且仍然可能出现各种安全问题,我们将在下面看到。

7.10.1.2. 临时文件

正确执行原子操作的问题在创建临时文件时尤其突出。类 Unix 系统中的临时文件传统上是在 /tmp 或 /var/tmp 目录中创建的,这些目录由所有用户共享。攻击者常用的技巧是在临时目录中创建指向其他文件(例如 /etc/passwd)的符号链接,同时您的安全程序正在运行。攻击者的目标是创建一种情况,即安全程序确定给定的文件名不存在,然后攻击者创建指向另一个文件的符号链接,然后安全程序执行某些操作(但现在它实际上打开了一个意外的文件)。通常,重要的文件可能以这种方式被破坏或修改。这种攻击有很多变体,例如创建普通文件,所有这些都基于攻击者可以在安全程序用于临时文件的同一目录中创建(或有时以其他方式访问)文件系统对象的想法。

Michal Zalewski 在 2002 年揭露了临时目录的另一个严重问题,该问题涉及临时目录的自动清理。有关更多信息,请参阅他于 2002 年 12 月 20 日发布到 Bugtraq 的帖子(主题为“[RAZOR] Problems with mkstemp()”)。基本上,Zalewski 指出,常见做法是让程序自动扫描临时目录,如 /tmp 和 /var/tmp,并删除一段时间(例如,几天)未访问的“旧”文件。此类程序有时称为“tmp 清理器”(发音为“temp cleaners”)。可能最常见的 tmp 清理器是 Red Hat Software 的 Erik Troan 和 Preston Brown 的“tmpwatch”;另一个常见的清理器是 Stanislav Shalunov 的“stmpclean”;许多管理员也自己编写。不幸的是,tmp 清理器的存在为新的安全关键竞争条件创造了机会;攻击者可能能够安排事情,以便 tmp 清理器干扰安全程序。例如,攻击者可以创建一个“旧”文件,安排 tmp 清理器计划删除该文件,自己删除该文件,并运行一个安全程序来创建相同的文件 - 现在 tmp 清理器将删除安全程序的文件!或者,想象一下,安全程序在使用文件后可能会有很长的延迟(例如,一个 setuid 程序被 SIGSTOP 停止,并在许多天后通过 SIGCONT 恢复,或者只是故意创建大量工作)。如果临时文件长时间未使用,则其临时文件很可能被 tmp 清理器删除。

在这些共享目录中创建文件时,一般问题是您必须保证您计划使用的文件名在创建时不存在,并原子地创建该文件。在创建文件“之前”进行检查不起作用,因为在检查发生之后但在创建之前,另一个进程可以使用该文件名创建该文件。使用“不可预测”或“唯一”文件名通常不起作用,因为另一个进程通常可以重复猜测直到成功。一旦您原子地创建了文件,您必须始终使用返回的文件描述符(或文件流,如果从文件描述符使用 fdopen() 等例程创建)。您绝不能重新打开文件,或使用任何使用文件名的操作作为参数 - 始终使用文件描述符或关联的流。否则,上面提到的 tmpwatch 竞争问题将导致问题。您甚至不能创建文件、关闭它并重新打开它,即使权限限制了谁可以打开它。请注意,比较描述符和重新打开的文件以验证 inode 号、创建时间或文件所有权是不够的 - 请参阅 Olaf Kirch 的“符号链接和低温睡眠”。

从根本上说,要在共享(粘滞)目录中创建临时文件,您必须重复执行:(1)创建一个“随机”文件名,(2)使用 O_CREAT | O_EXCL 和非常狭窄的权限打开它(这将原子地创建文件,如果未创建则失败),以及(3)当 open 成功时停止重复。

根据 1997 年的“Single Unix Specification”,创建任意临时文件(使用 C 接口)的首选方法是 tmpfile(3)。tmpfile(3) 函数创建一个临时文件并打开相应的流,返回该流(如果失败则返回 NULL)。不幸的是,规范没有保证文件将被安全地创建。在本书的早期版本中,我表示担心,因为我无法向自己保证所有实现都安全地执行此操作。此后我发现,较旧的 System V 系统具有 tmpfile(3) 的不安全实现(以及 tmpnam(3) 和 tempnam(3) 的不安全实现),因此在至少某些系统上,它完全没用。tmpfile(3) 的库实现应该安全地创建此类文件,当然,但用户并不总是意识到他们的系统库存在此安全缺陷,有时他们对此无能为力。

Kris Kennaway 建议通常使用 mkstemp(3) 来创建临时文件。他的理由是,您应该使用众所周知的库函数来执行此任务,而不是自己编写函数,并且此函数具有众所周知的语义。这当然是一个合理的立场。我想补充一点,如果您使用 mkstemp(3),请务必使用 umask(2) 将生成的临时文件权限限制为仅所有者。这是因为 mkstemp(3) 的一些实现(基本上是较旧的实现)使此类文件对所有人可读写,从而创建了一种条件,在这种条件下,攻击者可以读取或写入此目录中的私有数据。一个小的麻烦是 mkstemp(3) 不直接支持环境变量 TMP 或 TMPDIR(如下所述),因此如果您想支持它们,您必须添加代码来执行此操作。这是一个 C 程序,演示了如何将 mkstemp(3) 用于此目的,包括直接使用以及添加对 TMP 和 TMPDIR 的支持时使用。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>

void failure(msg) {
 fprintf(stderr, "%s\n", msg);
 exit(1);
}

/*
 * Given a "pattern" for a temporary filename
 * (starting with the directory location and ending in XXXXXX),
 * create the file and return it.
 * This routines unlinks the file, so normally it won't appear in
 * a directory listing.
 * The pattern will be changed to show the final filename.
 */

FILE *create_tempfile(char *temp_filename_pattern)
{
 int temp_fd;
 mode_t old_mode;
 FILE *temp_file;

 old_mode = umask(077);  /* Create file with restrictive permissions */
 temp_fd = mkstemp(temp_filename_pattern);
 (void) umask(old_mode);
 if (temp_fd == -1) {
   failure("Couldn't open temporary file");
 }
 if (!(temp_file = fdopen(temp_fd, "w+b"))) {
   failure("Couldn't create temporary file's file descriptor");
 }
 if (unlink(temp_filename_pattern) == -1) {
   failure("Couldn't unlink temporary file");
 }
 return temp_file;
}


/*
 * Given a "tag" (a relative filename ending in XXXXXX),
 * create a temporary file using the tag.  The file will be created
 * in the directory specified in the environment variables
 * TMPDIR or TMP, if defined and we aren't setuid/setgid, otherwise
 * it will be created in /tmp.  Note that root (and su'd to root)
 * _will_ use TMPDIR or TMP, if defined.
 * 
 */
FILE *smart_create_tempfile(char *tag)
{
 char *tmpdir = NULL;
 char *pattern;
 FILE *result;

 if ((getuid()==geteuid()) && (getgid()==getegid())) {
   if (! ((tmpdir=getenv("TMPDIR")))) {
     tmpdir=getenv("TMP");
   }
 }
 if (!tmpdir) {tmpdir = "/tmp";}

 pattern = malloc(strlen(tmpdir)+strlen(tag)+2);
 if (!pattern) {
   failure("Could not malloc tempfile pattern");
 }
 strcpy(pattern, tmpdir);
 strcat(pattern, "/");
 strcat(pattern, tag);
 result = create_tempfile(pattern);
 free(pattern);
 return result;
}



main() {
 int c;
 FILE *demo_temp_file1;
 FILE *demo_temp_file2;
 char demo_temp_filename1[] = "/tmp/demoXXXXXX";
 char demo_temp_filename2[] = "second-demoXXXXXX";

 demo_temp_file1 = create_tempfile(demo_temp_filename1);
 demo_temp_file2 = smart_create_tempfile(demo_temp_filename2);
 fprintf(demo_temp_file2, "This is a test.\n");
 printf("Printing temporary file contents:\n");
 rewind(demo_temp_file2);
 while (  (c=fgetc(demo_temp_file2)) != EOF) {
   putchar(c);
 }
 putchar('\n');
 printf("Exiting; you'll notice that there are no temporary files on exit.\n");
}

Kennaway 表示,如果您不能使用 mkstemp(3),那么请使用 mkdtemp(3) 自己创建一个目录,该目录受到外界的保护。然而,正如 Michal Zalewski 指出的那样,如果有 tmp 清理器在使用,这是一个坏主意;相反,请在用户的 HOME 中使用一个目录。最后,如果您真的必须使用不安全的 mktemp(3),请使用大量的 X - 他建议使用 10 个(如果您的 libc 允许),以便文件名不容易被猜测(仅使用 6 个 X 意味着 5 个被 PID 占用,只留下一个随机字符,并允许攻击者发起一个简单的竞争条件)。请注意,这从根本上是不安全的,因此您通常不应这样做。我补充说,您也应该避免 tmpnam(3) - 它的一些用途在存在线程时不可靠,并且它不保证在 TMP_MAX 次使用后能够正确工作(但大多数实际用途必须在循环内)。

一般来说,您应该避免使用不安全的函数,如 mktemp(3) 或 tmpnam(3),除非您采取特定措施来应对其不安全性,或者在安装例程中测试安全的库实现。如果您想在 /tmp 或世界可写目录(或组可写目录,如果您不信任该组)中创建文件,并且不想使用 mk*temp()(例如,您打算使文件名可预测),那么始终使用 O_CREAT 和 O_EXCL 标志来 open() 并检查返回值。如果您的 open() 调用失败,则优雅地恢复(例如,退出)。

GNOME 编程指南建议在共享(临时)目录中创建文件系统对象以安全地打开临时文件时使用以下 C 代码 [Quintero 2000]
 char *filename;
 int fd;

 do {
   filename = tempnam (NULL, "foo");
   fd = open (filename, O_CREAT | O_EXCL | O_TRUNC | O_RDWR, 0600);
   free (filename);
 } while (fd == -1);
请注意,尽管使用了不安全的函数 tempnam(3),但它被包装在使用 O_CREAT 和 O_EXCL 的循环中以抵消其安全弱点,因此这种使用是可以的。请注意,您需要 free() 文件名。完成操作后,您应该 close() 和 unlink() 文件。如果您想使用标准 C I/O 库,您可以使用 fdopen() 和模式“w+b”将文件描述符转换为 FILE *。请注意,此方法在 NFS 版本 2 (v2) 系统上不起作用,因为较旧的 NFS 不正确地支持 O_EXCL。请注意,此方法的一个小缺点是,由于 tempnam 可能被不安全地使用,因此各种编译器和安全扫描器可能会就其使用发出虚假的警告。mkstemp(3) 不存在此问题。

如果您需要在 shell 脚本中使用临时文件,您可能最好使用管道,使用本地目录(例如,用户主目录中的某些内容),或者在某些情况下使用当前目录。这样,除非用户允许,否则不会有共享。如果您真的想要/需要临时文件位于共享目录(如 /tmp)中,请不要使用传统的 shell 技术,即在模板中使用进程 ID,并仅使用诸如“>”之类的普通操作创建文件。Shell 脚本可以使用“$$”来指示 PID,但 PID 可以很容易地被攻击者确定或猜测,攻击者可以预先创建具有相同名称的文件或链接。因此,以下“典型”的 shell 脚本是不安全
   echo "This is a test" > /tmp/test$$  # DON'T DO THIS.

如果您需要在 shell 脚本中使用临时文件或目录,并且您希望它位于 /tmp 中,有时建议的解决方案是使用 mktemp(1),它旨在用于 shell 脚本(请注意 mktemp(1) 和 mktemp(3) 是不同的东西)。然而,正如 Michal Zalewski 指出的那样,在许多运行 tmp 清理器的环境中,这是不安全的;问题是,当特权程序扫描临时目录时,它可能会暴露竞争条件。即使这不是真的,我也不建议使用在共享目录中创建临时文件的 shell 脚本;在私有目录中创建此类文件或改用管道通常更可取,即使您确定您的 tmpwatch 程序没问题(或者您没有本地用户)。如果您必须使用 mktemp(1),请注意 mktemp(1) 接受一个模板,然后使用 O_EXCL 创建文件或目录并返回结果名称;因此,mktemp(1) 在 NFS 版本 2 文件系统上不起作用。以下是 Bourne shell 脚本中正确使用 mktemp(1) 的一些示例;这些示例直接来自 mktemp(1) 手册页
 # Simple use of mktemp(1), where the script should quit
 # if it can't get a safe temporary file.
 # Note that this will be INSECURE on many systems, since they use
 # tmpwatch-like programs that will erase "old" files and expose race
 # conditions.

   TMPFILE=`mktemp /tmp/$0.XXXXXX` || exit 1
   echo "program output" >> $TMPFILE

  # Simple example, if you want to catch the error:

   TMPFILE=`mktemp -q /tmp/$0.XXXXXX`
   if [ $? -ne 0 ]; then
      echo "$0: Can't create temp file, exiting..."
      exit 1
   fi

Perl 程序员应该使用 File::Temp,它试图提供一种跨平台安全创建临时文件的方法。但是,请先仔细阅读有关如何正确使用它的文档;它也包括不安全函数的接口。我建议显式地将其 safe_level 设置为 HIGH;这将调用额外的安全检查。File::Temp 的 Perl 5.8 文档在线提供

不要重用临时文件名(即删除并重新创建它),无论您最初如何获得“安全”临时文件名。攻击者可以观察原始文件名并在您第二次重新创建它之前劫持它。当然,始终使用适当的文件权限。例如,仅在您需要世界/组访问文件时才允许世界/组访问,否则保持模式 0600(即,只有所有者可以读取或写入它)。

在您自己之后进行清理,可以通过使用退出处理程序,或利用 UNIX 文件系统语义并在创建后立即 unlink() 文件,以便目录条目消失,但文件本身仍然可以访问,直到指向它的最后一个文件描述符关闭。然后,您可以在程序中通过传递文件描述符继续访问它。取消链接文件对于代码维护有很多优点:文件会自动删除,无论您的程序如何崩溃。它还降低了维护人员不安全地使用文件名的可能性(他们需要改用文件描述符)。立即取消链接的一个小问题是,管理员更难查看磁盘空间的使用情况,因为他们不能简单地按名称查看文件系统。

您可以考虑确保您的类 Unix 系统代码尊重环境变量 TMP 或 TMPDIR,前提是这些变量值的提供者是受信任的。通过这样做,您可以让用户将其临时文件移动到非共享目录中(并消除此处讨论的问题),例如其主目录中的子目录。最新版本的 Bastille 可以设置这些变量以减少用户之间的共享。不幸的是,许多用户将 TMP 或 TMPDIR 设置为共享目录(例如 /tmp),因此即使设置了这些环境变量,您的安全程序仍然必须正确创建临时文件。这是 GNOME 方法的一个优势,因为至少在某些系统上,tempnam(3) 会自动使用 TMPDIR,而 mkstemp(3) 方法需要更多代码才能做到这一点。请不要为临时目录创建更多环境变量(例如 TEMP),尤其不要为每个应用程序创建不同的环境名称(例如,不要使用“MYAPP_TEMP”)。这样做会大大增加管理系统的复杂性,并且想要特定应用程序的特殊临时目录的用户只需在运行该特定应用程序时专门设置环境变量即可。当然,如果这些环境变量可能是由不受信任的源设置的,您应该忽略它们 - 如果您遵循第 5.2.3 节中的建议,您无论如何都会这样做。

如果临时目录是使用 NFS 版本 2 (NFSv2) 远程挂载的,则这些技术不起作用,因为 NFSv2 不正确地支持 O_EXCL。有关更多信息,请参阅第 7.10.2.1 节。NFS 版本 3 及更高版本正确支持 O_EXCL;简单的解决方案是确保临时目录是本地的,或者,如果使用 NFS 挂载,则使用 NFS 版本 3 或更高版本挂载。有一种在 NFS v2 上安全创建临时文件的技术,涉及使用 link(2) 和 stat(2),但这很复杂;请参阅第 7.10.2.1 节,其中包含有关此内容的更多信息。

顺便说一句,值得注意的是,FreeBSD 最近更改了 mk*temp() 系列,以消除文件名的 PID 组件,并将整个内容替换为 base-62 编码的随机性。这大大提高了“默认”使用 6 个 X 的临时文件的数量,这意味着即使使用 6 个 X 的 mktemp(3) 也具有合理的(概率上的)安全性,可以防止猜测,除非在非常频繁的使用情况下。但是,如果您也遵循此处的指导,您将消除他们正在解决的问题。

关于临时文件的许多信息都来自Kris Kennaway 于 2000 年 12 月 15 日发布到 Bugtraq 的关于临时文件的帖子

我应该注意到,来自 http://www.openwall.com/linux/ 的 Openwall Linux 补丁包括一个可选的“临时文件目录”策略,该策略可以应对许多基于临时文件的攻击。Linux 安全模块 (LSM) 项目包括一个“owlsm”模块,该模块实现了 OpenWall 的一些想法,因此带有 LSM 的 Linux 内核可以快速将这些规则插入到正在运行的系统中。启用后,它具有两个保护功能

  • 硬链接:在某些情况下,进程可能无法创建到文件的硬链接。OpenWall 文档指出“进程可能无法创建到它们没有写入权限的文件的硬链接。”在 LSM 版本中,规则如下:如果进程的 uid 和 fsuid(通常与 euid 相同)与链接文件的 uid 不同,进程 uid 不是 root,并且进程缺少 FOWNER 能力,则禁止硬链接。针对进程 uid 的检查可能会在将来某个时候被删除(它们是 atd(8) 程序的解决方法),届时规则将是:如果进程的 fsuid(通常与 euid 相同)与链接文件的 uid 不同,并且进程缺少 FOWNER 能力,则禁止硬链接。换句话说,您只能创建到您拥有的文件的硬链接,除非您具有 FOWNER 能力。

  • 符号链接(symlinks):某些符号链接不被遵循。原始 OpenWall 文档指出“root 进程可能不遵循 root 不拥有的符号链接”,但实际规则(从查看代码来看)更复杂。在 LSM 版本中,如果目录是粘滞的(“+t”模式,用于 /tmp 等共享目录),如果符号链接是由目录的所有者或当前进程的 fsuid(通常是有效 uid)以外的任何人创建的,则不遵循符号链接。

许多系统未实现此 openwall 策略,因此您通常不能依赖此策略来保护您的系统。但是,我鼓励在您自己的系统上使用此策略,并请确保您的应用程序在此策略到位时可以正常工作。

7.10.2. 锁定

在许多情况下,程序必须确保它对某些事物拥有独占权(例如,文件、设备和/或特定服务器进程的存在)。任何锁定资源的系统都必须处理锁的标准问题,即死锁(“致命拥抱”)、活锁以及在程序未清理其锁时释放“卡住”的锁。如果程序卡住等待彼此释放资源,则可能会发生死锁。例如,如果进程 1 锁定资源 A 并等待资源 B,而进程 2 锁定资源 B 并等待资源 A,则会发生死锁。许多死锁可以通过简单地要求所有锁定多个资源的进程以相同的顺序锁定它们来防止(例如,按锁定名称的字母顺序)。

7.10.2.1. 使用文件作为锁

在类 Unix 系统上,资源锁定传统上是通过创建文件来指示锁来完成的,因为这非常可移植。它还使“修复”卡住的锁变得容易,因为管理员只需查看文件系统即可查看已设置哪些锁。卡住的锁可能是因为程序未能自行清理(例如,它崩溃或发生故障)或因为整个系统崩溃而发生。请注意,这些是“建议性”锁(而不是“强制性”锁) - 所有需要资源的进程都必须合作才能使用这些锁。

但是,有几个陷阱要避免。首先,不要使用非常旧的 Unix C 程序使用的技术,即调用 creat() 或其 open() 等效项,即 open() 模式 O_WRONLY | O_CREAT | O_TRUNC,并将文件模式设置为 0(无权限)。对于普通文件系统上的普通用户,这可以工作,但是当用户具有 root 权限时,此方法无法锁定文件。即使文件已存在,Root 始终可以执行此操作。实际上,旧版本的 Unix 在旧编辑器“ed”中存在此特定问题 -- 症状是密码文件的某些部分偶尔会被放置在用户的文件中 [Rochkind 1985, 22]!相反,如果您正在为本地文件系统上的进程创建锁,则应使用带有 O_WRONLY | O_CREAT | O_EXCL 标志的 open()(再次,无权限,以便具有相同所有者的其他进程不会获得锁)。请注意 O_EXCL 的使用,这是创建“独占”文件的官方方式;这甚至适用于本地文件系统上的 root。[Rochkind 1985, 27]。

其次,如果锁定文件可能位于 NFS 挂载的文件系统上,那么您会遇到 NFS 版本 2 不完全支持正常文件语义的问题。即使对于应该“本地”于客户端的工作,这也可能是一个问题,因为某些客户端没有本地磁盘,并且可能通过 NFS 远程挂载所有文件。open(2) 的手册解释了如何在这种情况下处理事情(这也处理了 root 程序的情况)

“... 依赖于 [open(2) 的 O_CREAT 和 O_EXCL 标志在通过 NFS 版本 2 访问的文件系统上工作] 来执行锁定任务的程序将包含竞争条件。使用锁定文件执行原子文件锁定的解决方案是在同一文件系统上创建一个唯一文件(例如,合并主机名和 pid),使用 link(2) 创建到锁定文件的链接,并在唯一文件上使用 stat(2) 检查其链接计数是否增加到 2。不要使用 link(2) 调用的返回值。”

显然,此解决方案仅在所有执行锁定的程序都合作,并且所有不合作的程序都不允许干扰的情况下才有效。特别是,您用于文件锁定的目录不得具有用于创建和删除文件的宽松文件权限。

NFS 版本 3 在 open(2) 中添加了对 O_EXCL 模式的支持;请参阅 IETF RFC 1813,特别是“CREATE”的“mode”参数的“EXCLUSIVE”值。可悲的是,在撰写本文时,并非所有人都已切换到 NFS 版本 3 或更高版本,因此您还不能在可移植程序中依赖这一点。尽管如此,从长远来看,希望这个问题会消失。

如果您要锁定本地计算机上的设备或进程的存在,请尝试使用标准约定。我建议使用文件系统层次结构标准 (FHS);它被 Linux 系统广泛引用,但它也试图包含其他类 Unix 系统的思想。FHS 描述了此类锁定文件的标准约定,包括命名、放置和这些文件的标准内容 [FHS 1997]。如果您只想确保您的服务器在一台给定的计算机上不会多次执行,您通常应该创建一个进程标识符作为 /var/run/NAME.pid,其中 pid 作为其内容。类似地,您应该将诸如设备锁定文件的锁定文件放在 /var/lock 中。这种方法有一个小的缺点,即如果程序突然停止,则会留下悬空的文件,但这是标准做法,并且可以通过其他系统工具轻松处理该问题。

重要的是,使用文件来表示锁的协作程序使用相同的目录,而不仅仅是相同的目录名称。这是网络系统的一个问题:FHS 明确指出 /var/run 和 /var/lock 是不可共享的,而 /var/mail 是可共享的。因此,如果您希望锁在单台计算机上工作,但不干扰其他计算机,请使用不可共享的目录,如 /var/run(例如,您希望允许每台计算机运行自己的服务器)。但是,如果您希望网络中共享文件的所有计算机都遵守锁,则需要使用它们正在共享的目录;/var/mail 是这样的位置之一。有关此主题的更多信息,请参阅 FHS 第 2 节。

7.10.2.2. 其他锁定方法

当然,您不必使用文件来表示锁。网络服务器通常不需要费心;绑定到端口的行为本身就是一种锁,因为如果给定端口上存在现有服务器,则没有其他服务器能够绑定到该端口。

另一种锁定方法是使用 POSIX 记录锁,通过 fcntl(2) 实现为“自由裁量锁”。这些是自由裁量的,也就是说,使用它们需要需要锁的程序的合作(就像使用文件来表示锁的方法一样)。POSIX 记录锁有很多值得推荐的地方:POSIX 记录锁定在几乎所有类 Unix 平台上都受支持(POSIX.1 强制要求),它可以锁定文件的部分(而不仅仅是整个文件),并且它可以处理读锁和写锁之间的差异。更方便的是,如果进程死亡,其锁会自动删除,这通常是期望的结果。

您也可以使用强制锁,强制锁基于 System V 的强制锁定方案。这些仅适用于锁定文件的 setgid 位已设置,但组执行位未设置的文件。此外,您必须挂载文件系统以允许强制文件锁。在这种情况下,将检查每个 read(2) 和 write(2) 的锁定;虽然这比建议性锁更彻底,但也更慢。此外,强制锁不能广泛移植到其他类 Unix 系统(它们在 Linux 和基于 System V 的系统上可用,但不一定在其他系统上可用)。请注意,具有 root 权限的进程也可能被强制锁阻止,这使得它可能成为拒绝服务攻击的基础。