由golang nsenter会逃逸引发的胡思乱想
Posted 固执的马哲
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了由golang nsenter会逃逸引发的胡思乱想相关的知识,希望对你有一定的参考价值。
最近遇到golang直接调用nsenter会逃逸的坑,由此看了看并发和并行的概念(因为并发是golang的主要特色)
放一个视频:Google I/O 2012 - Go Concurrency Patterns的讲义:
https://v.youku.com/v_show/id_XNDI1NjgxMTAw.html
这个golang大神举了gopher的例子,放了一堆小鼹鼠的图,本来还清楚一点,看完感觉很懵逼,被大神搞得糊涂了。。
“简单点,说话的方式简单点”
并行是结果,并发是编程语言是否支持。
打个比喻,有些人多任务处理就会很好,有些人只能单任务处理,那么可以说第一个人具备了并发能力,当资源够的时候(多核),就可以实现并发(多任务处理)
比方说,你早上要干3件事:
洗漱(洗脸、刷牙、打扮);记为A任务
做饭(煎蛋、煮面、打豆浆);记为B任务
穿衣服(穿鞋、穿上衣、穿裤子);记为C任务
假如你是处女座,必须要按照洗漱、做饭、穿衣服(A-- >B--->C)这个顺序来,那么你就是顺序执行的(处女座受到万点暴击)
假如你是双子座,可以分裂出多个人格,你可以先洗脸,再穿衣服,再做饭,A--->C--->B,也可以B--->C--->A,也可以C--->B---->A,总之这3个事情你可以打乱执行,那么,当你突然有一个平行宇宙的兄弟时,你俩就可以独立的干两件事啊,比如你洗脸,他做饭,你更衣来他沐浴。。。
跑偏了,其实你所具有的同时干多个事情的能力,就是并发能力!是不是跟goroutine很像?但假如你没有那个平行宇宙的兄弟,你还是得一个一个来干,即便是不按顺序。这说明,并行只是一个结果,假如没有能力,理论上就无法并行;具备并发能力,理论上可以并行。
再假如,你特牛逼,可以按照更细粒度的任务划分,比如A1--->C2--->B3--->B1---->A2;洗脸打豆浆穿裤子XXXX等
那么,并发的效率就更高了,golang也是这么做的。很多lib接口,其实是起了一个goroutine啊,亲。
这样有个问题咯,我是并发能力特别强
我可以先打上豆浆,再洗脸(豆浆机可以视为一个cpu了),但我不能边洗脸边穿鞋,是吧,跟什么有关?跟我的手的数量有关,跟豆浆机的数量,跟天然气灶的数量有关,有的时候我想并发,也是并发不了的。
这就是golang中GOMAXPROCS == cpu core的原因,只有这么多core,那就做个限制吧,别整太多了,再来三杯,是真搞不定啊,500~~
这两个图,就是golang的调度策略,具体可以看runtime包里的runtime2.go的M、P、G3个struct的实现,和proc.go里的调度函数schedule()的实现
再打个比喻,G是项目,P是PL,M是奋斗者
大概的流程是,我要启动一个goroutine(项目),那么我去找M奋斗者,来干活
M得匹配到一个P(小头目)才行,作为全局的限制
P上挂了一堆G,M轮流翻牌子,咱菊花厂的奋斗者需要有多任务处理能力
而调度函数(LM?误。。),每一个小周期,会查一遍小头目(P)的状态(runqget函数),从P挂的G队列中,取出来一个要执行的G,让M干活。
如果没有G了,那么赶紧去找新的G(findrunnable函数),从全局的队列(golbrunqget)里找,从异步调用结束的队列(netpoll)里找,从其他的P的队列里偷(sunqsteal)
毕竟每个项目组不能压榨太多嘛,要人尽其用...
这里有2个特殊情况:
1.当程序发起一个channal call,程序可能会阻塞,但不会阻塞M,G的状态会设置为waiting,M继续执行其他的G。当G的调用完成,会有一个可用的M继续执行它
可以这么简单理解下:一个奋斗者M在定位问题,结果发现,靠,这个外部接口的数据不对啊,得让别的模块继续定位,好吧,我手上还有好几个问题等着看,赶紧先把这个问题甩出去,哪怕一会还是我们的问题,那一会再说了。反正一会再找过来也许我还在会议中,那就别的兄弟给他看咯。
2.当程序发生system call时,M会阻塞,同时唤醒(创建)一个新的M继续执行其他的G
这么理解:也是在定位问题,发现,靠这个涉及要一线客户了,必须出差搞定,那奋斗者M就要跟着G一起处于阻塞状态,这时候P手里还有活啊,所以搞个新的奋斗者吧
到此,golang 执行nsenter,正是触发了第2种情况
即golang进入到一个进程的namespace当中,然后调用了system call,本来你看到的可能没新起一个goroutine,但golang的lib可能给你做了。
那么这个M,kernel thread,就会clone一个新的thread,但要注意,这个新thread是在新的ns当中。新thread也许去干别的goroutine去了,并不影响当前的程序,
但这已经逃逸了,新thread再去执行别的golang,都是在那个ns当中,那么获取的数据,执行的结果,如果涉及到ns的区别,就会有问题。
而GC回收M,需要等待2分钟,不是立即回收的,就会有问题
解决办法:用nsenter的C库,看一下说明,就知道为啥能搞定了,是在runtime之前,避免了上述问题。
## nsenter
The `nsenter` package registers a special init constructor that is called before
the Go runtime has a chance to boot. This provides us the ability to `setns` on
existing namespaces and avoid the issues that the Go runtime has with multiple
threads. This constructor will be called if this package is registered,
imported, in your go application.
The `nsenter` package will `import "C"` and it uses [cgo](https://golang.org/cmd/cgo/)
package. In cgo, if the import of "C" is immediately preceded by a comment, that comment,
called the preamble, is used as a header when compiling the C parts of the package.
So every time we import package `nsenter`, the C code function `nsexec()` would be
called. And package `nsenter` is now only imported in Docker execdriver, so every time
before we call `execdriver.Exec()`, that C code would run.
`nsexec()` will first check the environment variable `_LIBCONTAINER_INITPID`
which will give the process of the container that should be joined. Namespaces fd will
be found from `/proc/[pid]/ns` and set by `setns` syscall.
And then get the pipe number from `_LIBCONTAINER_INITPIPE`, error message could
be transferred through it. If tty is added, `_LIBCONTAINER_CONSOLE_PATH` will
have value and start a console for output.
Finally, `nsexec()` will clone a child process , exit the parent process and let
the Go runtime take over.
以上是关于由golang nsenter会逃逸引发的胡思乱想的主要内容,如果未能解决你的问题,请参考以下文章