Go 內存管理系列之二

概述

之前在 povilasv.me[1] 上,我們一起探討了 GO 內存管理 [2] ,並且留下了兩個小的 Go 程序,它們運行時分配的虛擬內存大小顯著不同。

首先,我們一起來看一下佔用很多虛擬內存的程序 ex1。它的代碼如下:

func main() {
 http.HandleFunc("/bar",
  func(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello, %q",
   HTML.EscapeString(r.URL.Path))
 })

 http.ListenAndServe(":8080", nil)
}

我執行了 ps 命令來查看虛擬內存大小,以下是它的輸出。注意,輸出中的內存大小單位是千字節(KiB),388496 KiB 約等於 379.390625 MiB。

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT
povilasv 16609  0.0  0.0 388496  5236 pts/9    Sl+

接下來,我們看一下只佔用少量虛擬內存的程序 ex2

func main() {
 go func() {
  for {
   var m runtime.MemStats
   runtime.ReadMemStats(&m)

   log.Println(float64(m.Sys) / 1024 / 1024)
   log.Println(float64(m.HeapAlloc) / 1024 / 1024)
   time.Sleep(10 * time.Second)
  }
 }()

 fmt.Println("hello")
 time.Sleep(1 * time.Hour)
}

最後,我們看一下這個程序的 ps 命令的輸出,你可以看到它運行時只佔用了少量的虛擬內存:4900 KiB,約等於 4.79 MiB。

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT
povilasv  3642  0.0  0.0   4900   948 pts/10   Sl+

有一點需要說明,這些程序是使用較老的 Go 1.10 版編譯的;如果使用新版本的 Go 編譯的話,這些數字將會不同。比如拿 ex1 來說,使用 Go 1.11 版編譯,佔用的虛擬內存爲 466 MiB,常駐內存爲 3.22 MiB;改用 Go 1.12 版編譯,則這兩者分別爲 100.37 MiB 和 1.44 MiB。

由此我們可以看出,HTTP 服務程序和簡單的命令行程序之間的差異導致了運行時佔用虛擬內存大小的差異。

靈光乍現

看到這些,我突然靈機一動,也許可以用 strace 來調查這個有趣的現象。先看一下 strace 的描述:

strace[3] 是一個 Linux 平臺下的用於診斷、調試和學習目的的用戶空間實用程序。它可用於監視和篡改進程與 Linux 內核之間的交互,包括系統調用、信號傳遞和進程狀態變化。

接下來要做的就是使用 strace 運行兩個程序來比較操作系統的行爲。strace 的使用非常簡單,你只需要在你要執行的程序前面加上 strace 即可。以 ex1 爲例,我們執行命令:

strace ./ex1

將會產生以下輸出:

execve("./ex1"["./ex1"], 0x7fffe12acd60 /* 97 vars */) = 0
brk(NULL)                               = 0x573000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/local/lib/tls/haswell/x86_64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat("/usr/local/lib/tls/haswell/x86_64", 0x7ffdaa923fa0) = -1 ENOENT (No such file or directory)
...
stat("/lib/x86_64", 0x7ffdaa923fa0)     = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\340b\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=146152, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc8a8d11000
mmap(NULL, 2225248, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fc8a88cd000
mprotect(0x7fc8a88e8000, 2093056, PROT_NONE) = 0
mmap(0x7fc8a8ae7000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a000) = 0x7fc8a8ae7000
mmap(0x7fc8a8ae9000, 13408, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fc8a8ae9000
close(3)                                = 0
openat(AT_FDCWD, "/usr/local/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\0\34\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1857312, ...}) = 0
mmap(NULL, 3963464, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fc8a8505000
mprotect(0x7fc8a86c3000, 2097152, PROT_NONE) = 0
mmap(0x7fc8a88c3000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1be000) = 0x7fc8a88c3000
mmap(0x7fc8a88c9000, 14920, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fc8a88c9000
close(3)                                = 0
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc8a8d0e000
arch_prctl(ARCH_SET_FS, 0x7fc8a8d0e740) = 0
mprotect(0x7fc8a88c3000, 16384, PROT_READ) = 0
mprotect(0x7fc8a8ae7000, 4096, PROT_READ) = 0
mprotect(0x7fc8a8d13000, 4096, PROT_READ) = 0
set_tid_address(0x7fc8a8d0ea10)         = 2109
set_robust_list(0x7fc8a8d0ea20, 24)     = 0
rt_sigaction(SIGRTMIN, {sa_handler=0x7fc8a88d2ca0, sa_mask=[]sa_flags=SA_RESTORER|SA_SIGINFO, sa_restorer=0x7fc8a88e1140}, NULL, 8) = 0
rt_sigaction(SIGRT_1, {sa_handler=0x7fc8a88d2d50, sa_mask=[]sa_flags=SA_RESTORER|SA_RESTART|SA_SIGINFO, sa_restorer=0x7fc8a88e1140}, NULL, 8) = 0
rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
brk(NULL)                               = 0x573000
brk(0x594000)                           = 0x594000
sched_getaffinity(0, 8192, [0, 1, 2, 3]) = 8
mmap(0xc000000000, 65536, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xc000000000
munmap(0xc000000000, 65536)             = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc8a8cce000
mmap(0xc420000000, 1048576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xc420000000
mmap(0xc41fff8000, 32768, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xc41fff8000
mmap(0xc000000000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xc000000000
mmap(NULL, 65536, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc8a8cbe000
mmap(NULL, 65536, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc8a8cae000
rt_sigprocmask(SIG_SETMASK, NULL, [], 8) = 0
sigaltstack(NULL, {ss_sp=NULL, ss_flags=SS_DISABLE, ss_size=0}) = 0
sigaltstack({ss_sp=0xc420002000, ss_flags=0, ss_size=32768}, NULL) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
gettid()                                = 2109
...

類似的,對於 ex2,我們執行:

strace ./ex2

產生輸出:

execve("./ex2"["./ex2"], 0x7ffc2965ca40 /* 97 vars */) = 0
arch_prctl(ARCH_SET_FS, 0x5397b0)       = 0
sched_getaffinity(0, 8192, [0, 1, 2, 3]) = 8
mmap(0xc000000000, 65536, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xc000000000
munmap(0xc000000000, 65536)             = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ff1c637b000
mmap(0xc420000000, 1048576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xc420000000
mmap(0xc41fff8000, 32768, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xc41fff8000
mmap(0xc000000000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xc000000000
mmap(NULL, 65536, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ff1c636b000
mmap(NULL, 65536, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ff1c635b000
rt_sigprocmask(SIG_SETMASK, NULL, [], 8) = 0
sigaltstack(NULL, {ss_sp=NULL, ss_flags=SS_DISABLE, ss_size=0}) = 0
sigaltstack({ss_sp=0xc420002000, ss_flags=0, ss_size=32768}, NULL) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
gettid()                                = 22982

實際的輸出比這要長,爲了可讀性,我只選取了從開頭到調用 gettid() 的部分。之所以選擇到這一行,是因爲它在兩個程序的 strace 輸出裏都只出現了一次。

讓我們來比較一下這兩個輸出。首先,ex1 的輸出更長一些。ex1 尋找一些 .so 庫文件並把它們加載到內存裏。比如,下面是加載 libpthread.so.0 時產生的輸出:

...
openat(AT_FDCWD, "/lib/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\340b\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=146152, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc8a8d11000
mmap(NULL, 2225248, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fc8a88cd000
mprotect(0x7fc8a88e8000, 2093056, PROT_NONE) = 0
mmap(0x7fc8a8ae7000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a000) = 0x7fc8a8ae7000
mmap(0x7fc8a8ae9000, 13408, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fc8a8ae9000
close(3)                                = 0

在這個例子裏,我們可以看到文件先是被打開,然後讀取到內存裏,最後被關閉。在對文件做內存映射的時候,有一些內存區域被設置了 PROTO_EXEC 的標誌,這樣做是爲了讓我們的程序能夠執行位於這些區域的代碼。我們可以看到同樣的事情出現在 libc.so.6 庫文件上:

...
openat(AT_FDCWD, "/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\0\34\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1857312, ...}) = 0
mmap(NULL, 3963464, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fc8a8505000
mprotect(0x7fc8a86c3000, 2097152, PROT_NONE) = 0
mmap(0x7fc8a88c3000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1be000) = 0x7fc8a88c3000
mmap(0x7fc8a88c9000, 14920, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fc8a88c9000
close(3)                                = 0

加載完庫文件之後,兩個程序開始表現出相似的行爲。它們映射了相同的內存區域,執行了相似的指令,直到 gettid() 這一行。

ex1 加載了 libpthreadlibc,而 ex2 並沒有。這有點意思。

cgo 該出場了。

CGO

讓我們一起來探究一下 cgo 是什麼以及它是如何工作的。godoc[4] 上是這樣解釋的:

Cgo 讓 go 程序包能夠調用 C 代碼。

爲了調用 C 代碼,你需要添加一段特殊的註釋並導入一個特殊的包:C。讓我們一起來看一下下面這個小例子:

package main

// #include
import "C"
import "fmt"

func main() {
 char := C.getchar()
 fmt.Printf("%T %#v", char, char)
}

這個程序引用了 C 標準庫裏的 stdio.h 頭文件,接着調用了 getchar() 並打印其返回值。getchar() 從標準輸入讀入一個字符(一個 unsigned char)。我們來試一下:

go build
./ex3

執行這個程序的時候,它要求你輸入一個字符,並簡單地將其打印出來。下面是這個過程的一個例子:

a
main._Ctype_int 97

我們可以看到,它表現的就像一個普通的 Go 程序一樣。有趣的是你可以像編譯一段 Go 原生代碼一樣編譯它,只是簡單的執行一下 go build。我敢打賭,如果你事先沒有看過這段代碼,你可能一點都意識不到這其中的差別。

顯而易見的是,cgo 還有許多有趣的特點。比如,如果你把幾個 .c.h 文件與 Go 原生代碼放到同一個目錄下面,go build 也會編譯它們並將它們與你的 Go 原生代碼鏈接在一起。

如果你想了解更多的話,我建議你閱讀一下 godoc[5] 和 C? Go? Cgo![6] 這篇博文。現在,讓我們回到之前那個有趣的問題,爲什麼 ex1 使用了 cgo 而 ex2 沒有?

探究差異

ex1ex2 的差別在於前者導入了 net/http,而後者沒有。使用 grepnet/http 包裏搜索了一遍並沒有發現任何使用 C 語句的跡象。但只要再往上一級,你就可以在 net 包裏找到證據。

看一下 net 包裏的文件:

例如,net/cgo_linux.go 包含以下代碼:

// +build !android,cgo,!netgo

package net

/*
#include <netdb.h>
*/
import "C"

// NOTE(rsc): In theory there are approximately balanced
// arguments for and against including AI_ADDRCONFIG
// in the flags (it includes IPv4 results only on IPv4 systems,
// and similarly for IPv6), but in practice setting it causes
// getaddrinfo to return the wrong canonical name on Linux.
// So definitely leave it out.
const cgoAddrInfoFlags = C.AI_CANONNAME | C.AI_V4MAPPED | C.AI_ALL

我們可以看到 net 包裏引用了 C 頭文件 netdb.h 並且使用了這個文件裏的幾個變量。爲什麼需要這些東西?讓我們接着調查。

什麼是 netdb.h

如果你查閱過 netdb.h 的說明文檔,你就會發現它其實是 libc 的一部分。它的說明文檔裏這樣寫道:

netdb.h 提供了網絡數據庫操作的定義。

另外,文檔裏也對這裏涉及的幾個常量進行了說明。讓我們來看一下:

探尋一下這些標誌是如何使用的,就會發現它們最終會被傳遞給 getaddrinfo(),一個使用 libc 來解析 DNS 域名的函數。簡而言之,這些標誌控制 DNS 域名解析如何發生。

同樣地,如果你打開 net/cgo_bsd.go[10],你會看到常量 cgoAddrInfoFlags 的一個略有差異的版本。一起來看一下:

// +build cgo,!netgo
// +build darwin dragonfly freebsd

package net

/*
#include <netdb.h>
*/
import "C"

const cgoAddrInfoFlags = (C.AI_CANONNAME | C.AI_V4MAPPED |
 C.AI_ALL) & C.AI_MASK

這暗示我們,有一種機制可以爲 DNS 解析設置操作系統特定的標誌,而我們正在使用 cgo 正確地進行 DNS 查詢。這真的很酷。讓我們再深入一點探索 net 包。

讀一讀 net[11] 包的文檔:

名稱解析

指使用類似於 Dial 的函數或者類似於 LookupHostLookupAddr 的函數進行間接地或直接地解析域名的方法,具體的函數隨操作系統不同而不同。

在 Unix 系統上,解析器解析名稱的時候有兩種選擇。一種是使用純粹的 Go 解析器直接向列在 /etc/resolv.conf 文件裏的服務器發送 DNS 請求,另一種是使用基於 cgo 的解析器通過調用 C 庫函數,比如 getaddrinfogetnameinfo,來實現。

默認情況下,使用純 Go 解析器進行解析,這是因爲一個阻塞的 DNS 請求只需要消耗一個 Go 例程;而一個阻塞的 C 函數調用卻要佔用一個系統線程。如果 cgo 可用的話,在很多情況下都需要使用基於 cgo 的解析器:在不允許程序直接發送 DNS 請求的系統上(比如 OS X);當 LOCALDOMAIN 環境變量被定義時(即使是個空值);當 RES_OPTIONSHOSTALIAS 環境變量非空時;當 ASR_CONFIG 環境變量非空時(僅 OpenBSD 系統);當 /etc/resolv.conf/etc/nsswitch.conf 裏面使用了 Go 解析器沒有實現的特性時;當被查詢的名字以 .local 結尾或者是一個 mDNS 名字。

你還可以在 GODEBUG 環境變量(詳見 runtime 包)裏爲 netdns 指定 Go 或 cgo 來強制指定使用對應的解析器,就像下面那樣:

export GODEBUG=netdns=go    # 強制使用純 Go 解析器
export GODEBUG=netdns=cgo   # 強制使用 cgo 解析器

你也可以通過在構建 Go 源碼樹時設置 netgonetcgo 構建標誌來強制選擇對應的解析器。

如果給 netdns 指定一個數字,比如這樣 GODEBUG=netdns=1,解析器就會打印它所選擇的解析方式。

-- https://golang.org/pkg/net/#hdr-Name_Resolution

文檔已經讀的夠多了。下面,就讓我們一起來嘗試使用不同的 DNS 客戶端實現吧。

使用構建標籤

正如文檔裏描述的那樣,我們可以使用環境變量來指定 DNS 客戶端實現。這種方式很靈活,因爲你不需要重新編譯代碼就可以在兩種方式之間自由切換。

另外,從代碼裏來看,我發現我們也可以使用 Go 構建標籤在編譯時指定解析方式。除此之外,我們還可以通過設置 GODEBUG=netdns=1 環境變量並做一次真實的 DNS 查詢來查看到底使用了哪種方式。

看了 net 包裏的源文件,我發現一共有 3 種構建模式。它們都可以通過使用不同的構建標籤來指定。這三種構建模式分別是:

  1. !cgo -- 不使用 cgo,也就是說強制使用 Go 版本的解析器

  2. netcgocgo -- 使用 libc 的 DNS 解析方式

  3. netgo + cgo -- 使用 Go 原生的 DNS 解析方式,同時我們還可以包含 C 代碼

讓我們一起嘗試所有這些組合來看看結果如何。

由於之前的程序不會發起 DNS 查詢,我們需要編寫新的程序。下面就是我們要用的代碼:

func main() {
 addr, err := net.LookupHost("povilasv.me")
 fmt.Println(addr, err)
}

然後,執行構建:

export CGO_ENABLED=0
export GODEBUG=netdns=1
go build -tags netgo

運行程序:

./testnetgo

程序輸出:

go package net: built with netgo build tag; using Go's DNS resolver
104.28.1.75 104.28.0.75 2606:4700:30::681c:4b 2606:4700:30::681c:14b <nil>

現在讓我們使用 libc 的解析器來重新構建:

export GODEBUG=netdns=1
go build -tags netcgo

運行程序:

./testnetgo

程序輸出:

go package net: using cgo DNS resolver
104.28.0.75 104.28.1.75 2606:4700:30::681c:14b 2606:4700:30::681c:4b <nil>

最後,我們來使用 netgo cgo 進行構建:

export GODEBUG=netdns=1
go build -tags 'netgo cgo' .

運行程序:

./testnetgo

輸出:

go package net: built with netgo build tag; using Go's DNS resolver
104.28.0.75 104.28.1.75 2606:4700:30::681c:14b 2606:4700:30::681c:4b <nil>

可以看到,構建標籤真的起了作用。現在,讓我們回到虛擬內存的問題上來。

回到虛擬內存

現在,我想分別使用這 3 組標誌來重新構建我們那個簡單的 HTTP 網頁服務器 ex1,看看它們會對虛擬內存產生怎樣的影響。

使用 netgo 模式編譯:

export CGO_ENABLED=0
go build -tags netgo

ps 輸出:

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT
povilasv  3524  0.0  0.0   7216  4076 pts/17   Sl+

可以看到在這種模式下虛擬內存的佔用是很低的。

現在來看看 netcgo 的情況:

go build -tags netcgo

ps 輸出:

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT
povilasv  6361  0.0  0.0 382296  4988 pts/17   Sl+

可以看到在這種模式下,佔用了大量虛擬內存(382296 KiB)。

最後,我們來看看 netgo cgo 模式:

go build -tags 'netgo cgo' .

ps 輸出:

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT
povilasv  8175  0.0  0.0   7216  3968 pts/17   Sl+

可以看到在這種模式下虛擬內存的佔用也是是很低的(7216 KiB)。

可以肯定的是,netgo 模式下不會佔用很多虛擬內存。從另一方面來講,我們還不能將虛擬內存消耗過多的責任歸咎於 cgo,因爲 ex1 程序裏並沒有包含任何 C 代碼,netgo cgo 模式實際上和 netgo 模式一樣,會跳過編譯和鏈接 C 文件這一整套 cgo 的工作流程。

因而,我們還需要加入額外的 C 代碼再來分別嘗試 netcgonetgo cgo 兩種模式。這可以讓我們弄清楚,在 cgo 模式下啓用和禁用 libc 的 DNS 客戶端,程序分別會有怎樣的表現。

我們來嘗試一下這段代碼:

package main

// #include
import "C"
import "fmt"

func main() {
 char := C.getchar()
 fmt.Printf("%T %#v", char, char)

 http.HandleFunc("/bar",
  func(w http.ResponseWriter, r *http.Request) {
   fmt.Fprintf(w, "Hello, %q",
    HTML.EscapeString(r.URL.Path))
 })

 http.ListenAndServe(":8080", nil)
}

可以看到,這段代碼應該能夠達到我們的目的。因爲它既使用了 cgo,也能夠根據構建標籤來選擇使用 netgo 還是 libc 的 DNS 客戶端實現。

讓我們試一試:

go build -tags netcgo .

ps 輸出:

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT
povilasv 12594  0.0  0.0 382208  4824 pts/17   Sl+

可以看到虛擬內存佔用沒有變化。現在來試一下 netgo cgo

go build -tags 'netgo cgo' .

ps 輸出:

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT
povilasv  1026  0.0  0.0 382208  4824 pts/17   Sl+

最後,終於可以排除 libc 的 DNS 客戶端實現的影響,因爲禁用它並沒有帶來任何變化。我們清楚的看到這一切都跟 cgo 有關。

爲了深入探索這個問題,我們先來簡化一下我們的程序。ex1 啓動了一個 HTTP 服務器,調試一個這樣的程序遠比調試一個簡單的命令行程序困難的多。看一下這段代碼:

package main

// #include
// #include
import "C"
import (
 "time"
 "unsafe"
)

func main() {
 cs := C.CString("Hello from stdio")
 C.puts(cs)

 time.Sleep(1 * time.Second)

 C.free(unsafe.Pointer(cs))
}

運行一下並查看一下內存佔用:

go build .
./ex6

ps 輸出:

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT
povilasv 15972  0.0  0.0 378228  2476 pts/17   Sl+

酷!它真的佔用了許多虛擬內存,我們真的需要調查 cgo 了。


via: https://povilasv.me/go-memory-management-part-2/

作者:Povilas[15] 譯者:Stonelgh[16] 校對:polaris1119[17]

本文由 GCTT[18] 原創編譯,Go 中文網 [19] 榮譽推出,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。

參考資料

[1]

povilasv.me: https://povilasv.me/

[2]

GO 內存管理: https://povilasv.me/go-memory-management/

[3]

strace: https://strace.io/

[4]

godoc: https://golang.org/cmd/cgo/

[5]

godoc: https://golang.org/cmd/cgo/

[6]

C? Go? Cgo!: https://blog.golang.org/c-go-cgo

[7]

net/cgo_unix.go: https://golang.org/src/net/cgo_unix.go

[8]

net/cgo_linux.go: https://golang.org/src/net/cgo_linux.go

[9]

net/cgo_stub.go: https://golang.org/src/net/cgo_stub.go

[10]

net/cgo_bsd.go: https://golang.org/src/net/cgo_bsd.go

[11]

net: https://golang.org/pkg/net/#hdr-Name_Resolution

[12]

Go 內存管理之三: https://povilasv.me/go-memory-management-part-3/

[13]

簡報: https://povilasv.me/newsletter

[14]

願望清單: https://www.amazon.com/hz/wishlist/ls/2NLKE1Z1SND3W?ref=wl_share_

[15]

Povilas: https://povilasv.me/about/

[16]

Stonelgh: https://github.com/stonglgh

[17]

polaris1119: https://github.com/polaris1119

[18]

GCTT: https://github.com/studygolang/GCTT

[19]

Go 中文網: https://studygolang.com/

福利

我爲大家整理了一份從入門到進階的 Go 學習資料禮包,包含學習建議:入門看什麼,進階看什麼。關注公衆號 「polarisxu」,回覆 ebook 獲取;還可以回覆「進羣」,和數萬 Gopher 交流學習。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/RyDvCBFDm14xw_f7mOvCwg