使用dup2时的竞争条件

vit*_*aut 13 c linux posix race-condition dup2

此手册页dup2系统调用说:

EBUSY(仅适用于Linux)这可以通过DUP2开放(2)和DUP()的竞争条件时返回()或DUP3().

它谈什么竞争条件有关,我应该怎样做,如果dup2给出了EBUSY错误?我应该像我一样重试EINTR吗?

Gra*_*sus 13

有一个解释fs/file.c,do_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;
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

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

为了记住fds已经分配了一个被调用的位向量open_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. */
Run Code Online (Sandbox Code Playgroud)

  • 你能否详细说明何时会发生这种情况?我的印象是它肯定不会发生在单线程进程中(不共享其fd表(`CLONE_FILES`)的任务,而是内核源代码中的注释("所有这些都超出了POSIX或SUS的范围,因为考虑共享描述符表...")似乎可能是错误的,因为线程确实共享描述符表并由POSIX指定. (3认同)

Art*_*Art 8

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

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

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

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

在OpenBSD中,这可以通过跟踪具有复杂引用计数的已分配但尚未打开的文件描述符来解决.大多数操作只是假装文件描述符不存在(但它也不可分配)并且只会返回EBADF.但是因为dup2我们不能假装它不在那里,因为它是.最终的结果是,如果两个线程同时调用opendup2,开放,实际上对文件执行一个全开放的操作,但由于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只读文件关闭错误另一个线程意外行为的例子以及文件描述符如何表现奇怪因为什么我一直在谈论这里看到这个问题:套接字描述符没有在为多线程UDP客户端做'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;
}
Run Code Online (Sandbox Code Playgroud)

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

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