NJU OS mutex
南京大学操作系统(mutex)
practical implementation of mutex
Review
同时读写共享内存导致出现冲突
- 过去的过去的**值
- store的结果不能保证下一次load得到store的结果
Spin Lock
硬件上保证store+load操作是原子的
- 避免其它线程在某线程读写共享变量时修改共享变量
- 单一资源竞争(裁判)
x86原子操作
1 | lock add1 $1,(sum) |
借助原子指令控制共享变量访问
设置共享锁,访问共享变量前申请获得锁,只有获得锁的进程才能进入临界区
原子指令模型
- lock对所有线程可见(存在全局可见的lock顺序)
- 多处理器程序间构造序
实现硬件锁
指令前缀指令前缀令前缀**,译码根据前缀内容向总线发送上锁命令,由总线完成仲裁
控制控制**其它核的缓存?
所有core共享L2 cache,一条lock指令需要删除所有core cache上的对象副本
RISC-V实现原子指令
原子指令分成三步(Read-modify-write)
- 读入内存变量到寄存器中
- 寄存器执行运算
- 寄存器的运算写道对应内存位置
Load-Reserved/Store Conditional(LR/SC)
打标记(load reserved,load link)nk)**,再读入共享变量到局部寄存器,进行本地计算,随后写入到内存需要满足Store Conditional,即操作的共享变量上仍然有这个标记才能进行写操作
多个load reseved产生并发写入,导致标记消失,此时需要重新打标记再次写入
compare-and-swap
- 读入内存(
lr w
)- 判断内存是否等于给定值(
bne
)- 满足条件,则执行操作(写入新值)
sc,w
- 判断写入是否成功执行
写入未完成,则代表锁拥堵(多个进程并发操作)
缺陷
- 原子操作必须保证指令乱序
- 多个线程只有一个线程在工作,其他线程在死循环
- 分时多线程,抢到锁的线程被切换出去了。。
- 多线程并不能提升性能(空等)
- 几乎只有一个进程会在某一时刻
- 避免进程切换(内核程序,避免中断)
Fast Path和Slow path
- 只有一个进程竞争锁(Fast Path),Spin lock的代价只是一条原子指令+判断
- 多个进程等待(Slow Path)
OS API实现锁的释放和返回(睡眠锁)
SYSCALL_lock
获得锁
1 | syscall(SYSCALL_lock,&lck) |
否则将自己切换出去否则将自己切换出去则将自己切换出去**
SySCALL_unlock
释放锁
1 | syscall(SYSCALL_unlock,&lck) |
唤醒因等待lck而sleep的进程eep的进程**
需要为每个锁维护一个等待队列,相比Spin lock提高了Fast Path开销(需要进入内核)
Trade Off
进入临界区
- 获得锁(单步原子指令,对内存用户态的锁进行fetch)
- 成功则进入临界区
- 失败进入系统调用,插入等待队列
退出临界区
所有退出临界区的进程都需要进入系统调用唤醒队列中的等待进程
Note of awk
awk由模式和操作组成
模式
- 正则表达式
- 关系表达式
- 模式匹配表达式
~
匹配!~
不匹配 - BEGIN pattern END 语句块
操作
- 变量数组赋值
- 输出命令
- 内置函数
- 控制流
例子
1 | awk 'BEGIN {print "start"} pattern{ commands} END{print "end"}' file |
脚本在单引号中
工作原理
- 执行
BEGIN {commands}
语句 - 文件或标准输入读取一行,执行
pattern{commands}
- 执行到输入流末尾,执行
END{commands}
预定义变量
- $\$ n$当前记录的第n个字段,从1开始
- $\$ 0$执行过程中当前行文本内容
- NR记录项
求解第一列所有元素和
1 | awk 'BEGIN {sum = 0} {sum += $1} END{print sum}' |
传递外部变量
-v选项可以将外部变量传递给awk,例如
1 | let var=10000 |
正则表达式
- $.$匹配任意一个字符
a*
匹配任意个a[^ab]
匹配除了ab以外的任意字符^
匹配行首- $匹配行尾
打印文本第i列
1 | awk '{print $1}' filename |
比较,满足条件则打印
1 | awk '{if($5>20) {print $1}}' filename |
如果第5列大于20则打印对应行的第1列,打印也可以用printf,例如以字符串形式打印第一列
1 | awk '{printf "%s\n",$1}' filename |
C处理命令行参数getopt
函数定义为
1 | int getopt(int argc, char * const argv[], const char *optstring); |
第三个参数是命令行参数选项,字符代表选项
- 只有字符没有冒号:纯选项,无需参数
- 一个冒号:选项必须有参数,可以使用空格分割选项和参数,无参数则解析失败
- 两个冒号:参数缺省
例如vha:b:c::
表示支持-v,-h
选项(无需参数),支持-a
选项并带一个参数,支持-c
选项可以缺省参数
解析参数的结果存在全局变量中,维护四个全局变量
1 | extern char * oprarg;// 存放选项参数字符串 |
通过while循环遍历所有命令行参数,直到返回-1
getopt_long()
接收长选项,通过设置一个结构体struct option
实现,参数是
1 | int getopt_long(int argc,char * const argv[],const char *optsring,const struct option * longopts,int *longindex); |
- optstring 用于接收短选项,只接受短选项则将longopts设置为NULL即可(貌似这个选项不能设置为NULL)
- 长选项个数可以是
--arg=param
或--arg param
- longopts指向struct option数组第一个元素
- longindex返回longopts数组元素位置指针(有多少个长选项)
- 参数存在问题返回
?
option结构体包含
1 | struct option{ |
longopts
数组最后一个元素需要全0填充
内联汇编实现寄存器现场切换
内联汇编语法
1 | __asm__(assembler template |
- assemble template 汇编指令
- output operand 指示汇编指令的计算结果输出到那个C操作数中(左值)
- input operands 指示汇编指令操作数来自哪些C操作数
- list of register表示汇编指令修改的寄存器列表
实现赋值x=y
1 | asm ("movl %1, %%eax; |
- b是输出操作数,对应第二条汇编中movl指令的目的操作数,用%0refer
- a是输入操作数,用%1refer
- %%用于确定操作数来自真正的寄存器
=r,r
称为constrain,意思是存到寄存器/从寄存器中load,m代表操作数来自memory
实现原子+
1 | __asm__ __volatile__( |
等效于计算my_var = my_int+my_var
,lock
是指令前缀,满足
- 修饰的指令不能被中断打断,实现需要锁住变量对应的cache line,只有执行这一指令的线程可以存取cache line,其余core需要直接从内存读取(变量存储在单一cache line的情况)
- 内存屏障,写对象一定刷新到内存
不会改变寄存器值
理解线程库中的保护现场
1 | "movq %0, %%rsp; movq %2, %%rdi; jmp *%1" |
实现了栈帧切换、传递参数并执行函数调用,指定操作数
- %0 (uint_ptr_t)sp栈帧指针
- %1 entry 入口地址
- %2 args 参数,赋值给%rdi(reserved用于参数传递)
b,d,a制定了操作数加载的寄存器,jmp *%1
表示跳转到寄存器间接存储的内存位置,x86堆栈需要按照16字节对齐
Constrain
r
目标操作数写道寄存器中,寄存器值加载到内存对象中,可以指定使用的寄存器
m
直接对内存对象操作
Digital constraints
数据可能既作为输入也是输出,通过数字指定操作符
读文档setjmp&&longjump
Description
函数用于实现nonlocal goto,即控制流从一个函数转移到另一个函数某个位置
setjmp(jmp_buf)
接收一个jmp_buf
参数,预定义一个跳转目标,这个函数保存了调用处的运行时状态,包括
- stack pointer
- instruction point
- values of register
- signal mask
忽略忽略忽略略**某些信号,这也是运行时信息的一部分
longjmp(jmp_buf,int)
用于将控制权传递给setjmp调用处并恢复调用处运行时环境(restor the stack to its state at the time of the setjmp)
返回值
setjmp
直接调用时返回0,在fake goto的情形返回指定的参数val
M2
co_yield
选择线程切换,选择的线程有两种情况
- 协程新创建,此时需要切换堆栈,再执行代码
- yield切换出的协程已经通过setjmp保存寄存器,直接调用
longjmp
恢复寄存器现场
co_yield
决定协程何时释放执行流,yield之前需要用setjmp
保存寄存器现场
保护函数堆栈
需要在用户态保存函数堆栈,即再co结构体中维护一个数组
1 | struct co{ |
还需要保存函数指针和参数
1 | void *arg; |
维护协程状态,用于co_wait
等待目标协程执行结束或者唤醒方式
1 | enum co_status status; |
维护唤醒队列
1 | struct co *waiter; |
同时需要维护一个current指针,指向当前运行的协程(用于协程切换)