重新链接一个匿名(未链接但已打开)的文件。

41
在Unix中,你可以通过使用creat()创建并打开文件,然后使用unlink()删除目录链接的方式,创建一个匿名文件句柄——这将留下一个拥有inode和存储空间但没有可能重新打开它的文件。这种文件通常用作临时文件(并且通常是tmpfile()返回给你的结果)。
我的问题是:有没有办法将这样的文件重新附加到目录结构中?如果可以这样做,则意味着您可以实现文件写入,使得文件显示为原子性和完全形成。这很符合我的强迫整洁症;)
当浏览相关的系统调用函数时,我希望找到一个名为flink()的link()版本(类似于chmod()/fchmod()),但至少在Linux上没有这个功能。
额外奖励积分的方法是告诉我如何创建匿名文件,而不会在磁盘目录结构中暴露文件名。
5个回答

44

几年前提交了一个针对Linux flink()系统调用的补丁A patch for a proposed Linux flink() system call, 但是当Linus说“除非我们进行主要的其他侵入,否则我们绝对无法安全地执行此操作”"there is no way in HELL we can do this securely without major other incursions",这基本上结束了是否添加它的辩论。

更新:从Linux 3.11开始,现在可以使用带有新标志O_TMPFILEopen()创建无目录项文件,并使用具有AT_SYMLINK_FOLLOW标志的linkat()将其链接到文件系统中的/proc/self/fd/fd

以下示例提供在open()手册页面上:

    char path[PATH_MAX];
    fd = open("/path/to/dir", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR);

    /* File I/O on 'fd'... */

    snprintf(path, PATH_MAX,  "/proc/self/fd/%d", fd);
    linkat(AT_FDCWD, path, AT_FDCWD, "/path/for/file", AT_SYMLINK_FOLLOW);

请注意,linkat() 不允许在使用 unlink() 删除最后一个链接后重新连接已打开的文件。

2
令人困惑的是,linkat() 在尝试重新连接一个普通的打开但未链接的文件时会返回 ENOENT。(使用 AT_SYMLINK_FOLLOWAT_EMPTY_PATH - Peter Cordes
我发布了一个 Perl 包装器(实际上并不实用,因为您仍然无法重新链接没有现有链接的文件),作为单独的答案。 - Peter Cordes
linkat 在没有 /proc 的系统上(例如 macOS)也可以使用吗?如果可以,第一个路径参数是什么? - splicer
@mattsven 这是因为你试图硬链接 /dev/fd/X 符号链接本身。对于 ln,你需要指定 -L 来硬链接此链接的目标(给出 ENOENT Peter Cordes 提到的)。 - user185953
@PeterCordes 我同意这很令人困惑。我会期望 EPERM。但是这就是 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=aae8a97d3ec30788790d1720b71d76fd8eb44b73 所做的事情。感谢 cdhowie 在下面链接的 SF 问题中提到的 rustyx。 - user185953
显示剩余3条评论

3

感谢@mark4o发布关于linkat(2)的帖子,有关详细信息请参阅他的答案。

我想试一试,看看将匿名文件链接回存储它的文件系统时实际发生了什么。(通常是/tmp,例如Firefox正在播放的视频数据)。


截至Linux 3.16,似乎仍然没有办法恢复已删除但仍被打开的文件。即使作为root用户,对于曾经有名称的已删除文件,linkat(2)AT_SYMLINK_FOLLOWAT_EMPTY_PATH也无法做到这一点。

唯一的替代方法是tail -c +1 -f /proc/19044/fd/1 > data.recov,它会创建一个单独的副本,并且完成后必须手动杀死它。


这是我为测试而编写的perl包装器。使用strace -eopen,linkat linkat.pl - </proc/.../fd/123 newname来验证您的系统仍然无法恢复打开的文件。(即使使用sudo也是如此)。显然,在运行它之前,您应该阅读在互联网上找到的代码,或使用沙箱帐户。

#!/usr/bin/perl -w
# 2015 Peter Cordes <peter@cordes.ca>
# public domain.  If it breaks, you get to keep both pieces.  Share and enjoy

# Linux-only linkat(2) wrapper (opens "." to get a directory FD for relative paths)
if ($#ARGV != 1) {
    print "wrong number of args.  Usage:\n";
    print "linkat old new    \t# will use AT_SYMLINK_FOLLOW\n";
    print "linkat - <old  new\t# to use the AT_EMPTY_PATH flag (requires root, and still doesn't re-link arbitrary files)\n";
    exit(1);
}

# use POSIX qw(linkat AT_EMPTY_PATH AT_SYMLINK_FOLLOW);  #nope, not even POSIX linkat is there

require 'syscall.ph';
use Errno;
# /usr/include/linux/fcntl.h
# #define AT_SYMLINK_NOFOLLOW   0x100   /* Do not follow symbolic links.  */
# #define AT_SYMLINK_FOLLOW 0x400   /* Follow symbolic links.  */
# #define AT_EMPTY_PATH     0x1000  /* Allow empty relative pathname */
unless (defined &AT_SYMLINK_NOFOLLOW) { sub AT_SYMLINK_NOFOLLOW() { 0x0100 } }
unless (defined &AT_SYMLINK_FOLLOW  ) { sub AT_SYMLINK_FOLLOW  () { 0x0400 } }
unless (defined &AT_EMPTY_PATH      ) { sub AT_EMPTY_PATH      () { 0x1000 } }


sub my_linkat ($$$$$) {
    # tmp copies: perl doesn't know that the string args won't be modified.
    my ($oldp, $newp, $flags) = ($_[1], $_[3], $_[4]);
    return !syscall(&SYS_linkat, fileno($_[0]), $oldp, fileno($_[2]), $newp, $flags);
}

sub linkat_dotpaths ($$$) {
    open(DOTFD, ".") or die "open . $!";
    my $ret = my_linkat(DOTFD, $_[0], DOTFD, $_[1], $_[2]);
    close DOTFD;
    return $ret;
}

sub link_stdin ($) {
    my ($newp, ) = @_;
    open(DOTFD, ".") or die "open . $!";
    my $ret = my_linkat(0, "", DOTFD, $newp, &AT_EMPTY_PATH);
    close DOTFD;
    return $ret;
}

sub linkat_follow_dotpaths ($$) {
    return linkat_dotpaths($_[0], $_[1], &AT_SYMLINK_FOLLOW);
}


## main
my $oldp = $ARGV[0];
my $newp = $ARGV[1];

# link($oldp, $newp) or die "$!";
# my_linkat(fileno(DIRFD), $oldp, fileno(DIRFD), $newp, AT_SYMLINK_FOLLOW) or die "$!";

if ($oldp eq '-') {
    print "linking stdin to '$newp'.  You will get ENOENT without root (or CAP_DAC_READ_SEARCH).  Even then doesn't work when links=0\n";
    $ret = link_stdin( $newp );
} else {
    $ret = linkat_follow_dotpaths($oldp, $newp);
}
# either way, you still can't re-link deleted files (tested Linux 3.16 and 4.2).

# print STDERR 
die "error: linkat: $!.\n" . ($!{ENOENT} ? "ENOENT is the error you get when trying to re-link a deleted file\n" : '') unless $ret;

# if you want to see exactly what happened, run
# strace -eopen,linkat  linkat.pl

2
我的问题是:是否有办法将这样的文件重新附加到目录结构中?如果您能做到这一点,那么您可以实现文件写入,使文件以原子方式完整地显示。这符合我的强迫症倾向。 ;)
如果这是您唯一的目标,那么您可以以更简单、更广泛使用的方式实现。如果您要输出到a.dat:
1. 打开a.dat.part进行写操作。 2. 写入您的数据。 3. 将a.dat.part重命名为a.dat。
我可以理解想要整洁的心情,但是为了“整洁”而取消链接文件并重新链接它似乎有些愚蠢。
此外,这个问题在ServerFault上提出,表明这种重新链接是不安全且不受支持的。

cdhowie是正确的,只写入临时文件要好得多。请注意,您链接的问题基本上说它无法完成:您无法从/proc硬链接到另一个文件系统。 - poolie
@poolie 不知何故我错过了这个。将链接更改为serverfault上更合适的问题。 - cdhowie
2
区别在于,在serverfault的问题中,程序是一个不透明的东西(因为它是一个系统管理员论坛 - 在这里我指的是实际上从进程内部以编程方式处理文件句柄)。如果您可以明确排除这一点,那么我们就有了答案;) - ijw
2
此外,我认为这并不傻。该文件具有临时文件的所有优点 - 基本上不存在,保证只有一个写入器/读取器等,直到您决定将其提供给用户。如果程序崩溃,它也会消失,而不是成为损坏的半写文件。 - ijw
想这样做并不是本质上的愚蠢,但考虑到我们今天使用的Unix模型和API,想这样做有点傻。 - poolie
@ijw同意,一个在准备好之前不存在的文件并不愚蠢。引用https://manpages.debian.org/bullseye/manpages-dev/linkat.2.en.html:“如果文件的链接计数为零,则通常不起作用(使用O_TMPFILE创建且没有O_EXCL的文件是一个例外)”,也就是说,如果您像这样创建文件,它将起作用。 - user185953

0

显然,这是可能的--例如fsck就可以。 但是,fsck使用了主要的本地化文件系统技巧,并且显然不可移植,也不能作为非特权用户执行。 这类似于上面的debugfs注释。

编写flink(2)调用将是一项有趣的练习。 正如ijw指出的那样,它会比当前的临时文件重命名做法(重命名注意,是保证原子性的)提供一些优势。


-2

2
正如我所预料的,那只是 cat /proc/<pid>/fd/N > newfile。如果你不知道 /proc/fd 的话,那很好,但这并不是这个问题的答案。在使用 cpcat 拍摄快照后,对已删除文件的进一步更改将不会反映出来。(如果写入它的进程只是添加,则使用 tail -c +1 -f /proc/<pid>/fd/N > newfile 应该会让你得到内容的副本。) - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接