从零到一手写操作系统(五、同步 1)原子中断锁信号量)
如果追忆会荡起涟漪,那么今天的秋红落叶和晴空万里都归你
https://aeneag.xyz
微信公众号:技术乱舞
艾恩凝
手写操作系统目录
同步
就个人来说,关于同步的知识已经有了很多了解,在此重新温习一次。
5.1)原子操作
原子操作代码不多做展示,可以查看https://pic.aeneag.xyz/linux_kernel.pdf 这是本人整理的关于linux的一些知识。
重新温习一下C编译器支持嵌入汇编代码
1__asm__ __volatile__(代码部分:输出部分列表: 输入部分列表:损坏部分列表);
可以看到代码模板从 asm 开始(当然也可以是 asm),紧跟着 volatile,然后是跟着一对括号,最后以分号结束。括号里大致分为 4 个部分:
- 汇编代码部分,这里是实际嵌入的汇编代码。
- 输出列表部分,让 GCC 能够处理 C 语言左值表达式与汇编代码的结合。
- 输入列表部分,也是让 GCC 能够处理 C 语言表达式、变量、常量,让它们能够输入到汇编代码中去。
- 损坏列表部分,告诉 GCC 汇编代码中用到了哪些寄存器,以便 GCC 在汇编代码运行前,生成保存它们的代码,并且在生成的汇编代码运行后,恢复它们(寄存器)的代码。
1static inline void atomic_add(int i, atomic_t *v)
2{
3 __asm__ __volatile__("lock;" "addl %1,%0"
4 : "+m" (v->a_count)
5 : "ir" (i));
6}
7//"lock;" "addl %1,%0" 是汇编指令部分,%1,%0是占位符,它表示输出、输入列表中变量或表态式,占位符的数字从输出部分开始依次增加,这些变量或者表态式会被GCC处理成寄存器、内存、立即数放在指令中。
8//: "+m" (v->a_count) 是输出列表部分,“+m”表示(v->a_count)和内存地址关联
9//: "ir" (i) 是输入列表部分,“ir” 表示i是和立即数或者寄存器关联
5.2)中断
原子操作适合单体变量,例如整数之类的,但有时候会处理一些数据,那么我们可以关中断达到这段代码必须执行结束后才能做别的事情,这样也可以保证数据同步。
x86 CPU 上关闭、开启中断有专门的指令,即 cli、sti 指令,它们主要是对 CPU 的 eflags 寄存器的 IF 位(第 9 位)进行清除和设置,CPU 正是通过此位来决定是否响应中断信号。这两条指令只能 Ring0 权限才能执行。
采用压栈出栈的方式,保证中断
1typedef u32_t cpuflg_t;
2static inline void hal_save_flags_cli(cpuflg_t* flags)
3{
4 __asm__ __volatile__(
5 "pushfl \t\n" //把eflags寄存器压入当前栈顶
6 "cli \t\n" //关闭中断
7 "popl %0 \t\n"//把当前栈顶弹出到flags为地址的内存中
8 : "=m"(*flags)
9 :
10 : "memory"
11 );
12}
13static inline void hal_restore_flags_sti(cpuflg_t* flags)
14{
15 __asm__ __volatile__(
16 "pushl %0 \t\n"//把flags为地址处的值寄存器压入当前栈顶
17 "popfl \t\n" //把当前栈顶弹出到eflags寄存器中
18 :
19 : "m"(*flags)
20 : "memory"
21 );
22}
5.3)自旋锁
中断方法只能处理单核的CPU,如果CPU是多核心,那么中断就不一定会成功维护数据安全,那么就需要新的技术,自旋锁。
自旋锁的原理:首先读取锁变量,判断其值是否已经加锁,如果未加锁则执行加锁,然后返回,表示加锁成功;如果已经加锁了,就要返回第一步继续执行后续步骤,因而得名自旋锁。为了让你更好理解,下面来画一个图描述这个算法。
想要正确执行它,就必须保证读取锁变量和判断并加锁的操作是原子执行的。x86 CPU 给我们提供了一个原子交换指令,xchg,它可以让寄存器里的一个值跟内存空间中的一个值做交换。
1//自旋锁结构
2typedef struct
3{
4 volatile u32_t lock;//volatile可以防止编译器优化,保证其它代码始终从内存加载lock变量的值
5} spinlock_t;
6//锁初始化函数
7static inline void x86_spin_lock_init(spinlock_t * lock)
8{
9 lock->lock = 0;//锁值初始化为0是未加锁状态
10}
11//加锁函数
12static inline void x86_spin_lock(spinlock_t * lock)
13{
14 __asm__ __volatile__ (
15 "1: \n"
16 "lock; xchg %0, %1 \n"//把值为1的寄存器和lock内存中的值进行交换
17 "cmpl $0, %0 \n" //用0和交换回来的值进行比较
18 "jnz 2f \n" //不等于0则跳转后面2标号处运行
19 "jmp 3f \n" //若等于0则跳转后面3标号处返回
20 "2: \n"
21 "cmpl $0, %1 \n"//用0和lock内存中的值进行比较
22 "jne 2b \n"//若不等于0则跳转到前面2标号处运行继续比较
23 "jmp 1b \n"//若等于0则跳转到前面1标号处运行,交换并加锁
24 "3: \n" :
25 : "r"(1), "m"(*lock));
26}
27//解锁函数
28static inline void x86_spin_unlock(spinlock_t * lock)
29{
30 __asm__ __volatile__(
31 "movl $0, %0\n"//解锁把lock内存中的值设为0就行
32 :
33 : "m"(*lock));
34}
上面是自旋锁的实现,但是有中断时会打断这个自旋,还要进行改进,把中断的同步方法结合起来。
1static inline void x86_spin_lock_disable_irq(spinlock_t * lock,cpuflg_t* flags)
2{
3 __asm__ __volatile__(
4 "pushfq \n\t"
5 "cli \n\t"
6 "popq %0 \n\t"
7 "1: \n\t"
8 "lock; xchg %1, %2 \n\t"
9 "cmpl $0,%1 \n\t"
10 "jnz 2f \n\t"
11 "jmp 3f \n"
12 "2: \n\t"
13 "cmpl $0,%2 \n\t"
14 "jne 2b \n\t"
15 "jmp 1b \n\t"
16 "3: \n"
17 :"=m"(*flags)
18 : "r"(1), "m"(*lock));
19}
20static inline void x86_spin_unlock_enabled_irq(spinlock_t* lock,cpuflg_t* flags)
21{
22 __asm__ __volatile__(
23 "movl $0, %0\n\t"
24 "pushq %1 \n\t"
25 "popfq \n\t"
26 :
27 : "m"(*lock), "m"(*flags));
28}
5.4)信号量
上面谈到的几个方法只适合短暂的情况,如果时间过长的话,会造成资源的浪费。
信号量就可以解决这个问题。
具体步骤如下:
第一步,获取信号量。
- 首先对用于保护信号量自身的自旋锁 sem_lock 进行加锁。
- 对信号值 sem_count 执行“减 1”操作,并检查其值是否小于 0。
- 上步中检查 sem_count 如果小于 0,就让进程进入等待状态并且将其挂入 sem_waitlst 中,然后调度其它进程运行。否则表示获取信号量成功。当然最后别忘了对自旋锁 sem_lock 进行解锁。
第二步,代码执行流开始执行相关操作,例如读取键盘缓冲区。
第三步,释放信号量。
- 首先对用于保护信号量自身的自旋锁 sem_lock 进行加锁。
- 对信号值 sem_count 执行“加 1”操作,并检查其值是否大于 0。
- 上步中检查 sem_count 值如果大于 0,就执行唤醒 sem_waitlst 中进程的操作,并且需要调度进程时就执行进程调度操作,不管 sem_count 是否大于 0(通常会大于 0)都标记信号量释放成功。当然最后别忘了对自旋锁 sem_lock 进行解锁。
1//获取信号量
2void krlsem_down(sem_t* sem)
3{
4 cpuflg_t cpufg;
5start_step:
6 krlspinlock_cli(&sem->sem_lock,&cpufg);
7 if(sem->sem_count<1)
8 {//如果信号量值小于1,则让代码执行流(线程)睡眠
9 krlwlst_wait(&sem->sem_waitlst);
10 krlspinunlock_sti(&sem->sem_lock,&cpufg);
11 krlschedul();//切换代码执行流,下次恢复执行时依然从下一行开始执行,所以要goto开始处重新获取信号量
12 goto start_step;
13 }
14 sem->sem_count--;//信号量值减1,表示成功获取信号量
15 krlspinunlock_sti(&sem->sem_lock,&cpufg);
16 return;
17}
18//释放信号量
19void krlsem_up(sem_t* sem)
20{
21 cpuflg_t cpufg;
22 krlspinlock_cli(&sem->sem_lock,&cpufg);
23 sem->sem_count++;//释放信号量
24 if(sem->sem_count<1)
25 {//如果小于1,则说数据结构出错了,挂起系统
26 krlspinunlock_sti(&sem->sem_lock,&cpufg);
27 hal_sysdie("sem up err");
28 }
29 //唤醒该信号量上所有等待的代码执行流(线程)
30 krlwlst_allup(&sem->sem_waitlst);
31 krlspinunlock_sti(&sem->sem_lock,&cpufg);
32 krlsched_set_schedflgs();
33 return;
34}
手写操作系统目录
则移山填海之难,
终有成功之日!
——孙文