一、用户态、内核态、系统调用、中断 现代cpu通常有多种特权级别,一般来说特权级总共有4个,编号从Ring 0(最高特权)到Ring 3(最低特权),在Linux上之用到Ring 0和RIng 3,用户态对应Ring 3,内核态对应Ring 0
系统调用,例如操作文件、进行网络通讯或者申请内存资源等
中断一般有两个属性,一个是中断号,一个是中断处理程序。不同的中断有不同的中断号,每个中断号都对应了一个中断处理程序。在内核中有一个叫中断向量表的数组来映射这个关系。当中断到来时,cpu会暂停正在执行的代码,根据中断号去中断向量表找出对应的中断处理程序并调用。中断处理程序执行完成后,会继续执行之前的代码。
二、多路复用技术(三种常见的实现方式:poll、select、epoll,由于epoll是最新技术,性能要比其他两个老技术好太多,因此现在基本只会使用epoll)有了这个东西,协程才有用武之地
1.select的fd_set是一个静态的数组,所以支持的文件描述符数量有限(默认为1024),而 poll和epoll 传入的相当于一个动态数组(指针 + 元素个数),所以支持的文件描述符数量没有限制
2.如果在 select/poll 调用之前,如果没有事件发生。select/poll将阻塞,进程休眠,知道超时或者被中断。内核将 fd_set/ pollfd 结果拷贝到用户态,同时用户态调用返回。用户态遍历所有监控的文件描述符,检查返回结果
3.对于epoll,内核使用红黑树来快速的添加删除需要监控的文件描述符,同时基于事件驱动,文件描述符 fd 有事件发生时,内核的回调函数会将该 fd 加入到内核维护的 ready list 内。所以调用 epoll_ctl 时,内核只需要去检查 ready list 并拷贝结果到用户态即可
水平触发通知:如果文件描述符上可以非阻塞地执行I/O系统调用,此时认为它已经就绪,触发通知。 边沿触发通知:如果文件描述符自上次状态检查以来有了新的I/O活动(比如新的输入),此时需要触发通知。
三、GPM模型
M:一个用户空间线程,同时对应一个内核线程,类似posix pthread P:代表运行的上下文环境, 有个一个runqueue(里面存放着goroutine) G:goroutine,即协程
P的数量由GOMAXPROCS()来设置,默认为CPU的核数
当一个OS线程M0陷入阻塞(系统调用)时,绑定它的P会转而运行M1,M1可能是正被创建,或者从线程缓存中取出。
当MO返回时,它必须尝试取得一个P来运行goroutine,如果没有拿到的话,它就把goroutine放在一个global runqueue里,然后自己睡眠(放入线程缓存里)。
P也会周期性的检查global runqueue并运行其中的goroutine
P何时创建:在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P。
M何时创建:没有足够的M来关联P并运行其中的可运行的G。比如所有的M此时都阻塞住了,而P中还有很多就绪任务,就会去寻找空闲的M,而没有空闲的,就会去创建新的M。
M的最大数量,默认10000,也可以通过runtime/debug中的SetMaxThreads函数去重新设置
Gwaitting:内部channel或者mutex阻塞,会触发P runqueue的轮换 Gsyscall:调用了syscall(没有事件准备的Nonblocking IO除外),会触发P和M的解绑
GoV1.3- 普通标记清除法,整体过程需要启动STW,效率极低。 GoV1.5- 三色标记法, 堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要STW),效率普通 GoV1.8-三色标记法,混合写屏障机制, 栈空间不启动,堆空间启动。整个过程几乎不需要STW,效率较高。
由于没有分代回收,因此对于会产生小对象的case,尽量用sync.Pool
详细文章教程: https://studygolang.com/articles/27243?fr=sidebar