背景

目的:充分利用cpu并发执行任务 发展:

进程作为调度单位:

进程资源:

注:可以通过研究PCB进程控制块的现场信息分析

  1. 独立的内存空间(页表、TLB快表)
  2. 堆栈空间(堆栈指针)
  3. 运行的代码区,数
  4. 处理器现场: 除pc程序计数器、sp栈顶指针之外的寄存器
  5. 设备占用
  6. 打开的文件描述符
  • 工作空间1、2
  • 干活的任务清单和初始数据3
  • 暂停干活时的状态 4.
  • 干活的占用工具 5,6

进程切换代价:

保存进程上下文,恢复进程上下文

  1. 切换页表全局目录
  2. 切换内核态堆栈
  3. 切换硬件上下文(进程恢复前,必须装入寄存器的数据统称为硬件上下文)
    • ip(instruction pointer):指向当前执行指令的下一条指令
    • bp(base pointer): 用于存放执行中的函数对应的栈帧的栈底地址
    • sp(stack poinger): 用于存放执行中的函数对应的栈帧的栈顶地址
    • cr3:页目录基址寄存器,保存页目录表的物理地址 …..
  4. 刷新TLB(进程切换旧TLB必无法命中)
  5. 系统调度器的代码执行

缺点:进程切换代价大

线程作为调度单位

线程占用资源

单单指的是内核级线程 共享:内存、代码段、数据段、(设备、打开文件) 独占:栈空间

切换代价

  1. 切换内核态栈指针
  2. 切换硬件上下文(进程恢复前,必须装入寄存器的数据统称为硬件上下文)

与之进程相比没有了页表的删除

协程作为调度单位

协程也可以叫做用户级线程,必须绑定内核级线程才能工作。 绑定方式可以分为 内核级:用户级

  • 1:1 一对一绑定,需要创建的内核级线程较多
  • 1:N 一对多绑定,N个协程中阻塞一个,其他协程都阻塞算法
  • M:N 多对多绑定,设计复杂,但是灵活golang的GPM就是这样设计的

协程切换过程

不涉及绑定关系接触的情况下,调度是在用户态下就可以实现

GPM模型

本质上就是M:N的绑定模式

初始化

func schedinit() {
    // 初始化栈空间复用管理列表
   	stackinit()
    // 初始化当前 M
	mcommoninit(gp.m, -1)
    // 创建GOMAXPro数量的P
    procresize(procs)
}

创建P的过程

// 可以调整p的数量,初始化来说肯定是增多
func procresize(nprocs int32) *p {
    // 判断操作略 将allp 数组调整为最新的数量

    // 如果P数量变多,初始化新的p
	for i := old; i < nprocs; i++ {
		pp := allp[i]
		if pp == nil {
			pp = new(p) // P数量增多,需要新建p
		}
		pp.init(i) // 将所有p初始化
		atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
	}

    // 如果当前P的id小于新容量即当前P之后会被清理掉,那当前P改为allp[0]
    if gp.m.p != 0 && gp.m.p.ptr().id < nprocs {
            // continue to use the current P
            gp.m.p.ptr().status = _Prunning
            gp.m.p.ptr().mcache.prepareForSweep()
        } else {
            // release the current P and acquire allp[0].
            //
            // We must do this before destroying our current P
            // because p.destroy itself has write barriers, so we
            // need to do that from a valid P.
            if gp.m.p != 0 {
                if trace.enabled {
                    // Pretend that we were descheduled
                    // and then scheduled again to keep
                    // the trace sane.
                    traceGoSched()
                    traceProcStop(gp.m.p.ptr())
                }
                gp.m.p.ptr().m = 0
            }
            gp.m.p = 0
            pp := allp[0]
            pp.m = 0
            pp.status = _Pidle
            acquirep(pp)
            if trace.enabled {
                traceGoStart()
            }
        }



    // 如果我们是缩减p数量的话,那清除多余P,放入复用队列
	for i := nprocs; i < old; i++ {
		pp := allp[i]
		pp.destroy() // 还回mcache
		// can't free P itself because it can be referenced by an M in syscall
	}
    // 除当前p外,所有没有G队列的P放到P空闲队列里面
    for i := nprocs - 1; i >= 0; i-- {
		pp := allp[i]
		if gp.m.p.ptr() == pp {
			continue
		}
		pp.status = _Pidle
		if runqempty(pp) {
			pidleput(pp, now)
		} else {
			pp.m.set(mget())
			pp.link.set(runnablePs)
			runnablePs = pp
		}
	}
    // 返回可运行P链表的头结点,第一个可运行P
    return runnablePs
}

初始化P的过程

// P清空或重建,进行初始化
func (pp *p) init(id int32) {

    // 分配mcache用于运行时分配对象内存
	if pp.mcache == nil {
		if id == 0 {
			if mcache0 == nil {
				throw("missing mcache?")
			}
			// Use the bootstrap mcache0. Only one P will get
			// mcache0: the one with ID 0.
			pp.mcache = mcache0
		} else {
			pp.mcache = allocmcache()
		}
	}
}

创建G的过程

go func()调用newproc

// Create a new g running fn.
// Put it on the queue of g's waiting to run.
// The compiler turns a go statement into a call to this.
func newproc(fn *funcval) {
	gp := getg()
    // 获取当前协程pc计数器,用于创建完继续执行父协程
	pc := getcallerpc()
    // systemstack切换到m的g0系统栈
	systemstack(func() {
        // newproc1执行具体的创建工作
		newg := newproc1(fn, gp, pc)
        // getg返回当前协程(即父线程协程)
		pp := getg().m.p.ptr()
        // 将新创建子协程g放到当前p上
		runqput(pp, newg, true)
        // 
		if mainStarted {
			wakep()
		}
	})
}
  1. 父协程栈上存储 子协程函数参数、地址、参数大小的信息
  2. g0系统栈进行子协程的创建工作
  3. 新创建的子协程放到当前P执行队列里面

Mysql表空间存储结构 如何存储数据? 逻辑上管理–段 一个段有区链表 未用的区 有碎片的区() 碎片已经满的区 从表空间那个资源链表去获取分配

表空间 组(256个区) 区(64个页 1M) 页(16kb)

一个索引对应两个段 方便遍历,叶子、非叶子数据分开存放 叶子段 非叶子段

第一个组记录表索引两个段信息 段所在的第一个区的位置

目的:管理里面的区,分配给段 内容: 组记录256每个区的信息 区是否未分配 区空闲的碎片 区是否已分配给某个段

表空间

目的:分配区给段 表空间也会有3个状态的区链表 未分配区 有空闲碎片的区 无空闲碎片的区

段的结构

目的: 记录当前段数据所在的区 INode Entry

  • 数据所在区的链表 空闲 空闲有碎片页 空闲无碎片页面 使用的碎片页

Mysql Undo日志逻辑

  1. Insert undo日志 操作: 插入新纪录 记录内容: 插入记录的主键列数据 回滚操作: 将主键对应的记录删除 提交操作:

  2. delete 操作: 删除记录,将指定记录标记为删除 记录内容: 删除记录的主键列数据和索引列数据 回滚操作: 去掉delete标记 提交操作: 将记录由正常链表移动到垃圾链表 正式删除,之后可复用空间

  3. update undo日志

    • 更新主键 操作: 记录两条undo日志。 第一条 删除 旧记录标记删除,不实际删除; 记录信息:被删除记录的主键列数据 第二条 插入 新增一条记录插入到聚簇索引中 记录内容:新增记录的主键列数据
    • 不更新主键
      • 被更新记录占用空间不变,原地修改记录 操作: 直接原纪录位置修改对应列的值 记录内容: 主键列数据和索引列数据(更新后),被更新列旧数据 回滚操作: 根据主键定位到更新行,用旧数据恢复被更新列 提交操作:
      • 占用空间改变 操作: 真实删除原记录,插入一条新纪录 记录内容: 主键列数据和索引列数据(更新后),被更新列旧数据 回滚操作: 根据主键删除插入新纪录,用旧数据将删除原纪录重新插入 提交操作:

Be Water, My Friend.