Featured image of post 《操作系统真象还原》第十章(一) —— 同步机制之互斥锁的实现

《操作系统真象还原》第十章(一) —— 同步机制之互斥锁的实现

本文介绍了操作系统的同步机制——锁,并实现了一个简单的锁机制,同时借此实现一个简单的终端

本章节所有代码托管在miniOS_32

章节任务介绍

问题复现

上一节中,我们实现了线程轮转调度,并分别实现了三个线程并发的在终端进行输出打印

  • 主线程
  • thread_work_a
  • thread_work_b
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
void thread_work_a(void *arg);
void thread_work_b(void *arg);

int main(void)
{
    put_str("I am kernel\n");
    init_all();

    thread_start("thread_work_a", 31, thread_work_a, "pthread_create_A\n");
    thread_start("thread_work_b", 8, thread_work_b, "pthread_create_B\n");

    /*打开中断,主要是打开时钟中断,以让时间片轮转调度生效*/
    intr_enable();
    while (1)
    {
        put_str("Main");
    }
    return 0;
}

/* 线程执行函数 */
void thread_work_a(void *arg)
{
    char *para = (char *)arg;
    while (1)
        put_str(para);
}
/* 线程执行函数 */
void thread_work_b(void *arg)
{
    char *para = (char *)arg;
    while (1)
        put_str(para);
}

但如果持续输出会发现终端会爆出一个GP异常

其原因在于,代码中三个线程对put_str函数的异步访问造成了数据混乱,因为put_str函数内部是通过对显存操作输出字符的,因此在这里三个线程对显存的并发访问造成了显存段的越界,从而爆出了GP异常

image-20241223225125958

其解决办法也简单,就是对put_str函数的访问上锁,其中最原始的上锁原理就是在访问公共资源前关中断,这样其余线程就无法通过中断切换上处理器访问显存这段公共资源,然后在访问资源结束后开中断,允许其余线程被调度上处理器访问显存

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int main(void)
{
    put_str("I am kernel\n");
    init_all();

    thread_start("thread_work_a", 31, thread_work_a, "pthread_create_A\n");
    thread_start("thread_work_b", 8, thread_work_b, "pthread_create_B\n");

    /*打开中断,主要是打开时钟中断,以让时间片轮转调度生效*/
    intr_enable();
    while (1)
    {
        intr_disable();
        put_str("Main");
        intr_disable();
    }
    return 0;
}

/* 线程执行函数 */
void thread_work_a(void *arg)
{
    char *para = (char *)arg;
    while (1)
    {
        intr_disable();
        put_str(para);
        intr_enable();
    }
}
/* 线程执行函数 */
void thread_work_b(void *arg)
{
    char *para = (char *)arg;
    while (1)
    {
        intr_disable();
        put_str(para);
        intr_enable();
    }
}

但这种通过开关中断的方式实现互斥锁的弊端是很明显的

  • 禁用中断意味着当前代码段执行期间,系统无法响应中断。这不仅会影响进程或线程的切换,还可能使得系统无法及时响应硬件中断(如 I/O 操作),降低系统的实时性和响应性
  • 使用禁用中断实现互斥可能使代码变得难以理解和维护,尤其是在多线程环境中。因为禁用中断的作用是全局性的,可能会影响整个系统的稳定性和响应性。
  • 在多核或多处理器系统中,禁用中断虽然能够阻止当前线程的中断,但它并不能防止其他核心/处理器上的线程执行。如果某个核心禁用了中断并进入临界区,但其他核心依然可以执行,导致多核系统中可能出现不同步的问题。

任务简介

综上,本节我们将在多线程轮转调度的基础上介绍操作系统的同步机制——锁,同时实现一个互斥锁

本节的主要任务有:

  1. 实现操作系统的同步机制——锁
  2. 实现一个终端

信号量简介及其实现

信号量简介

信号量(Semaphore) 是一种用于控制对共享资源访问的同步机制,常用于并发编程中。它可以有效地解决多个线程或进程同时访问共享资源时可能发生的冲突或竞争条件。信号量通常用于协调线程或进程之间的执行顺序,确保并发程序能够正确地进行同步。

同步机制的实现方式不止一种,我们在这里给出信号量与其余同步机制的区别和联系

  • 互斥锁(Mutex)
    • 互斥锁用于确保同一时刻只有一个线程可以访问某个资源,通常只有两种状态(锁定和解锁)。而信号量可以表示更复杂的情况(比如多个线程可以同时访问一个资源)。事实上,互斥锁是一种二元信号量
  • 条件变量(Condition Variable)
    • 条件变量通常用于线程间的通知机制,允许一个线程等待某个条件的成立****,而信号量则是通过计数来控制资源访问**的。
  • 事件(Event)
    • 事件通常用于线程间的通知机制,允许一个线程等待另一个线程的某个事件发生。而信号量不仅仅用于通知,还能用来管理资源的数量

信号量的基本操作

信号量可以看作是一个整型变量,用来表示资源的数量或许可以访问的共享资源的“可用”数量。信号量提供了两种基本操作:

  • P操作(Proberen,通常叫做Wait或Down)
    • 这个操作会检查信号量的值。如果信号量的值大于0,信号量的值会减1,执行线程或进程可以继续执行
    • 如果信号量的值等于0,表示没有可用的资源,当前线程或进程会被阻塞,直到有资源可用
  • V操作(Verhogen,通常叫做Signal或Up)
    • 这个操作会将信号量的值加1,并且如果有线程或进程因信号量值为0而被阻塞,则唤醒其中一个阻塞的线程

信号量实现

基于上述我们对信号量基本操作的介绍,要实现信号量,首先要实现线程的阻塞过程和解除阻塞过程

/thread/thread.h

1
2
3
4
/*将线程阻塞*/
void thread_block(enum task_status status);
/*解除线程的阻塞状态*/
void thread_unblock(struct task_struct *pthread);

/thread/thread.c

线程的阻塞是一种主动行为,因此要阻塞自己的线程首先需要将自己插入阻塞队列(通常由第三方维护)中,然后调用阻塞函数将自己的PCB状态设置阻塞态再调用调度函数从就绪队列中摘出一块准备好的线程切换到处理器执行,如下所示是阻塞函数的实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void thread_block(enum task_status status)
{
    ASSERT(((status == TASK_BLOCKED) || (status == TASK_HANGING) || (status == TASK_WAITING)));
    enum intr_status old_status = intr_get_status();
    /*修改当前线程状态为阻塞态
    然后调用调度器从就绪队列摘取一块新线程执行*/
    struct task_struct *cur_pthread = running_thread();
    cur_pthread->status = status;
    schedule();

    intr_set_status(old_status);
}

解除阻塞的行为由处理器完成,主要过程就是阻塞线程或进程的PCB状态设置为就绪态,然后将其插入就绪队列的头部,优先调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void thread_unblock(struct task_struct *pthread)
{
    enum intr_status old_status = intr_get_status();
    /*修改PCB状态为就绪态,同时插入就绪队列头部,优先调用*/
    enum task_status status = pthread->status;
    ASSERT(((status == TASK_BLOCKED) || (status == TASK_HANGING) || (status == TASK_WAITING)));
    if (status != TASK_READY)
    {
        if (elem_find(&thread_ready_list, &pthread->general_tag))
            PANIC("thread_unblock: blocked thread in ready_list\n");
        list_append(&thread_ready_list, &pthread->general_tag);
        pthread->status = TASK_READY;
    }

    intr_set_status(old_status);
}

接下来就是信号量的实现

/thread/sync.h

以下是信号量结构体的组成及其相关操作定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/*信号量结构体*/
struct semaphore
{
    // 信号量值
    uint8_t value;
    // 阻塞在当前信号量上的线程的阻塞队列
    struct list waiters;
};
void sema_init(struct semaphore *psema, uint8_t value);
void sema_down(struct semaphore *psema);
void sema_up(struct semaphore *psema);

/thread/sync.c

以下是信号量操作的具体实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 用于初始化信号量,传入参数就是指向信号量的指针与初值
void sema_init(struct semaphore *psema, uint8_t value)
{
    psema->value = value;       // 为信号量赋初值
    list_init(&psema->waiters); // 初始化信号量的等待队列
}

/*信号量的P操作*/
void sema_down(struct semaphore *psema)
{
    // 对阻塞队列公共资源的访问需要关中断以避免访问过程中被打断
    enum intr_status old_status = intr_disable();

    // 如果当前可用资源(信号量的值)为0,则应当阻塞当前线程
    while (psema->value == 0)
    {
        if (elem_find(&psema->waiters, &running_thread()->general_tag))
            PANIC("sema_down: thread blocked has been in waiters_list\n");
        list_append(&psema->waiters, &running_thread()->general_tag);
        thread_block(TASK_BLOCKED);
    }
    // 可以让当前线程访问公共资源,同时让可访问的资源数减去一
    psema->value--;
    ASSERT(psema->value == 0);
    // 恢复中断之前的状态
    intr_set_status(old_status);
}

// 信号量的up操作,也就是+1操作,传入参数是指向要操作的信号量的指针。且释放信号量时,应唤醒阻塞在该信号量阻塞队列上的一个进程
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);
}

同步机制——锁的实现

互斥锁的实现

/thread/sync.h

互斥锁是一种二元信号量,因此基于信号量我们可以实现互斥锁

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/*锁结构*/
struct lock
{
    // 锁的持有者
    struct task_struct *holder;
    // 锁上的信号量
    struct semaphore semaphore;
    // 加锁者加锁的次数,也就是加锁者访问公共资源的次数
    uint32_t holder_repeat_nr;
};
void lock_init(struct lock *plock);
void lock_acquire(struct lock *plock);
void lock_release(struct lock *plock);

/thread/sync.c

同理以下是互斥锁的具体操作实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*初始化锁*/
void lock_init(struct lock *plock)
{
    plock->holder = NULL;
    sema_init(&plock->semaphore, 1);
    plock->holder_repeat_nr = 0;
}

// 获取锁的函数,传入参数是指向锁的指针
void lock_acquire(struct lock *plock)
{
    // 这是为了排除掉线程自己已经拿到了锁,但是还没有释放就重新申请的情况
    if (plock->holder != running_thread())
    {
        sema_down(&plock->semaphore); // 对信号量进行down操作
        plock->holder = running_thread();
        ASSERT(plock->holder_repeat_nr == 0);
        plock->holder_repeat_nr = 1; // 申请了一次锁
    }
    else
    {
        plock->holder_repeat_nr++;
    }
}

// 释放锁的函数,参数是指向锁的指针
void lock_release(struct lock *plock)
{
    ASSERT(plock->holder == running_thread());
    // 如果>1,说明自己多次申请了该锁,现在还不能立即释放锁
    if (plock->holder_repeat_nr > 1)
    {
        plock->holder_repeat_nr--;
        return;
    }
    ASSERT(plock->holder_repeat_nr == 1); // 判断现在lock的重复持有数是不是1只有为1,才能释放

    plock->holder = NULL; // 这句必须放在up操作前,因为现在并不在关中断下运行,有可能会被切换出去,如果在up后面,就可能出现还没有置空,
                          // 就切换出去,此时有了信号量,下个进程申请到了,将holder改成下个进程,这个进程切换回来就把holder改成空,就错了
    plock->holder_repeat_nr = 0;
    sema_up(&plock->semaphore); // 信号量的V操作,也是原子操作
}

输出终端实现

基于互斥锁我们重新定义一个终端,以实现多线程的字符串打印

代码逻辑其实很简单,就是在原来的字符打印、字符串打印和整数打印函数加上一层我们实现的互斥锁

/device/console.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#ifndef __DEVICE_CONSOLE_H
#define __DEVICE_CONSOLE_H
#include "stdint.h"
void console_init(void);
void console_acquire(void);
void console_release(void);
void console_put_str(char *str);
void console_put_char(uint8_t char_asci);
void console_put_int(uint32_t num);
#endif

/device/console.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include "console.h"
#include "print.h"
#include "stdint.h"
#include "sync.h"
#include "thread.h"
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)
{
    console_acquire();
    put_str(str);
    console_release();
}

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

/* 终端中输出16进制整数 */
void console_put_int(uint32_t num)
{
    console_acquire();
    put_int(num);
    console_release();
}

然后我们重新在main函数中进行测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int main(void)
{
    put_str("I am kernel\n");
    init_all();

    thread_start("thread_work_a", 31, thread_work_a, "pthread_A ");
    thread_start("thread_work_b", 8, thread_work_b, "pthread_B ");

    /*打开中断,主要是打开时钟中断,以让时间片轮转调度生效*/
    intr_enable();
    while (1)
    {
        console_put_str("Main ");
    }
    return 0;
}

/* 线程执行函数 */
void thread_work_a(void *arg)
{
    char *para = (char *)arg;
    while (1)
    {
        console_put_str(para);
    }
}
/* 线程执行函数 */
void thread_work_b(void *arg)
{
    char *para = (char *)arg;
    while (1)
    {
        console_put_str(para);
    }
}

编译运行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
mkdir -p bin
#编译mbr
nasm -o $(pwd)/bin/mbr -I $(pwd)/boot/include/ $(pwd)/boot/mbr.S
dd if=$(pwd)/bin/mbr of=~/bochs/hd60M.img bs=512 count=1 conv=notrunc

#编译loader
nasm -o $(pwd)/bin/loader -I $(pwd)/boot/include/ $(pwd)/boot/loader.S
dd if=$(pwd)/bin/loader of=~/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc

#编译print函数
nasm -f elf32 -o $(pwd)/bin/print.o $(pwd)/lib/kernel/print.S
# 编译kernel
nasm -f elf32 -o $(pwd)/bin/kernel.o $(pwd)/kernel/kernel.S
# 编译switch
nasm -f elf32 -o $(pwd)/bin/switch.o $(pwd)/thread/switch.S

#编译main文件
gcc-4.4 -o $(pwd)/bin/main.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/device/ -I $(pwd)/thread/ $(pwd)/kernel/main.c
#编译interrupt文件
gcc-4.4 -o $(pwd)/bin/interrupt.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/interrupt.c
#编译init文件
gcc-4.4 -o $(pwd)/bin/init.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ -I $(pwd)/device/ $(pwd)/kernel/init.c
# 编译debug文件
gcc-4.4 -o $(pwd)/bin/debug.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/debug.c
# 编译string文件
gcc-4.4 -o $(pwd)/bin/string.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/string.c
# 编译bitmap文件
gcc-4.4 -o $(pwd)/bin/bitmap.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/kernel/bitmap.c
# 编译memory文件
gcc-4.4 -o $(pwd)/bin/memory.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/kernel/memory.c
# 编译thread文件
gcc-4.4 -o $(pwd)/bin/thread.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/thread/thread.c
# 编译list文件
gcc-4.4 -o $(pwd)/bin/list.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ $(pwd)/lib/kernel/list.c
# 编译timer文件
gcc-4.4 -o $(pwd)/bin/timer.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/timer.c
# 编译sync文件
gcc-4.4 -o $(pwd)/bin/sync.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/thread/sync.c
# 编译console文件
gcc-4.4 -o $(pwd)/bin/console.o -c -fno-builtin -m32 -I $(pwd)/lib/kernel/ -I $(pwd)/lib/ -I $(pwd)/kernel/ -I $(pwd)/thread/ $(pwd)/device/console.c

#将main函数与print函数进行链接
ld -m elf_i386 -Ttext 0xc0001500 -e main -o $(pwd)/bin/kernel.bin $(pwd)/bin/main.o $(pwd)/bin/kernel.o  $(pwd)/bin/init.o  $(pwd)/bin/thread.o $(pwd)/bin/switch.o $(pwd)/bin/list.o $(pwd)/bin/sync.o $(pwd)/bin/console.o $(pwd)/bin/timer.o $(pwd)/bin/interrupt.o $(pwd)/bin/memory.o $(pwd)/bin/bitmap.o  $(pwd)/bin/print.o  $(pwd)/bin/string.o $(pwd)/bin/debug.o

#将内核文件写入磁盘,loader程序会将其加载到内存运行
dd if=$(pwd)/bin/kernel.bin of=~/bochs/hd60M.img bs=512 count=200 conv=notrunc seek=9

#rm -rf bin/*

运行结果如下所示

image-20241223232700706

网站已运行
发表了16篇文章 · 总计 110,440字