为什么多个克隆系统调用调用单个 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 中的 newosproc
和 newosproc0
。其他非 Linux OSes 有自己独立的实现。如果您搜索对 newosproc
的调用,您只会在函数 newm1
中找到 proc.go
中的调用。这是从 proc.go
中的另外两个地方调用的:newm
和 templateThread
。 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
,一个用于处理信号。它可能会或可能不会解释第三个线程。全部基于阅读代码,而不是实际运行编译和测试代码,因此它可能包含错误。
我创建了一个小示例程序来检查子例程系统调用。
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)
您要求提供更多详细信息
在 proc.go 的顶部有一个非常翔实的(和大的)评论,它讨论了 goroutines ("G"s) 如何映射到工作线程 ("M"s)具有处理器资源 ("P")。这仅与为什么最初有三个 OS clone
调用(总共导致 4 个线程)间接相关,但这很重要。请注意,额外的 OS 级线程可以并且将在以后创建,如果它看起来有用,尤其是当 M 阻塞在系统调用中时。
实际的 clone
系统调用发生在 os_linux.go 中的 newosproc
和 newosproc0
。其他非 Linux OSes 有自己独立的实现。如果您搜索对 newosproc
的调用,您只会在函数 newm1
中找到 proc.go
中的调用。这是从 proc.go
中的另外两个地方调用的:newm
和 templateThread
。 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
callssigenable
(insignal_unix.go
) which, depending on values insigtable
(fromsigtab_linux_generic.go
) 中绝对正确,最后调用ensureSigM
(也在signal_unix.go
中),它调用LockOSThread
,这确保我们将创建另一个 M。(ensureSigM
中的闭包中的go
创建了 G 以绑定到这个新的锁定到-OS-线程 M。)因为这些调用是从init
函数触发的,我认为它们发生在startTheWorldWithSema
之前,因此它在上面提到的循环中创建了额外的 M。它们可能会在开始世界之后发生,但在那种情况下,仍然需要在输入main
. 之前创建 M
所有这些肯定占了 两个 线程:一个用于 运行 sysmon
,一个用于处理信号。它可能会或可能不会解释第三个线程。全部基于阅读代码,而不是实际运行编译和测试代码,因此它可能包含错误。