曹春暉:談一談 Go 和 Syscall
桔妹導讀:syscall 是語言與系統互動的唯一手段,理解 Go 語言中的 syscall,本文可以幫助讀者理解 Go 語言怎麼與系統打交道,同時瞭解底層 runtime 在 syscall 最佳化方面的一些小心思,從而更為深入地理解 Go 語言。
—————
▎閱讀索引
概念
入口
系統呼叫管理
runtime 中的 SYSCALL
和排程的互動
entersyscall
exitsyscallfast
exitsyscall
entersyscallblock
entersyscallblock_handoff
entersyscall_sysmon
entersyscall_gcwait
總結
▎概念
▎入口
syscall 有下面幾個入口,在 syscall/asm_linux_amd64.s 中。
1func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno)
2
3func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
4
5func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err syscall.Errno)
6
7func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
8
這些函式的實現都是彙編,按照 linux 的 syscall 呼叫規範,我們只要在彙編中把引數依次傳入暫存器,並呼叫 SYSCALL 指令即可進入核心處理邏輯,系統呼叫執行完畢之後,返回值放在 RAX 中:
Syscall 和 Syscall6 的區別只有傳入引數不一樣:
1// func Syscall(trap int64, a1, a2, a3 uintptr) (r1, r2, err uintptr);
2TEXT ·Syscall(SB),NOSPLIT,$0-56
3 CALL runtime·entersyscall(SB)
4 MOVQ a1+8(FP), DI
5 MOVQ a2+16(FP), SI
6 MOVQ a3+24(FP), DX
7 MOVQ $0, R10
8 MOVQ $0, R8
9 MOVQ $0, R9
10 MOVQ trap+0(FP), AX // syscall entry
11 SYSCALL
12 // 0xfffffffffffff001 是 linux MAX_ERRNO 取反 轉無符號,source/include/linux/err.h#L17
13 CMPQ AX, $0xfffffffffffff001
14 JLS ok
15 MOVQ $-1, r1+32(FP)
16 MOVQ $0, r2+40(FP)
17 NEGQ AX
18 MOVQ AX, err+48(FP)
19 CALL runtime·exitsyscall(SB)
20 RET
21ok:
22 MOVQ AX, r1+32(FP)
23 MOVQ DX, r2+40(FP)
24 MOVQ $0, err+48(FP)
25 CALL runtime·exitsyscall(SB)
26 RET
27
28// func Syscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr)
29TEXT ·Syscall6(SB),NOSPLIT,$0-80
30 CALL runtime·entersyscall(SB)
31 MOVQ a1+8(FP), DI
32 MOVQ a2+16(FP), SI
33 MOVQ a3+24(FP), DX
34 MOVQ a4+32(FP), R10
35 MOVQ a5+40(FP), R8
36 MOVQ a6+48(FP), R9
37 MOVQ trap+0(FP), AX // syscall entry
38 SYSCALL
39 CMPQ AX, $0xfffffffffffff001
40 JLS ok6
41 MOVQ $-1, r1+56(FP)
42 MOVQ $0, r2+64(FP)
43 NEGQ AX
44 MOVQ AX, err+72(FP)
45 CALL runtime·exitsyscall(SB)
46 RET
47ok6:
48 MOVQ AX, r1+56(FP)
49 MOVQ DX, r2+64(FP)
50 MOVQ $0, err+72(FP)
51 CALL runtime·exitsyscall(SB)
52 RET
兩個函式沒什麼大區別,為啥不用一個呢?個人猜測,Go 的函式引數都是棧上傳入,可能是為了節省一點棧空間。。在正常的 Syscall 操作之前會通知 runtime,接下來我要進行 syscall 操作了 runtime·entersyscall ,退出時會呼叫 runtime·exitsyscall 。
1// func RawSyscall(trap, a1, a2, a3 uintptr) (r1, r2, err uintptr)
2TEXT ·RawSyscall(SB),NOSPLIT,$0-56
3 MOVQ a1+8(FP), DI
4 MOVQ a2+16(FP), SI
5 MOVQ a3+24(FP), DX
6 MOVQ $0, R10
7 MOVQ $0, R8
8 MOVQ $0, R9
9 MOVQ trap+0(FP), AX // syscall entry
10 SYSCALL
11 CMPQ AX, $0xfffffffffffff001
12 JLS ok1
13 MOVQ $-1, r1+32(FP)
14 MOVQ $0, r2+40(FP)
15 NEGQ AX
16 MOVQ AX, err+48(FP)
17 RET
18ok1:
19 MOVQ AX, r1+32(FP)
20 MOVQ DX, r2+40(FP)
21 MOVQ $0, err+48(FP)
22 RET
23
24// func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr)
25TEXT ·RawSyscall6(SB),NOSPLIT,$0-80
26 MOVQ a1+8(FP), DI
27 MOVQ a2+16(FP), SI
28 MOVQ a3+24(FP), DX
29 MOVQ a4+32(FP), R10
30 MOVQ a5+40(FP), R8
31 MOVQ a6+48(FP), R9
32 MOVQ trap+0(FP), AX // syscall entry
33 SYSCALL
34 CMPQ AX, $0xfffffffffffff001
35 JLS ok2
36 MOVQ $-1, r1+56(FP)
37 MOVQ $0, r2+64(FP)
38 NEGQ AX
39 MOVQ AX, err+72(FP)
40 RET
41ok2:
42 MOVQ AX, r1+56(FP)
43 MOVQ DX, r2+64(FP)
44 MOVQ $0, err+72(FP)
45 RET
RawSyscall 和 Syscall 的區別也非常微小,就只是在進入 Syscall 和退出的時候沒有通知 runtime,這樣 runtime 理論上是沒有辦法透過排程把這個 g 的 m 的 p 排程走的,所以如果使用者程式碼使用了 RawSyscall 來做一些阻塞的系統呼叫,是有可能阻塞其它的 g 的,下面是官方開發的原話:
Yes, if you call RawSyscall you may block other goroutines from running. The system monitor may start them up after a while, but I think there are cases where it won't. I would say that Go programs should always call Syscall. RawSyscall exists to make it slightly more efficient to call system calls that never block, such as getpid. But it's really an internal mechanism.
1// func gettimeofday(tv *Timeval) (err uintptr)
2TEXT ·gettimeofday(SB),NOSPLIT,$0-16
3 MOVQ tv+0(FP), DI
4 MOVQ $0, SI
5 MOVQ runtime·__vdso_gettimeofday_sym(SB), AX
6 CALL AX
7
8 CMPQ AX, $0xfffffffffffff001
9 JLS ok7
10 NEGQ AX
11 MOVQ AX, err+8(FP)
12 RET
13ok7:
14 MOVQ $0, err+8(FP)
15 RET
▎系統呼叫管理
先是系統呼叫的定義檔案:
1/syscall/syscall_linux.go
可以把系統呼叫分為三類:
阻塞系統呼叫
非阻塞系統呼叫
wrapped 系統呼叫
阻塞系統呼叫會定義成下面這樣的形式:
1//sys Madvise(b []byte, advice int) (err error)
非阻塞系統呼叫:
1//sysnb EpollCreate(size int) (fd int, err error)
然後,根據這些註釋,mksyscall.pl 指令碼會生成對應的平臺的具體實現。mksyscall.pl 是一段 perl 指令碼,感興趣的同學可以自行檢視,這裡就不再贅述了。
看看阻塞和非阻塞的系統呼叫的生成結果:
1func Madvise(b []byte, advice int) (err error) {
2 var _p0 unsafe.Pointer
3 if len(b) > 0 {
4 _p0 = unsafe.Pointer(&b[0])
5 } else {
6 _p0 = unsafe.Pointer(&_zero)
7 }
8 _, _, e1 := Syscall(SYS_MADVISE, uintptr(_p0), uintptr(len(b)), uintptr(advice))
9 if e1 != 0 {
10 err = errnoErr(e1)
11 }
12 return
13}
14
15func EpollCreate(size int) (fd int, err error) {
16 r0, _, e1 := RawSyscall(SYS_EPOLL_CREATE, uintptr(size), 0, 0)
17 fd = int(r0)
18 if e1 != 0 {
19 err = errnoErr(e1)
20 }
21 return
22}
顯然,標記為 sys 的系統呼叫使用的是 Syscall 或者 Syscall6,標記為 sysnb 的系統呼叫使用的是 RawSyscall 或 RawSyscall6。
wrapped 的系統呼叫是怎麼一回事呢?
1func Rename(oldpath string, newpath string) (err error) {
2 return Renameat(_AT_FDCWD, oldpath, _AT_FDCWD, newpath)
3}
可能是覺得系統呼叫的名字不太好,或者引數太多,我們就簡單包裝一下。沒啥特別的。
▎runtime 中的 SYSCALL
除了上面提到的阻塞非阻塞和 wrapped syscall,runtime 中還定義了一些 low-level 的 syscall,這些是不暴露給使用者的。
提供給使用者的 syscall 庫,在使用時,會使 goroutine 和 p 分別進入 Gsyscall 和 Psyscall 狀態。但 runtime 自己封裝的這些 syscall 無論是否阻塞,都不會呼叫 entersyscall 和 exitsyscall。雖說是 “low-level” 的 syscall。
不過和暴露給使用者的 syscall 本質是一樣的。這些程式碼在 runtime/sys_linux_amd64.s中,舉個具體的例子:
1TEXT runtime·write(SB),NOSPLIT,$0-28
2 MOVQ fd+0(FP), DI
3 MOVQ p+8(FP), SI
4 MOVL n+16(FP), DX
5 MOVL $SYS_write, AX
6 SYSCALL
7 CMPQ AX, $0xfffffffffffff001
8 JLS 2(PC)
9 MOVL $-1, AX
10 MOVL AX, ret+24(FP)
11 RET
12
13TEXT runtime·read(SB),NOSPLIT,$0-28
14 MOVL fd+0(FP), DI
15 MOVQ p+8(FP), SI
16 MOVL n+16(FP), DX
17 MOVL $SYS_read, AX
18 SYSCALL
19 CMPQ AX, $0xfffffffffffff001
20 JLS 2(PC)
21 MOVL $-1, AX
22 MOVL AX, ret+24(FP)
23 RET
下面是所有 runtime 另外定義的 syscall 列表:
1#define SYS_read 0
2#define SYS_write 1
3#define SYS_open 2
4#define SYS_close 3
5#define SYS_mmap 9
6#define SYS_munmap 11
7#define SYS_brk 12
8#define SYS_rt_sigaction 13
9#define SYS_rt_sigprocmask 14
10#define SYS_rt_sigreturn 15
11#define SYS_access 21
12#define SYS_sched_yield 24
13#define SYS_mincore 27
14#define SYS_madvise 28
15#define SYS_setittimer 38
16#define SYS_getpid 39
17#define SYS_socket 41
18#define SYS_connect 42
19#define SYS_clone 56
20#define SYS_exit 60
21#define SYS_kill 62
22#define SYS_fcntl 72
23#define SYS_getrlimit 97
24#define SYS_sigaltstack 131
25#define SYS_arch_prctl 158
26#define SYS_gettid 186
27#define SYS_tkill 200
28#define SYS_futex 202
29#define SYS_sched_getaffinity 204
30#define SYS_epoll_create 213
31#define SYS_exit_group 231
32#define SYS_epoll_wait 232
33#define SYS_epoll_ctl 233
34#define SYS_pselect6 270
35#define SYS_epoll_create1 291
這些 syscall 理論上都是不會在執行期間被排程器剝離掉 p 的,所以執行成功之後 goroutine 會繼續執行,而不像使用者的 goroutine 一樣,若被剝離 p 會進入等待佇列。
▎和排程的互動
既然要和排程互動,那友好地通知我要 syscall 了: entersyscall,我完事了: exitsyscall。
所以這裡的互動指的是使用者程式碼使用 syscall 庫時和排程器的互動。runtime 裡的 syscall 不走這套流程。
▎entersyscall
1// syscall 庫和 cgo 呼叫的標準入口
2//go:nosplit
3func entersyscall() {
4 reentersyscall(getcallerpc(), getcallersp())
5}
6
7//go:nosplit
8func reentersyscall(pc, sp uintptr) {
9 _g_ := getg()
10
11 // 需要禁止 g 的搶佔
12 _g_.m.locks++
13
14 // entersyscall 中不能呼叫任何會導致棧增長/分裂的函式
15 _g_.stackguard0 = stackPreempt
16 // 設定 throwsplit,在 newstack 中,如果發現 throwsplit 是 true
17 // 會直接 crash
18 // 下面的程式碼是 newstack 裡的
19 // if thisg.m.curg.throwsplit {
20 // throw("runtime: stack split at bad time")
21 // }
22 _g_.throwsplit = true
23
24 // Leave SP around for GC and traceback.
25 // 儲存現場,在 syscall 之後會依據這些資料恢復現場
26 save(pc, sp)
27 _g_.syscallsp = sp
28 _g_.syscallpc = pc
29 casgstatus(_g_, _Grunning, _Gsyscall)
30 if _g_.syscallsp < _g_.stack.lo || _g_.stack.hi < _g_.syscallsp {
31 systemstack(func() {
32 print("entersyscall inconsistent ", hex(_g_.syscallsp), " [", hex(_g_.stack.lo), ",", hex(_g_.stack.hi), "]\n")
33 throw("entersyscall")
34 })
35 }
36
37 if atomic.Load(&sched.sysmonwait) != 0 {
38 systemstack(entersyscall_sysmon)
39 save(pc, sp)
40 }
41
42 if _g_.m.p.ptr().runSafePointFn != 0 {
43 // runSafePointFn may stack split if run on this stack
44 systemstack(runSafePointFn)
45 save(pc, sp)
46 }
47
48 _g_.m.syscalltick = _g_.m.p.ptr().syscalltick
49 _g_.sysblocktraced = true
50 _g_.m.mcache = nil
51 _g_.m.p.ptr().m = 0
52 atomic.Store(&_g_.m.p.ptr().status, _Psyscall)
53 if sched.gcwaiting != 0 {
54 systemstack(entersyscall_gcwait)
55 save(pc, sp)
56 }
57
58 _g_.m.locks--
59}
可以看到,進入 syscall 的 G 是鐵定不會被搶佔的。
▎exitsyscall
1// g 已經退出了 syscall
2// 需要準備讓 g 在 cpu 上重新執行
3// 這個函式只會在 syscall 庫中被呼叫,在 runtime 裡用的 low-level syscall
4// 不會用到
5// 不能有 write barrier,因為 P 可能已經被偷走了
6//go:nosplit
7//go:nowritebarrierrec
8func exitsyscall(dummy int32) {
9 _g_ := getg()
10
11 _g_.m.locks++ // see comment in entersyscall
12 if getcallersp(unsafe.Pointer(&dummy)) > _g_.syscallsp {
13 // throw calls print which may try to grow the stack,
14 // but throwsplit == true so the stack can not be grown;
15 // use systemstack to avoid that possible problem.
16 systemstack(func() {
17 throw("exitsyscall: syscall frame is no longer valid")
18 })
19 }
20
21 _g_.waitsince = 0
22 oldp := _g_.m.p.ptr()
23 if exitsyscallfast() {
24 if _g_.m.mcache == nil {
25 systemstack(func() {
26 throw("lost mcache")
27 })
28 }
29 // 目前有 p,可以執行
30 _g_.m.p.ptr().syscalltick++
31 // 把 g 的狀態修改回 running
32 casgstatus(_g_, _Gsyscall, _Grunning)
33
34 // 垃圾收集未在執行(因為我們這段邏輯在執行)
35 // 所以清理掉 syscallsp 是安全的
36 _g_.syscallsp = 0
37 _g_.m.locks--
38 if _g_.preempt {
39 // 防止在 newstack 中清理掉 preemption 標記
40 _g_.stackguard0 = stackPreempt
41 } else {
42 // 否則恢復在 entersyscall/entersyscallblock 中破壞掉的正常的 _StackGuard
43 _g_.stackguard0 = _g_.stack.lo + _StackGuard
44 }
45 _g_.throwsplit = false
46 return
47 }
48
49 _g_.sysexitticks = 0
50 _g_.m.locks--
51
52 // 呼叫 scheduler
53 mcall(exitsyscall0)
54
55 if _g_.m.mcache == nil {
56 systemstack(func() {
57 throw("lost mcache")
58 })
59 }
60
61 // 排程器返回了,所以我們可以清理掉在 syscall 期間為垃圾收集器
62 // 準備的 syscallsp 資訊了
63 // 需要一直等待到 gosched 返回,我們不確定垃圾收集器是不是在執行
64 _g_.syscallsp = 0
65 _g_.m.p.ptr().syscalltick++
66 _g_.throwsplit = false
67}
這裡還呼叫了 exitsyscallfast 和 exitsyscall0。
▎exitsyscallfast
1//go:nosplit
2func exitsyscallfast() bool {
3 _g_ := getg()
4
5 // Freezetheworld sets stopwait but does not retake P's.
6 if sched.stopwait == freezeStopWait {
7 _g_.m.mcache = nil
8 _g_.m.p = 0
9 return false
10 }
11
12 // Try to re-acquire the last P.
13 if _g_.m.p != 0 && _g_.m.p.ptr().status == _Psyscall && atomic.Cas(&_g_.m.p.ptr().status, _Psyscall, _Prunning) {
14 // There's a cpu for us, so we can run.
15 exitsyscallfast_reacquired()
16 return true
17 }
18
19 // Try to get any other idle P.
20 oldp := _g_.m.p.ptr()
21 _g_.m.mcache = nil
22 _g_.m.p = 0
23 if sched.pidle != 0 {
24 var ok bool
25 systemstack(func() {
26 ok = exitsyscallfast_pidle()
27 })
28 if ok {
29 return true
30 }
31 }
32 return false
33}
總之就是努力獲取一個 P 來執行 syscall 之後的邏輯。如果哪都沒有 P 可以給我們用,那就進入 exitsyscall0 了。
1mcall(exitsyscall0)
呼叫 exitsyscall0 時,會切換到 g0 棧。
▎exitsyscall0
1// 在 exitsyscallfast 中吃癟了,沒辦法,慢慢來
2// 把 g 的狀態設定成 runnable,先進 runq 等著
3//go:nowritebarrierrec
4func exitsyscall0(gp *g) {
5 _g_ := getg()
6
7 casgstatus(gp, _Gsyscall, _Grunnable)
8 dropg()
9 lock(&sched.lock)
10 _p_ := pidleget()
11 if _p_ == nil {
12 // 如果 P 被人偷跑了
13 globrunqput(gp)
14 } else if atomic.Load(&sched.sysmonwait) != 0 {
15 atomic.Store(&sched.sysmonwait, 0)
16 notewakeup(&sched.sysmonnote)
17 }
18 unlock(&sched.lock)
19 if _p_ != nil {
20 // 如果現在還有 p,那就用這個 p 執行
21 acquirep(_p_)
22 execute(gp, false) // Never returns.
23 }
24 if _g_.m.lockedg != 0 {
25 // 設定了 LockOsThread 的 g 的特殊邏輯
26 stoplockedm()
27 execute(gp, false) // Never returns.
28 }
29 stopm()
30 schedule() // Never returns.
31}
▎entersyscallblock
知道自己會 block,直接就把 p 交出來了。
1// 和 entersyscall 一樣,就是會直接把 P 給交出去,因為知道自己是會阻塞的
2//go:nosplit
3func entersyscallblock(dummy int32) {
4 _g_ := getg()
5
6 _g_.m.locks++ // see comment in entersyscall
7 _g_.throwsplit = true
8 _g_.stackguard0 = stackPreempt // see comment in entersyscall
9 _g_.m.syscalltick = _g_.m.p.ptr().syscalltick
10 _g_.sysblocktraced = true
11 _g_.m.p.ptr().syscalltick++
12
13 // Leave SP around for GC and traceback.
14 pc := getcallerpc()
15 sp := getcallersp(unsafe.Pointer(&dummy))
16 save(pc, sp)
17 _g_.syscallsp = _g_.sched.sp
18 _g_.syscallpc = _g_.sched.pc
19 if _g_.syscallsp < _g_.stack.lo || _g_.stack.hi < _g_.syscallsp {
20 sp1 := sp
21 sp2 := _g_.sched.sp
22 sp3 := _g_.syscallsp
23 systemstack(func() {
24 print("entersyscallblock inconsistent ", hex(sp1), " ", hex(sp2), " ", hex(sp3), " [", hex(_g_.stack.lo), ",", hex(_g_.stack.hi), "]\n")
25 throw("entersyscallblock")
26 })
27 }
28 casgstatus(_g_, _Grunning, _Gsyscall)
29 if _g_.syscallsp < _g_.stack.lo || _g_.stack.hi < _g_.syscallsp {
30 systemstack(func() {
31 print("entersyscallblock inconsistent ", hex(sp), " ", hex(_g_.sched.sp), " ", hex(_g_.syscallsp), " [", hex(_g_.stack.lo), ",", hex(_g_.stack.hi), "]\n")
32 throw("entersyscallblock")
33 })
34 }
35
36 // 直接呼叫 entersyscallblock_handoff 把 p 交出來了
37 systemstack(entersyscallblock_handoff)
38
39 // Resave for traceback during blocked call.
40 save(getcallerpc(), getcallersp(unsafe.Pointer(&dummy)))
41
42 _g_.m.locks--
43}
這個函式只有一個呼叫方 notesleepg,這裡就不再贅述了。
▎entersyscallblock_handoff
1func entersyscallblock_handoff() {
2 handoffp(releasep())
3}
比較簡單。
▎entersyscall_sysmon
1func entersyscall_sysmon() {
2 lock(&sched.lock)
3 if atomic.Load(&sched.sysmonwait) != 0 {
4 atomic.Store(&sched.sysmonwait, 0)
5 notewakeup(&sched.sysmonnote)
6 }
7 unlock(&sched.lock)
8}
▎entersyscall_gcwait
1func entersyscall_gcwait() {
2 _g_ := getg()
3 _p_ := _g_.m.p.ptr()
4
5 lock(&sched.lock)
6 if sched.stopwait > 0 && atomic.Cas(&_p_.status, _Psyscall, _Pgcstop) {
7 _p_.syscalltick++
8 if sched.stopwait--; sched.stopwait == 0 {
9 notewakeup(&sched.stopnote)
10 }
11 }
12 unlock(&sched.lock)
13}
▎總結
提供給使用者使用的系統呼叫,基本都會通知 runtime,以 entersyscall,exitsyscall 的形式來告訴 runtime,在這個 syscall 阻塞的時候,由 runtime 判斷是否把 P 騰出來給其它的 M 用。解繫結指的是把 M 和 P 之間解綁,如果繫結被解除,在 syscall 返回時,這個 g 會被放入執行佇列 runq 中。
同時 runtime 又保留了自己的特權,在執行自己的邏輯的時候,我的 P 不會被調走,這樣保證了在 Go 自己“底層”使用的這些 syscall 返回之後都能被立刻處理。
所以同樣是 epollwait,runtime 用的是不能被別人打斷的,你用的 syscall.EpollWait 那顯然是沒有這種特權的。
▎END
參考資料如下
曹春暉
滴滴 | 資深工程師
網名 Xargin,開源愛好者。活躍在 Github 和各種技術社群。熱衷於技術互懟。著有開源書 《Go 高階程式設計》
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69908606/viewspace-2642153/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Gopher China 2021 講師專訪 — 曹春暉Go
- 曹大談記憶體重排記憶體
- 【曹工雜談】說說Maven框架和外掛的契約Maven框架
- 談一談動態規劃和dfs動態規劃
- Go介面詳談Go
- 【曹工雜談】Maven原始碼除錯工程搭建Maven原始碼除錯
- 談一談PromisePromise
- 談一談 DDD
- 【曹工雜談】Maven IOC容器的下半場:Google GuiceMavenGoGUI
- 【曹工雜談】詳解Maven外掛除錯方法Maven除錯
- 談一談元件化元件化
- 談談RxSwift和狀態管理Swift
- 【曹工雜談】 2021在鵝廠幹了一年,我的一些感悟
- 談一談javascript非同步JavaScript非同步
- 【曹工雜談】Maven IOC 容器-- Guice內部有什麼MavenGUI
- 20190312_淺談go&java差異(一)GoJava
- 談談WhatsApp一年設計經歷和收穫APP
- 談談引用和Threadlocal的那些事thread
- 談談import和require的區別ImportUI
- 談談 "JS 和 設計泛型"JS泛型
- 談談mysql和redis的區別MySqlRedis
- 談談 JDK 和 SAPMachine 的關係JDKMac
- 談談浮動和清除浮動?
- 談談hive中join下on和whereHive
- 談談最近的一點感悟和之後的學習安排
- 和同事談談Flood Fill 演算法演算法
- 談談Hadoop MapReduce和Spark MR實現HadoopSpark
- 談談對MVC、MVP和MVVM的理解?MVCMVPMVVM
- 談談JavaScript中裝箱和拆箱JavaScript
- 談談JavaScript中的call、apply和bindJavaScriptAPP
- 一起來談談 Spring AOP!Spring
- 談談一些學習心得
- 談一談資料管理的格局
- 談一談資料中臺的原罪
- JVM探究(一)談談雙親委派機制和沙箱安全機制JVM
- 【曹工雜談】Maven和Tomcat能有啥聯絡呢,都穿打補丁的衣服嗎MavenTomcat
- 每日一問:淺談 onAttachedToWindow 和 onDetachedFromWindow
- 談談近況,談談自由職業,談談“金飯碗”