【问题标题】:Why multiple clone system calls called for single go subroutine?为什么单个 go 子例程调用多个克隆系统调用?
【发布时间】:2019-08-30 10:20:00
【问题描述】:

我创建了一个小示例程序来检查子程序系统调用。

package main

func print() {
}

func main() {
    go print()
}

go 子程序的痕迹

clone(child_stack=0xc000044000, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM) = 27010
clone(child_stack=0xc000046000, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM) = 27011
clone(child_stack=0xc000040000, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM) = 27012
futex(0x4c24a8, FUTEX_WAIT_PRIVATE, 0, NULL) = 0
futex(0xc000034848, FUTEX_WAKE_PRIVATE, 1) = 1
exit_group(0)                           = ?

观察到,单个子程序调用了 3 次 clone 系统调用,但堆栈大小如 go 所声称的那样小。你能告诉我为什么三个克隆系统调用调用单个子程序。

以类似的方式在创建 pthread 时调用单次克隆系统调用。但堆栈大小很大。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> //Header file for sleep(). man 3 sleep for details.
#include <pthread.h>

void *myThreadFun(void *vargp)
{
        return NULL;
}

int main()
{
        pthread_t thread_id;
        pthread_create(&thread_id, NULL, myThreadFun, NULL);
        pthread_join(thread_id, NULL);
        exit(0);
}

pthread的痕迹

clone(child_stack=0x7fb49d960ff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARET_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fb49d9619d0, tls=0x7fb49d961700, child_tidptr=0x7fb49d9619d0) = 27370
futex(0x7fb49d9619d0, FUTEX_WAIT, 27370, NULL) = -1 EAGAIN (Resource temporarily unavailable)
exit_group(0) = ?

为什么单个 go 子例程调用多个克隆系统调用?因为在程序中只创建了一个子例程,就像在 C 语言的第二个程序中的单个 pthread 一样。其他两个克隆的目的是什么?

【问题讨论】:

  • 你正在观察 Go 程序的初始化。 Go 管理自己的堆栈。微测试和基准测试很少有用。

标签: linux go linux-kernel pthreads subroutine


【解决方案1】:

运行这个无操作程序:

package main

func main() {
}

跟踪克隆调用显示相同的三个 clone 调用:

$ go build nop.go
$ strace -e trace=clone ./nop
clone(child_stack=0xc000060000, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM) = 12602
clone(child_stack=0xc000062000, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM) = 12603
clone(child_stack=0xc00005c000, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM) = 12605
+++ exited with 0 +++

所以你在这里展示的是 Go 能够使用 no 克隆调用创建一个 goroutine:

$ cat oneproc.go
package main

func dummy() {
}

func main() {
    go dummy()
}
$ go build oneproc.go
$ strace -e trace=clone ./oneproc
clone(child_stack=0xc000060000, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM) = 13090
clone(child_stack=0xc000062000, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM) = 13091
clone(child_stack=0xc00005c000, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM) = 13092
+++ exited with 0 +++

(这并不奇怪——Goroutines 不是线程)。

Go 运行时(Go 1.11/12-ish)

您要求提供更多详细信息incomments。当前系统有一个design document(如果还没有的话,它无疑会过时),当然还有Go runtime source itself

proc.go 顶部有一条信息量很大(而且很大)的注释,它讨论了 goroutine(“G”)如何映射到具有处理器资源(“P”)的工作线程(“M”) )。这仅与为什么最初有三个 OS clone 调用(导致总共 4 个线程)间接相关,但这都很重要。请注意,如果并且当它看起来有用时,可以并且将在以后创建额外的操作系统级线程,尤其是当 M 在系统调用中阻塞时。

实际的clone 系统调用通过os_linux.go 中的newosprocnewosproc0 发生。其他非 Linux 操作系统有自己独立的实现。如果您搜索对newosproc 的调用,您会在函数newm1 中找到proc.go 中的调用。这是从proc.go 中的另外两个地方调用的:newmtemplateThread。 templateThread 是一个可能永远不会使用的特殊助手,并且(我相信)不是三个初始clones 的一部分,所以我们可以忽略它,只寻找对newm 的调用。其中有6个,都在proc.go

  • main 呼叫systemstack(func() { newm(sysmon, nil) })sysmon也在proc.go中;查看它的作用,部分是根据需要触发垃圾收集,部分是为了让调度程序的其余部分继续运行。

  • startTheWorldWithSema,让运行时系统启动,为每个 P 调用 newm(nil, p)。总是至少有一个 P,所以这可能是第二个。但是,有一个初始的m0 对象,所以这可能不是/第二个clone——不清楚。

  • sigqueue.go 中,signal_enable 调用sigenable(在signal_unix.go),这取决于sigtable(来自sigtab_linux_generic.go)中的值,这绝对是正确的,最终调用ensureSigM (也在signal_unix.go中),它调用LockOSThread,这确保我们将创建另一个M。(ensureSigM内的闭包中的go创建了要绑定到这个新的锁定到操作系统的G -thread M。)由于这些调用是从init函数触发的,我认为它们发生在startTheWorldWithSema之前,因此它会在上述循环中创建额外的M。它们可能会在创建世界之后发生,但在这种情况下,仍然需要在输入 main 之前创建 M。

所有这些肯定占了两个线程:一个运行sysmon,一个处理信号。它可能会也可能不会解释第三个线程。这一切都是基于阅读代码,而不是实际运行和测试它,所以它可能包含错误。

【讨论】:

  • 你知道为什么这些克隆是为了 go 程序的简单 main 函数吗?这些是否用于程序的初始化。
  • 运行时需要多个系统线程,用于运行时本身。这在任何地方都没有指定,将来可能会改变。 目前,有一个用于垃圾收集器的线程,一个用于处理来自操作系统的信号传递,一个用于您现有的调度程序,然后在适当的时候创建更多线程以在额外的 CPU 上调度更多的 goroutine。 (这些细节可能因操作系统而异,但目前没有。)
  • 请将此假设添加到您的答案中。这是有用的信息。
  • 这不仅仅是一个假设:我检查了源代码。但它是具体的实现细节,随时可能发生变化。例如,没有特别的理由必须立即创建 gc 线程。
  • 感谢您的检查。这是很好的信息,但毫无疑问,它对其他开发人员来说是非常有用的信息。那些深入了解它是如何发生的人。请添加到答案中。 **为什么单个 go 子例程需要多个克隆系统调用?这也是个问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-05
  • 1970-01-01
  • 1970-01-01
  • 2014-12-13
  • 1970-01-01
相关资源
最近更新 更多