0
点赞
收藏
分享

微信扫一扫

K8S日志收集方案-EFK部署

一.协程的引入

1.通过案例文章引入并发,协程概念

2.内核线程态和用户态

(1).线程切换方面

1).内核态线程切换

2).用户态线程切换

实现用户态线程的方式
typedef struct ucontext
{
 unsigned long int uc_flags;
 struct ucontext *uc_link;
 stack_t uc_stack;
 mcontext_t uc_mcontext;
 __sigset_t uc_sigmask;
} ucontext_t;
  • 用户态线程运行时各寄存器的值,其中就包括eip和esp,eip指向代码运行到何处,esp指向栈指针指向何处
  • 用户态线程使用的栈信息,当某个进程中有多个用户态线程时,各线程使用独立的栈,以使彼此互不影响
int getcontext(ucontext_t *ucp);
int setcontext(const ucontext_t *ucp);
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);
int swapcontext(ucontext_t *oucp, ucontext_t *ucp);
  • getcontext用于获取当前的用户上下文,并保存进ucp中
  • setcontext用于将ucp设置为当前上下文
  • makecontext用于修改getcontext获得的用户上下文ucp
  • swapcontext将当前的用户上下文保存到oucp,将ucp指向的用户上下文设置为当前上下文

(2).竞态方面

 (3).调度器

二.协程讲解

1.协程概念

2.GMP 模型

(1).概念

  • G
    • 代表Go协程Goroutine,包含自己的执行栈信息、状态、任务函数、程序计数器等信息
    • G的数量无限制,理论上只受内存的影响,创建一个G的初始栈大小为2-4K,配置一般的机器也能简简单单开启数十万个 Goroutine ,而且Go语言在 G 退出的时候还会把 G 清理之后放到 P 本地或者全局的闲置列表 gFree 中以便复用
  • M
    • Machine,对操作系统线程(OS thread)的封装,代表操作系统内核级线程
    • G中的代码就是在M上运行的
    • 一个 M 对应一个线程
    • 想要在CPU上执行代码必须有线程,通过系统调用 clone 创建,M在绑定有效的 P 后,进入一个调度循环,而调度循环的机制大致是从 P 的本地运行队列以及全局队列中获取 G,切换到 G 的执行栈上并执行 G 的函数,调用 goexit 做清理工作并回到 M,如此反复
    • M 并不保留 G 状态这是 G 可以跨 M 调度的基础,M的数量有限制,默认数量限制是 10000,但是内核很难支持这么多的线程数,所以这个限制可以忽略
    • 可以通过 debug.SetMaxThreads() 方法进行设置,如果有M空闲,那么就会回收或者睡眠
  • P
    • Processor,指虚拟处理器(调度器),M执行G所需要的资源和上下文,主要用途是用来执行 Goroutine,维护一个 Goroutine 队列,同时还有一个全局队列,它是一个联通M与G的桥梁
    • 每一个运行的 M 都必须绑定一个 P,就像线程必须在一个 cpu 核上执行一样,这样才能让 P 的 runq 中的 G 真正运行起来
    • P的数量决定了系统内最大可并行的G的数量P的数量受本机的CPU核数影响,可通过环境变量GOMAXPROCS或在runtime.GOMAXPROCS()来设置,默认为CPU核心数
    • 线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列,M运行G,G执行之后,M会从P获取下一个G,不断重复下去
  • Sched:代表调度器,维护 M 和 G 的全局队列和状态信息

(2).调度流程 

(3).核心代码分析 

//src/runtime/runtime2.go
type g struct {
    goid    int64 // 唯一的goroutine的ID
    sched gobuf // goroutine切换时,用于保存g的上下文
    stack stack // 栈
    atomicstatus
    gopc        // pc of go statement that created this goroutine
    startpc    uintptr // pc of goroutine function
    ...
}

type p struct {
    lock mutex
    id          int32
    status      uint32 // one of pidle/prunning/...

    // Queue of runnable goroutines. Accessed without lock.
    runqhead uint32 // 本地队列队头
    runqtail uint32 // 本地队列队尾
    runq     [256]guintptr // 本地队列,大小256的数组,数组往往会被都读入到缓存中,对缓存友好,效率较高
    runnext guintptr // 下一个优先执行的goroutine(一定是最后生产出来的),为了实现局部性原理,runnext中的G永远会被最先调度执行
    ... 
}

type m struct {
    g0            *g     // 一类特殊的调度协程,不用于执行用户函数,负责执行 g 之间的切换调度. 与 m 的关系为 1:1;每个M都有一个自己的G0,不指向任何可执行的函数,在调度或系统调用时,M会切换到G0,使用G0的栈空间来调度
    curg          *g    // 当前正在执行的G
    mOS
    ... 
}

type schedt struct {
    ...
    runq     gQueue // 全局队列,链表(长度无限制)
    runqsize int32  // 全局队列长度
    ...
}
  • g结构体拿几个重要的变量来进行说明

const(
  _Gidle = itoa // 0  为协程开始创建时的状态,此时尚未初始化完成;
  _Grunnable // 1  协程在待执行队列中,等待被执行;
  _Grunning // 2  协程正在执行,同一时刻一个 p 中只有一个 g 处于此状态;
  _Gsyscall // 3  协程正在执行系统调用;
  _Gwaiting // 4  协程处于挂起态,需要等待被唤醒. gc、channel 通信或者锁操作时经常会进入这种状态;
  _Gdead // 6  协程刚初始化完成或者已经被销毁,会处于此状态
  _Gcopystack // 8  协程正在栈扩容流程中;
  _Gpreempted // 9  协程被抢占后的状态
)
  •  m结构体拿几个重要的变量来进行说明:

  •  p结构体拿几个重要的变量来进行说明:

  • schedt拿几个重要变量来进行说明:
type schedt struct {
    // ...
    lock mutex
    // ...
    runq     gQueue
    runqsize int32
    // ...
}

(4).调度器

抢占式调度
  • M 注册SIGURG信号(该信号其他地方用的很少)的处理函数sighandler
  • GC工作(GC工作意味着某些线程停了),然后sysmon启动后会间隔性的进行监控,最长间隔10ms,最短间隔20us,如果发现某协程独占P超过10ms,会给M发送抢占信号
  • M 收到信号后,内核执行sighandler函数把当前协程的状态从_Grunning正在执行改成 _Grunnable可执行,抢占的协程放到全局队列里,M继续寻找其他goroutine来运行
  • 被抢占的G再次调度过来执行时,会继续原来的执行流
    • 抢占分为_Prunning_Psyscall
      • _Psyscall抢占通常是由于阻塞性系统调用引起的,比如磁盘io、cgo
      • _Prunning抢占通常是由于一些类似死循环的计算逻辑引起的

调度器的设计思想 
调度流程 

调度器生命周期

package main
import "fmt"
func main() {
    fmt.Println("Hello world")
}
 Go调度器调度场景过程全解析
场景 1
 场景 2
 场景 3

场景 4

场景 5

 场景 6 

场景 7

场景 8 

 场景 9

场景 10
 场景 11

举报

相关推荐

0 条评论