#0 runtime.gopark (unlockf={void (runtime.g *, void *, bool *)} 0x0, lock=0x7fffd00a0700, reason=2 '\002', traceEv=27 '\033', traceskip=5) at /home/dou/work/go/src/runtime/proc.go:349 #1 0x000000000042e5fe in runtime.netpollblock (pd=<optimized out>, mode=<optimized out>, waitio=<optimized out>) at /home/dou/work/go/src/runtime/netpoll.go:445 #2 0x000000000045c489 in internal/poll.runtime_pollWait (pd=<optimized out>, mode=140736683706112) at /home/dou/work/go/src/runtime/netpoll.go:229 #3 0x000000000048b0b2 in internal/poll.(*pollDesc).wait (pd=<optimized out>, mode=140736683706112, isFile=2) at /home/dou/work/go/src/internal/poll/fd_poll_runtime.go:84 #4 0x000000000048ba1a in internal/poll.(*pollDesc).waitRead (isFile=2, pd=<optimized out>) at /home/dou/work/go/src/internal/poll/fd_poll_runtime.go:89 #5 internal/poll.(*FD).Read (fd=0xc0001a8000, p=..., ~r1=<optimized out>, ~r2=...) at /home/dou/work/go/src/internal/poll/fd_unix.go:167 #6 0x00000000004ac629 in net.(*netFD).Read (fd=0xc0001a8000, p=...) at /home/dou/work/go/src/net/fd_posix.go:56 #7 0x00000000004b6965 in net.(*conn).Read (c=0xc000094008, b=...) at /home/dou/work/go/src/net/net.go:183 #8 0x00000000004c1d2e in net.(*TCPConn).Read (b=...) at <autogenerated>:1
补充一点:gopark 的核心逻辑是,切换到 g0 栈,执行 park_m。在 park_m (g0 栈)中再把当前 G 挂起。
恢复执行
上一步的 park_m 函数,除了会挂起当前 G,另外一个重要的任务就是执行 schedule 函数,挑一个新的 G 开始运行。
在上一篇介绍过,挑选一个新的 G 的过程中,就有 检查 netpoll 这一步。 我们可以在 gdb 中看到如下的调用栈:
1 2 3 4 5
#0 runtime.netpollready (toRun=0x7fffcaffc6c8, pd=0x7fffd00606d8, mode=119) at /home/dou/work/go/src/runtime/netpoll.go:372 #1 0x000000000042f057 in runtime.netpoll (delay=<optimized out>) at /home/dou/work/go/src/runtime/netpoll_epoll.go:176 #2 0x000000000043abd3 in runtime.findrunnable () at /home/dou/work/go/src/runtime/proc.go:2947 #3 0x000000000043bdd9 in runtime.schedule () at /home/dou/work/go/src/runtime/proc.go:3367 #4 0x000000000043c32d in runtime.park_m (gp=0xc000202000) at /home/dou/work/go/src/runtime/proc.go:3516
netpoll 中的核心逻辑即是调用 epoll_wait,获取一批已经准备就绪的 events,恢复这批 G 到 runable 状态,并运行第一个 G。
总结
对于非阻塞网络的实现,核心点是 EAGAIN 和 epoll_wait。 go 语言把这个细节隐藏到了语言/标准库内部,确实很大程度的降低了程序员的心智负担。
git clone git@github.com:tmate-io/tmate-ssh-server.git cd tmate-ssh-server ./create_keys.sh
会在当前目录生成 keys 目录,并且会得到如下的输出,这些可以先记录下来,后续会用到。
1 2 3 4
set -g tmate-server-host localhost set -g tmate-server-port 22 set -g tmate-server-rsa-fingerprint SHA256:JOhvYoiBO0kVltwqKHTgKVCfvyajWV/cGwNXSyTNRcE set -g tmate-server-ed25519-fingerprint SHA256:+N/UZ8BNfTVNoOQjH4yEe0GoTFpapu/I0G4c6RI7HWo
配置 .tmate.conf 把客户端部署第一步的输出,写入到本机的 ~/.tmate.conf 文件。注意其中的 host 和 port 需要改成自助部署的,比如我这里是这样的:
1 2 3 4
set -g tmate-server-host tmate.uncledou.site set -g tmate-server-port 8022 set -g tmate-server-rsa-fingerprint SHA256:JOhvYoiBO0kVltwqKHTgKVCfvyajWV/cGwNXSyTNRcE set -g tmate-server-ed25519-fingerprint SHA256:+N/UZ8BNfTVNoOQjH4yEe0GoTFpapu/I0G4c6RI7HWo
开始飞吧
至此 tmate 客户端就安装配置好了,执行 tmate 命令就可以得到类似的输出:
1 2 3 4 5 6 7 8
Tip: if you wish to use tmate only for remote access, run: tmate -F [0/0] To see the following messages again, run in a tmate session: tmate show-messages Press <q> or <ctrl-c> to continue --------------------------------------------------------------------- Connecting to tmate.uncledou.site... Note: clear your terminal before sharing readonly access ssh session read only: ssh -p8022 ro-RyrBkE6v5rAhpqP6FN6mCWgm2@tmate.uncledou.site ssh session: ssh -p8022 XtydPFjTpbaz8eXxB77RGZeNZ@tmate.uncledou.site
Lua 虚拟机的实现中,有这样一个 for (;;) 无限循环(在 luaV_execute 函数中)。 其核心工作跟物理 CPU 类似,读取 pc 地址的字节码(同时 pc 地址 +1),解析操作指令,然后根据操作指令,以及对应的操作数,执行字节码。 例如上面我们解释过的 MOVE 字节码指令,也就是在这个循环中执行的。其他的字节码指令,也是类似的套路来完成执行的。
pc 指针也只是一个 Lua 虚拟机位置的内存地址,并不是物理 CPU 中的 pc 寄存器。
函数
几个基本点:
Lua 函数,可以简单的理解为一堆字节码的集合。
Lua 虚拟机里,也有栈帧的,每个栈帧实际就是一个 C struct 描述的内存结构体。
执行一个 Lua 函数,也就是执行其对应的字节码。
总结
Lua 这种带虚拟机的语言,逻辑上跟物理 CPU 是很类似的。生成字节码,然后由虚拟机来具体执行字节码。
只是多了一层抽象虚拟,字节码解释执行的效率,是比不过机器指令的。
物理内存的读写速度,比物理寄存器要慢几倍甚至几百倍(取决于是否命中 CPU cache)。 所以 Lua 的虚拟寄存器读写,也是比真实寄存器读写要慢很多的。