10.1 锁
没锁之前,输出的字符串被中途打断就是一个例子。
破坏了完整性
所以一个简单的方法是加中断
kernel / main.c
#include "print.h"
#include "init.h"
#include "debug.h"
#include "thread.h"
/*在线程中运行的函数*/
void k_thread_a(void *arg);
void k_thread_b(void *arg);
int main(void){
put_str("I am kernel\n");
init_all();
thread_start("k_thread_a", 31, k_thread_a, "argA ");
thread_start("k_thread_b", 8, k_thread_b, "argB ");
intr_enable();// 打开中断,使时钟中断起作用
while(1){
intr_disable();
put_str("Main ");
intr_enable();
}
return 0;
}
/*在线程中运行的函数*/
void k_thread_a(void *arg)
{
char *para = arg;
while(1){
intr_disable();
put_str(para);
intr_enable();
}
while(1);
}
void k_thread_b(void *arg)
{
char *para = arg;
while(1){
intr_disable();
put_str(para);
intr_enable();
}
while(1);
}
运行截图
10.1.2 找出代码中的临界区、互斥、竞争条件
- 公共资源
- 临界区
各任务中访问公共资源的指令代码组成的区域就称为临界区 - 互斥
也可称为排他 - 竞争条件
多个线程并行混杂访问了公共的资源,也就是说,这种访问方式不满足互斥,从而产生了竞争条件
即使多个线程是真并行执行,对于访问共享资源也会有个前后顺序,因此显存和光标寄存器这两个公共资源的状态取决于所有线程的访问时序。
多线程访问公共资源时出问题的原因是产生了竞争条件,也就是多个任务同时出现在自己的临界区。为避免产生竞争条件,必须保证任意时刻只能有一个任务处于临界区。因此,只要保证各线程自己临界区中的所有代码都是原子操作,即临界区中的指令要么一条不做,要么一气呵成全部执行完,执行期间绝对不能被换下处理器。
所以我们必须提供一种互斥的机制,互斥能使临界区具有原子性,避免
产生竞争条件,从而避免了多任务访问公共资源时出问题。
10.1.3 信号量
当多个线程访问同一公共资源时(当然这也属于线程合作),为了保证结果正确,必然要用一套额外的机制来控制它们的工作步调,也就是使线程们同步工作。
信号量就是个计数器,它的计数值是自然数,用来记录所积累信号的数量。
所谓信号,是个泛指,取决于环境。
信号量是计数值,必然要有对计数增减的方法。由于 Dijkstra 是荷兰人,他用 P、V 操作来表示信号量的减、增,这两个都是荷兰语中的单词的缩写。
P 是指 Proberen,表示减少,
V 是指单词 Verhogen,表示增加。
增加操作 up 包括两个微操作。
(1)将信号量的值加 1。
(2)唤醒在此信号量上等待的线程。
减少操作 down 包括三个子操作。
(1)判断信号量是否大于 0。
(2)若信号量大于 0,则将信号量减 1。
(3)若信号量等于 0,当前线程将自己阻塞,以在此信号量上等待。
信号量的初值代表是信号资源的累积量,也就是剩余量,若初值为 1 的话,它的取值就只能为 0 和 1,这便称为二元信号量,我们可以利用二元信号量来实现锁
在二元信号量中,
down 操作就是获得锁,
up 操作就是释放锁。
流程:
- A down 获取锁 信号量-1,进入临界区,信号量变为0
- B down 发现信号量为0,B睡眠
- A 出,up 信号量+1,信号量=1, B被A唤醒。
- B 醒,获得锁,进入临近区
10.1.4 线程的阻塞和唤醒
thread_block 实现阻塞
thread_unblock 实现了线程唤醒
线程是主动阻塞自己,被动被别人唤醒
唤醒已阻塞的线程是由别的线程,通常是锁的持有者来做的。
thread / thread.c
添加了
thread_block
thread_unblock 在最后
#include "thread.h"
#include "stdint.h"
#include "string.h"
#include "global.h"
#include "memory.h"
#include "debug.h"
#include "interrupt.h"
#include "list.h"
#define PG_SIZE 4096
struct task_struct* main_thread; // 主线程 PCB
struct list thread_ready_list; // 就绪队列
struct list thread_all_list; // 所有任务队列
static struct list_elem* thread_tag;// 用于保存队列中的线程结点
extern void switch_to(struct task_struct* cur, struct task_struct* next);
/* 获取当前线程 pcb 指针 */
struct task_struct *running_thread(){
uint32_t esp;
asm ("mov %%esp, %0" : "=g" (esp));
/* 取 esp 整数部分,即 pcb 起始地址 */
return (struct task_struct*)(esp & 0xfffff000);
}
/* 由 kernel_thread 去执行 function(func_arg) */
static void kernel_thread(thread_func *function, void *func_arg){
/* 执行 function 前要开中断,避免后面的时钟中断被屏蔽,而无法调度其他线程 */
intr_enable();
function(func_arg);
}
/* 初始化线程栈 thread_stack,
将待执行的函数和参数放到 thread_stack 中相应的位置 */
void thread_create(struct task_struct *pthread, thread_func function, void *func_arg){
/*先预留中断使用栈的空间,可见 thread.h 中定义的结构 */
pthread->self_kstack -= sizeof(struct intr_stack);
/*在留出线程栈空间,可见 thread.h 中定义 */
pthread->self_kstack -= sizeof(struct thread_stack);
struct thread_stack *kthread_stack = (struct thread_stack *)pthread->self_kstack;
kthread_stack->eip = kernel_thread;
kthread_stack->function = function;
kthread_stack->func_arg = func_arg;
kthread_stack->ebp = kthread_stack->ebx = \
kthread_stack->esi = kthread_stack->edi = 0;
}
/* 初始化线程基本信息 */
void init_thread(struct task_struct *pthread, char *name, int prio)
{
memset(pthread, 0, sizeof(*pthread));
strcpy(pthread->name, name);
if(pthread == main_thread){
/* 由于把 main 函数也封装成一个线程,
并且它一直是运行的,故将其直接设为 TASK_RUNNING */
pthread->status = TASK_RUNNING;
}else{
pthread->status = TASK_READY;
}
/* self_kstack 是线程自己在内核态下使用的栈顶地址 */
pthread->self_kstack = (uint32_t *)((uint32_t)pthread + PG_SIZE);
pthread->priority = prio;
pthread->ticks = prio;
pthread->elapsed_ticks = 0;
pthread->pgdir = NULL;
pthread->stack_magic = 0x19870916; //自定义的魔数.
}
/* 创建一优先级为 prio 的线程,线程名为 name,
线程所执行的函数是 function(func_arg) */
struct task_struct *thread_start(char *name, int prio, thread_func function, void *func_arg){
/* pcb 都位于内核空间,包括用户进程的 pcb 也是在内核空间 */
struct task_struct *thread = get_kernel_pages(1);
ASSERT(thread != 0);
init_thread(thread, name, prio);
thread_create(thread, function, func_arg);
/* 确保之前不在队列中 */
ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
/* 加入就绪线程队列 */
list_append(&thread_ready_list, &thread->general_tag);
/* 确保之前不在队列中 */
ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));
/* 加入全部线程队列 */
list_append(&thread_all_list, &thread->all_list_tag);
return thread;
}
/* 将 kernel 中的 main 函数完善为主线程 */
static void make_main_thread(void) {
/* 因为 main 线程早已运行, 咱们在 loader.S 中进入内核时的 mov esp,0xc009f000,
* 就是为其预留 pcb 的,因此 pcb 地址为 0xc009e000,不需要通过 get_kernel_page 另分配一页*/
main_thread = running_thread();
init_thread(main_thread, "main", 31);
/* main 函数是当前线程,当前线程不在 thread_ready_list 中,
* 所以只将其加在 thread_all_list 中 */
ASSERT(!elem_find(&thread_all_list, &main_thread->all_list_tag));
list_append(&thread_all_list, &main_thread->all_list_tag);
}
/* 实现任务调度 */
void schedule()
{
ASSERT(intr_get_status() == INTR_OFF);
struct task_struct* cur = running_thread();
if (cur->status == TASK_RUNNING) {
// 若此线程只是 cpu 时间片到了,将其加入到就绪队列尾
ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
list_append(&thread_ready_list, &cur->general_tag);
cur->ticks = cur->priority;
// 重新将当前线程的 ticks 再重置为其 priority
cur->status = TASK_READY;
}else{
/* 若此线程需要某事件发生后才能继续上 cpu 运行,
不需要将其加入队列,因为当前线程不在就绪队列中 */
}
ASSERT(!list_empty(&thread_ready_list));
thread_tag = NULL; // thread_tag 清空
/* 将 thread_ready_list 队列中的第一个就绪线程弹出,
准备将其调度上 cpu */
thread_tag = list_pop(&thread_ready_list);
struct task_struct* next = elem2entry(struct task_struct, \
general_tag, thread_tag);
next->status = TASK_RUNNING;
switch_to(cur, next);
}
/* 当前线程将自己阻塞,标志其状态为 stat. */
void thread_block(enum task_status stat) {
/* stat 取值为
TASK_BLOCKED、
TASK_WAITING、
TASK_HANGING,
也就是只有这三种状态才不会被调度*/
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; // 置其状态为 stat
schedule(); // 将当前线程换下处理器
intr_set_status(old_status);
}
/* 将线程 pthread 解除阻塞
按常理说就绪队列中不会出现已阻塞的线程
为防止已经在就绪队列中的线程再次被添加使用了ASSERT来判断,
但 ASSERT 只是调试期间用的,最后会把它去掉*/
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);
pthread->status = TASK_READY;
}
intr_set_status(old_status);
}
/*初始化线程环境*/
void thread_init(void)
{
put_str("thread_init start\n");
list_init(&thread_ready_list);
list_init(&thread_all_list);
/* 将当前 main 函数创建为线程 */
make_main_thread();
put_str("thread_init done\n");
}
10.1.5 锁的实现
thread / sync.h
#ifndef __THREAD_SYNC_H
#define __THREAD_SYNC_H
#include "list.h"
#include "stdint.h"
#include "interrupt.h"
#include "thread.h"
#include "debug.h"
/* 信号量结构 */
struct semaphore {
uint8_t value;
struct list waiters; //记录在此信号量上等待(阻塞)的所有线程
};
/* 锁结构 */
struct lock{
struct task_struct* holder; // 锁的持有者
struct semaphore semaphore; // 用二元信号量实现锁
/*在未释放锁之前,有可能会再次调用重复申请此锁的函数,这样一来,内外层函数在释
放锁时会对同一个锁释放两次,为了避免这种情况的发生,
用此变量来累积重复申请的次数,释放锁时会
根据变量 holder_repeat_nr 的值来执行具体动作*/
uint32_t holder_repeat_nr; // 锁的持有者重复申请锁的次数
};
#endif
thread \ sync.c
#include "sync.h"
/* 初始化信号量 */
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,表示已经被别人持有
ASSERT(!elem_find(&psema->waiters, &running_thread()->general_tag));
/* 当前线程不应该已在信号量的 waiters 队列中 */
if (elem_find(&psema->waiters, &running_thread()->general_tag)) {
PANIC("sema_down: thread blocked has been in waiters_list\n");
}
/*若信号量的值等于 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();
ASSERT(plock->holder_repeat_nr == 0);
plock->holder_repeat_nr = 1;
}else{
plock->holder_repeat_nr++;
}
}
/* 释放锁 plock */
void lock_release(struct lock* plock) {
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; // 把锁的持有者置空放在 V 操作之前
plock->holder_repeat_nr = 0;
sema_up(&plock->semaphore); // 信号量的 V 操作,也是原子操作
}
10.2 用锁实现终端输出
device / console.h
#ifndef __DEVICE_CONSOLE_H
#define __DEVICE_CONSOLE_H
#include "stdint.h"
/* 初始化终端 */
void console_init();
/* 获取终端 */
void console_acquire() ;
/* 释放终端 */
void console_release() ;
/* 终端中输出字符串 */
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
#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();
}
/* 终端中输出十六进制整数 */
void console_put_int(uint32_t num) {
console_acquire();
put_int(num);
console_release();
}
kernel / init.c
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"
#include "memory.h"
/*负责初始化所有模块*/
void init_all()
{
put_str("init_all\n");
idt_init(); //初始化中断
mem_init();
thread_init();
timer_init();
console_init();
}
kernel / main.c
#include "print.h"
#include "init.h"
#include "debug.h"
#include "thread.h"
#include "sync.h"
#include "console.h"
/*在线程中运行的函数*/
void k_thread_a(void *arg);
void k_thread_b(void *arg);
int main(void){
put_str("I am kernel\n");
init_all();
thread_start("k_thread_a", 31, k_thread_a, "argA ");
thread_start("k_thread_b", 8, k_thread_b, "argB ");
intr_enable();// 打开中断,使时钟中断起作用
int n = 100;
while(n--){
console_put_str("Main ");
// console_put_int(0x2000);
}
while(1);
return 0;
}
/*在线程中运行的函数*/
void k_thread_a(void *arg)
{
char *para = arg;
int n = 100;
while(n--){
// intr_disable();
console_put_str(para);
// intr_enable();
}
while(1);
}
void k_thread_b(void *arg)
{
char *para = arg;
int n = 100;
while(n--){
// intr_disable();
console_put_str(para);
// intr_enable();
}
while(1);
}
Mafile
CC = clang
LIB = -I lib/kernel/ -I lib/ -I kernel/ -I thread/ -I device/
loader.bin:loader.asm inc/boot.inc
nasm -I inc/ -o loader.bin loader.asm
mbr.bin:mbr.asm inc/boot.inc
nasm -I inc/ -o mbr.bin mbr.asm
kernel.bin:kernel/main.c lib/kernel/print.asm kernel/kernel.asm kernel/interrupt.c kernel/init.c device/timer.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/main.o kernel/main.c
nasm -f elf -o build/print.o lib/kernel/print.asm
nasm -f elf -o build/kernel.o kernel/kernel.asm
nasm -f elf -o build/switch.o thread/switch.asm
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/interrupt.o kernel/interrupt.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/init.o kernel/init.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/timer.o device/timer.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/debug.o kernel/debug.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/memory.o kernel/memory.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/string.o lib/string.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/bitmap.o lib/kernel/bitmap.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/thread.o thread/thread.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/list.o lib/kernel/list.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/sync.o thread/sync.c
$(CC) $(LIB) -m32 -s -w -c -fno-builtin -o build/console.o device/console.c
ld -m elf_i386 -Ttext 0xc0001500 -e main -o build/kernel.bin build/main.o \
build/init.o build/interrupt.o build/print.o \
build/debug.o build/kernel.o build/timer.o \
build/string.o build/memory.o build/bitmap.o \
build/thread.o build/list.o build/switch.o \
build/sync.o build/console.o
dd: dd_mbr dd_loader dd_kernel
dd_mbr:
dd if=mbr.bin of=hd60M.img bs=512 count=1 conv=notrunc
dd_loader:
dd if=loader.bin of=hd60M.img bs=512 count=4 seek=2 conv=notrunc
dd_kernel:
dd if=build/kernel.bin of=hd60M.img bs=512 count=200 seek=9 conv=notrunc
device / timer.c
//...略
/* 初始化 PIT8253 */
void timer_init()
{
put_str("timer_init start\n");
/* 设置 8253 的定时周期,也就是发中断的周期 */
// frequency_set( CONTRER0_PORT,\
// COUNTER0_NO,\
// READ_WRITE_LATCH,\
// COUNTER_MODE,\
// COUNTER0_VALUE);
register_handler(0x20, intr_timer_handler);
put_str("timer_init done\n");
}