【问题标题】:Fork and core dump with threads带线程的分叉和核心转储
【发布时间】:2013-08-28 12:57:14
【问题描述】:

herehere 之前已经提出了与此问题中的类似点,并且我知道 Google coredump 库(我已经评估并发现缺少该库,尽管我可能会尝试并继续工作如果我能更好地理解问题)。

我想在不中断进程的情况下获取正在运行的 Linux 进程的核心转储。自然的做法是:

if (!fork()) { abort(); }

由于分叉的进程得到原始进程内存的固定快照副本,我应该得到一个完整的核心转储,并且由于副本使用写时复制,它通常应该很便宜。但是,这种方法的一个严重缺点是fork() 只分叉当前线程,而原进程的所有其他线程都不会存在于分叉的副本中。

我的问题是是否有可能以某种方式获得其他原始线程的相关数据。我不完全确定如何解决这个问题,但这里有几个我想出的子问题:

  1. 包含所有线程堆栈的内存在派生进程中是否仍然可用和可访问?

  2. 是否可以(快速)枚举原始进程中所有正在运行的线程并存储其堆栈基址的地址?据我了解,Linux 上线程堆栈的基础包含一个指向内核线程簿记数据的指针,所以...

  3. 有了存储的线程基地址,你能读出分叉进程中每个原始线程的相关数据吗?

如果可能的话,也许只需将其他线程的数据附加到核心转储中即可。但是,如果该数据在分叉点已经丢失,那么这种方法似乎没有任何希望。

【问题讨论】:

  • 我现在没有 pthreads 中的任何枚举功能... - 但关于fork()ing,您可能想看看pthread_atfork()。似乎使用后者注册的处理程序可以用来滚动自己的 pthread-enumerator。
  • 可能只fork当前线程的原因是Linux中线程的实现,使用clone()
  • @Marcus:我并不真正担心 为什么 fork() 的行为方式。我对它的功能感到非常满意,这对我来说似乎很理智。
  • This article 关于 IBM 的 Java 使用 fork+signal 生成核心转储说他们正在使用/proc/PID/maps 中的信息来检索“否则不会存在”的信息。

标签: c++ c linux pthreads coredump


【解决方案1】:

你熟悉进程检查点重启吗?特别是CRIU?在我看来,它可能会为您提供一个简单的选择。

我想在不中断进程的情况下获取正在运行的 Linux 进程的核心转储 [并] 以某种方式获取其他原始线程的相关数据。

忘记不要中断这个过程。如果您考虑一下,核心转储必须在转储期间中断进程;因此,您的真正目标必须是尽量减少这种中断的持续时间。您最初使用fork() 的想法确实会中断该过程,它只会中断很短的时间。

  1. 包含所有线程堆栈的内存在分叉进程中是否仍然可用和可访问?

没有。 fork() 只保留执行实际调用的线程,其余线程的堆栈丢失。

这是我将使用的程序,假设 CRIU 不合适:

  • 有一个父进程在子进程停止时生成子进程的核心转储。 (请注意,可能会生成多个连续的停止事件;应该只对下一个继续事件之前的第一个事件进行处理。)

    您可以使用waitpid(child,,WUNTRACED|WCONTINUED) 检测停止/继续事件。

  • 可选:使用sched_setaffinity() 将进程限制为单个CPU,并使用sched_setscheduler()(可能还有sched_setparam())将进程优先级降低到IDLE

    您可以从父进程执行此操作,它只需要 CAP_SYS_NICE 功能(如果您像大多数当前 Linux 发行版一样启用了文件系统功能,您可以使用 setcap 'cap_sys_nice=pe' parent-binary 将其提供给父二进制文件),在有效集和允许集。

    目的是在线程决定它想要快照/转储的那一刻与所有线程都已停止的那一刻之间最小化其他线程的进度。我没有测试过更改需要多长时间才能生效——当然它们最早只会在当前时间片结束时发生。所以,这一步可能应该提前完成。

    就我个人而言,我不介意。在我的四核机器上,单独使用以下 SIGSTOP 在线程之间产生与互斥锁或信号量相似的延迟,因此我认为没有必要争取更好的同步。

  • 当子进程中的线程决定要为自己拍摄快照时,它会向自己发送一个SIGSTOP(通过kill(getpid(), SIGSTOP))。这会停止进程中的所有线程。

    父进程会收到子进程停止的通知。它将首先检查/proc/PID/task/ 以获取子进程的每个线程的TID(可能还有/proc/PID/task/TID/ 伪文件以获取其他信息),然后使用ptrace(PTRACE_ATTACH, TID) 附加到每个TID。显然,ptrace(PTRACE_GETREGS, TID, ...) 将获得每个线程的寄存器状态,可以与/proc/PID/task/TID/smaps/proc/PID/task/TID/mem 结合使用以获得每个线程的堆栈跟踪以及您感兴趣的任何其他信息。(例如,您可以为每个线程创建一个与调试器兼容的核心文件。)

    当父进程完成抓取转储时,它让子进程继续。我相信你需要发送一个单独的SIGCONT 信号让整个子进程继续,而不是仅仅依赖ptrace(PTRACE_CONT, TID),但我没有检查这个;请验证一下。

我确实相信上述方法会在进程停止中的线程之间产生最小的挂钟时间延迟。在 Xubuntu 上对 AMD Athlon II X4 640 和 3.8.0-29 通用内核进行快速测试表明,在其他线程中增加 volatile 变量的紧密循环只会将计数器提前几千,具体取决于线程数(太多在我进行的几次测试中产生了更具体的噪音)。

将进程限制为单个 CPU,甚至限制为空闲优先级,将进一步大大减少延迟。 CAP_SYS_NICE 能力允许父进程不仅可以降低子进程的优先级,还可以将优先级提升回原来的水平;文件系统功能意味着父进程甚至不必是 setuid,因为仅CAP_SYS_NICE 就足够了。 (我认为它足够安全——在父程序中进行一些良好的检查——安装在例如大学计算机中,学生们非常积极地寻找有趣的方法来利用已安装的程序。)

可以创建一个内核补丁(或模块),提供一个提升的kill(getpid(), SIGSTOP),同时尝试从正在运行的 CPU 中启动其他线程,从而尝试使线程停止之间的延迟更小。就个人而言,我不会打扰。即使没有 CPU/优先级操作,我也可以获得足够的同步(线程停止时间之间的延迟足够小)。

您需要一些示例代码来说明我上面的想法吗?

【讨论】:

  • 非常感谢!我正在处理这个(以及你的其他答案),我一定会回来回答问题!
  • 这里有一个问题:我想把setup里的父子关系倒过来,让孩子做tracing、dump、dying。那么:是否可以(始终)将 SIGSTOP 发送到不是您自己的孩子的进程?对于不是您孩子的进程,有没有办法wait
  • 没关系:我发现您可以等待任何被跟踪的进程,并且在跟踪期间被跟踪者成为准孩子。
  • @KerrekSB:非常正确,您可以等待正在跟踪的任何进程。另请注意,Ubuntu 等人。默认情况下不允许子进程跟踪其父进程,除非被跟踪进程调用prctl(PR_SET_PTRACER,..) 或跟踪器具有CAP_SYS_PTRACE 功能。 (您可以使用sudo setcap 'cap_sys_ptrace=pe' tracer-binary 为跟踪程序提供通过文件功能。)
【解决方案2】:

当您fork 时,您将获得正在运行的进程内存的完整副本。这包括所有线程的堆栈(毕竟您可以拥有指向它们的有效指针)。但只有调用线程在子进程中继续执行。

您可以轻松地对此进行测试。制作一个多线程程序并运行:

pid_t parent_pid = getpid();

if (!fork()) {
    kill(parent_pid, SIGSTOP);

    char buffer[0x1000];

    pid_t child_pid = getpid();
    sprintf(buffer, "diff /proc/%d/maps /proc/%d/maps", parent_pid, child_pid);

    system(buffer);

    kill(parent_pid, SIGTERM);

    return 0;
} else for (;;);

因此,您的所有内存都在那里,当您创建核心转储时,它将包含所有其他线程堆栈(前提是您的最大核心文件大小允许)。唯一会丢失的是他们的寄存器组。如果你需要这些,那么你必须ptrace你的父母才能获得它们。

您应该记住,虽然核心转储并非旨在包含多个线程的运行时信息 - 导致核心转储的线程。

回答您的其他一些问题:

您可以通过/proc/[pid]/tasks 枚举线程,但在ptrace 之前您无法识别它们的堆栈基础。

是的,您可以完全访问来自分叉进程的其他线程堆栈快照(见上文)。确定它们并非易事,但如果核心文件大小允许,它们确实会被放入核心转储中。如果可以的话,最好在创建时将它们保存在某个全局可访问的结构中。

【讨论】:

  • 通过/proc/pid/tasks 的问题是它没有同步到核心转储,因为我无法停止父线程...
  • @KerrekSB 你可以:kill(parent_pid, SIGSTOP)
  • 但这不是同步的。到我发送信号,更重要的是,当信号被传递到所有线程时,父级已经关闭并拥有不同的内存内容。
【解决方案3】:

如果你想在非特定位置获取核心文件,并且只获取进程运行的核心映像而不杀死,那么你可以使用gcore

如果您打算在特定位置(条件)获取核心文件并继续运行该进程 - 一种粗略的方法是从该位置以编程方式执行 gcore

一种更经典、更简洁的方法是检查 gcore 使用的 API 并将其嵌入到您的应用程序中 - 但与大多数时候的需要相比,这将是一项繁重的工作。

HTH!

【讨论】:

  • gcore如何保证进程持续运行?
  • @KerrekSB:阅读man 页面。选项-s“在收集核心映像时停止进程,并在完成后恢复它。这保证了生成的核心转储将处于一致状态。即使进程已经停止,它也会恢复。同样的效果可以用 kill(1) 手动实现"
  • @Linuxios:嗯,这是针对 BSD 的。我想我应该说我想要一个 Linux 解决方案。
【解决方案4】:

如果您的目标是对整个进程进行快照,以便了解所有线程在特定点的确切状态,那么我看不到任何不需要某种中断服务例程的方法来做到这一点。您必须停止所有处理器并记录每个线程的当前状态。

我不知道有任何系统提供这种全进程核心转储。该过程的大致轮廓是:

  1. 在所有 CPU(逻辑和物理内核)上发出中断。
  2. 忙于等待所有内核同步(这应该不会花很长时间)。
  3. 克隆所需进程的内存空间:复制页表并将所有页标记为写入时复制。
  4. 让每个处理器检查其当前线程是否在目标进程中。如果是,则记录该线程的当前堆栈指针。
  5. 每隔一个线程检查当前堆栈指针的线程数据块并记录它。
  6. 创建内核线程以保存复制的内存空间和线程堆栈指针
  7. 恢复所有内核。

这应该捕获整个进程状态,包括在发出处理器间中断时正在运行的任何进程的快照。因为所有线程都被中断(通过标准调度程序挂起过程,或者通过我们的自定义中断过程)所有寄存器状态都将在进程内存中的某个堆栈上。然后,您只需要知道每个线程堆栈的顶部在哪里。使用写时复制机制克隆页表允许透明保存,同时允许恢复原始进程。

这是一个相当重量级的选项,因为它的主要功能需要暂停所有处理器很长一段时间(同步、克隆、遍历所有线程)。但是,这应该允许您准确捕获所有线程的状态,并确定到达检查点时哪些线程正在运行(以及在哪些 CPU 上)。我会假设存在一些执行此过程的框架(例如在 CRIU 中)。当然,恢复进程会导致页面分配风暴,因为写入时复制机制保护了检查点系统状态。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-09-09
    • 1970-01-01
    • 2012-11-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-13
    • 1970-01-01
    相关资源
    最近更新 更多