首页 > 解决方案 > 当文件被另一个程序打开时自动文件缓冲区刷新*仅当*其他程序尚未运行 (macOS)

问题描述

我在 macOS 上遇到了一个奇怪的行为,我开始调查这是因为在告诉操作系统打开我刚刚处理但没有关闭或明确刷新的 RTF 文件时会发生意外的竞争条件。

如果文件处理程序(Word、TextEdit 等)尚未打开,则调用system("open test.rtf")将很好地打开文件并且文件将完整。

但是,如果文件处理程序已经打开,则调用system("open test.rtf")将导致文件已损坏或被截断的错误消息(因为缓冲区未完全刷新)。

明显的解决方法是在打开文件之前fflush()和/或我的文件。fclose()但是,我对我的程序运行时和 macOS 之间的底层交互更感兴趣。我的问题是:文件处理程序的运行/未运行状态如何以及为什么会影响我的缓冲区是否被刷新?

(这不仅仅是打开程序所需的时间问题——我在版本中添加了一个睡眠延迟,它没有显式刷新缓冲区,它没有任何区别。)

未刷新的版本(仅在文件处理程序尚未运行时才有效):

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>

int main(void) {
    FILE *fin  = fopen("src.rtf", "rb");
    FILE *fout = fopen("test.rtf", "wb");
    int c;

    assert(fin && fout);
    while ((c=fgetc(fin)) != EOF) fputc(c, fout);

    sleep(3);
    system("open test.rtf");

    fclose(fin);
    fclose(fout);
    
    return 0;
}

显式刷新版本(一直有效):

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main(void) {
    FILE *fin  = fopen("src.rtf", "rb");
    FILE *fout = fopen("test.rtf", "wb");
    int c;

    assert(fin && fout);
    while ((c=fgetc(fin)) != EOF) fputc(c, fout);

    fflush(fout);
    
    system("open test.rtf");

    fclose(fin);
    fclose(fout);
    
    return 0;
}

我正在使用的示例 RTF 文件在这里:https ://pastebin.com/mXLk85G1

标签: cmacosfilesystemsfflush

解决方案


好的,所以,我最好的猜测是:

system()→<code>fork()→<code>exec()→<code>/bin/sh

/bin/sh处理其参数并发送命令:

fork()→<code>exec()→<code>打开

/usr/bin/open处理其参数,通过 LaunchServices 查找文件处理程序,并尝试使用附属程序打开文件。

这是纯粹的猜测:

  • 已经运行/usr/bin/open使用 IPC 告诉已经运行的应用程序尝试打开文件。应用程序在其现有文件描述符表中打开一个新文件描述符并将其读入,获得截断版本,因为原始流尚未被fflush()'d 或fclose()'d 。

  • Not Yet Running/usr/bin/open发现应用程序尚未运行,并通过fork()→<code>exec() 启动它。这意味着应用程序仍然具有来自原始程序的原始 fd 表。Cocoa 运行时检查 fd 表,发现它已经打开以供写入,并在重新打开以供读取之前将其关闭,从而导致输出缓冲区被刷新。

我已经验证关闭子文件中的文件fork()将导致输出缓冲区被刷新。以下将始终如一地工作:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>

int main(void) {
    FILE *fin  = fopen("src.rtf", "rb");
    FILE *fout = fopen("test.rtf", "wb");
    int c;
    int pid, wpid, wstat; 

    assert(fin && fout);
    while ((c=fgetc(fin)) != EOF) fputc(c, fout);

    pid = fork();
    if (!pid) {
        fclose(fout);
        fout = NULL;
    } else {
        wpid = waitpid(pid, &wstat, WUNTRACED);
        execl("/bin/sh", "sh", "-c", "open test.rtf", (char *)0);
    }

    if (fin)  fclose(fin); 
    if (fout) fclose(fout); 
    
    return 0;
}

但是,我的理论存在一个主要问题:虽然 fd 表可能会持续fork()exec()整天,但在覆盖图像FILE*时,进程内存(包括 )将被破坏。exec()

在进一步研究之后,我发现打开 Cocoa 文件处理程序会导致launchd启动/sbin/filecoordinationd,这是一个“协调对文件的访问”的守护进程。https://www.unix.com/man-page/osx/8/filecoordinationd/。而且,确实,TextEdit 注册为NSFilePresenterProxy. macOS 有一个完整的底层文件访问机制来监视文件更改并确保不同进程访问的文件保持良好状态。有意义的是,一旦 TextEdit 注册为NSFilePresenter, invoking /sbin/filecoordinationd,守护程序将确保它知道的任何已打开的缓冲区都将进入良好状态。

但是它如何使用我的程序来做到这一点,它不使用 Cocoa 并且没有注册为NSFile-anything?最可能的答案是 NS 类的文件协调机制是在 中实现的libSystem.dylib,它也用作系统 C 库。macOS 系统 C 库可能带有内置功能,可以让操作系统刷新进程运行时缓冲区。

那么,当 Cocoa 应用程序已经运行时,它为什么不这样做呢?它可能不知道它应该这样做。如果 TextEdit 没有打开文件,并且打开文件的进程没有向 注册NSFile...并且 TextEdit 没有继承文件描述符表,那么 Cocoa 生态系统中的任何人都不会知道它应该告诉/sbin/filecoordinationd以确保输出文件的缓冲区被刷新。

这似乎是最好的工作理论,并且在没有 macOS 工程师输入或访问源代码的情况下/usr/bin/open,我将得到答案。/sbin/filecoordinationd


推荐阅读