go语言初始化过程
这里解释一下本地线程存储。比如说每个goroutine都有自己的控制信息,这些信息是存放在一个结构体G中。假设我们有一个全局变量g是结构体G的指针,我们希望只有唯一的全局变量g,而不是g0,g1,g2…但是我们又希望不同goroutine去访问这个全局变量g得到的并不是同一个东西,它们得到的是相对自己线程的结构体G,这种情况下就需要本地线程存储。g确实是一个全局变量,却在不同线程有多份不同的副本。每个goroutine去访问g时,都是对应到自己线程的这一份副本。针对goroutine部分,
1 | CLD // convention is D is always left cleared |
go关键字的调用协议:先将参数进栈,再被调函数指针和参数字节数进栈,接着调用runtime.newproc函数。所以这里其实就是新开个goroutine执行runtime.main
1 | 找到一个等待运行的g |
1 | func M() { |
退出goroutine
1 | func exitsyscall() { |
内存管理
在多线程方面,很自然的做法就是每条线程都有自己的本地的内存,然后有一个全局的分配链,当某个线程中内存不足后就向全局分配链中申请内存。这样就避免了多线程同时访问共享变量时的加锁。 在避免内存碎片方面,大块内存直接按页为单位分配,小块内存会切成各种不同的固定大小的块,申请做任意字节内存时会向上取整到最接近的块,将整块分配给申请者以避免随意切割。
分配器的数据结构包括:
- FixAlloc: 固定大小(128kB)的对象的空闲链分配器,被分配器用于管理存储
- MHeap: 分配堆,按页的粒度进行管理(4kB),用于直接分配较大(>32kB)的内存空间
- MSpan: 一些由MHeap管理的页
- MCentral: 对于给定尺寸类别的共享的free list
- MCache: 用于小对象的每M一个的cache
我们可以将Go语言的内存管理看成一个两级的内存管理结构,MHeap和MCache。
非阻塞io
底层非阻塞io是如何实现的呢?简单地说,所有文件描述符都被设置成非阻塞的,某个goroutine进行io操作,读或者写文件描述符,如果此刻io还没准备好,则这个goroutine会被放到系统的等待队列中,这个goroutine失去了运行权,但并不是真正的整个系统“阻塞”于系统调用。
后台还有一个poller会不停地进行poll,所有的文件描述符都被添加到了这个poller中的,当某个时刻一个文件描述符准备好了,poller就会唤醒之前因它而阻塞的goroutine,于是goroutine重新运行起来