没错,就是大家这群高级程序员(其实我也是)所耳熟能详但又讲不明白的 锁,只是本章不是如何用,也不是讲它是什么原理,而是在实现咱们操做系统的过程当中所天然而然地产生的一个需求,而且咱们从零开始来实现 锁html
本章须要和上一章 【自制操做系统12】熟悉而陌生的多线程 连起来看,由于正是上一章咱们多线程输出字符串时,发现了一些问题,致使咱们须要想个办法来解决,用大家高级程序员的牛逼的话来说,就是 为了解决线程不安全的问题,提出了锁这种技术手段。git
为了让你们清楚目前的程序进度,画了到目前为止的程序流程图,以下。(红色是咱们要实现的)程序员
上篇文章咱们建立了两个线程,加上主线程,一共三个线程循环打印字符串,最终的输出是这样的安全
先忽略上面那个异常,看下面话框的地方,argA 尚未打印完,就从中间断开了,开始打印了 argB微信
其实很好解释,由于 打印一个字符串 put_str 是经过一次次调用 put_char 来实现的,假如任务切换恰好发生在打印字符串 "argA" 刚刚打印到 "ar” 的时候切换了(实际上这几率很大),就会出现上面的问题。再往细了说,单单一个 put_char 函数,也是分红 获取光标、打印字符、更新光标值 等多个步骤实现的,假如在中间某处发生了任务切换,不但字符串被分割,还会出现少字符的状况,你们能够想一想为何。至于最上面的异常,固然也是因为相似的缘由形成的。多线程
上面的种种问题,概括起来就是,虽然咱们的任务切换能够发生在任何一个指令和下一条指令之间,但有的时候咱们但愿多条指令是具备 原子性 的,也就是要么不执行,要执行就所有执行完,这中间不容许发生任务切换。考虑到这点,咱们能够经过简单的开关中断来实现,就像这样。app
void k_thread_a(void* arg) { char* para = arg; while(1) { intr_disable(); // 关中断 put_str(para); intr_enable(); // 开中断 } }
咱们再运行程序,就会发现上述问题被完美解决了。可别瞧不起这粗暴的方法,关中断是实现互斥最简单的方法,没有之一。咱们从此实现的各类互斥手段也将以它为基础。ide
刚刚提到的问题只是特例,咱们把它概括总结为通常描述,就是:函数
在咱们这个例子中,对应关系就是学习
总结起来,多线程的问题就是,多个任务同时出如今临界区,也就是产生了竞争条件。那解决问题的办法就只有一个,那就是 不要让多个任务同时出如今临界区。怎么作到这一点呢?刚刚简单粗暴的 开关中断 是一种方法,下面要说的更灵活的 锁 也是一种方法,再后面把多条指令从新用 一条原子指令 实现,如 CAS,也是一种方法。千万不要被再后面各类各样五花八门的各类技术绕晕,多线程解决的问题都是,不要让多个任务同时出如今临界区,仅此而已。
有了这两个操做,两个线程在进入临界区时,即可以这样操做
sync.h
1 // 信号量结构 2 struct semaphore { 3 uint8_t value; 4 struct list waiters; 5 }; 6 7 // 锁结构 8 struct lock { 9 struct task_struct* holder; // 持有者 10 struct semaphore semaphore; // 二元信号量 11 uint32_t holder_repeat_nr; // 持有者重复申请锁的次数 12 };
sync.c
1 #include "sync.h" 2 #include "list.h" 3 #include "global.h" 4 #include "interrupt.h" 5 6 // 初始化信号量 7 void sema_init(struct semaphore* psema, uint8_t value) { 8 psema->value = value; // 为信号量赋初值 9 list_init(&psema->waiters); // 初始化信号量的等待队列 10 } 11 12 // 初始化锁 plock 13 void lock_init(struct lock* plock) { 14 plock->holder = NULL; 15 plock->holder_repeat_nr = 0; 16 sema_init(&plock->semaphore, 1); // 信号量初值为1 17 } 18 19 // 信号量 down 操做 20 void sema_down(struct semaphore* psema) { 21 // 关闭中断保证原子操做 22 enum intr_status old_status = intr_disable(); 23 while(psema->value == 0) { 24 // 表示已经被别人持有,当前线程把本身加入该锁的等待队列,而后阻塞本身 25 list_append(&psema->waiters, &running_thread()->general_tag); 26 thread_block(TASK_BLOCKED); 27 } 28 // value不为0,则能够得到锁 29 psema->value--; 30 intr_set_status(old_status); 31 } 32 33 // 信号量的 up 操做 34 void sema_up(struct semaphore* psema) { 35 // 关闭中断保证原子操做 36 enum intr_status old_status = intr_disable(); 37 38 if (!list_empty(&psema->waiters)) { 39 struct task_struct* thread_blocked = elem2entry(struct task_struct, general_tag, list_pop(&psema->waiters)); 40 thread_unblock(thread_blocked); 41 } 42 43 psema->value++; 44 intr_set_status(old_status); 45 } 46 47 // 获取锁 plock 48 void lock_acquire(struct lock* plock) { 49 if (plock->holder != running_thread()) { 50 sema_down(&plock->semaphore); 51 plock->holder = running_thread(); 52 plock->holder_repeat_nr = 1; 53 } else { 54 plock->holder_repeat_nr++; 55 } 56 } 57 58 // 释放锁 plock 59 void lock_release(struct lock* plock) { 60 if (plock->holder_repeat_nr > 1) { 61 plock->holder_repeat_nr--; 62 return; 63 } 64 plock->holder = NULL; 65 plock->holder_repeat_nr = 0; 66 sema_up(&plock->semaphore); 67 }
thread.c
1 ... 2 3 // 当前线程将本身阻塞,标志其状态为 stat(取值必须为 BLOCKED WAITING HANGING 之一) 4 void thread_block(enum task_status stat) { 5 enum intr_status old_status = intr_disable(); 6 struct task_struct* cur_thread = running_thread(); 7 cur_thread->status = stat; 8 schedule(); 9 intr_set_status(old_status); 10 } 11 12 // 解除阻塞 13 void thread_unblock(struct task_struct* pthread) { 14 enum intr_status old_status = intr_disable(); 15 if (pthread->status != TASK_READY) { 16 if (elem_find(&thread_ready_list, &pthread->general_tag)) { 17 // 错误!blocked thread in ready_list 18 } 19 // 放到队列的最前面,使其尽快获得调度 20 list_push(&thread_ready_list, &pthread->general_tag); 21 pthread->status = TASK_READY; 22 } 23 intr_set_status(old_status); 24 }
画黄线是重点要看的部分,也就是咱们的目的,实现 获取锁 和 释放锁 两个函数。看总体逻辑
上述两个函数中有两个子函数,是对信号量操做的,咱们看一下
上述函数中又有两个子函数,咱们继续拆解
忘记了 schedule 函数的,能够看下面回顾一下
1 // 实现任务调度 2 void schedule() { 3 struct task_struct* cur = running_thread(); 4 if (cur->status == TASK_RUNNING) { 5 // 只是时间片到了,加入就绪队列队尾 6 list_append(&thread_ready_list, &cur->general_tag); 7 cur->ticks = cur->priority; 8 cur->status = TASK_READY; 9 } else { 10 // 须要等某事件发生后才能继续上 cpu,不加入就绪队列 11 } 12 13 thread_tag = NULL; 14 // 就绪队列取第一个,准备上cpu 15 thread_tag = list_pop(&thread_ready_list); 16 struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag); 17 next->status = TASK_RUNNING; 18 switch_to(cur, next); 19 }
将全部这些都串起来,我画了个图,表示在各类状况下,各个变量是如何变化的(蓝色表明增长,绿色表明减小)
上一步咱们只是实现了锁(其实就是实现了 获取锁 和 释放锁 两个函数),但咱们尚未任何地方用它,接下来咱们就从新封装一个原来多线程调用会出错的 put_str 函数的升级版(原子化) console_put_str
1 static struct lock console_lock; 2 3 void console_init() { 4 lock_init(&console_lock); 5 } 6 7 void console_acquire() { 8 lock_acquire(&console_lock); 9 } 10 11 void console_release() { 12 lock_release(&console_lock); 13 } 14 15 void console_put_str(char* str) { 16 console_acquire(); 17 put_str(str); 18 console_release(); 19 }
能够看到,其实就是把 put_str 函数加了锁,又封装了一层而已。接下来咱们 main 函数调用一下新输出函数的试试
1 int main(void){ 2 put_str("I am kernel\n"); 3 init_all(); 4 thread_start("k_thread_a", 31, k_thread_a, "argA "); 5 thread_start("k_thread_b", 8, k_thread_b, "argB "); 6 intr_enable(); 7 8 while(1) { 9 put_str("Main "); 10 console_put_str("Main "); 11 } 12 return 0; 13 } 14 15 void k_thread_a(void* arg) { 16 char* para = arg; 17 while(1) { 18 console_put_str(para); 19 } 20 } 21 22 void k_thread_b(void* arg) { 23 char* para = arg; 24 while(1) { 25 console_put_str(para); 26 } 27 }
能够看到画黄线的部分,咱们只是把原来的 put_str 函数,更换成了 console_put_str 函数了而已,这样在输出的时候就有了锁的保护,多线程再也不有上一章出现的问题了。简单吧!
这回终于没有报错,且字符都整齐无误地输出在了屏幕上,再也不有覆盖字符的现象了
若是你对自制一个操做系统感兴趣,不妨跟随这个系列课程看下去,甚至加入咱们(下方有公众号和小助手微信),一块儿来开发。
《操做系统真相还原》这本书真的赞!强烈推荐
当你看到该文章时,代码可能已经比文章中的又多写了一些部分了。你能够经过提交记录历史来查看历史的代码,我会慢慢梳理提交历史以及项目说明文档,争取给每一课都准备一个可执行的代码。固然文章中的代码也是全的,采用复制粘贴的方式也是彻底能够的。
若是你有兴趣加入这个自制操做系统的大军,也能够在留言区留下您的联系方式,或者在 gitee 私信我您的联系方式。
本课程打算出系列课程,我写到哪以为能够写成一篇文章了就写出来分享给你们,最终会完成一个功能全面的操做系统,我以为这是最好的学习操做系统的方式了。因此中间遇到的各类坎也会写进去,若是你能持续跟进,跟着我一块写,必然会有很好的收货。即便没有,交个朋友也是好的哈哈。
目前的系列包括
微信公众号
我要去阿里(woyaoquali)
小助手微信号
Angel(angel19980323)
while