从零到一手写操作系统(五、同步 1)原子中断锁信号量)

image.png

如果追忆会荡起涟漪,那么今天的秋红落叶和晴空万里都归你
https://aeneag.xyz
微信公众号:技术乱舞
艾恩凝

手写操作系统目录

同步

就个人来说,关于同步的知识已经有了很多了解,在此重新温习一次。

5.1)原子操作

原子操作代码不多做展示,可以查看https://pic.aeneag.xyz/linux_kernel.pdf 这是本人整理的关于linux的一些知识。

重新温习一下C编译器支持嵌入汇编代码

1__asm__ __volatile__(代码部分:输出部分列表: 输入部分列表:损坏部分列表);

可以看到代码模板从 asm 开始(当然也可以是 asm),紧跟着 volatile,然后是跟着一对括号,最后以分号结束。括号里大致分为 4 个部分:

  1. 汇编代码部分,这里是实际嵌入的汇编代码。
  2. 输出列表部分,让 GCC 能够处理 C 语言左值表达式与汇编代码的结合。
  3. 输入列表部分,也是让 GCC 能够处理 C 语言表达式、变量、常量,让它们能够输入到汇编代码中去。
  4. 损坏列表部分,告诉 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是多核心,那么中断就不一定会成功维护数据安全,那么就需要新的技术,自旋锁。

自旋锁的原理:首先读取锁变量,判断其值是否已经加锁,如果未加锁则执行加锁,然后返回,表示加锁成功;如果已经加锁了,就要返回第一步继续执行后续步骤,因而得名自旋锁。为了让你更好理解,下面来画一个图描述这个算法。
040000自旋锁.png

想要正确执行它,就必须保证读取锁变量和判断并加锁的操作是原子执行的。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)信号量

上面谈到的几个方法只适合短暂的情况,如果时间过长的话,会造成资源的浪费。

信号量就可以解决这个问题。

具体步骤如下:

第一步,获取信号量。

  1. 首先对用于保护信号量自身的自旋锁 sem_lock 进行加锁。
  2. 对信号值 sem_count 执行“减 1”操作,并检查其值是否小于 0。
  3. 上步中检查 sem_count 如果小于 0,就让进程进入等待状态并且将其挂入 sem_waitlst 中,然后调度其它进程运行。否则表示获取信号量成功。当然最后别忘了对自旋锁 sem_lock 进行解锁。

第二步,代码执行流开始执行相关操作,例如读取键盘缓冲区。

第三步,释放信号量。

  1. 首先对用于保护信号量自身的自旋锁 sem_lock 进行加锁。
  2. 对信号值 sem_count 执行“加 1”操作,并检查其值是否大于 0。
  3. 上步中检查 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}

手写操作系统目录


    


公众号'艾恩凝'
个人公众号
个人微信
个人微信
    吾心信其可行,
          则移山填海之难,
                  终有成功之日!
                                  ——孙文
    评论
    0 评论
avatar

取消