13

这个dup2系统调用的手册页说:

EBUSY(仅限 Linux)这可能由 dup2() 或 dup3() 在与 open(2) 和 dup() 的竞争条件下返回。

dup2它谈论什么竞争条件,如果EBUSY出现错误我该怎么办?我应该重试EINTR吗?

4

2 回答 2

13

,中有解释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_openfdtable

编辑(更多信息并且想要赏金)

为了了解如何!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了进程。他们都使用锁,所以在这两个调用中没有什么不好的。

为了记住fds已分配的位向量open_fdsfdt. 之后get_unused_fd_flags()fd已分配并设置了相应的位open_fds。上的锁fdt已经被释放,但真正的 VFS 工作还没有完成。

在这个精确的时刻,另一个线程(或 shared 情况下的另一个进程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. */
于 2014-05-03T12:33:51.393 回答
8

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

由于实现的方式open,它首先分配一个文件描述符,然后它实际上尝试在解锁文件描述符表的情况下完成打开操作。一个原因可能是我们实际上不想引起 open 的副作用(最简单的方法是更改​​文件的时间,但例如打开设备可能会产生更严重的副作用),因为我们退出了文件描述符。这同样适用于分配文件描述符的所有其他操作,当您阅读下面的文本时,只需替换open为“任何分配文件描述符的系统调用”。我不记得这是 POSIX 强制要求的,还是只是事情一直以来的做法。

open可以分配内存,进入文件系统并做一堆可能长时间阻塞的事情。对于像 fuse 这样的文件系统,在最坏的情况下,它甚至可能会回到用户态。由于这个原因(和其他原因),我们实际上并不想在整个打开操作期间锁定文件描述符表。内核中的锁在睡眠时很难保持,如果完成锁定操作可能需要与用户空间[1]进行交互,则更是如此。

当有人open在一个线程(或共享相同文件描述符表的进程)中调用时,就会出现问题,它分配了一个文件描述符并且尚未完成它,而同时另一个线程dup2指向相同的文件描述符open得到了。由于未完成的文件描述符仍然无效(例如readwrite当您尝试使用它时将返回 EBADF),我们实际上还不能关闭它。

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

当您参加这场比赛时,您问题中的错误很可能会发生。为避免这种情况,请不要dup2访问您不知道状态的文件描述符,除非您确定没有其他人将同时访问文件描述符表。唯一可以确定的方法是成为唯一运行的线程(文件描述符一直由库在你背后打开)或者确切地知道你正在覆盖什么文件描述符。dup2首先允许未分配的文件描述符的原因是关闭 fds 0、1 和 2 以及 dup2 /dev/null 进入它们是一种常见的习惯用法。

另一方面,之前不关闭文件描述符dup2将丢失从close. 不过我不担心,因为错误close是愚蠢的,一开始就不应该存在:处理 C 只读文件关闭错误另一个线程意外行为的示例以及文件描述符如何奇怪地表现,因为什么我一直在谈论这里看到这个问题:Socket descriptor not getting released on doing 'close ()' for a multi-threaded UDP client

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

#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;
}

FIFO 是导致open阻塞的标准方法,只要您愿意。正如预期的那样,这在 OpenBSD 上静默运行,在 Linux 上dup2返回 EBUSY。在 MacOS 上,由于某种原因,它会杀死我执行“echo foo > xxx”的 shell,而打开它进行编写的普通程序可以正常工作,我不知道为什么。

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

于 2014-06-03T09:48:40.033 回答