将过程传送到另一台计算机! 

一次,一位同事分享了他对分布式计算集群的API的想法,我开玩笑地回答:“显然,理想的API是一个简单的调用,telefork()以便您的进程在每台集群计算机上唤醒,并返回实例ID的值。”但是最后,这个主意让我占有了。我不明白为什么它如此愚蠢和简单,比任何用于远程工作的API都简单得多,以及为什么计算机系统似乎没有这种能力。我似乎也理解如何实现它,并且我已经有了一个好名字,这是任何项目中最困难的部分。所以我开始工作。

在第一个周末,他做了一个基本的原型,第二个周末带来了一个可以将其转移到云中的巨型虚拟机上,驱逐多核上的路径跟踪渲染,然后回传该过程。所有这些都包装在一个简单的API中。

该视频显示,在云中的64核VM上的渲染在8秒内完成(加上来回电传叉6秒)。在笔记本电脑上的容器中本地进行相同的渲染需要40秒:


如何传送这个过程?这是本文应解释的内容!基本思想是,在较低层次上,Linux进程仅包含一些组件。您只需要一种方法即可从供体中还原它们,然后通过网络进行传输并将其复制到克隆的过程中。

您可能会想:“但是,如何复制[有些困难,例如TCP连接]?” 真。实际上,为了使代码保持简单,我们不会容忍如此复杂的事情。也就是说,这只是一个有趣的技术演示,可能不应该在生产中使用。但是她仍然知道如何传送种类繁多的大部分计算任务!

它是什么样子的


我将代码实现为Rust库,但是从理论上讲,您可以将程序包装在C API中,然后通过FFI绑定运行以传送甚至Python进程。该实现仅约500行代码(加上200行注释):

use telefork::{telefork, TeleforkLocation};

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let destination = args.get(1).expect("expected arg: address of teleserver");

    let mut stream = std::net::TcpStream::connect(destination).unwrap();
    match telefork(&mut stream).unwrap() {
        TeleforkLocation::Child(val) => {
            println!("I teleported to another computer and was passed {}!", val);
        }
        TeleforkLocation::Parent => println!("Done sending!"),
    };
}

我还yoyo向服务器编写了一个称为“ 电传叉” 的助手,执行传输的关闭,然后回传电叉。这产生了一种幻觉,即您可以轻松地在远程服务器上运行一段代码,例如,具有更大的处理能力。

// load the scene locally, this might require loading local scene files to memory
let scene = create_scene();
let mut backbuffer = vec![Vec3::new(0.0, 0.0, 0.0); width * height];
telefork::yoyo(destination, || {
  // do a big ray tracing job on the remote server with many cores!
  render_scene(&scene, width, height, &mut backbuffer);
});
// write out the result to the local file system
save_png_file(width, height, &backbuffer);

Linux进程剖析


让我们看看在Linux(运行母主机OS的Linux)上的过程是什么样的telefork



  • (memory mappings): , . «» 4 . /proc/<pid>/maps. , , .

    • , , ( ).
  • : , . , , - , , , . , .
  • : , . - , . , , , TCP-, .
    • . stdin/stdout/stderr, 0, 1 2.
    • , , , .
  • 杂项:进程状态的其他一些部分在复制复杂性方面有所不同。但是在大多数情况下,它们并不重要,例如brk(堆指针)。其中一些只能借助奇怪的技巧或特殊的系统调用(如PR_SET_MM_MAP)来恢复,这会使恢复过程变得复杂。

因此,基本实现telefork可以通过主线程的内存和寄存器的简单映射来完成。对于主要执行计算而不与OS资源(例如文件)进行交互的简单程序而言,这已经足够了(从原则上讲,对于传送来说,只需在系统中打开文件并在调用之前将其关闭即可telefork)。

如何用电传处理


我不是第一个考虑在另一台计算机上重新创建进程的人。因此,rr调试和记录调试器的作用非常相似。我向该程序的作者@rocallahan发送了几个问题,他告诉我有关CRIU系统的信息,该系统用于在主机之间“热”迁移容器。 CRIU可以将Linux进程转移到另一个系统,支持恢复各种文件描述符和其他状态,但是,代码确实很复杂,并且使用许多需要特殊内核程序集和root权限的系统调用。使用CRIU Wiki页面上的链接,我发现为超级计算机上的分布式任务的快照创建了DMTCP,以便稍后可以重新启动它们,并且使用此程序原来的代码更简单

这些示例并没有强迫我放弃尝试实现自己的系统的尝试,因为它们是极其复杂的程序,需要特殊的运行程序和基础结构,因此,我想以库调用的形式实现尽可能简单的进程传送。因此,我研究了源代码的片段rr,CRIU,DMTCP和一些ptrace示例-并组合了自己的过程telefork。我的方法以其自己的方式工作,它融合了各种技术。

要传送一个进程,您需要在调用的原始进程中做一些工作telefork,并在函数调用端进行一些工作,该函数在服务器上接收流处理并从流中重新创建它(函数telepad它们可以同时发生,但是所有序列化也可以在下载之前完成,例如,将其拖放到文件中,然后再下载。

以下是这两个过程的简化概述。如果您想详细了解,我建议阅读源代码它包含在一个文件中,并进行了严格注释,以按顺序阅读并理解所有内容。

使用提交流程 telefork


该函数telefork接收具有写功能的流,通过该流可以传输其过程的整个状态。

  1. «» . , , . fork .
  2. . /proc/<pid>/maps , . proc_maps crate.
  3. . DMTCP, , , . , [vdso], , , .
  4. . , , process_vm_readv , .
  5. 转移寄存器我将选项PTRACE_GETREGS用于ptrace系统调用它允许您获取子进程寄存器的所有值。然后,我将它们写在频道的消息中。

在子进程中运行系统调用


要将目标进程转换为传入进程的副本,您将需要强制该进程对其自身执行一堆系统调用,而无需访问任何代码,因为我们删除了所有内容。我们使用ptrace进行远程系统调用,ptrace是用于处理和检查其他进程的通用系统调用:

  1. syscall. syscall , . , process_vm_readv [vdso] , , , syscall Linux, . , [vdso].
  2. , PTRACE_SETREGS. syscall, rax Linux, rdi, rsi, rdx, r10, r8, r9.
  3. 使用参数PTRACE_SINGLESTEP执行第一步,执行syscall命令。
  4. 读寄存器PTRACE_GETREGS恢复系统调用的返回值,看看它是否成功。

流程接受 telepad


使用这个和已经描述过的原语,我们可以重新创建过程:

  1. 分叉一个冻结的子进程与发送类似,但是这次我们需要一个子进程,我们可以对其进行操作以将其转换为已传输进程的克隆。
  2. 检查内存分配卡这次,我们需要了解所有现有的内存分配卡,以便将其删除并为传入的进程腾出空间。
  3. . , munmap.
  4. . mremap, .
  5. . mmap , process_vm_writev .
  6. . PTRACE_SETREGS , , rax. raise(SIGSTOP), . , telepad.
    • 使用一个任意值,以便电传服务器可以传输该进程进入的TCP连接的文件描述符,并可以将数据发送回,或者在yoyo传输回传到同一连接的情况下
  7. 使用重新启动新内容的过程PTRACE_DETACH

更胜任的实施


我的Telefork实施的某些部分设计不完善。我知道如何修复它们,但是以目前的形式,我喜欢该系统,有时它们确实很难修复。以下是一些有趣的示例:

  • (vDSO). mremap vDSO , DMTCP, , . vDSO, . - , CPU glibc vDSO . , vDSO, syscall, rr, vDSO vDSO .
  • brk . DMTCP, , brk , brk . , , — PR_SET_MM_MAP, .
  • . Rust « », , FS GS, , , - glibc pid tid, . CRIU, PID TID .
  • . , , , / , / FUSE. , TCP-, DMTCP CRIU , perf_event_open.
  • . fork() Unix , , .


我认为您已经了解到,使用正确的低级接口,您可以实现某些人似乎无法实现的疯狂事情。这里有一些关于如何发展电传基本概念的想法。尽管以上大部分内容可能只能在全新的或固定的内核上完全实现:

  • 集群叉Telefork的最初灵感来源是将流程流式传输到计算集群中的所有机器的想法。甚至可能会实现UDP多播或对等方法,以加快整个群集的分发。您可能还希望拥有通信原语。
  • . CRIU , - userfaultfd. , SIGSEGV mmap. , ,  — .
  • ! , . userfaultfd userfaultfd, , , MESI, . , , .  — , . , , , . : syscall, -, syscall, . , . , , , . , , . , , ( , ) , .


我真的很喜欢它,因为这是我最喜欢的技术之一的示例-深入到鲜为人知的抽象层,该层相对容易地实现了我们认为几乎不可能实现的目标。传送计算似乎是不可能的或非常困难的。您可能会认为这将需要诸如序列化整个状态,将二进制可执行文件复制到远程计算机,然后使用特殊的命令行标志在此处启动以重新加载状态的方法。但是不,一切都简单得多。在您最喜欢的编程语言下是一个抽象层,您可以在其中选择一个相当简单的函数子集-并在周末用任何编程语言以500行代码实现对大多数纯净计算的传送。我认为这种跳到另一个抽象级别通常会导致更简单,更通用的解决方案。我的另一个类似项目是Numderline

乍一看,这样的项目似乎是极端的黑客,而且在很大程度上是这样。他们做的事情像没有人期望的那样,当它们崩溃时,它们会在抽象级别执行此操作,而在该级别上,类似的程序将不起作用–例如,文件描述符神秘地消失了。但是有时您可以正确设置抽象级别并对任何可能的情况进行编码,以便最终一切都能顺利神奇地进行。我认为这里的好例子是rr(尽管Telefork设法解决了这个问题)和虚拟机的实时云迁移(实际上是虚拟机管理程序级别的Telefork)。

我还想将这些内容作为工作计算机系统替代方法的想法。为什么我们的集群计算API比将功能转换为集群的简单程序复杂得多?为什么网络系统编程比多线程复杂得多?当然,您可以给出各种各样的充分理由,但是它们通常是基于建立现有系统的示例的难度。还是通过正确的抽象或充分的努力,一切都将轻松,无缝地运行?从根本上讲,没有什么不可能的。





All Articles