本文前置内容:浅谈锁机制
本节对应分支:lock

在上节内容中我们提到,当线程申请锁时,如果该锁已经被其他线程拥有,则此线程必须在该锁上陷入睡眠,直到锁的拥有者将其叫醒。所以我们先实现进程的睡眠与觉醒。

//thread.c
void thread_block(enum task_status stat)
{
    assert(((stat == TASK_BLOCKED) || (stat == TASK_WAITING) || (stat == TASK_HANGING)));
    enum intr_status old_status = intr_disable();
    struct task_struct* cur_thread = running_thread();
    cur_thread->status = stat;
    schedule();                  //将当前线程换下处理器
    intr_set_status(old_status); //待当前线程被解除阻塞后才继续运行
}

void thread_unblock(struct task_struct* pthread)
{
    enum intr_status old_status = intr_disable();
    assert(((pthread->status == TASK_BLOCKED) || (pthread->status == TASK_WAITING) || (pthread->status == TASK_HANGING)));
    if (pthread->status != TASK_READY)
    {
        if (elem_find(&thread_ready_list, &pthread->general_tag))
            panic("thread_unblock: blocked thread in ready_list\n",__FILE__,__LINE__,__func__);
        list_push(&thread_ready_list, &pthread->general_tag);    // 放到队列的最前面,使其尽快得到调度
        pthread->status = TASK_READY;
    }
    intr_set_status(old_status);
}
  • 第 4 行,只有为 TASK_BLOCKED、TASK_WAITING、TASK_HANGING 三种状态才会进行睡眠。
  • 第 20 行,为了使觉醒的线程尽快得到调度,使用 list_push 而非 list_append 。
  • 注意,thread_block() 是由当前线程主动执行来进入睡眠的,如果要觉醒,则只能等待其他线程来唤醒,此时是被动的。

再来看锁的实现:

//sync.h
struct semaphore  //信号量
{
    uint8_t  value;       //锁的状态
    struct list waiters;  //在此信号量上等待的线程
};

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

void sema_init(struct semaphore* psema, uint8_t value);
void sema_down(struct semaphore* psema);
void sema_up(struct semaphore* psema);
void lock_init(struct lock* plock);
void lock_acquire(struct lock* plock);
void lock_release(struct lock* plock);
  • holder_repeat_nr 是同一线程对锁的申请次数。这是为了 1)防止重复申请锁导致陷入死锁;2)防止多次释放锁而出错。
//sync.c

void sema_init(struct semaphore* psema, uint8_t value) {
    psema->value = value;       // 为信号量赋初值
    list_init(&psema->waiters); //初始化信号量的等待队列
}

/* 初始化锁plock */
void lock_init(struct lock* plock) {
    plock->holder = NULL;
    plock->holder_repeat_nr = 0;
    sema_init(&plock->semaphore, 1);  // 信号量初值为1
}

/* 信号量down操作 */
void sema_down(struct semaphore* psema) {
/* 关中断来保证原子操作 */
    enum intr_status old_status = intr_disable();
    while(psema->value == 0)// 若value为0,表示已经被别人持有
    {
        /* 当前线程不应该已在信号量的waiters队列中 */
        if (elem_find(&psema->waiters, &running_thread()->general_tag))
        {
            panic("sema_down: thread blocked has been in waiters_list\n",__FILE__,__LINE__,__func__);
        }
/* 若信号量的值等于0,则当前线程把自己加入该锁的等待队列,然后阻塞自己 */
        list_append(&psema->waiters, &running_thread()->general_tag);
        thread_block(TASK_BLOCKED);    // 阻塞线程,直到被唤醒
    }
/* 若value为1或被唤醒后,会执行下面的代码,也就是获得了锁。*/
    psema->value--;
    assert(psema->value == 0);
/* 恢复之前的中断状态 */
    intr_set_status(old_status);
}

/* 信号量的up操作 */
void sema_up(struct semaphore* psema) {
/* 关中断,保证原子操作 */
    enum intr_status old_status = intr_disable();
    assert(psema->value == 0);
    if (!list_empty(&psema->waiters)) {
        struct task_struct* thread_blocked = elem2entry(struct task_struct, general_tag, list_pop(&psema->waiters));
        thread_unblock(thread_blocked);
    }
    psema->value++;
    assert(psema->value == 1);
/* 恢复之前的中断状态 */
    intr_set_status(old_status);
}

/* 获取锁plock */
void lock_acquire(struct lock* plock) {
/* 排除曾经自己已经持有锁但还未将其释放的情况。*/
    if (plock->holder != running_thread()) {
        sema_down(&plock->semaphore);    // 对信号量P操作,原子操作
        plock->holder = running_thread();
        plock->holder_repeat_nr = 1;
    } else {
        plock->holder_repeat_nr++;
    }
}

/* 释放锁plock */
void lock_release(struct lock* plock) {
    if (plock->holder_repeat_nr > 1) {
        plock->holder_repeat_nr--;
        return;
    }
    plock->holder = NULL;	   // 把锁的持有者置空放在V操作之前
    plock->holder_repeat_nr = 0;
    sema_up(&plock->semaphore);	   // 信号量的V操作,也是原子操作
}
  • 第 60 行,如果自己已经持有该锁,则仅将 holder_repeat_nr 加 1,不做其他操作,否则重复进行 sema_down 会导致死锁!

    为什么重复申请同一把锁会产生死锁?
    在已经持有锁的情况下继续申请该锁,若仍 sema_down ,则线程会陷入睡眠,等待锁的持有者将自己叫醒。而锁的持有者又是其本身,自己可不能叫醒自己,因此系统陷入死锁。

    所以这里为了应对重复申请锁的情况,当第二次申请时(内层),仅 holder_repeat_nr++ ;当释放锁时,肯定是先从内层释放,所以仅 holder_repeat_nr-- ;外层释放时,再 sema_up 。

  • 第 70 行,必须将置空操作放在 sema_up 之前 。如果顺序放反,则可能出现这样的情况:线程 A 刚执行完 sema_up 还没来得及置空 holder 就被换下了处理器,轮到线程 B 执行。线程 B 申请该锁,因为线程 A 已经释放,所以 B 申请成功,成为该锁的持有人。当线程 B 还没来得及释放锁时,线程 A 重新被换上 CPU,执行的第一条语句就是置空 holder,然而此锁现在依然属于线程 B ,这就引发了错误。

  • 第 19 行为什么使用 while 而非 if,这是因为锁也是通过抢占来获得的,一次抢占可能无法获得锁,举个例子:线程 A 执行 down 操作时发现锁已经被 B 占用,于是陷入睡眠;线程 B 解锁,叫醒 A ;而线程 C 却排在 A 之前,优先被调度,所以锁又被 C 占用,A 继续陷入睡眠。
    但这里也可以用 if 呢?见上面 thread.c 第 20 行,我们把叫醒的线程放在了首位,不存在线程 C 排在 A 之前的情况,所以可以用 if 。

    再次强调,叫醒并不是立刻调度,而是将其放入 thread_ready_list 中。

本文件代码在源代码基础上删除了许多 assert 断言,因为笔者发现即使没有触发这些断言,程序最终总会停留在某个任务中,不再调度其他任务,这令笔者非常疑惑,怎么 assert 还会影响程序结果?即使其没有被触发?这里折磨了我很久,最终也是胡乱改,把这些 assert 删除之后才得到了满意的结果。有明白其原理的朋友麻烦在评论区指点一二,感谢!

实现终端输出

emm,终端输出,这玩意儿听起来高端,实际就是给打印函数添了个锁,来看实现:

static struct lock console_lock;    // 控制台锁

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

void console_acquire()
{
    lock_acquire(&console_lock);
}

void console_release()
{
    lock_release(&console_lock);
}

void console_put_str(char* str, uint8_t clr)
{
    console_acquire();
    put_str(str,clr);
    console_release();
}

void console_put_char(uint8_t char_asci,uint8_t clr)
{
    console_acquire();
    put_char(char_asci,clr);
    console_release();
}


void console_put_int(uint32_t num,uint8_t clr,uint8_t radix)
{
    console_acquire();
    put_int(num,clr,radix);
    console_release();
}

void console_put_uint(uint32_t num,uint8_t clr,uint8_t radix)
{
    console_acquire();
    put_uint(num,clr,radix);
    console_release();
}

就是这么简单。直接看结果吧:

{%
dplayer
“url=/2022/video/lock.mp4”
“pic=/2022/image/1.jpg” //设置封面图,同样是放在根目录下面
“loop=false” //循环播放
“theme=#FADFA3” //主题
“autoplay=false” //自动播放
“screenshot=true” //允许截屏
“hotkey=true” //允许hotKey,比如点击空格暂停视频等操作
“preload=auto” //预加载:auto
“volume=0.9” //初始音量
“playbackSpeed=1”//播放速度1倍速,可以选择1.5,2等
“lang=zh-cn”//语言
“mutex=true”//播放互斥,就比如其他视频播放就会导致这个视频自动暂停

​ “id=9E2E3368B56CD123BB4”
​ “api=https://api.prprpr.me/dplayer/”
​ “token=tokendemo”
​ “maximum=1000”
​ “addition=[‘https://api.prprpr.me/dplayer/v3/bilibili?aid=4157142’]”
​ “user=DIYgod”
​ “bottom=15%”
​ “unlimited=true”

%}

本文结束。

文章作者: 极简
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 后端技术分享
自制操作系统
喜欢就支持一下吧