实验16.用锁实现终端输出

已完成实验

已完成实验链接

简介

实验 16.用锁实现终端输出

总结

注意:gcc一定不能用11.4,换成书上环境,我是用的4.4.7能过正确。
使用gcc11一直错误,怀疑人生了都。

主要代码

引导

省略

内核

console.c

// 文件: console.c
// 时间: 2024-07-29
// 来自: ccj
// 描述: 控制台打印相关,用一个锁保证打印正常

#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();
}

sync.h

// 文件: sync.h
// 时间: 2024-07-29
// 来自: ccj
// 描述: 信号量、琐数据结构

#ifndef __THREAD_SYNC_H
#define __THREAD_SYNC_H
#include "list.h"
#include "stdint.h"
#include "thread.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);
#endif

sync.c

// 文件: sync.c
// 时间: 2024-07-29
// 来自: ccj
// 描述: 锁的实现
//          1.二元信号亮的p操作会尝试获取信号量,获取失败阻塞线程
//          2.二元信号亮的v操作释放信号量,把所里等待队列队首加入就绪队列队首

#include "sync.h"
#include "list.h"
#include "global.h"
#include "debug.h"
#include "interrupt.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
}

/// @brief 信号量down操作,获取信号量,获取失败阻塞
/// @param psema 信号量
void sema_down(struct semaphore* psema) {
    /* 关中断来保证原子操作 */
    enum intr_status old_status = intr_disable();

    // 若value为0,表示已经被别人持有
    while (psema->value == 0) {
        /* 当前线程不应该已在信号量的waiters队列中 */
        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");
        }

        /* 若信号量的值等于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);
}

/// @brief 信号量的up操作
/// @param psema 释放信号量,把所里等待队列队首加入就绪队列队首
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操作,也是原子操作
}

thread.h

// 文件: thread.h
// 时间: 2024-07-29
// 来自: ccj
// 描述: 定义线程pcb、线程栈、线程中断栈数据结构

#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H

#include "stdint.h"
#include "list.h"

typedef void thread_func(void*);

/// @brief 进程或线程的状态
enum task_status {
    TASK_RUNNING,  //
    TASK_READY,
    TASK_BLOCKED,
    TASK_WAITING,
    TASK_HANGING,
    TASK_DIED
};

/// @brief 中断栈,中断发生时,保护程序(线程或进程)的上下文环境
/// 进程或线程被外部中断或软中断打断时,会按照此结构压入上下文寄存器
/// kernel.s中intr_exit的出栈操作是此结构的逆操作
/// 中断栈在pcb的最顶端
struct intr_stack {
    // kernel.s 宏VECTOR中push %1压入的中断号
    uint32_t vec_no;

    // kernel.s 中pushad压入的寄存器
    uint32_t edi;
    uint32_t esi;
    uint32_t ebp;
    uint32_t esp_dummy;  // 虽然pushad把esp也压入,但esp是不断变化的,所以会被popad忽略
    uint32_t ebx;
    uint32_t edx;
    uint32_t ecx;
    uint32_t eax;

    // kernel.s 中保存上下文环境压入的寄存器
    uint32_t gs;
    uint32_t fs;
    uint32_t es;
    uint32_t ds;

    // 以下由cpu从低特权级进入高特权级时压入
    uint32_t err_code;  // err_code会被压入在eip之后
    void (*eip)(void);
    uint32_t cs;
    uint32_t eflags;
    void* esp;
    uint32_t ss;
};

/// @brief 线程栈 保存线程的上下文
/// ABI规则:主调函数调用被调函数,被调函数一定要保存ebp、ebx、edi、esi、esp
/// eip:线程下一步
struct thread_stack {
    uint32_t ebp;
    uint32_t ebx;
    uint32_t edi;
    uint32_t esi;

    // 线程第一次执行时,eip指向待调用的函数kernel_thread
    // 其它时候,eip是指向switch_to的返回地址
    void (*eip)(thread_func* func, void* func_arg);

    // 以下仅供第一次被调度上cpu时使用
    void(*unused_retaddr);  // 占位置充数为返回地址
    thread_func* function;  // 由Kernel_thread所调用的函数名
    void* func_arg;         // 由Kernel_thread所调用的函数所需的参数
};

/// @brief 进程或线程的pcb process control block 4096字节
/// 一个pcb包含1个中断栈,1个线程栈,
struct task_struct {
    uint32_t* self_kstack;    // pcb中线程栈的地址
    enum task_status status;  // 状态
    char name[16];            //
    uint8_t priority;         // 线程优先级

    uint8_t ticks;           // 每次在处理器上执行的时间嘀嗒数
    uint32_t elapsed_ticks;  // 处理器上执行的时间嘀嗒总数

    struct list_elem general_tag;   // 放入就绪队列
    struct list_elem all_list_tag;  // 放入全部队列

    uint32_t* pgdir;       // pcb的虚拟地址
    uint32_t stack_magic;  // pcb魔数,用于检测栈的溢出
};

#endif

thread.c

// 文件: thread.c
// 时间: 2024-07-23
// 来自: ccj
// 描述: 线程相关定义
//          1.添加线程,申请pcb,加入就绪队列
//          2.线程调度,切换pcb
//          3.线程阻塞,设置状态非就绪,不加入就绪队列,启动调度
//          4.线程唤醒,设置状态就绪,加入就绪队列

#include "thread.h"

#include "stdint.h"
#include "string.h"
#include "global.h"
#include "memory.h"
#include "interrupt.h"
#include "debug.h"
#include "print.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;  // 用于保存队列中的线程结点

// 引用声明.s的switch_to
extern void switch_to(struct task_struct* cur, struct task_struct* next);

/// @brief 获取当前线程pcb指针
/// @return
struct task_struct* running_thread() {
    // 获取esp指针
    uint32_t esp;
    asm("mov %%esp, %0" : "=g"(esp));

    // esp % 4096就是pcb起始地址
    return (struct task_struct*)(esp & 0xfffff000);
}

/// @brief 由kernel_thread去执行function(func_arg)
/// @param function 函数指针
/// @param func_arg 函数参数
static void kernel_thread(thread_func* function, void* func_arg) {
    // 执行function前要开中断,避免后面的时钟中断被屏蔽,而无法调度其它线程
    intr_enable();
    function(func_arg);
}

/// @brief 初始化线程栈,将待执行的函数和参数放到pcb中相应的位置
/// @param pthread pcb
/// @param function 待执行的函数
/// @param func_arg 函数参数
void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) {
    // 先预留中断使用栈的空间,可见thread.h中定义的结构
    pthread->self_kstack -= sizeof(struct intr_stack);

    // 再留出线程栈空间
    pthread->self_kstack -= sizeof(struct thread_stack);
    // 此时的self_kstack看作线程栈的首地址
    struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack;
    kthread_stack->eip = kernel_thread;  // 指向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;
}

/// @brief 初始化pcb
/// @param pthread pcb
/// @param name 线程名
/// @param prio 优先级
void init_thread(struct task_struct* pthread, char* name, int prio) {
    // 清0
    memset(pthread, 0, sizeof(*pthread));

    // 设置状态
    if (pthread == main_thread) {  // 如果是主线程pcb
        pthread->status = TASK_RUNNING;
    } else {
        pthread->status = TASK_READY;
    }

    // 初始化名字、优先级、时钟、总时钟、魔数
    strcpy(pthread->name, name);
    pthread->priority = prio;
    pthread->ticks = prio;
    pthread->elapsed_ticks = 0;
    pthread->pgdir = NULL;
    pthread->stack_magic = 0x19870916;  // 自定义的魔数

    // self_kstack是线程自己在内核态下使用的栈顶地址
    pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE);
}

/// @brief 创建一优先级为prio的线程,线程名为name,线程所执行的函数是function(func_arg)
/// @param name 线程名
/// @param prio 优先级
/// @param function 线程要执行的函数
/// @param void* 函数参数
struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg) {
    // 申请1个物理页来存放pcb
    struct task_struct* thread = get_kernel_pages(1);

    // 初始化pcb
    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;
}

/// @brief 将kernel中的main函数完善为主线程
/// @param
static void make_main_thread(void) {
    // 咱们在loader.S中进入内核时的mov
    // esp,0xc009f000,在压入四个4个栈指针,所以主线程的pcb地址为0xc009e000
    // 不需要通过get_kernel_page另分配一页
    main_thread = running_thread();
    init_thread(main_thread, "main", 31);

    // 主线程不加入就绪队列,只用加入全部队列
    ASSERT(!elem_find(&thread_all_list, &main_thread->all_list_tag));
    list_append(&thread_all_list, &main_thread->all_list_tag);
}

/// @brief 实现任务调度
void schedule() {
    ASSERT(intr_get_status() == INTR_OFF);

    struct task_struct* cur = running_thread();

    // 若此线程只是cpu时间片到了,将其加入到就绪队列尾
    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;  // 重新将当前线程的ticks再重置为其priority;
        cur->status = TASK_READY;
    } else {
        /* 若此线程需要某事件发生后才能继续上cpu运行,
        不需要将其加入队列,因为当前线程不在就绪队列中。*/
    }

    // 就绪队列队首弹出,准别切换到这个线程
    ASSERT(!list_empty(&thread_ready_list));
    thread_tag = NULL;
    thread_tag = list_pop(&thread_ready_list);

    // 拿到队首的pcb
    struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag);
    next->status = TASK_RUNNING;

    // 切换
    switch_to(cur, next);
}

/// @brief 当前线程将自己阻塞,标志其状态为stat
/// @param 状态
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 */
    intr_set_status(old_status);
}

/// @brief 将线程pthread解除阻塞
/// @param pthread pcb
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);
}

/// @brief 初始化线程环境
/// @param
void thread_init(void) {
    put_str("[thread] thread_init start\n");

    list_init(&thread_ready_list);
    list_init(&thread_all_list);

    // 将当前main函数创建为线程
    make_main_thread();
    put_str("[thread] thread_init done\n");
}

init.c

// 文件: init.c
// 时间: 2024-07-22
// 来自: ccj
// 描述: 内核所有初始化操作

#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
#include "thread.h"

/// @brief 内核所有初始化
void init_all() {
    put_str("init all\n");

    idt_init();      // 初始化中断
    timer_init();    // 调快时钟、注册时钟中断来调度线程
    mem_init();      // 初始化内存管理系统
    thread_init();   // 初始化线程
    console_init();  // 控制台初始化最好放在开中断之前
}

main.c

// 文件: main.c
// 时间: 2024-07-19
// 来自: ccj
// 描述: 内核从此处开始

#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"

void k_thread(void*);

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

    init_all();

    thread_start("k_thread_a", 4, k_thread, "argA\n");
    thread_start("k_thread_a", 16, k_thread, "argB\n");
    thread_start("k_thread_a", 1, k_thread, "argC\n");
    thread_start("k_thread_a", 1, k_thread, "argD\n");
    thread_start("k_thread_a", 1, k_thread, "argE\n");

    intr_enable();  // 打开中断,使时钟中断起作用
    while (1) { console_put_str("Main\n"); };
    return 0;
}

void k_thread(void* arg) {
    char* para = arg;
    while (1) { console_put_str(para); }
}

编译

makefile

# 文件: makefile
# 时间: 2024-07-20
# 来自: ccj
# 描述: 自动化构建
#		1.创建硬盘文件
#		2.编译bootloader、kernel
#		3.复制bootloader、kernel到硬盘

BUILD_DIR   = build
KERNEL_BIN ?= ${BUILD_DIR}/kernel.bin
HARD_DISK  ?= /home/c/tityos/hd60M.img

#---编译kernel begin---

# 汇编器、编译器、链接器
AS = nasm
CC = gcc
LD = ld

# 头文件目录
INCDIRS   := lib/kernel \
			 lib \
			 kernel \
			 device \
			 thread \
			 
# 遍历头文件目录并且加上-I 前缀 include -> -I include
INCLUDE   := $(patsubst %, -I %, $(INCDIRS))

# 源文件目录
SRCDIRS   := kernel \
			 lib/kernel \
			 device \
			 lib \
			 thread \

# 拿到所有的源文件相对路径
SFILES    := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.s))
CFILES    := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.c))

# 去除源文件的目录,只保留文件名
SFILENDIR := $(notdir  $(SFILES))
CFILENDIR := $(notdir  $(CFILES))

# 遍历源文件并添加上前缀  xxx.s -> build/xxx.o xxx.c -> build/xxx.o
SOBJS	  := $(patsubst %, ${BUILD_DIR}/%, $(SFILENDIR:.s=.o))
COBJS     := $(patsubst %, ${BUILD_DIR}/%, $(CFILENDIR:.c=.o))
OBJS      := $(SOBJS) $(COBJS)

# 重要!!! 给编译器指定.c文件的目录,否则会编译时找不到源文件
VPATH	  := $(SRCDIRS)

# 编译和链接选项
ASFLAGS = -f elf
CFLAGS = -Wall -m32 -fno-stack-protector -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
LDFLAGS =  -m elf_i386 -Ttityos.lds -e main -Map $(BUILD_DIR)/kernel.map

# 编译全部源文件
$(SOBJS) : ${BUILD_DIR}/%.o : %.s
	$(AS) $(ASFLAGS) $< -o $@
$(COBJS) : ${BUILD_DIR}/%.o : %.c
	$(CC) $(CFLAGS) $(INCLUDE) -o $@ $<

# 链接全部.o文件,生成内核二进制
${KERNEL_BIN} : $(OBJS)
	$(LD) $(LDFLAGS) $^ -o $@
#---编译kernel end---


# 以下为make的操作

.PHONY : print mk_dir create_harddisk bootloader kernel all

# 打印变量
print:
	@echo $(OBJS)
	@echo $(HARD_DISK)

# 创建生成文件目录
mk_dir:
	if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fi

# 创建硬盘 注意:bochs2.6.8版本可以
create_harddisk:
	./bin/bximage -mode=create -hd=60M -imgmode=flat -q $(HARD_DISK)

# 编译 bootloader
bootloader: 
	nasm -I boot/include/ -o $(BUILD_DIR)/mbr.bin boot/mbr.s
	nasm -I boot/include/ -o $(BUILD_DIR)/loader.bin boot/loader.s
	dd if=/home/c/tityos/${BUILD_DIR}/mbr.bin of=${HARD_DISK} bs=512 count=1 conv=notrunc
	dd if=/home/c/tityos/${BUILD_DIR}/loader.bin of=${HARD_DISK} bs=512 count=3 seek=2 conv=notrunc

# 编译 kernel
kernel: ${KERNEL_BIN}
	dd if=${KERNEL_BIN} of=${HARD_DISK} bs=512 count=200 seek=9 conv=notrunc

# 删除生成文件
clean: 
	rm $(HARD_DISK)
	cd $(BUILD_DIR) && rm -f  ./*
	rmdir $(BUILD_DIR)

all: mk_dir create_harddisk bootloader kernel


运行

start.sh

#/bin/bash
# 文件: start.sh
# 描述: 启动bochs
# 时间: 2024-07-19
# 来自: ccj


set -x

bin/bochs -f bochsrc.disk

在这里插入图片描述

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值