《操作系统真相还原》实验记录2.6——同步与互斥机制之锁的实现
零、项目说明
- 从项目开始到当前同步互斥机制实现完成,各阶段的流程图已同步上传至GitHub:-HC-OS-操作系统设计项目。
一、同步与互斥机制的实现——————锁
1.1 排查 GP 异常,理解原子操作
- 当前多线程调度程序执行结果
- 待解决的几个问题:
- 输出中,有些字符串看似少了字符;
- put_str()操作的原子性没有得到保证,当下可通过在put_str()操作前后增加开关中断函数暂时保证其执行的原子性。
- 输出中,有大片连续的空缺;
- GP异常;
- 输出中,有些字符串看似少了字符;
- 总结
- 有关以上的三个问题,根本原因是访问公共资源需要多个操作,而这多个操作的执行过程不具备原子性,执行过程被任务调度器断开,从而让其他线程有机会破坏显存和光标寄存器这两类公共资源的现场。
1.2 找出代码中的临界区、互斥、竞争条件
- 要介绍的这些概念,本质上都是围绕着多个任务如何访问公共资源,因此,为解释清楚概念,必须在这样的前提下,即假设多个任务共享同一套公共资源。
- 公共资源
- 可以是公共内存、公共文件、公共硬件等,总之是被所有任务共享的一套资源。
- 临界区
- 程序要想使用某些资源,必然通过一些指令去访问这些资源,若多个任务都访问同一公共资源,那么各任务中访问公共资源的指令代码组成的区域就称为临界区。怕有同学看得不仔细,强调一下,临界区是指程序中那些访问公共资源的指令代码,即临界区是指令,并不是受访的静态公共资源。
- 互斥
- 互斥也可称为排他,是指某一时刻公共资源只能被 1 个任务独享,即不允许多个任务同时出现在自己的临界区中。公共资源在任意时刻只能被一个任务访问,即只能有一个任务在自己的临界区中执行,其他任务想访问公共资源时,必须等待当前公共资源的访问者完全执行完他自己的临界区代码后(使用完资源后)再开始访问。
- 竞争条件
- 竞争条件是指多个任务以非互斥的方式同时进入临界区,大家对公共资源的访问是以竞争的方式并行(包括伪并行)的,因此公共资源的最终状态依赖于这些任务的临界区中的微操作执行次序。【在“获取锁及更新信号量”代码中可详细观察】
- 公共资源
- 中断是实现互斥最简单的方法,没有之一,因为中断可使调度器停止工作即停止线程在处理器上的换进换出。我们今后实现的各种互斥手段也将以中断为基础。
- 【总结】
- 多线程访问公共资源时出问题的原因是产生了竞争条件,也就是多个任务同时出现在自己的临界区。
- 为避免产生竞争条件,必须保证任意时刻只能有一个任务处于临界区。
- 因此,若想避免产生竞争条件,只需保证各线程自己临界区中的所有代码都是原子操作,即临界区中的指令要么一条不做,要么一气呵成全部执行完,执行期间绝对不能被换下处理器。
- 其实,之所以出现竞争条件,归根结底是因为临界区中的指令太多了,如果临界区仅有一条指令的话,这本身已属于原子操作,完全不需要互斥。因此,在临界区中指令多于一条时才需要互斥。当然,临界区中很少存在只有一条指令的情况,因此我们必须提供一种互斥的机制,互斥能使临界区具有原子性,避免产生竞争条件,从而避免了多任务访问公共资源时出问题。
1.3 信号量
- 我们的锁是用信号量来实现的。
- 线程不像人那样有判断“配合时序”的意识,它的执行会很随意,这就使合作出错成为必然。因此,当多个线程访问同一公共资源时(当然这也属于线程合作),为了保证结果正确,必然要用一套额外的机制来控制它们的工作步调,也就是使线程们同步工作。线程同步的目的是不管线程如何混杂、穿插地执行,都不会影响结果的正确性。
- 同步一般是指合作单位之间为协作完成某项工作而共同遵守的工作步调,强调的是配合时序,就像十字路口的红绿灯,只有在绿灯亮起的情况下司机才能踩油门把车往前开,这就是一种同步。
- 同步简单来说就是不能随时随意工作,工作必须在某种条件具备的情况下才能开始,工作条件具备的时间顺序就是时序。
- 信号量就是个计数器,它的计数值是自然数,用来记录所积累信号的数量。这里的信号是个泛指,取决于信号量的实际应用环境。
- 既然信号量是计数值,必然要有对计数值增减的方法:
- 增加信号量操作 up 包括两个微操作:
- 将信号量的值加1。 【互斥】
- 唤醒在此信号量上等待的线程。 【互斥】
- 减少信号量操作 down 包括三个子操作:
- 判断信号量是否大于0。 【特殊:对锁的获取竞争属于非互斥,故代码中没有屏蔽中断的函数】
- 若信号量大于0,则将信号量减1。【互斥】
- 若信号量等于0,当前线程将自己阻塞,以在此信号量上等待。【互斥】
- 信号量是个全局共享变量,up 和 down 又都是读写这个全局变量的操作,而且它们都包含一系列的子操作,因此它们必须都是原子操作。
- 增加信号量操作 up 包括两个微操作:
- 信号量的初值代表的是信号资源的累积量,也就是剩余量,若初值为 1 的话,它的取值就只能为 0 和 1,这便称为二元信号量,我们可以利用二元信号量来实现锁。
- 在二元信号量中,down 操作就是获得锁,up 操作就是释放锁。我们可以让线程通过锁进入临界区,可以借此保证只有一个线程可以进入临界区,从而做到互斥。大致流程如下:
- 线程 A 进入临界区前先通过 down 操作获得锁(我们有强制通过锁进入临界区的手段),此时信号量的值便为0。
- 后续线程 B 再进入临界区时也通过 down 操作获得锁,由于信号量为0,线程 B 便在此信号量上等待,也就是相当于线程 B 进入了阻塞态。
- 当线程 A 从临界区出来后执行 up 操作释放锁,此时信号量的值重新变成1,之后线程 A 将线程 B 唤醒(将其添加到就绪队列的首位)。
- 线程 B 醒来后获得了锁,进入临界区。
- 注意:我们可根据需要设置各种类型的锁,以确保各线程进入对应临界区的互斥。
- 在二元信号量中,down 操作就是获得锁,up 操作就是释放锁。我们可以让线程通过锁进入临界区,可以借此保证只有一个线程可以进入临界区,从而做到互斥。大致流程如下:
1.4 线程的阻塞与唤醒
- 我们使用二元信号量来实现锁,信号量 down 操作中的第 3 个微操作提到了阻塞当前线程的功能,信号量 up 操作中的第 2 个微操作提到了唤醒线程的功能,因此在实现锁之前,我们必须提前实现这两个功能。
- 我们用函数 thread_block() 实现了线程阻塞,用函数 thread_unblock() 实现了线程唤醒。
- thread_block阻塞的原理
- 阻塞是线程自己发出的动作,也就是线程自己阻塞自己,并不是被别人阻塞的,阻塞是线程主动的行为。已阻塞的线程是由别人来唤醒的,唤醒是被动的。
- “阻塞”就是指不能运行,或者说不让运行,这只是个概念,不同的系统有不同的实现,但实现原理都是一样的,即不让线程在就绪队列中出现,这样线程便没有机会运行,也就是实现了线程的阻塞。
- 阻塞是一种意愿,表达的是线程运行中发生了一些事情,这些事情通常是由于缺乏了某些运行条件造成的,以至于线程不得不暂时停下来,必须等到运行的条件再次具备时才能上处理器继续运行。因此,阻塞发生的时间是在线程自己的运行过程中,是线程自己阻塞自己,并不是被谁阻塞。
- 线程阻塞是线程执行时的“动作”,因此线程的时间片还没用完,在唤醒之后,线程会继续在剩余的时间片内运行,但调度器并不会将该线程的时间片“充满”。
- thread_unblock唤醒的原理
- 唤醒已阻塞的线程是由别的线程,通常是锁的持有者来做的。
1.5 锁的实现
1.5.1 用锁实现终端互斥输出
main.c:主线程完成初始化函数调用及线程创建函数调用
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
void k_thread_a(void*);
void k_thread_b(void*);
int main(void) {
put_str("I am kernel\n");
init_all();
thread_start("k_thread_a", 2, k_thread_a, "argA ");
thread_start("k_thread_b", 2, k_thread_b, "argB ");
intr_enable(); //Let CPU receive clock interrupt by set IF bit to 1.
while(1) {
console_put_str("Main ");
};
return 0;
}
void k_thread_a(void* arg) {
char* para = arg;
while(1) {
console_put_str(para);
}
}
void k_thread_b(void* arg) {
char* para = arg;
while(1) {
console_put_str(para);
}
}
init.c:init_all()函数调用时钟中断处理函数、线程及终端输出锁的初始化函数
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
#include "thread.h"
#include "console.h"
#include "keyboard.h"
void init_all() {
put_str("init_all\n");
idt_init(); //now, IF bit is 0, we must use "sti" to receive clock interrupt.
timer_init(); //change clock frequency
mem_init();
thread_init();
console_init();
}
console.c:功能详情请见代码
#include "console.h"
#include "print.h"
#include "stdint.h"
#include "sync.h"
#include "thread.h"
static struct lock console_lock; //the lock of console
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();
}
void console_put_int(uint32_t num) {
console_acquire();
put_int(num);
console_release();
}
sync.h:sync.c 的头文件,定义了锁及信号量的结构体
#ifndef __THREAD_SYNC_H
#define __THREAD_SYNC_H
#include "list.h"
#include "stdint.h"
#include "thread.h"
#define NULL ((void*)0)
struct semaphore {
uint8_t value;
struct list waiters;
};
struct lock {
struct task_struct* holder; //the lock's holder
struct semaphore semaphore;
uint32_t holder_repeat_nr;
};
void sema_init(struct semaphore* psema, uint8_t value);
void lock_init(struct lock* plock);
void sema_down(struct semaphore* psema);
void sema_up(struct semaphore* psema);
void lock_acquire(struct lock* plock);
void lock_release(struct lock* plock);
#endif
sync.c:完成锁及信号量的相关处理函数
#include "sync.h"
#include "stdint.h"
#include "list.h"
#include "interrupt.h"
#include "debug.h"
/*initialized semaphore*/
void sema_init(struct semaphore* psema, uint8_t value) {
psema->value = value;
list_init(&psema->waiters);
}
/*initialized lock*/
void lock_init(struct lock* plock) {
plock->holder = NULL;
plock->holder_repeat_nr = 0;
sema_init(&plock->semaphore, 1);
}
/*"down" opention of semaphore*/
void sema_down(struct semaphore* psema) {
enum intr_status old_status = intr_disable();
while(psema->value == 0) { //the lock has not be released
ASSERT(!elem_find(&psema->waiters, &running_thread()->general_tag));
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--; //the lock has be released
ASSERT(psema->value == 0);
intr_set_status(old_status);
}
/*"up" opention of semaphore*/
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);
}
/*acquire the plock*/
void lock_acquire(struct lock* plock) { //!重点注意!:lock_acquire()和lock_release()这两个函数并不是原子操作!
if(plock->holder != running_thread()) {
sema_down(&plock->semaphore);
plock->holder = running_thread();
ASSERT(plock->holder_repeat_nr == 0);
plock->holder_repeat_nr = 1;
}
else { //avoid deadlock
plock->holder_repeat_nr++;
}
}
/*release the plock*/
void lock_release(struct lock* plock) {
/*即在lock_release()函数内时可能会被换下进程,
从而导致semaphore->value没有释放为1,因此sema_down()中的while(psema->value == 0)必须用while而不能用if!*/
ASSERT(plock->holder == running_thread());
if(plock->holder_repeat_nr > 1) {
plock->holder_repeat_nr--;
return;
}
ASSERT(plock->holder_repeat_nr == 1);
plock->holder = NULL;
plock->holder_repeat_nr = 0;
sema_up(&plock->semaphore);
}
thread.c:包含thread_block()和thread_unblock()及schedule()函数实现
//略
void schedule() {
//replace current thread
ASSERT(intr_get_status() == INTR_OFF);
struct task_struct* cur = running_thread();
if(cur->status == TASK_RUNNING) {
ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
list_append(&thread_ready_list, &cur->general_tag);
cur->ticks = cur->priority;
cur->status = TASK_READY;
}
else {
//temporary empty
}
//change new thread which is the first element in thread_ready_list.
ASSERT(!list_empty(&thread_ready_list));
thread_tag = NULL;
thread_tag = list_pop(&thread_ready_list);
struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag); //general_tag is the name of member.
next->status = TASK_RUNNING;
switch_to(cur, next);
}
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; //change current thread's status.
schedule(); //replace current thread from CPU.
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) {
ASSERT(!elem_find(&thread_ready_list, &pthread->general_tag));
if(elem_find(&thread_ready_list, &pthread->general_tag)) {
PANIC("thread_unblock: blocked thread in ready_list\n");
}
list_push(&thread_ready_list, &pthread->general_tag); //pay attention: we use "list_push" to make this thread get quickly schedule.
pthread->status = TASK_READY;
}
intr_set_status(old_status);
}
//略
1.6 执行效果
本文作者:宇星海
本文链接:https://www.cnblogs.com/Yu-Xing-Hai/p/18683211/Lock
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步