概述
- 在上一篇笔记中,多个线程同时访问显存时会造成字符覆盖、显存段溢出的问题,虽然在最后使用前后关开中断解决了这个问题,但这种解决方式是十分粗糙的。就字符串打印函数
put_str()来说,它的打印实际上是通过调用字符打印函数put_char()实现的,也就是说更优秀的并发应该是在临界区put_char()前后关闭、开启中断。这种方式是可行,可每个需要原子操作的功能都使用该方法一一封装未免太过繁琐。而且对于不调用该临界区的线程来说,只能干等着调用临界区的线程开中断,这样同样不利于高效的并发。所以在本篇笔记中将利用同步机制——锁解决这个问题,并使用它封装终端输出函数。
概念理解
信号量
- 信号量是一个全局共享的变量,且有 up 和 down 两个操作对它进行更改
信号量操作规则
- up:
将信号量加1。
唤醒在该信号量上阻塞的线程。
- down:
判断信号量是否大于0。
大于0,信号量减1。
等于0,将调用该操作的线程阻塞在此信号量上。
由上述操作规则可见,信号量能够有效地解决我们遇到的问题,但对 up 和 down 两个操作来说,它门必须是原子的,而在信号量还未实现的时候,原子操作只能使用关闭、开启中断来完成。虽然这样会使不修改信号量的线程也陷入等待,但这却意味着从此之后,使用信号量封装的临界区不会再使不调用它的线程陷入这种 “无缘无故” 的等待。
具体实现
锁
- 借助二元信号量实现:
//信号量结构体
typedef struct semaphore {
uint8_t value; //信号量值
list waiters; //信号量等待队列
} semaphore;
//锁结构体
typedef struct lock {
task_struct *holder; //拥有该锁的线程
semaphore semaphore; //锁的信号量
uint32_t holder_repeat_nr; //拥有该锁的线程重复申请该锁的次数
} lock;
- 信号量和锁的初始化函数:
//初始化信号量
void sema_init(semaphore *psema, uint8_t value) {
//信号量值赋值
psema->value = value;
//初始化信号量等待队列
list_init(&psema->waiters);
}
//初始化锁plock
void lock_init(lock *plock) {
plock->holder = NULL;
plock->holder_repeat_nr = 0;
//使用二元信号量
sema_init(&plock->semaphore, 1);
}
初始化函数的分开实现,有利于后期使用多元信号量时的扩展。
- 线程的阻塞与唤醒:
//阻塞当前运行的线程,将其状态置为stat
void thread_block(task_status stat) {
//检测将置为的状态是否符合阻塞要求
ASSERT(stat == TASK_BLOCKED || stat == TASK_WAITING || stat == TASK_HANGING);
//关闭中断,保证阻塞操作的原子性
intr_status old_status = intr_disable();
//将当前线程的状态设为stat
task_struct *cur_thread = running_thread();
cur_thread->status = stat;
//调用任务调度器
schedule();
//恢复中断状态(在下次该线程被调上CPU时执行)
intr_set_status(old_status);
}
//唤醒线程pthread
void thread_unblock(task_struct *pthread) {
//检测pthread是否符合阻塞状态线程
ASSERT(pthread->status == TASK_BLOCKED || pthread->status == TASK_HANGING || pthread->status == TASK_WAITING);
//关闭中断,保证唤醒操作的原子性
intr_status old_status = intr_disable();
//pthread不为就绪态
if (pthread->status != TASK_READY) {
ASSERT(!elem_find(&thread_ready_list, &pthread->general_tag));
//若阻塞的pthread出现在就绪队列中
if (elem_find(&thread_ready_list, &pthread->general_tag)){
PANIC("thread_unblock: blocked thread in ready_list\n");
}
//修改pthread状态
pthread->status = TASK_READY;
//将pthread加入就绪队列队首,使其尽快被调上CPU
list_push(&thread_ready_list, &pthread->general_tag);
}
//恢复中断状态
intr_set_status(old_status);
}
看到这段代码的实现,不知道有没有哪位有这样的疑惑: “线程调度相关的很多操作也是原子的,需不需要也用信号量封装一下,提高下效率” 。首先需要清楚的是,线程调度操作的共享资源——就绪队列等,是所有线程都必须访问的共享资源,也就是说只要有线程在进行调度操作,所有线程都得停下,关中断状态在这种情况下就体现出了优势,因此我们也就不需要多此一举。
- 信号量的 up、down 操作:
//信号量up操作
void sema_up(semaphore *psema) {
//关中断保证up操作的原子状态
intr_status old_status = intr_disable();
//信号量值必须等于0
ASSERT(0 == psema->value);
//该信号量的等待队列中有线程,唤醒队首线程
if (!list_empty(&psema->waiters)) {
//弹出等待队列队首线程
task_struct *thread_blocked = elem2entry(task_struct, general_tag, list_pop(&psema->waiters));
//唤醒阻塞线程
thread_unblock(thread_blocked);
}
//信号量up,即释放了该信号量资源
psema->value++;
ASSERT(1 == psema->value);
//恢复中断状态
intr_set_status(old_status);
}
//信号量down操作
void sema_down(semaphore *psema) {
//关中断保证down操作的原子状态
intr_status old_status = intr_disable();
//当信号量的值为0时,该信号量资源已被申请完
while (0 == psema->value) {
//检测当前线程是否在该信号量的等待队列中
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);
}
//获取信号量资源后将该信号量down
psema->value--;
ASSERT(0 == psema->value);
//恢复之前的中断状态
intr_set_status(old_status);
}
将 down 操作中的while ()替换成if ()是否可行,这个问题的要点在于:“是否有线程先于刚被唤醒的线程申请锁”。根据本系统的调度方式来说,刚被唤醒的线程会直接插入到就绪队列队首,不会被“其他线程”提前获取锁。这样来看仿佛是可以替换的。
- 锁的申请与释放:
//获取锁plock
void lock_acquire(lock *plock) {
//当前线程第一次申请该锁
if (plock->holder != running_thread()) {
//对该锁的信号量down,即申请该锁(原子操作)
sema_down(&plock->semaphore);
//设置该锁的holder
plock->holder = running_thread();
ASSERT(0 == plock->holder_repeat_nr);
//设当前线程对该锁的申请次数为1
plock->holder_repeat_nr = 1;
}
else {
//当前线程重复申请该锁
plock->holder_repeat_nr++;
}
}
//释放锁plock
void lock_release(lock *plock) {
ASSERT(plock->holder == running_thread());
//当前线程重复申请过该锁
if (plock->holder_repeat_nr > 1) {
//减小重复申请次数,并不释放该锁
plock->holder_repeat_nr--;
return;
}
ASSERT(1 == plock->holder_repeat_nr);
//将该锁的holder及holder_repeat_nr重置
plock->holder = NULL;
plock->holder_repeat_nr = 0;
//将该锁的信号量up,即释放该锁(原子操作)
sema_up(&plock->semaphore);
}
终端输出封装
static 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();
}
实验检测
- 仿照书中的main.c,整个系统跑得很稳定(让它跑了1个多小时),不过突然想起
sema_down()函数中while()和if()的互换,真的可以换?大可试试:

test.png
这是什么情况?难道不能换?还记得前面说的“其他线程”吗,难道还有哪个线程能插在刚刚唤醒的线程前面?嗯,CPU上不就正跑着一个么,它如果多次对锁进行申请、释放,问题就出现了。因此,这个还真换不了。