背景
目的:充分利用cpu并发执行任务 发展:
进程作为调度单位:
进程资源:
注:可以通过研究PCB进程控制块的现场信息分析
- 独立的内存空间(页表、TLB快表)
- 堆栈空间(堆栈指针)
- 运行的代码区,数
- 处理器现场: 除pc程序计数器、sp栈顶指针之外的寄存器
- 设备占用
- 打开的文件描述符
- 工作空间1、2
- 干活的任务清单和初始数据3
- 暂停干活时的状态 4.
- 干活的占用工具 5,6
进程切换代价:
保存进程上下文,恢复进程上下文
- 切换页表全局目录
- 切换内核态堆栈
- 切换硬件上下文(进程恢复前,必须装入寄存器的数据统称为硬件上下文)
- ip(instruction pointer):指向当前执行指令的下一条指令
- bp(base pointer): 用于存放执行中的函数对应的栈帧的栈底地址
- sp(stack poinger): 用于存放执行中的函数对应的栈帧的栈顶地址
- cr3:页目录基址寄存器,保存页目录表的物理地址 …..
- 刷新TLB(进程切换旧TLB必无法命中)
- 系统调度器的代码执行
缺点:进程切换代价大
线程作为调度单位
线程占用资源
单单指的是内核级线程 共享:内存、代码段、数据段、(设备、打开文件) 独占:栈空间
切换代价
- 切换内核态栈指针
- 切换硬件上下文(进程恢复前,必须装入寄存器的数据统称为硬件上下文)
与之进程相比没有了页表的删除
协程作为调度单位
协程也可以叫做用户级线程,必须绑定内核级线程才能工作。 绑定方式可以分为 内核级:用户级
- 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()
}
})
}
- 父协程栈上存储 子协程函数参数、地址、参数大小的信息
- g0系统栈进行子协程的创建工作
- 新创建的子协程放到当前P执行队列里面
Mysql表空间存储结构 如何存储数据? 逻辑上管理–段 一个段有区链表 未用的区 有碎片的区() 碎片已经满的区 从表空间那个资源链表去获取分配
表空间 组(256个区) 区(64个页 1M) 页(16kb)
一个索引对应两个段 方便遍历,叶子、非叶子数据分开存放 叶子段 非叶子段
第一个组记录表索引两个段信息 段所在的第一个区的位置
组
目的:管理里面的区,分配给段 内容: 组记录256每个区的信息 区是否未分配 区空闲的碎片 区是否已分配给某个段
表空间
目的:分配区给段 表空间也会有3个状态的区链表 未分配区 有空闲碎片的区 无空闲碎片的区
段的结构
目的: 记录当前段数据所在的区 INode Entry
- 数据所在区的链表 空闲 空闲有碎片页 空闲无碎片页面 使用的碎片页
Mysql Undo日志逻辑
-
Insert undo日志 操作: 插入新纪录 记录内容: 插入记录的主键列数据 回滚操作: 将主键对应的记录删除 提交操作:
-
delete 操作: 删除记录,将指定记录标记为删除 记录内容: 删除记录的主键列数据和索引列数据 回滚操作: 去掉delete标记 提交操作: 将记录由正常链表移动到垃圾链表 正式删除,之后可复用空间
-
update undo日志
- 更新主键 操作: 记录两条undo日志。 第一条 删除 旧记录标记删除,不实际删除; 记录信息:被删除记录的主键列数据 第二条 插入 新增一条记录插入到聚簇索引中 记录内容:新增记录的主键列数据
- 不更新主键
- 被更新记录占用空间不变,原地修改记录 操作: 直接原纪录位置修改对应列的值 记录内容: 主键列数据和索引列数据(更新后),被更新列旧数据 回滚操作: 根据主键定位到更新行,用旧数据恢复被更新列 提交操作:
- 占用空间改变 操作: 真实删除原记录,插入一条新纪录 记录内容: 主键列数据和索引列数据(更新后),被更新列旧数据 回滚操作: 根据主键删除插入新纪录,用旧数据将删除原纪录重新插入 提交操作: