【问题标题】:C file pointer changing after fork and (failed) exec在 fork 和(失败)exec 之后更改 C 文件指针
【发布时间】:2021-04-30 01:21:37
【问题描述】:

我制作了制作 fork 的程序,我认为 child 不会影响 parent。

但文件指针发生了变化,尽管我没有在父级中进行任何更改。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void) {
    FILE *fp = fopen("sm.c", "r");
    char buf[1000];
    char *args[] = {"invailid_command", NULL};

    fgets(buf, sizeof(buf), fp);
    printf("I'm one %d %ld\n", getpid(), ftell(fp));
    if (fork() == 0) {
        execvp(args[0], args);
        exit(EXIT_FAILURE);
    }
    wait(NULL);
    printf("I'm two %d %ld\n", getpid(), ftell(fp));
} 

这个输出

I'm one 21500 20
I'm two 21500 -1

我想让文件指针在两个printf 调用之间不发生变化。

为什么文件指针会改变,即使execvp 失败,我能否使文件指针不可改变?

【问题讨论】:

  • 嗯。你真的在尝试执行“invalid_command”吗?
  • 是的。命令不可用,因此它返回错误“没有这样的文件或目录”
  • strace 下运行您的流程:strace -f -o /path/to/output/file ... 将整个输出作为代码发布到问题中。看起来您对fork() 的调用实际上是在使用vfork(),其中两个进程在调用之后和子进程中的任何后续exec*() 函数调用之前共享相同的地址空间。你运行的是哪个版本的 Linux?
  • 我认为您很有可能遇到Why does forking my process cause the file to be read indefinitely? 中讨论/诊断的问题,或该问题的变体。另请参阅链接的问题(来自其他问题 - 链接是 Uwanted child processes being created while file reading)。
  • 尝试在孩子中使用_exit 而不是exit。这为我修复了它(Ubuntu 16.04,gcc 5.4.0)。

标签: c linux file fork stdio


【解决方案1】:

感谢 Jonathan Leffler 为我们指明了正确的方向。

虽然您的程序在 CentOS 7 / GCC 4.8.5 / GLIBC 2.17 上不会对我产生相同的意外行为,但您观察到不同的行为是合理的。根据 POSIX(您依赖 fork),您的程序的行为实际上是未定义。以下是the relevant section 的部分摘录(已添加重点):

可以通过文件描述符访问打开的文件描述, 它是使用诸如open()pipe()之类的函数创建的,或者通过 使用fopen()popen() 等函数创建的流。 文件描述符或流在打开时称为“句柄” 它所指的文件描述;打开的文件描述可能有 几个把手。

[...]

涉及任何一个句柄的函数调用的结果(“活动 handle") 在本卷 POSIX.1-2017 的其他地方定义,但如果 使用了两个或多个句柄,其中任何一个都是流, 应用程序应确保他们的行动是协调的 如下面所描述的。 如果不这样做,结果是不确定的

[...]

要使句柄成为活动句柄,应用程序应确保 在最后一次使用之间执行以下操作 句柄(当前活动句柄)和第一个使用第二个 句柄(未来的活动句柄)。然后第二个句柄变成 主动句柄。 [...]

要应用这些规则,句柄不必在同一个进程中。

请注意,在fork() 之后,存在两个句柄,而之前存在一个句柄。 应用程序应确保,如果两个句柄都可以 访问时,他们都处于另一个可能成为 主动句柄优先。 【符合前项资格的,】申请应准备fork() 就好像它是活动句柄的变化一样。 (如果唯一的动作 由其中一个进程执行的是 exec 函数之一或 _exit()(不是exit()),在该进程中永远不会访问句柄。

对于第一个句柄,以下第一个适用条件适用。 [一长串不适用于 OP 情况的替代方案......]

  • 如果流以允许读取的模式打开,并且底层打开文件描述指的是能够读取的设备 寻求,应用程序应执行fflush(),或 流应关闭。

对于第二个句柄:

  • 如果任何先前的活动句柄已被显式更改文件偏移量的函数使用,但上述要求除外 第一个句柄,应用程序应执行lseek()fseek()(如 适合手柄类型)到适当的位置。

因此,对于 OP 的程序要在父子节点和子节点中访问相同的流,POSIX 要求父节点 fflush() stdin 在分叉之前,子节点 fseek() 在启动之后。然后,在等待子进程终止后,父进程必须fseek() 流。然而,鉴于我们知道孩子的 exec 将失败,因此可以通过让孩子使用 _exit()(不访问流)而不是 exit() 来避免所有刷新和查找的要求。

遵守 POSIX 的规定会产生以下结果:

当遵循这些规则时,无论句柄的顺序如何 使用时,实现应确保应用程序,甚至是一个 由多个过程组成,应产生正确的结果:无数据 写入时应丢失或重复,所有数据应 按顺序书写,除非寻求者要求。

值得注意的是,

这是 实现定义是否以及在什么条件下,所有输入 只出现一次。


我很理解,仅仅听到相关标准不证明您对程序行为的期望是合理的,这可能有点令人不满意,但这就是全部。父进程和子进程确实有一些以公共打开文件描述的形式存在的相关共享数据(与它们相关联的单独句柄),这似乎是意外的车辆(和undefined) 行为,但没有任何基础可以预测您看到的特定行为,也没有预测我在同一个程序中看到的不同行为。

【讨论】:

    【解决方案2】:

    我能够使用 gcc 5.4.0 在 Ubuntu 16.04 上重现此问题。这里的罪魁祸首是exit 以及子进程的创建方式。

    exit 的手册页声明如下:

    exit() 函数导致正常的进程终止和值 的状态 & 0377 返回给父级(请参阅等待(2))。

    所有用 atexit(3) 和 on_exit(3) 注册的函数都是 以注册的相反顺序调用。 (有可能的 让这些函数之一使用 atexit(3) 或 on_exit(3) 来 注册一个在退出处理期间要执行的附加函数; 新注册被添加到列表的前面 仍有待调用的函数。)如果这些函数之一确实 不返回(例如,它调用 _exit(2),或者用 信号),那么剩余的函数都不会被调用,并且进一步 退出处理(特别是 stdio(3) 流的刷新) 被遗弃。如果一个函数被多次注册 使用 atexit(3) 或 on_exit(3),那么它被调用的次数为 已注册。

    所有打开的 stdio(3) 流都被刷新和关闭。创建的文件 tmpfile(3) 被删除。

    C 标准指定了两个常量,EXIT_SUCCESS 和 EXIT_FAILURE,可以传递给 exit() 以指示成功或 分别终止不成功。

    因此,当您在孩子中调用 exit 时,它会关闭由 fp 表示的 FILE

    通常在创建子进程时,它会获取父进程文件描述符的副本。然而,在这种情况下,孩子的记忆似乎仍然指向父母的记忆。因此,当exit 关闭FILE 时,它会影响父级。

    如果您将子代改为调用_exit,它会关闭子代的文件描述符,但设法不触及FILE 对象,并且父代中对ftell 的第二次调用将成功。无论如何,在未执行的子级中使用 _exit 是一种很好的做法,因为它可以防止在子级中调用 atexit 处理程序。

    【讨论】:

    • 我不买它。一个进程的任何动作都不能直接影响另一个进程的内存,无论两者之间的关系如何(或者如果确实如此,那么实现就是不合格的)。但@JonathanLeffler 似乎确实在解释为什么从exit() 更改为_exit() 会有所作为。
    • 但是,在这种情况下,孩子的记忆似乎仍然指向父母的。因此,当 exit 关闭 FILE 时,它会影响父进程。 我认为是子进程中底层文件描述符上的 lseek() 等调用作为exit() 上的部分刷新缓冲区影响了偏移量父文件描述符。与跨进程内存污染相比,这肯定是一个不那么严重的错误。我还没有阅读@JonathanLeffler 链接的所有数据,但他的理论对我来说似乎更有可能。
    • 我必须在退出前调用 fclose ,这样我的程序才能正常运行!谢谢你的回答。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-12-19
    • 2012-10-25
    • 2014-02-26
    • 1970-01-01
    • 1970-01-01
    • 2011-10-06
    相关资源
    最近更新 更多