CGO segmentation violation 問題

今天增加了一個并發(fā)的測試用例棺耍,用于驗證新增的Cony On Write 在并發(fā)場景下的正確性删窒,結果 go test -v 執(zhí)行之后螟蝙,測試用例直接崩潰源武,然后黑漆漆的終端上出現了如下報錯:

fatal error: unexpected signal during runtime execution
[signal SIGSEGV: segmentation violation code=0x1 addr=0x60 pc=0x9e8b6c]

從內容上來看限番,關鍵的信息是 segmentation violation舱污,也叫作段違規(guī)

那么什么是 segmentation violation 以及為什么會出現 segmentation violation 呢?經過一番搜索后弥虐,終于找到了我認為對 segmentation violation 解釋比較貼切的一篇文章扩灯,以下是部分引用:

A "segmentation violation" signal is sent to a process of which the memory management unit detected an attempt to use a memory address that does not belong to it.

原文鏈接: What is a "segmentation violation"?

現代硬件設備都會包含一個 memory management unit(MMU) 的硬件來保護內存訪問,以防止不同的進程修改彼此的內存霜瘪。MMU檢查到一個進程試圖訪問不屬于自己的內存時(無效的內存引用)珠插,就會發(fā)送一個SIGSEGV 的signal,進程就會出現segmentation violation 錯誤颖对。

看到這里捻撑,了解協程實現的同學可能會問:為什么Go編寫的測試用例會出現這個錯誤呢?因為Go是一門包含GC的語言惜互,runtime管理內存的分配和回收布讹,哪怕是并發(fā)調用的,在指針訪問安全的情況下训堆,最多也就會出現競態(tài)條件描验,而不是內存訪問錯誤啊坑鱼?

是的膘流,正常來說確實如此,不過在真正分析問題前鲁沥,先交代一下問題的背景呼股,讓你有一個直觀的了解。

背景

下面是之前非并發(fā)的測試用例(該用例是正確的):

func TestDispatch_V710(t *testing.T) {
    gen := datacenter.Gen{
        RealOrderCount:         60,
        RelayOrderCount:        20,
        ShortAppointOrderCount: 15,
        LongAppointOrderCount:  5,
    }

    // 生成數據
    gen.Do(nil, nil)

    // 初始化策略引擎
    if err := strategy.Init("../../conf/strategy_engine_conf.yaml"); err != nil {
        t.Error(err)
        os.Exit(1)
    }

    // 模擬計算策略
    dataCenter := gen.GetDataCenter()
    utils.NewSimulationStrategy(dataCenter, nil, strategy.GetStrategyTree()).Do()

    // 算法引擎做最優(yōu)化匹配
    dispatch := optimal.Dispatch{}
    dispatch.OptimalDispatch(dataCenter)
}

下面是增加并發(fā)后的測試用例:

func TestDispatch_OnApolloChanged_V710(t *testing.T) {
    // 初始化策略引擎
    if err := strategy.Init("../../conf/strategy_engine_conf.yaml"); err != nil {
        t.Error(err)
        os.Exit(1)
    }

    manager, err := pkgUtils.NewApolloManager(&pkgUtils.ApolloManagerConfig{
        ConfigServerURL: "http://192.168.205.10:8080",
        AppID:           "strategydispatch",
        Cluster:         "default",
        Namespaces:      []string{strategy.ApolloNamespace},
        BackupFile:      "",
        IP:              "",
        AccessKey:       "",
    })
    if err != nil {
        t.Error(err)
        os.Exit(1)
    }

    // 注冊策略引擎配置事件回調
    manager.RegisterHandler(strategy.ApolloNamespace, strategy.ApolloNotifyHandler, pkgUtils.ApolloErrHandler)

    go manager.Run()

    wg := sync.WaitGroup{}
    // 測試并發(fā)執(zhí)行
    for i := 0; i < 2; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            // 20 * 50s, 執(zhí)行計算, 并測試apollo變更
            for i := 0; i < 3; i++ {
                gen := datacenter.Gen{
                    RealOrderCount:         60,
                    RelayOrderCount:        20,
                    ShortAppointOrderCount: 15,
                    LongAppointOrderCount:  5,
                }

                // 生成數據
                gen.Do(nil, nil)

                // 模擬計算策略
                dataCenter := gen.GetDataCenter()
                utils.NewSimulationStrategy(dataCenter, nil, strategy.GetStrategyTree()).Do()

                // 算法引擎做最優(yōu)化匹配
                dispatch := optimal.Dispatch{}
                dispatch.OptimalDispatch(dataCenter)
                fmt.Println(dataCenter.DispatchResult)
                time.Sleep(time.Second * 5)
            }
        }()
    }

    wg.Wait()
}

仔細觀察代碼你會發(fā)現變量是在goroutine內部初始化的画恰,也就是說都屬于goroutine stack的 local變量彭谁,唯一一個共享的變量是

strategy.GetStrategyTree(),不過這個是為了測試COW的正確性允扇。

同時該部分的代碼存在cgo缠局,這也是唯一有盲點的地方则奥,因為cgo對于使用者來說是透明的,那么可能產生segmentation violation 應該只有cgo的部分了狭园。

cgo代碼

dispatch.OptimalDispatch(dataCenter) 這行代碼包含cgo調用读处,OptimalDispatch 的函數如下:

func (d *Dispatch) OptimalDispatch(dataCenter *common.DataCenter) {
    // ......省略部分代碼
    
    degrade := km.Entrance(orderCarPair, dataCenter)
    
    if degrade {
        subStart := time.Now()
        
        km.Greedy(orderCarPair, dataCenter)
        // ......省略部分代碼
    }

    // ......省略部分代碼
}

其中km.Entrance(orderCarPair, dataCenter)會真調用C++代碼

func Entrance(Graphy map[string][]common.OrderWithCarInfo, dataCenter *common.DataCenter) (degrade bool) {
    // ......省略部分代碼
    
    // 這里會調用c++代碼
    result := C.entrance((*C.double)(unsafe.Pointer(&cArray[0])), C.long(max_v_num))
    
    // ......省略部分代碼

}

C++的接口聲明如下

long* entrance(double * input_weight, long input_max_v_num);

其中Go會向C++傳遞一個slice, C++也會返回給Go一個long array

定位

在文章開始的時候,由于計算部分用了goroutine pool, 錯誤信息沒有全部復制唱矛,現在來看一下錯誤信息中的runtime.stack部分

 === RUN   TestDispatch_OnApolloChanged_V710
fatal error: unexpected signal during runtime execution
[signal SIGSEGV: segmentation violation code=0x1 addr=0x1d8 pc=0xa1328c]

runtime stack:
runtime.throw(0xb9420c, 0x2a)
        /usr/local/lib/go/src/runtime/panic.go:1114 +0x72
runtime.sigpanic()
        /usr/local/lib/go/src/runtime/signal_unix.go:679 +0x46a

goroutine 90 [syscall]:
runtime.cgocall(0xa12360, 0xc00350ebf8, 0xf7a7d668e8941901)
        /usr/local/lib/go/src/runtime/cgocall.go:133 +0x5b fp=0xc00350ebc8 sp=0xc00350eb90 pc=0x4059eb
fabu.ai/IntelligentTransport/strategy_dispatch/pkg/service/algorithm/unit/km._Cfunc_entrance(0xc0046c2000, 0x64, 0x0)
        _cgo_gotypes.go:48 +0x4e fp=0xc00350ebf8 sp=0xc00350ebc8 pc=0x89e7be
fabu.ai/IntelligentTransport/strategy_dispatch/pkg/service/algorithm/unit/km.Entrance(0xc0000a2540, 0xc0035fe500, 0x1313ae0)
        /mnt/d/Workspace/Onedrive/wordspace/code/t3go.cn/strategy_dispatch/pkg/service/algorithm/unit/km/km.go:108 +0xaad fp=0xc00350f4a8 sp=0xc00350ebf8 pc=0x89f2cd
fabu.ai/IntelligentTransport/strategy_dispatch/pkg/service/algorithm/optimal.(*Dispatch).OptimalDispatch(0xc00350ff98, 0xc0035fe500)        /mnt/d/Workspace/Onedrive/wordspace/code/t3go.cn/strategy_dispatch/pkg/service/algorithm/optimal/dispatch.go:32 +0x49b fp=0xc00350ff38 sp=0xc00350f4a8 pc=0x8a07eb
fabu.ai/IntelligentTransport/strategy_dispatch/tests/dispatch.TestDispatch_OnApolloChanged_V710.func1(0xc00358f530)
        /mnt/d/Workspace/Onedrive/wordspace/code/t3go.cn/strategy_dispatch/tests/dispatch/dispatch_v710_test.go:67 +0x93 fp=0xc00350ffd8 sp=0xc00350ff38 pc=0xa11f73
runtime.goexit()
        /usr/local/lib/go/src/runtime/asm_amd64.s:1373 +0x1 fp=0xc00350ffe0 sp=0xc00350ffd8 pc=0x468e31
created by fabu.ai/IntelligentTransport/strategy_dispatch/tests/dispatch.TestDispatch_OnApolloChanged_V710
        /mnt/d/Workspace/Onedrive/wordspace/code/t3go.cn/strategy_dispatch/tests/dispatch/dispatch_v710_test.go:47 +0x2f2

goroutine 1 [chan receive]:
testing.(*T).Run(0xc0035b7c20, 0xb8c69c, 0x21, 0xba8468, 0x48c901)
        /usr/local/lib/go/src/testing/testing.go:1044 +0x37e
testing.runTests.func1(0xc0035b7b00)
        /usr/local/lib/go/src/testing/testing.go:1285 +0x78
testing.tRunner(0xc0035b7b00, 0xc0035f5e10)
        /usr/local/lib/go/src/testing/testing.go:992 +0xdc
testing.runTests(0xc003581960, 0x12c0ec0, 0x4, 0x4, 0x0)
        /usr/local/lib/go/src/testing/testing.go:1283 +0x2a7
testing.(*M).Run(0xc0035ae200, 0x0)
        /usr/local/lib/go/src/testing/testing.go:1200 +0x15f
main.main()
        _testmain.go:54 +0x135

錯誤信息的runtime statck部分出現了cgo調用相關錯誤罚舱,其中km._Cfunc_entrance(0xc0046c2000, 0x64, 0x0)cgo編譯過程中生成的中間代碼

runtime.cgocall(0xa12360, 0xc00350ebf8, 0xf7a7d668e8941901)
        /usr/local/lib/go/src/runtime/cgocall.go:133 +0x5b fp=0xc00350ebc8 sp=0xc00350eb90 pc=0x4059eb
fabu.ai/IntelligentTransport/strategy_dispatch/pkg/service/algorithm/unit/km._Cfunc_entrance(0xc0046c2000, 0x64, 0x0)

因此可以確定是cgo部分的代碼導致了該問題。

在非并發(fā)下CGO調用是正常的绎谦,也就是說CGO代碼本身是正常的管闷。

在并發(fā)下調用CGO部分出現了問題,有可能和Go的runtime的一些機制有關系燥滑,因此需要定位到runtime部分渐北,也就是runtime 在做cgo調用的時候哪一步出發(fā)了segmentation violation

coredump

熟悉C/C++的同學都知道,在Linux系統下铭拧,如果程序出現了內存相關的異常錯誤赃蛛,會產生coredump文件。順著這個思路搀菩,Go能否產生core文件呢呕臂?答案是可以的:

?  ~ ulimit -c
0
?  ~ ulimit -c unlimited
?  ~ ulimit -c          
unlimited

默認的coredump文件大小為0,我設置為unlimited , 也可以合理的設置其大小肪跋。

之后編譯運行程序歧蒋,讓其產生coredump文件

?  ~ GOTRACEBACK=crash ./strategy_dispatch_test

GOTRACEBACK=crash 環(huán)境變量 設置為 crash 就是允許生成coredump文件了。

不過由于我是測試用例州既,嘗試先設置GOTRACEBACK=crash 谜洽,然后 go test 無效,只能將測試用例的代碼轉換為可編譯的main 程序吴叶。

coredump文件分析

coredump文件運行不會導致進程崩潰阐虚,有了coredump文件,就可以加載coredump文件做更進一步的分析了蚌卤。

我通過dlv工具去加載coredump文件:

dlv core ./strategy_dispatch_test core

然后輸入stack实束,打印出stack trace信息

Type 'help' for list of commands.
(dlv) stack
0  0x0000000000466931 in runtime.raise
   at /usr/local/lib/go/src/runtime/sys_linux_amd64.s:165
1  0x00000000004644a2 in runtime.asmcgocall
   at /usr/local/lib/go/src/runtime/asm_amd64.s:640
2  0x000000000040593f in runtime.cgocall
   at /usr/local/lib/go/src/runtime/cgocall.go:143
3  0x000000000087acae in fabu.ai/IntelligentTransport/strategy_dispatch/pkg/service/algorithm/unit/km._Cfunc_entrance
   at _cgo_gotypes.go:48
4  0x000000000087b7bd in fabu.ai/IntelligentTransport/strategy_dispatch/pkg/service/algorithm/unit/km.Entrance
   at /mnt/d/Workspace/Onedrive/wordspace/code/t3go.cn/strategy_dispatch/pkg/service/algorithm/unit/km/km.go:108
5  0x000000000087ccdb in fabu.ai/IntelligentTransport/strategy_dispatch/pkg/service/algorithm/optimal.(*Dispatch).OptimalDispatch
   at /mnt/d/Workspace/Onedrive/wordspace/code/t3go.cn/strategy_dispatch/pkg/service/algorithm/optimal/dispatch.go:32
6  0x00000000009e7903 in main.main.func1
   at /mnt/d/Workspace/Onedrive/wordspace/code/t3go.cn/strategy_dispatch/tests/dispatch/tmp/tmp.go:68
7  0x0000000000464d91 in runtime.goexit
   at /usr/local/lib/go/src/runtime/asm_amd64.s:1373

通過stack trace 信息,發(fā)現在3處

3 0x000000000087acae in fabu.ai/IntelligentTransport/strategy_dispatch/pkg/service/algorithm/unit/km._Cfunc_entrance
   at _cgo_gotypes.go:48

出現了C++中間代碼的調用

//go:cgo_unsafe_args
func _Cfunc_entrance(p0 *_Ctype_double, p1 _Ctype_long) (r1 *_Ctype_long) {
    _cgo_runtime_cgocall(_cgo_743da1d4b169_Cfunc_entrance, uintptr(unsafe.Pointer(&p0)))
    if _Cgo_always_false { 
        _Cgo_use(p0) 
        _Cgo_use(p1)
    }
    return
}

可以更加確定是CGO出了問題逊彭,繼續(xù)跟蹤stack trace信息咸灿,在2處告訴我們cgocall.go:143, 程序進入了runtime部分,

2  0x000000000040593f in runtime.cgocall
   at /usr/local/lib/go/src/runtime/cgocall.go:143

查看runtime部分對應的代碼

// Call from Go to C.
//
// This must be nosplit because it's used for syscalls on some
// platforms. Syscalls may have untyped arguments on the stack, so
// it's not safe to grow or scan the stack.
//
//go:nosplit
func cgocall(fn, arg unsafe.Pointer) int32 {
    // ... 省略一些錯誤處理

    mp := getg().m
    mp.ncgocall++
    mp.ncgo++

    // Reset traceback.
    mp.cgoCallers[0] = 0

    // Announce we are entering a system call
    // so that the scheduler knows to create another
    // M to run goroutines while we are in the
    // foreign code.
    //
    // The call to asmcgocall is guaranteed not to
    // grow the stack and does not allocate memory,
    // so it is safe to call while "in a system call", outside
    // the $GOMAXPROCS accounting.
    //
    // fn may call back into Go code, in which case we'll exit the
    // "system call", run the Go code (which may grow the stack),
    // and then re-enter the "system call" reusing the PC and SP
    // saved by entersyscall here.
    entersyscall()

    // Tell asynchronous preemption that we're entering external
    // code. We do this after entersyscall because this may block
    // and cause an async preemption to fail, but at this point a
    // sync preemption will succeed (though this is not a matter
    // of correctness).
    osPreemptExtEnter(mp)

    mp.incgo = true
    
    // 這里是143行
    errno := asmcgocall(fn, arg)

    // ... 省略部分代碼


    return errno
}

程序停在了cgocall 函數的這個位置 errno := asmcgocall(fn, arg), 這個函數是匯編實現侮叮,并且在stack trace也給出了對應代碼的位置提示

1  0x00000000004644a2 in runtime.asmcgocall
   at /usr/local/lib/go/src/runtime/asm_amd64.s:640

查看 asm_amd64.s 這個文件避矢,640行對應的匯編代碼是這部分

// func asmcgocall(fn, arg unsafe.Pointer) int32
// Call fn(arg) on the scheduler stack,
// aligned appropriately for the gcc ABI.
// See cgocall.go for more details.
TEXT ·asmcgocall(SB),NOSPLIT,$0-20
    MOVQ    fn+0(FP), AX
    MOVQ    arg+8(FP), BX

    MOVQ    SP, DX

    // Figure out if we need to switch to m->g0 stack.
    // We get called to create new OS threads too, and those
    // come in on the m->g0 stack already.
    get_tls(CX)
    MOVQ    g(CX), R8
    CMPQ    R8, $0
    JEQ nosave
    MOVQ    g_m(R8), R8
    MOVQ    m_g0(R8), SI
    MOVQ    g(CX), DI
    CMPQ    SI, DI
    JEQ nosave
    MOVQ    m_gsignal(R8), SI
    CMPQ    SI, DI
    JEQ nosave

    // Switch to system stack.
    MOVQ    m_g0(R8), SI
    CALL    gosave<>(SB) // 程序崩潰在這里
    MOVQ    SI, g(CX)
    MOVQ    (g_sched+gobuf_sp)(SI), SP

    // Now on a scheduling stack (a pthread-created stack).
    // Make sure we have enough room for 4 stack-backed fast-call
    // registers as per windows amd64 calling convention.
    SUBQ    $64, SP
    ANDQ    $~15, SP    // alignment for gcc ABI
    MOVQ    DI, 48(SP)  // save g
    MOVQ    (g_stack+stack_hi)(DI), DI
    SUBQ    DX, DI
    MOVQ    DI, 40(SP)  // save depth in stack (can't just save SP, as stack might be copied during a callback)
    MOVQ    BX, DI      // DI = first argument in AMD64 ABI
    MOVQ    BX, CX      // CX = first argument in Win64
    CALL    AX

    // Restore registers, g, stack pointer.
    get_tls(CX)
    MOVQ    48(SP), DI
    MOVQ    (g_stack+stack_hi)(DI), SI
    SUBQ    40(SP), SI
    MOVQ    DI, g(CX)
    MOVQ    SI, SP

    MOVL    AX, ret+16(FP)
    RET

nosave:
    // Running on a system stack, perhaps even without a g.
    // Having no g can happen during thread creation or thread teardown
    // (see needm/dropm on Solaris, for example).
    // This code is like the above sequence but without saving/restoring g
    // and without worrying about the stack moving out from under us
    // (because we're on a system stack, not a goroutine stack).
    // The above code could be used directly if already on a system stack,
    // but then the only path through this code would be a rare case on Solaris.
    // Using this code for all "already on system stack" calls exercises it more,
    // which should help keep it correct.
    SUBQ    $64, SP
    ANDQ    $~15, SP
    MOVQ    $0, 48(SP)      // where above code stores g, in case someone looks during debugging
    MOVQ    DX, 40(SP)  // save original stack pointer
    MOVQ    BX, DI      // DI = first argument in AMD64 ABI
    MOVQ    BX, CX      // CX = first argument in Win64
    CALL    AX
    MOVQ    40(SP), SI  // restore original stack pointer
    MOVQ    SI, SP
    MOVL    AX, ret+16(FP)
    RET

640行對應的部分是 CALL gosave<>(SB) ,不過我們先不著急分析這一行匯編代碼,我們先看 asmcgocall 這部分匯編代碼干了什么(需要一些匯編和Plan9匯編知識)

asmcgocall匯編代碼分析

整個asmcgocall函數是執(zhí)行cgo調用审胸,那么在640行(gosave)之前分尸,函數做了什么事情呢?

TEXT ·asmcgocall(SB),NOSPLIT,$0-20
    MOVQ    fn+0(FP), AX
    MOVQ    arg+8(FP), BX

    MOVQ    SP, DX

    get_tls(CX)                 // 獲取g指針
    MOVQ    g(CX), R8           // R8 = g
    CMPQ    R8, $0              // if R8 == 0, goto nosave
    JEQ nosave                  
    MOVQ    g_m(R8), R8         // R8 = g.m
    MOVQ    m_g0(R8), SI        // SI = g.m.g0
    MOVQ    g(CX), DI           // DI = g
    CMPQ    SI, DI              // if g == g.m.g0, goto nosave
    JEQ nosave
    MOVQ    m_gsignal(R8), SI   // SI = g.m.gsingal
    CMPQ    SI, DI              // if g.m.gsingal == g, goto nosave
    JEQ nosave

在上面的匯編代碼中歹嘹,出現三次CMQPJEQ指令,它們都會跳轉到 nosave 孔庭,那么 如果CMQP成立執(zhí)行了JEQnosave 是做什么呢尺上?

nosave:
    // Running on a system stack, perhaps even without a g.
    // Having no g can happen during thread creation or thread teardown
    // (see needm/dropm on Solaris, for example).
    // This code is like the above sequence but without saving/restoring g
    // and without worrying about the stack moving out from under us
    // (because we're on a system stack, not a goroutine stack).
    // The above code could be used directly if already on a system stack,
    // but then the only path through this code would be a rare case on Solaris.
    // Using this code for all "already on system stack" calls exercises it more,
    // which should help keep it correct.
    SUBQ    $64, SP
    ANDQ    $~15, SP
    MOVQ    $0, 48(SP)      // where above code stores g, in case someone looks during debugging
    MOVQ    DX, 40(SP)  // save original stack pointer
    MOVQ    BX, DI      // DI = first argument in AMD64 ABI
    MOVQ    BX, CX      // CX = first argument in Win64
    CALL    AX
    MOVQ    40(SP), SI  // restore original stack pointer
    MOVQ    SI, SP
    MOVL    AX, ret+16(FP)
    RET

nosave部分略微有些復雜,簡單來說就是當前的cgo調用可以直接運行在 系統棧圆到,而不是協程棧

那么之前的代碼就很清晰了:

  • CMPQ R8, $0 表示當前沒有運行的g怎抛,自然也就不存在協程棧,可以直接運行在系統棧
  • CMPQ SI, DI g0指向的是系統棧芽淡,而如果g == g0马绝,就表示g0運行當前的g的fn函數,自然就可以到系統棧上操作
  • CMPQ SI, DI 這個表示具體的是什么挣菲,還沒有弄的很清楚富稻,不過也是滿足條件到系統棧上直接運行的。

那么當不滿足到系統棧上運行時白胀,會發(fā)生什么椭赋?asmgocall后半部分告訴了我們答案

TEXT ·asmcgocall(SB),NOSPLIT,$0-20
    // 省略前半部分代碼

    // Switch to system stack.
    MOVQ    m_g0(R8), SI  // SI = g.m.g0
    CALL    gosave<>(SB) // 程序崩潰在這里
    MOVQ    SI, g(CX) // g = g.m.g0
    MOVQ    (g_sched+gobuf_sp)(SI), SP // 保存狀態(tài)

    // Now on a scheduling stack (a pthread-created stack).
    // Make sure we have enough room for 4 stack-backed fast-call
    // registers as per windows amd64 calling convention.
    SUBQ    $64, SP
    ANDQ    $~15, SP    // alignment for gcc ABI
    MOVQ    DI, 48(SP)  // save g
    MOVQ    (g_stack+stack_hi)(DI), DI
    SUBQ    DX, DI
    MOVQ    DI, 40(SP)  // save depth in stack (can't just save SP, as stack might be copied during a callback)
    MOVQ    BX, DI      // DI = first argument in AMD64 ABI
    MOVQ    BX, CX      // CX = first argument in Win64
    CALL    AX

    // Restore registers, g, stack pointer.
    get_tls(CX)
    MOVQ    48(SP), DI
    MOVQ    (g_stack+stack_hi)(DI), SI
    SUBQ    40(SP), SI
    MOVQ    DI, g(CX)
    MOVQ    SI, SP

    MOVL    AX, ret+16(FP)

當不滿足時

  1. 會發(fā)生棧切換,首先通過gosave保存goroutine stack或杠,可以看一下gosave做了什么

    // func gosave(buf *gobuf)
    // save state in Gobuf; setjmp
    TEXT runtime·gosave(SB), NOSPLIT, $0-8
     MOVQ    buf+0(FP), AX       // 將 gobuf 賦值給 AX
     LEAQ    buf+0(FP), BX       // 取參數地址哪怔,也就是 caller 的 SP
     MOVQ    BX, gobuf_sp(AX)    // 保存 caller SP,再次運行時的棧頂
     MOVQ    0(SP), BX       
     MOVQ    BX, gobuf_pc(AX)    // 保存 caller PC向抢,再次運行時的指令地址
     MOVQ    $0, gobuf_ret(AX)
     MOVQ    BP, gobuf_bp(AX)
     // Assert ctxt is zero. See func save.
     MOVQ    gobuf_ctxt(AX), BX
     TESTQ   BX, BX
     JZ  2(PC)
     CALL    runtime·badctxt(SB)
     get_tls(CX)                 // 獲取 tls
     MOVQ    g(CX), BX           // 將 g 的地址存入 BX
     MOVQ    BX, gobuf_g(AX)     // 保存 g 的地址
     RET
    

    gosave會保存調度信息到g0.sched, 設置了 g0.sched.sp 和 g0.sched.pc

  2. 執(zhí)行goroutine stack -> system stack

  3. 執(zhí)行cgo調用(gosave之后)

問題原因猜測

協程切換

asmcgocall部分代碼分析中可以得出一個結論:goroutine stack 進行了切換认境。

同時go官方文檔中說過

calling a C function does not block other goroutines

熟悉go runtime的同學可能知道,goroutine的實現依賴TLS的挟鸠,如果在一個Thread上的goroutine切換叉信,無論怎么切換,都處于一個Thread TLS內兄猩, 但如果多個Thread之間進行切換茉盏,極有可能出現該問題

假如有Goroutine [G1, G2]

  1. G1被調度到Thread1,G1在Goroutine Stack 創(chuàng)建了變量cArray參數傳遞給C調用
  2. G2被調度到Thread2枢冤,假如cArray是全局變量鸠姨,如果不涉及CGO調用,程序也就race condition淹真,但涉及CGO調用讶迁,會出現: Thread2 訪問 Thread1棧空間, 也就會出現segmentation violation錯誤了核蘸。

但由于我們的cArray是在Goroutine局部創(chuàng)建的巍糯,因此這個問題可以排除掉啸驯。

TLS訪問越界

還有一種情況,G1和G2調度到了線程Thread1和Thtread2祟峦,G1先創(chuàng)建了CGO調用運行所需的地址罚斗,G2在運行時也使用了這個地址執(zhí)行CGO,但該地址在T1, G2處于Thread2宅楞。

也就是說是執(zhí)行過gosave做了棧切換针姿,執(zhí)行到CGO調用崩潰的。

調試驗證

為了驗證猜測厌衙,繼續(xù)使用dlv調試, 輸入grs 查看所有的goroutine距淫,可以看到 Goroutine 71Goroutine 71 的確在不同的線程上運行了執(zhí)行km._Cfunc_entrance

(dlv) grs
* Goroutine 71 - User: _cgo_gotypes.go:48 fabu.ai/IntelligentTransport/strategy_dispatch/pkg/service/algorithm/unit/km._Cfunc_entrance (0x87af3e) (thread 11217)
  Goroutine 72 - User: _cgo_gotypes.go:48 fabu.ai/IntelligentTransport/strategy_dispatch/pkg/service/algorithm/unit/km._Cfunc_entrance (0x87af3e) (thread 11214)

[324 goroutines]

既然這樣婶希,如果CPU只有一個core的時候榕暇,也就是只有一個Thread的時候,是否就不會出現問題呢喻杈?

通過如下代碼限制Go運行時可用的CPU Core沒有效果彤枢,CPU Core仍是多個。

println(runtime.NumCPU())
runtime.GOMAXPROCS(1)
println(runtime.NumCPU())

于是使用Docker容器(VM也一樣)筒饰,限制CPU Core = 1堂污,果然,程序是正常運行的龄砰。

于是也就驗證了之前的猜測盟猖,可能具體的原因并非是CGO的地址訪問越界(可能是返回值或者其他,不過不需要在繼續(xù)深挖匯編和runtime了)换棚,已經可以確定的是:多個Goroutine調度到多個Thread上執(zhí)行CGO調用式镐,會出現訪問其他Thread TLS的情況,從而產生segmentation violation

解決

通過限制CPU Core的方式并不算真正的解決方式固蚤,想要解決該問題的關鍵在于不同的Thread上的G執(zhí)行CGO調用時娘汞,不能是并發(fā)的,一種很自然的方式是 sync.Mutex

于是在Goroutine的部分增加了Lock后夕玩,即使不限制CPU仍然沒有問題

事情到此你弦,基本上可以結束了,但我們應該在試著問一下自己:sync.Mutex為什么能解決問題燎孟?

互斥鎖的是讓線程串行執(zhí)行禽作,Go中也不例外,Go的Mutex中Lock處于不同的模式時會使用不同的方式互斥揩页,感興趣的同學可以從這幾部分下手

  • spin-lock 與 runtime.procyield旷偿, 會涉及到:Inter PAUSE指令流水線優(yōu)化
  • sync_runtime_SemacquireMutex
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子萍程,更是在濱河造成了極大的恐慌幢妄,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,406評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件茫负,死亡現場離奇詭異蕉鸳,居然都是意外死亡,警方通過查閱死者的電腦和手機忍法,發(fā)現死者居然都...
    沈念sama閱讀 94,395評論 3 398
  • 文/潘曉璐 我一進店門置吓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人缔赠,你說我怎么就攤上這事∮烟猓” “怎么了嗤堰?”我有些...
    開封第一講書人閱讀 167,815評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長度宦。 經常有香客問我踢匣,道長,這世上最難降的妖魔是什么戈抄? 我笑而不...
    開封第一講書人閱讀 59,537評論 1 296
  • 正文 為了忘掉前任离唬,我火速辦了婚禮,結果婚禮上划鸽,老公的妹妹穿的比我還像新娘输莺。我一直安慰自己,他們只是感情好裸诽,可當我...
    茶點故事閱讀 68,536評論 6 397
  • 文/花漫 我一把揭開白布嫂用。 她就那樣靜靜地躺著,像睡著了一般丈冬。 火紅的嫁衣襯著肌膚如雪嘱函。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,184評論 1 308
  • 那天埂蕊,我揣著相機與錄音往弓,去河邊找鬼。 笑死蓄氧,一個胖子當著我的面吹牛函似,可吹牛的內容都是我干的。 我是一名探鬼主播喉童,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼缴淋,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起重抖,我...
    開封第一講書人閱讀 39,668評論 0 276
  • 序言:老撾萬榮一對情侶失蹤露氮,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后钟沛,有當地人在樹林里發(fā)現了一具尸體畔规,經...
    沈念sama閱讀 46,212評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,299評論 3 340
  • 正文 我和宋清朗相戀三年恨统,在試婚紗的時候發(fā)現自己被綠了叁扫。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,438評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡畜埋,死狀恐怖莫绣,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情悠鞍,我是刑警寧澤对室,帶...
    沈念sama閱讀 36,128評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站咖祭,受9級特大地震影響掩宜,放射性物質發(fā)生泄漏。R本人自食惡果不足惜么翰,卻給世界環(huán)境...
    茶點故事閱讀 41,807評論 3 333
  • 文/蒙蒙 一牺汤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧浩嫌,春花似錦檐迟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至伐坏,卻和暖如春怔匣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背桦沉。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評論 1 272
  • 我被黑心中介騙來泰國打工每瞒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人纯露。 一個月前我還...
    沈念sama閱讀 48,827評論 3 376
  • 正文 我出身青樓剿骨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親埠褪。 傳聞我的和親對象是個殘疾皇子浓利,可洞房花燭夜當晚...
    茶點故事閱讀 45,446評論 2 359