23

与此问题中的类似点已在此处此处提出,并且我知道 Google coredump 库(我已经评估并发现缺少该库,但如果我更好地理解问题,我可能会尝试解决这个问题)。

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

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

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

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

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

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

  3. 使用存储的线程基地址,您能否读出分叉进程中每个原始线程的相关数据?

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

4

4 回答 4

13

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

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

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

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

不。fork()唯一保留执行实际调用的线程,其余线程的堆栈丢失。

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

  • 有一个父进程在子进程停止时生成子进程的核心转储。(请注意,可能会生成一个以上的连续停止事件;只有第一个直到下一个继续事件才会被处理。)

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

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

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

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

    就个人而言,我不打扰。在我的四核机器上,SIGSTOP仅以下各项就会在线程之间产生与互斥锁或信号量类似的延迟,因此我认为没有必要争取更好的同步。

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

    父进程将收到子进程停止的通知。它将首先检查/proc/PID/task/以获取子进程的每个线程的 TID(可能还有/proc/PID/task/TID/其他信息的伪文件),然后使用ptrace(PTRACE_ATTACH, 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/优先级操作,我也可以获得足够的同步(线程停止时间之间的延迟足够小)。

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

于 2013-09-02T21:59:36.970 回答
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

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

于 2013-09-02T17:13:13.770 回答
1

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

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

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

于 2013-08-28T13:28:04.310 回答
0

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

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

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

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

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

于 2013-09-03T15:47:26.673 回答