为什么多个克隆系统调用调用单个 go 子程序?

Why multiple clone system calls called for single go subroutine?

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

package main

func print() {
}

func main() {
    go print()
}

go子程序的Straces

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)                           = ?

观察到 clone 系统调用调用了 3 次单个子例程,但堆栈大小与 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 的 Straces

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 一样。其他两个克隆出于什么目的调用?

运行 这个无操作程序:

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 能够创建一个 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)

您要求提供更多详细信息 . There is a design document for the current system (which no doubt will become out of date if it is not already), and of course, there is the Go runtime source itself

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

实际的 clone 系统调用发生在 os_linux.go 中的 newosprocnewosproc0。其他非 Linux OSes 有自己独立的实现。如果您搜索对 newosproc 的调用,您只会在函数 newm1 中找到 proc.go 中的调用。这是从 proc.go 中的另外两个地方调用的:newmtemplateThread。 templateThread 是一个可能永远不会被使用的特殊助手,并且(我相信)不是三个初始 clone 的一部分,所以我们可以忽略它,只查找对 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 calls sigenable (in signal_unix.go) which, depending on values in sigtable (from sigtab_linux_generic.go) 中绝对正确,最后调用 ensureSigM(也在 signal_unix.go 中),它调用 LockOSThread,这确保我们将创建另一个 M。(ensureSigM 中的闭包中的 go 创建了 G 以绑定到这个新的锁定到-OS-线程 M。)因为这些调用是从 init 函数触发的,我认为它们发生在 startTheWorldWithSema 之前,因此它在上面提到的循环中创建了额外的 M。它们可能会在开始世界之后发生,但在那种情况下,仍然需要在输入 main.

  • 之前创建 M

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