一个fork出来的子进程能够确定它是通过fork还是vfork创建的吗?

4

在子进程内部,有没有办法确定它是通过叠加内存启动的fork,还是共享内存的vfork?

基本上,我们的日志引擎在vfork中需要更加小心(并且不记录某些类别的活动)。在fork中,它需要以一种vfork中不需要的方式与父进程合作。我们知道如何做这两件事,但不知道如何决定。

我知道我可能可以拦截fork/vfork/clone调用,并将fork/vfork/映射状态存储为标志,但如果有一个API调用子进程可以使用来确定自己的状态会使生活变得更简单。

额外加分:理想情况下,我还需要找到库中已经进行了fork或vfork并回调到我们代码的任何地方。怎么做呢?我们有至少一个类似于popen的API提供了一个客户端回调,在执行前从fork子进程中调用。显然,在vfork中该回调的实用性受到显著限制。


5
将这些信息作为参数传递给孩子。 - Eugene Sh.
1
顺便提一下,您不能拦截 vfork(),因为 vfork() 子进程不得从调用它的函数中返回。编写一个 libc 包装器 vfork() => clone() 由于寄存器压力而难以实现。 - Joshua
1
@AndrewHenle:世界已经变了,vfork()再次回归。 posix_spawn()是基于vfork()实现的,因为不存在多线程问题,所以没有人报告过这个问题。 - Joshua
1
与“朋友不让朋友调用vfork”相比,这确实是更好的选择。 - Joshua
1
@Joshua,你有没有仔细阅读过Linux手册中vfork()的语义?“如果由vfork()创建的进程修改除用于存储从vfork()返回值的pid_t类型变量之外的任何数据,或从调用vfork()的函数中返回,或在成功调用_exit(2)exec(3)函数族之一之前调用任何其他函数,则其行为是未定义的。” 如果修改任何内存或调用任何函数都是未定义行为,那么它如何卷土重来呢? - Andrew Henle
显示剩余14条评论
4个回答

6

所有没有特别设计用于在 vfork() 下工作的代码都无法在 vfork() 下工作。

从技术上讲,可以通过调用 mmap() 并检查父进程是否继承了 /proc 下的内存映射来检查是否处于 vfork() 子进程中。但请不要编写此代码。这是一个非常糟糕的想法,没有人应该使用它。实际上,最好的方法是通过传递信息来确定您是否处于 vfork() 子进程中。但问题在于,你打算怎样处理它呢?

作为 vfork() 子进程,你不能调用 fprintf()puts()fopen() 或任何其他标准 I/O 函数,也不能使用 malloc()。除非代码经过非常小心的设计,否则最好不要调用日志框架,并且如果它经过精心设计,则无需知道子进程的状态。更好的设计很可能是在调用 vfork() 之前记录您的意图。

您在注释中询问库是否调用 fork() 然后返回到您的代码中。那已经有点糟糕了。但是,没有任何库应该在未经明确记录的情况下调用 vfork() 并返回到您的代码中。 vfork() 是一个受限制的环境,不应该发生调用不符合此环境预期的函数的情况。


1
一个简单的解决方案可以使用pthread_atfork()。在这个服务中注册的回调仅在fork()时触发。因此,该函数的第三个参数在子进程中被调用后立即更新全局变量。子进程可以检查变量,如果它被修改了,则表示已经进行了分叉:
/*
  Simple program which demonstrates a solution to
  make the child process know if it has been forked or vforked
*/
#include <pthread.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

pid_t forked;

void child_hdl(void)
{
  forked = getpid();
}


int main(void)
{
pid_t pid;

  pthread_atfork(0, 0, child_hdl);

  pid = fork();
  if (pid == 0) {
    if (forked != 0) {
      printf("1. It is a fork()\n");
    }
    exit(0);
  }

  // Father continues here
  wait(NULL);

  pid = vfork();
  if (pid == 0) {
    if (forked != 0) {
      printf("2. It is a fork()\n");
    }
    _exit(0);
  }

  // Father continues here
  wait(NULL);

  return 0;
}

构建/执行:

$ gcc fork_or_vfork.c
$ ./a.out
1. It is a fork()

2
改进:将pid存入变量中,以便在fork内部检测vfork。但现在你必须在启动时以某种方式初始化该变量的pid。 - Joshua
1
这是建议中最不具侵入性的 - 因此被接受!我建议比较是 (getpid()==forked),因为你可以从一个 fork 中进行 vfork。我仍然需要区分 vfork 和原始父进程 - 使用静态构造函数 pid_t forked = getpid(); 可能是可接受的。 - Gem Taylor
1
(我应该阅读其他人的评论) - Gem Taylor

0
我今天遇到了kcmp,它看起来可以回答基本问题-即两个tids或pids是否共享相同的VM。如果您知道它们表示分叉的父/子进程ID,这可能会告诉您它们是否是vfork()。当然,如果它们是同一进程组中的tids,则根据定义它们将共享VM。

https://man7.org/linux/man-pages/man2/kcmp.2.html

int syscall(SYS_kcmp, pid_t pid1, pid_t pid2, int type,
               unsigned long idx1, unsigned long idx2);

KCMP_VM 检查进程是否共享相同的地址空间。 参数idx1和idx2将被忽略。请参阅clone(2)中CLONE_VM标志的讨论。


这可能在Linux上运行,但要小心:POSIX说“如果由vfork()创建的进程在成功调用_exit()或exec函数族之一之前调用任何其他函数,则其行为是未定义的。” - Joseph Sible-Reinstate Monica
感谢@JosephSible-ReinstateMonica。当然,在使用for或vfork时会有很多问题。 - Gem Taylor

-3
如果你是通过vfork创建的,你的父进程将等待你终止。否则,它仍在运行。下面是一些非常丑陋的代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

void doCheck()
{
    char buf[512];
    sprintf(buf, "/proc/%d/wchan", (int) getppid());
    int j = open(buf, O_RDONLY);
    if (j < 0) printf("No open!\n");
    int k = read(j, buf, 500);
    if (k <= 0) printf("k=%d\n", k);
    close(j);
    buf[k] = 0;
    char *ptr = strstr(buf, "vfork");
    if (ptr != NULL)
       printf("I am the vfork child!\n");
    else
       printf("I am the fork child!\n");   
}

int main()
{
    if (fork() == 0)
    {
       doCheck();
       _exit(0);
    }
    sleep(1);
    if (vfork() == 0)
    {
        doCheck();
        _exit(0);
    }
    sleep(1);
}

这并不完美,父进程可能正在等待后续的vfork调用完成。


1
不起作用。D可能是非可中断IO调用中的父线程。虽然它们说是NFS,但即使在本地文件系统上也会短暂发生。 - Joshua
1
它也是不安全的 - 你不能从vfork()子进程中调用像*printf()这样的函数。如果其中一个调用使用了malloc()/free(),则父进程堆栈可能会被破坏,或者vfork()子进程可能会死锁。当然,vfork()已经在玩火,所以... - Andrew Henle
printf 调用仅用于演示技术,该技术仅需要 openreadclose - David Schwartz
vfork 的子进程中调用 open()read()close() 中的任何一个都会产生未定义行为,这也是 POSIX 规范所规定的。可选方案很少:将 vfork 的返回值存储在类型为 pid_t 的对象中,调用 _exit()(而不是 exit()),或者调用 exec 函数之一。 - John Bollinger
@JohnBollinger:你也可以调用任何异步信号安全的东西,而且不是多线程的(pthread.h 中没有任何内容)。但是你在子进程中写入的任何内容都会变得在父进程中未定义。我使用这个来清除子进程中的信号处理程序,并设置子进程应该忽略哪些信号。 - Joshua

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