草庐IT

[自制操作系统] 第16回 锁的实现

李知行 2023-03-28 原文

目录
一、前景回顾
二、锁的实现
三、使用锁实现console函数
四、运行测试

 

一、前景回顾

  上回我们实现了多线程,并且最后做了一个小小的实验,不过有一点小瑕疵。

   

  可以看到黄色部分的字符不连续,按道理应该是“argB Main”,这是为什么呢?其实仔细思考一下还是很好得出结论。我们的字符打印函数是put_str,实际上是调用的put_char函数。所以打印一个字符串需要多次调用put_char函数来打印一个个字符,如果我们当前线程刚好打印完了arg,正准备打印下一个字符B时,这时发生了调度,那么就造成了上面的这种情况。

  由此引申出来了公共资源、临界区和互斥的概念:

  公共资源:可以是公共内存、公共文件、公共硬件等,总之是被所有任务共享的一套资源。

  临界区:程序想要使用某些资源,必然通过一些指令去访问这些资源,若多个任务都访问同一公共资源,那么各任务中访问公共资源的指令代码组成的区域就被称为临界区。

  互斥:互斥又称为排他,是指某一时刻公共资源只能被一个任务独享,即不允许多个任务同时出现在自己的临界区中。其他任务想要访问公共资源时,必须等待当前公共资源的访问者完全执行完毕他自己的临界区代码后,才可以访问。

  现在联系实际情况,我们可以知道,显存区域是公共资源,每个线程的临界区便是put_str函数。每个线程之间的put_str函数是互斥的关系,也就是说,任何一个时刻,只能有一个线程可以访问操作显存,并且只有等这个线程访问操作完毕显存后,才可以让下一个线程访问操作显存。我们的代码中并没有具备互斥这个条件,所以便会造成上面的情况。

  基于上面的思路,如果我们对main.c文件做如下修改,在每个线程的put_str函数执行前后先执行关中断和开中断操作。

 1 #include "print.h"
 2 #include "init.h"
 3 #include "memory.h"
 4 #include "thread.h"
 5 #include "list.h"
 6 #include "interrupt.h"
 7 
 8 void k_thread_a(void *arg);
 9 void k_thread_b(void *arg);
10 
11 int main(void)
12 {
13     put_str("HELLO KERNEL\n");
14     init_all();
15     
16     thread_start("k_thread_a", 31, k_thread_a, "argA ");
17     thread_start("k_thread_b", 8, k_thread_b, "argB ");
18     intr_enable();
19     while(1) {
20         intr_disable();
21         console_put_str("Main ");
22         intr_enable();
23     }
24 }
25 
26 /*在线程中运行的函数k_thread_a*/
27 void k_thread_a(void *arg)
28 {
29     char *para = arg;
30     while (1) {
31         intr_disable();
32         put_str(para);
33         intr_enable();
34     }
35 }
36 
37 /*在线程中运行的函数k_thread_b*/
38 void k_thread_b(void *arg)
39 {
40     char *para = arg;
41     while (1) {
42         intr_disable();
43         put_str(para);
44         intr_enable();
45     }
46 }
main.c

  此时我们再运行系统,便可以看到字符输出就变得正常了。

  

  虽然关中断可以实现互斥,但是,关中断的操作应尽可能地靠近临界区,这样才更高效,毕竟只有临界区中的代码才用于访问公共资源,而访问公共资源的时候才需要互斥、排他,各任务临界区之外的代码并不会和其他任务有所冲突。关中断操作离临界区越远,多任务调度就越低效。

  总结一下:多线程访问公共资源时产生了竞争条件,也就是多个任务同时出现在了自己的临界区。为了避免产生竞争条件,必须保证任意时刻只能有一个任务处于临界区。虽然开闭中断的方式能够解决这个问题,但是效率并不是最高的,我们通过提供一种互斥的机制,互斥使临界区具有原子性,避免产生竞争条件,从而避免了多任务访问公共资源时出问题。

二、锁的实现

  我们的锁是通过信号量的方式来实现的。信号量是一个整数,用来记录所积累信号的数量。在我们的代码中,对信号量的加法操作是用up表示,减法操作是用down表示。

  增加操作up,可以理解为释放锁,包括两个微操作:

  1、将信号量的值加1。

  2、唤醒在此信号量上等待的线程。

  减少操作down,可以理解获取锁,包括三个微操作:

  1、判断信号量是否为0。

  2、若信号量大于0,则将信号量减1。

  3、若信号量等于0,当前线程将自己阻塞,以在此信号量上等待。

  所以有了这两个操作后,两个线程在进入临界区时,便是如下操作:

  1、线程A进入临界区前先通过down操作获取锁,此时信号量减去1为0。

  2、同样,线程B也要进入临界区,尝试使用down操作获取锁,但是信号量已经减为0,所以线程B便在此信号量上等待,也就是将自己阻塞。

  3、当线程A从临界区中出来后,将信号量加1,也就是释放锁,随后线程A将线程B唤醒

  4、线程B被唤醒后,获得锁,进入临界区。

  来看看代码吧,在project/kernel目录下新建sync.c和sync.h文件,关于阻塞和解除阻塞的函数我们放在了thread.c文件下。

 1 #include "sync.h"
 2 #include "interrupt.h"
 3 #include "debug.h"
 4 #include "thread.h"
 5 
 6 /*初始化信号量*/
 7 void sema_init(struct semaphore *psema, uint8_t value)
 8 {
 9     psema->value = value;
10     list_init(&psema->waiters);
11 }
12 
13 /*初始化锁lock*/
14 void lock_init(struct lock* plock)
15 {
16     plock->holder = NULL;
17     plock->holder_repeat_nr = 0;
18     sema_init(&plock->semaphore, 1);
19 }
20 
21 /*信号量down操作*/
22 void sema_down(struct semaphore *psema)
23 {
24     /*关中断来保证原子操作*/
25     enum intr_status state = intr_disable();
26     while (psema->value == 0) {
27         /*当前线程不应该在信号量的waiters队列中*/
28         ASSERT(!elem_find(&psema->waiters, &running_thread()->general_tag));
29         if (elem_find(&psema->waiters, &running_thread()->general_tag)) {
30             PANIC("sema_down: thread blocked has been in waiters_list\n");
31         }
32 
33         /*若信号量为0,则当前线程把自己加入到该锁的等待队列中*/
34         list_append(&psema->waiters, &running_thread()->general_tag);
35         thread_block(TASK_BLOCKED);
36     }
37 
38     /*若value为1或者被唤醒后,会执行以下代码*/
39     psema->value--;
40     ASSERT(psema->value == 0);
41     /*恢复之前的中断状态*/
42     intr_set_status(state);
43 }
44 
45 /*信号量UP操作*/
46 void sema_up(struct semaphore *psema)
47 {
48     /*关中断来保证原子操作*/
49     enum intr_status state = intr_disable();
50     ASSERT(psema->value == 0);
51     if (!list_empty(&psema->waiters)) {
52         struct task_struct *thread_blocked = elem2entry(struct task_struct, general_tag, list_pop(&psema->waiters));
53         thread_unblock(thread_blocked);
54     }
55     psema->value++;
56     ASSERT(psema->value == 1);
57     /*恢复之前的中断状态*/
58     intr_set_status(state);
59 }
60 
61 /*获取锁plock*/
62 void lock_acquire(struct lock *plock)
63 {
64     /*排除曾经自己有锁但还未释放锁的情况*/
65     if (plock->holder != running_thread()) {
66         sema_down(&plock->semaphore);
67         plock->holder = running_thread();
68         ASSERT(plock->holder_repeat_nr == 0);
69         plock->holder_repeat_nr = 1;
70     } else {
71         plock->holder_repeat_nr++;
72     }
73 }
74 
75 /*释放锁plock*/
76 void lock_release(struct lock *plock)
77 {
78     ASSERT(plock->holder == running_thread());
79     if (plock->holder_repeat_nr > 1) {
80         plock->holder_repeat_nr--;
81         return;
82     }
83     ASSERT(plock->holder_repeat_nr == 1);
84     plock->holder = NULL;
85     plock->holder_repeat_nr = 0;
86     sema_up(&plock->semaphore);
87 }
sync.c
#ifndef  __KERNEL_SYNC_H
#define  __KERNEL_SYNC_H
#include "stdint.h"
#include "list.h"

/*信号量结构*/
struct semaphore {
    uint8_t value;
    struct list waiters;
};

/*锁结构*/
struct lock {
    struct task_struct *holder;    //锁的持有者
    struct semaphore semaphore;    //用二元信号量实现锁
    uint32_t holder_repeat_nr;     //锁的持有者重复申请锁的次数 
}; 

void lock_release(struct lock *plock);
void lock_acquire(struct lock *plock);
void sema_up(struct semaphore *psema);
void sema_down(struct semaphore *psema);
void lock_init(struct lock* plock);
void sema_init(struct semaphore *psema, uint8_t value);
#endif
sync.h
 1 ...
 2 
 3 /*当前线程将自己阻塞,标志其状态为stat*/
 4 void thread_block(enum task_status stat)
 5 {
 6     /*stat取值为TASK_BLOCKED、TASK_WAITING、TASK_HANGING
 7     这三种状态才不会被调度*/
 8 
 9     ASSERT(((stat == TASK_BLOCKED) || (stat == TASK_WAITING) || (stat == TASK_HANGING)));
10     enum intr_status old_status = intr_disable();
11     struct task_struct *cur_thread = running_thread();
12     cur_thread->status = stat;
13     schedule();
14     intr_set_status(old_status);
15 }
16 
17 /*将线程thread解除阻塞*/
18 void thread_unblock(struct task_struct *thread)
19 {
20     enum intr_status old_status = intr_disable();
21     ASSERT(((thread->status == TASK_BLOCKED) || (thread->status == TASK_WAITING) || (thread->status == TASK_HANGING)));
22     if (thread->status != TASK_READY) {
23         ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
24         if (elem_find(&thread_ready_list, &thread->general_tag)) {
25             PANIC("thread_unblock: blocked thread in ready_list!\n");
26         }
27         list_push(&thread_ready_list, &thread->general_tag);
28         thread->status = TASK_READY;
29     }
30     intr_set_status(old_status);
31 }
thread.c

三、使用锁实现console函数

  在本回开始,我们通过在put_str函数前后关、开中断来保证任何时刻只有一个任务处于临界区代码,不过这种方式效率低下。现在我们已经实现了锁机制,所以可以利用锁来升级put_str函数。在project/kernel目录下新建console.c和console.h文件。

#include "stdint.h"
#include "sync.h"
#include "thread.h"
#include "print.h"
#include "console.h"

static struct lock console_lock;

/*初始化终端*/
void console_init(void)
{
    lock_init(&console_lock);
}

/*获取终端*/
void console_acquire(void)
{
    lock_acquire(&console_lock);
}

/*释放终端*/
void console_release(void)
{
    lock_release(&console_lock);
}

/*终端中输出字符串*/
void console_put_str(char *str)
{
    console_acquire();
    put_str(str);
    console_release();
}


/*终端中输出字符*/
void console_put_char(uint8_t char_asci)
{
    console_acquire();
    put_char(char_asci);
    console_release();
}

/*终端中输出十六进制整数*/
void console_put_int(uint32_t num)
{
    console_acquire();
    put_int(num);
    console_release();
}
console.c
 1 #ifndef  __KERNEL_CONSOLE_H
 2 #define  __KERNEL_CONSOLE_H
 3 
 4 void console_init(void);
 5 void console_acquire(void);
 6 void console_release(void);
 7 void console_put_str(char *str);
 8 void console_put_char(uint8_t char_asci);
 9 void console_put_int(uint32_t num);
10 
11 #endif
console.h

四、运行测试

  修改main.c函数并编译运行。可以看到,字符整齐无误地出现在屏幕上,不会出现字符短缺的现象。不要忘记在makefile中增加sync.o、console.o文件。

 1 #include "print.h"
 2 #include "init.h"
 3 #include "memory.h"
 4 #include "thread.h"
 5 #include "list.h"
 6 #include "interrupt.h"
 7 #include "console.h"
 8 
 9 void k_thread_a(void *arg);
10 void k_thread_b(void *arg);
11 
12 int main(void)
13 {
14     put_str("HELLO KERNEL\n");
15     init_all();
16     
17     thread_start("k_thread_a", 31, k_thread_a, "argA ");
18     thread_start("k_thread_b", 8, k_thread_b, "argB ");
19     intr_enable();
20     while(1) {
21         console_put_str("Main ");
22     }
23 }
24 
25 /*在线程中运行的函数k_thread_a*/
26 void k_thread_a(void *arg)
27 {
28     char *para = arg;
29     while (1) {
30         console_put_str(para);
31     }
32 }
33 
34 /*在线程中运行的函数k_thread_b*/
35 void k_thread_b(void *arg)
36 {
37     char *para = arg;
38     while (1) {
39         console_put_str(para);
40     }
41 }
42 
43 
44 
45 
46 
47 
48 
49 
50 //asm volatile("sti");
main.c

   

  本回到此结束,预知后事如何,请看下回分解。

自制操作系统spanstylecolor嵌入式

有关[自制操作系统] 第16回 锁的实现的更多相关文章

  1. ruby-on-rails - 带 Spring 锁的 Rails 4 控制台 - 2

    我正在使用Ruby2.1.1和Rails4.1.0.rc1。当执行railsc时,它被锁定了。使用Ctrl-C停止,我得到以下错误日志:~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.2/lib/spring/client/run.rb:47:in`gets':Interruptfrom~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.2/lib/spring/client/run.rb:47:in`verify_server_version'from~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.

  2. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  3. 电脑0x0000001A蓝屏错误怎么U盘重装系统教学 - 2

      电脑0x0000001A蓝屏错误怎么U盘重装系统教学分享。有用户电脑开机之后遇到了系统蓝屏的情况。系统蓝屏问题很多时候都是系统bug,只有通过重装系统来进行解决。那么蓝屏问题如何通过U盘重装新系统来解决呢?来看看以下的详细操作方法教学吧。  准备工作:  1、U盘一个(尽量使用8G以上的U盘)。  2、一台正常联网可使用的电脑。  3、ghost或ISO系统镜像文件(Win10系统下载_Win10专业版_windows10正式版下载-系统之家)。  4、在本页面下载U盘启动盘制作工具:系统之家U盘启动工具。  U盘启动盘制作步骤:  注意:制作期间,U盘会被格式化,因此U盘中的重要文件请注

  4. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  5. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  6. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  7. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  8. 在VMware16虚拟机安装Ubuntu详细教程 - 2

    在VMware16.2.4安装Ubuntu一、安装VMware1.打开VMwareWorkstationPro官网,点击即可进入。2.进入后向下滑动找到Workstation16ProforWindows,点击立即下载。3.下载完成,文件大小615MB,如下图:4.鼠标右击,以管理员身份运行。5.点击下一步6.勾选条款,点击下一步7.先勾选,再点击下一步8.去掉勾选,点击下一步9.点击下一步10.点击安装11.点击许可证12.在百度上搜索VM16许可证,复制填入,然后点击输入即可,亲测有效。13.点击完成14.重启系统,点击是15.双击VMwareWorkstationPro图标,进入虚拟机主

  9. kvm虚拟机安装centos7基于ubuntu20.04系统 - 2

    需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc

  10. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

随机推荐