C 使用dup2时的竞争条件

C 使用dup2时的竞争条件,c,linux,posix,race-condition,dup2,C,Linux,Posix,Race Condition,Dup2,对于dup2系统调用,显示: EBUSY(仅限Linux)在运行期间,dup2()或dup3()可能会返回此消息 开路(2)和dup()时的竞争条件 它讨论了什么竞争条件?如果dup2出现EBUSY错误,我该怎么办?我是否应该像在EINTR的情况下那样重试?在fs/file.c中有一个解释: 当要释放的描述符仍在打开时处于某种不完整状态(fd\u处于打开状态,但在fdtable中不存在),则返回EBUSY 编辑(更多信息和确实想要赏金) 为了了解如何!tofree&&fd_是打开的(fd,fdt

对于
dup2
系统调用,显示:

EBUSY(仅限Linux)在运行期间,dup2()或dup3()可能会返回此消息 开路(2)和dup()时的竞争条件


它讨论了什么竞争条件?如果
dup2
出现
EBUSY
错误,我该怎么办?我是否应该像在
EINTR
的情况下那样重试?

fs/file.c
中有一个解释:

当要释放的描述符仍在打开时处于某种不完整状态(
fd\u处于打开状态
,但在
fdtable
中不存在),则返回
EBUSY

编辑(更多信息和确实想要赏金)

为了了解如何
!tofree&&fd_是打开的(fd,fdt)
可以发生,让我们看看文件是如何打开的。这里是简化版的
sys\u 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
fdt
使用名为
open_fds
的位向量。在
get\u unused\u fd\u flags()
之后,已分配
fd
,并在
open\u fds
中设置了相应的位。fdt上的锁已解除,但真正的VFS工作尚未完成

此时,另一个线程(或共享
fdt
的情况下的另一个进程)可以调用dup2,因为锁已被释放,所以dup2不会阻塞。如果
dup2
在此处采用其正常路径,则将替换
fd
,但仍将为旧文件运行
fd\u安装。因此,检查并返回
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. */

我并不完全了解Linux所做的选择,但在另一个答案中来自Linux内核的评论指向了13年前我在OpenBSD中所做的工作,因此我试图记住到底发生了什么

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

open
可以分配内存,进入文件系统并执行一系列可能长期阻塞的操作。对于像fuse这样的文件系统来说,最糟糕的情况是它甚至可能返回到userland。出于这个原因(以及其他原因),我们实际上不希望在整个打开操作期间锁定文件描述符表。内核中的锁在睡眠时很难保持,如果锁定操作的完成可能需要与userland进行交互[1],则情况会更糟

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

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

你问题中的错误很可能发生在你参加这场比赛时。为避免此问题,请不要
dup2
复制到您不知道其状态的文件描述符,除非您确定没有其他人将同时访问文件描述符表。唯一可以确保的方法是成为唯一正在运行的线程(文件描述符总是在后台被库打开),或者确切地知道要覆盖的文件描述符。首先,允许对未分配的文件描述符执行dup2的原因是
/* 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. */
#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;
}