使用dup2时的竞争条件

时间:2014-05-03 04:21:24

标签: c linux posix race-condition dup2

dup2系统调用的

This manpage表示:

  

EBUSY(仅限Linux)在执行期间,可能由dup2()或dup3()返回   比赛条件有open(2)和dup()。

它会谈论什么竞争条件,如果dup2给出EBUSY错误,该怎么办?我是否应该像EINTR

那样重试

2 个答案:

答案 0 :(得分:12)

fs/file.cdo_dup2()中有一个解释:

/*
 * We need to detect attempts to do dup2() over allocated but still
 * not finished descriptor.  NB: OpenBSD avoids that at the price of
 * extra work in their equivalent of fget() - they insert struct
 * file immediately after grabbing descriptor, mark it larval if
 * more work (e.g. actual opening) is needed and make sure that
 * fget() treats larval files as absent.  Potentially interesting,
 * but while extra work in fget() is trivial, locking implications
 * and amount of surgery on open()-related paths in VFS are not.
 * FreeBSD fails with -EBADF in the same situation, NetBSD "solution"
 * deadlocks in rather amusing ways, AFAICS.  All of that is out of
 * scope of POSIX or SUS, since neither considers shared descriptor
 * tables and this condition does not arise without those.
 */
fdt = files_fdtable(files);
tofree = fdt->fd[fd];
if (!tofree && fd_is_open(fd, fdt))
    goto Ebusy;

当要释放的描述符仍处于打开状态(EBUSY但在fd_is_open中不存在时)处于某种不完整状态时,看起来会返回fdtable

编辑(更多信息并希望获得赏金)

为了理解!tofree && fd_is_open(fd, fdt)如何发生,让我们看看如何打开文件。这是sys_open的简化版本:

long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
    /* ... irrelevant stuff */
    /* allocate the fd, uses a lock */
    fd = get_unused_fd_flags(flags);
    /* HERE the race condition can arise if another thread calls dup2 on fd */
    /* do the real VFS stuff for this fd, also uses a lock */
    fd_install(fd, f);
    /* ... irrelevant stuff again */
    return fd;
}

基本上发生了两件非常重要的事情:分配文件描述符,然后才由VFS实际打开它。这两个操作会修改进程的fdt。他们都使用锁,所以在这两个电话中没什么不好的。

为了记住已分配了哪个fdsopen_fds使用了名为fdt的位向量。在get_unused_fd_flags()之后,已分配fd并在open_fds中设置相应的位。 fdt上的锁定已被释放,但真正的VFS作业尚未完成。

在这个精确的时刻,另一个线程(或共享fdt的情况下的另一个进程)可以调用dup2,因为锁已被释放,所以不会阻塞。如果dup2在此处采用其正常路径,则fd将被替换,但仍会为旧文件运行fd_install。因此检查并返回Ebusy

我在fd_install()的评论中找到了有关此竞争条件的其他信息,这证实了我的解释:

/* The VFS is full of places where we drop the files lock between
 * setting the open_fds bitmap and installing the file in the file
 * array.  At any such point, we are vulnerable to a dup2() race
 * installing a file in the array before us.  We need to detect this and
 * fput() the struct file we are about to overwrite in this case.
 *
 * It should never happen - if we allow dup2() do it, _really_ bad things
 * will follow. */

答案 1 :(得分:7)

我并不完全了解Linux所做出的选择,但是Linux内核在其他答案中的评论指的是13年前我在OpenBSD中所做的工作,所以我在这里尝试记住到底是怎么回事。

由于实现open的方式,它首先分配文件描述符,然后它实际上尝试在解锁文件描述符表的情况下完成打开操作。一个原因可能是我们实际上并不想造成打开的副作用(最简单的是改变文件上的时间,但是例如打开设备会有更严重的副作用)如果它失败,因为我们&# 39;重新出文件描述符。这同样适用于分配文件描述符的所有其他操作,当您阅读下面的文本时,只需将open替换为"任何分配文件描述符的系统调用"。我不记得这是否是POSIX要求的,或者只是事情总是如此。

open可以分配内存,转到文件系统并执行一些可能长时间阻塞的事情。在最糟糕的情况下,像保险丝这样的文件系统甚至可能会回到用户区。出于这个原因(和其他人),我们实际上并不想在整个打开操作期间保持文件描述符表被锁定。如果完成锁定操作可能需要与userland [1]进行交互,那么内核中的锁定在睡眠时是非常糟糕的。

当有人在一个线程(或共享相同文件描述符表的进程)中调用open时会发生问题,它会分配一个文件描述符并且还没有完成它同时另一个线程执行dup2指向open刚刚获得的同一文件描述符。由于未完成的文件描述符仍然无效(例如read并且write将在您尝试使用时返回EBADF),我们实际上无法关闭它。

在OpenBSD中,这是通过跟踪已分配但尚未打开的具有复杂引用计数的文件描述符来解决的。大多数操作只是假装文件描述符不在那里(但它也不可分配)并且只返回EBADF。但对于dup2,我们无法假装它不存在,因为它是。最终结果是,如果两个线程同时调用opendup2,open将实际对文件执行完全打开操作,但由于dup2赢得了文件描述符的竞争,最后一件事open的作用是减少刚刚分配的文件的引用计数并再次关闭它。与此同时,dup2赢得了比赛并且假装关闭了open得到的文件描述符(实际上并没有这样做open实际上是open。内核选择哪种行为并不重要,因为在这两种情况下,这种竞赛都会导致dup2dup2出现意外行为。最好的情况是,Linux返回EBUSY只是缩小了比赛的窗口,但比赛仍然存在,没有什么能阻止open调用发生,就像open正在返回另一个线程并在dup2的调用者有机会使用之前替换文件描述符。

当您参加此次比赛时,您的问题中的错误很可能会发生。为了避免它不dup2到文件描述符,你不知道状态,除非你确定没有其他人同时访问文件描述符表。确保唯一的方法是成为唯一运行的线程(文件描述符始终由库打开)或者确切知道您要覆盖的文件描述符。首先允许dup2超过未分配文件描述符的原因是它是将fds 0,1和2以及dup2 / dev / null关闭到它们中的常用习惯用法。

另一方面,在close之前不关闭文件描述符将失去close的错误返回。我不担心这一点,因为来自#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <err.h> #include <pthread.h> static void * do_bad_things(void *v) { int *ip = v; int fd; sleep(2); /* pretend this is proper synchronization. */ if ((fd = open("/dev/null", O_RDONLY)) == -1) err(1, "open 2"); if (dup2(fd, *ip)) warn("dup2"); return NULL; } int main(int argc, char **argv) { pthread_t t; int fd; /* This will be our next fd. */ if ((fd = open("/dev/null", O_RDONLY)) == -1) err(1, "open"); close(fd); if (mkfifo("xxx", 0644)) err(1, "mkfifo"); if (pthread_create(&t, NULL, do_bad_things, &fd)) err(1, "pthread_create"); if (open("xxx", O_RDONLY) == -1) err(1, "open fifo"); return 0; } 的错误是愚蠢的,并且不应该在那里出现:Handling C Read Only File Close Errors关于线程的意外行为的另一个例子以及如何文件描述符表现得很奇怪,因为我在这里谈论的是这个问题:Socket descriptor not getting released on doing 'close ()' for a multi-threaded UDP client

以下是一些触发此操作的示例代码:

open

FIFO是导致dup2阻止的标准方法,只要您愿意。正如预期的那样,这在OpenBSD上无声地工作,在Linux {{1}}上返回EBUSY。在MacOS上由于某种原因它杀死了我所做的shell&#34; echo foo&gt; xxx&#34;,虽然只是打开它进行写作的普通程序工作正常,但我不知道为什么。

[1]这里有一则轶事。我参与编写用于AFS实现的类似熔丝的文件系统。我们遇到的一个错误是我们在调用userland时持有文件对象锁。目录条目查找的锁定协议要求您保持目录锁定,然后查找目录条目,锁定该目录条目下的对象,然后释放目录锁定。由于我们保存了文件对象锁,因此其他一些进程进入并尝试查找该文件,这导致该进程在保持目录锁定的同时为文件锁休眠。另一个进程进来,试图查找目录,最后持有父目录的锁。长话短说,我们最终得到了一系列锁,直到我们到达根目录。同时,文件系统守护程序仍在通过网络与服务器通信。由于某种原因,网络操作失败,并且文件系统守护程序需要记录错误消息。要做到这一点,它必须阅读一些语言环境数据库。要做到这一点,它需要使用完整路径打开文件。但由于根目录被其他人锁定,守护进程等待该锁定。而且我们有一个长锁链8锁。这就是为什么内核经常执行复杂的柔术体操以避免在长时间操作(特别是文件系统操作)期间持锁。