Featured image of post 《操作系统真象还原》第九章(二) —— 多线程轮转调度

《操作系统真象还原》第九章(二) —— 多线程轮转调度

本文介绍了操作系统如何进行多线程轮转调度

本章节所有代码托管在miniOS_32

章节任务介绍

任务简介

上一节,我们成功模拟pthread_create创建了新的线程并成功执行

本节我们将介绍如何进行多线程轮转调度

本节的主要任务有:

  1. 创建多线程轮转调度的数据结构——双向链表
  2. 借时钟中断完成多线程的时间片轮转调度

任务目标

/kernel/main.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
#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);
}

如上所示,我们将实现多线程轮转——当用户创建thread_work_athread_work_b两个线程时,操作系统能够为每个线程分配时间片,并根据时间片进行轮转调度两个线程

线程组织队列——双向链表

image-20241222230536231

如图所示,为了实现多线程的轮转调度,我们需要使用队列将存储线程信息的PCB组织起来,如就绪队列、阻塞队列等,为此需要首先需要定义这种数据结构——双向链表

/lib/kernel/list.h

首先定义链表的节点类型,将来其将作为PCB中的数据成员,负责将线程的PCB串联起来

1
2
3
4
5
6
/*链表节点类型*/
struct list_elem
{
    struct list_elem *prev;
    struct list_elem *next;
};

接下来定义双向链表的结构

1
2
3
4
5
6
7
8
/*双链表类型*/
struct list
{
    /*头结点*/
    struct list_elem head;
    /*尾节点*/
    struct list_elem tail;
};

然后是对链表的操作函数原型的定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*链表初始化*/
void list_init(struct list *);
/*头插法*/
void list_insert_before(struct list_elem *cur, struct list_elem *elem);
/*在链表头部插入元素*/
void list_push(struct list *plist, struct list_elem *elem);
/*在链表尾部插入元素*/
void list_append(struct list *plist, struct list_elem *elem);
/*移除元素*/
void list_remove(struct list_elem *pelem);
/*弹出链表头部元素*/
struct list_elem *list_pop(struct list *plist);
/*判断链表是否为空*/
bool list_empty(struct list *plist);
/*获取链表长度*/
uint32_t list_len(struct list *plist);
/*在链表中查找节点*/
bool elem_find(struct list *plist, struct list_elem *obj_elem);
// 定义个叫function的函数类型,返回值是int,参数是链表结点指针与一个整形值
typedef bool(function)(struct list_elem *, int arg);
/*判断链表中是否有节点elem
使得func(elem,arg)成立*/
struct list_elem *list_traversal(struct list *plist, function func, int arg);
void list_iterate(struct list *plist);

以下是对链表操作的实现

/lib/kernel/list.h

  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
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
#include "list.h"
#include "interrupt.h"

/*初始化双向链表*/
void list_init(struct list *list)
{
    list->head.prev = NULL;
    list->head.next = &list->tail;
    list->tail.prev = &list->head;
    list->tail.next = NULL;
}
/*把链表元素elem插入在元素cur之前*/
void list_insert_before(struct list_elem *cur, struct list_elem *elem)
{
    // 插入元素之前先关中断,确保元素插入过程中不会被中断影响
    enum intr_status old_status = intr_disable();

    elem->prev = cur->prev;
    elem->next = cur;
    cur->prev->next = elem;
    cur->prev = elem;

    // 恢复关中断之前的状态
    intr_set_status(old_status);
}

/*在链表头部插入元素*/
void list_push(struct list *plist, struct list_elem *elem)
{
    list_insert_before(plist->head.next, elem);
}
/*在链表尾部添加元素*/
void list_append(struct list *plist, struct list_elem *elem)
{
    list_insert_before(&plist->tail, elem);
}

/*删除链表元素节点*/
void list_remove(struct list_elem *pelem)
{
    enum intr_status old_status = intr_disable();

    pelem->prev->next = pelem->next;
    pelem->next->prev = pelem->prev;

    intr_set_status(old_status);
}
/*弹出链表头部元素*/
struct list_elem *list_pop(struct list *plist)
{
    struct list_elem *elem = plist->head.next;
    list_remove(elem);
    return elem;
}
/*在链表中查找元素obj_elem*/
bool elem_find(struct list *plist, struct list_elem *obj_elem)
{
    struct list_elem *ptr = plist->head.next;
    while (ptr != &plist->tail)
    {
        if (ptr == obj_elem)
            return true;
        ptr = ptr->next;
    }
    return false;
}
/*判断链表是否为空*/
bool list_empty(struct list *plist)
{
    return (plist->head.next == &plist->tail);
}
/*返回链表长度*/
uint32_t list_len(struct list *plist)
{
    struct list_elem *ptr = plist->head.next;
    uint32_t len = 0;
    while (ptr != &plist->tail)
    {
        ++len;
        ptr = ptr->next;
    }
    return len;
}
/* 把链表plist中的每个元素elem和arg传给回调函数func,
 * arg给func用来判断elem是否符合条件.
 * 本函数的功能是遍历列表内所有节点elem
 * 逐个判断链表中是否有节点elem使得func(elem,arg)成立。
 * 找到符合条件的元素返回元素指针,否则返回NULL. */
struct list_elem *list_traversal(struct list *plist, function func, int arg)
{
    struct list_elem *ptr = plist->head.next;
    if (list_empty(plist))
        return NULL;
    while (ptr != &plist->tail)
    {
        if (func(ptr, arg))
            return ptr;
        ptr = ptr->next;
    }
    return NULL;
}

这部分内容相对简单,读者看代码即可明白

唯一需要主要的是**,由于队列是公共资源,对于它的修改一定要保证为原子操作**,所以在对队列的修改操作都需要通过intr disable将中断关闭,旧中断状态用变量 old status 保存,以此保证修改操作的原子性(不可拆分、连续性),操作结束后再通过“intr set status(old status)”将中断恢复。

多线程轮转调度

轮转调度前的数据准备

我们主要通过时钟中断进行线程调度,其主要过程如下:

  1. 每个线程在运行之前都被分配一个时间片,这个时间片其实就是PCB中的优先级priority
  2. 假如thread_work_a这个线程被分配的时间片为31个时钟周期,那么每当线程thread_work_a运行一个时钟周期(也就是没发生一次时钟中断)之后,时间片就减一
  3. 因此我们需要有一个变量来记录线程可运行的剩余时间,也就是PCB当中的ticks
  4. 当ticks的值减为0的时候,就表示该线程的时间片用完了
  5. 此时调度函数便将该线程的状态从运行态修改为就绪态,然后将其插入到就绪态的末尾,并从就绪队列的头部弹出一个新的线程上cpu运行,然后继续上述过程,每运行一个时钟就减去一个时钟,直到可用的时钟用完继续切换

经过以上描述,我们需要为线程的PCB新增一些数据成员:

  • 用以表示该线程可运行的剩余时间(时钟数):ticks
  • 用以表示线程从运行开始到当前时间一共运行的时间:elapsed_ticks
  • 用以将所有处于就绪状态的PCB连接起来,使其成为一个就绪队列的节点成员:general_tag
  • 用以将所有线程PCB连接起来(只要是线程就连接,不管其处于什么状态),使其成为一个记录所有线程的队列的节点成员:all_list_tag

如下所示,是我们增添新成员后的PCB结构体

/thread/thread.h

 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
/*PCB结构体*/
struct task_struct
{
    // 线程栈的栈顶指针
    uint32_t *self_kstack;
    // 线程状态
    enum task_status status;
    // 线程的优先级
    uint8_t priority;
    // 线程函数名
    char name[16];

    // 线程在一个时间片上可运行的剩余时间(时钟数)
    uint8_t ticks;
    //记录线程从运行开始,已经运行的时间(时钟数)
    uint32_t elapsed_ticks;
    //用于连接就绪队列或者阻塞队列
    struct list_elem general_tag;
    //用于连接存储所有线程的队列
    struct list_elem all_list_tag;
    /*
    进程自己页表的虚拟地址
    由于线程共享进程的虚拟地址空间
    因此线程PCB中此值为NULL
    */
    uint32_t pgdir;

    // 用于PCB结构体的边界标记
    uint32_t stack_magic;
};

同时我们需要定义就绪队列和存储所有线程的队列,以及其他必要的数据结构

/thread/thread.c

1
2
3
4
5
6
7
8
// 主线程PCB
struct task_struct *main_thread;
// 线程就绪队列
struct list thread_ready_list;
// 存储所有任务的队列
struct list thread_all_list;
// 存储临时队列节点
static struct list_elem *thread_tag;

由于PCB结构体已经修改,因此需要修改PCB的初始化逻辑,线程栈运行信息的初始化不必修改

轮转调度前的初始化

修改线程PCB初始化

首先是PCB的初始化

 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
/*初始化PCB*/
void init_thread(struct task_struct *pthread, char *name, int prio)
{
    memset(pthread, 0, sizeof(*pthread));
    strcpy(pthread->name, name);
    // pthread->status = TASK_RUNNGING;

    if (pthread == main_thread)
        pthread->status = TASK_RUNNING;
    else
        pthread->status = TASK_READY;
    pthread->priority = prio;

    // 初始化线程在一个时间片上可运行的剩余时间
    pthread->ticks = prio;
    // 记录线程从运行开始,已经运行的时间
    pthread->elapsed_ticks = 0;
    /*
    线程没有自己的地址空间
    进程的pcb这一项才有用,指向自己的页表虚拟地址
    */
    pthread->pgdir = (uint32_t)NULL;

    /*
    一个线程的栈空间分配一页空间,将PCB放置在栈底
    pthread是申请的一页空间的起始地址,因此加上一页的大小,就是栈顶指针
    */
    pthread->self_kstack = (uint32_t *)((uint32_t)pthread + PG_SIZE);
    /*PCB的边界标记,防止栈顶指针覆盖掉PCB的内容*/
    pthread->stack_magic = 0x20241221;
}

与之前PCB的初始化新增的部分是

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    if (pthread == main_thread)
        pthread->status = TASK_RUNNING;
    else
        pthread->status = TASK_READY;
    pthread->priority = prio;

    // 初始化线程在一个时间片上可运行的剩余时间
    pthread->ticks = prio;
    // 记录线程从运行开始,已经运行的时间
    pthread->elapsed_ticks = 0;
    /*
    线程没有自己的地址空间
    进程的pcb这一项才有用,指向自己的页表虚拟地址
    */
    pthread->pgdir = (uint32_t)NULL;

如前所述,priority是记录线程一个运行时间片的信息变量,ticks是记录线程剩余可运行的时钟数的信息变量,因此他们都被初始化为prio,其中prio是用户指定的,即线程的一个运行时间片时用户控制的;另外elapsed_ticks是记录线程从运行开始到当前时刻运行的所有时间,因此应该初始化为0

需要另外说明的有两点

其一,main_thread表示主线程,也就是内核main函数所代表的线程,由于之前我们在loader.S中加载内核并运行的时候,内核中并没有PCB,因此在这里我们需要也为其添加相关PCB信息,同时也因为main线程从一开始就是运行着的,因此应该对其PCB中的线程状态变量初始化为TASK_RUNNING

其二,PCB中新增的pgdir变量,表示进程自己的页表虚拟地址,该信息是给将来的进程使用的,线程共享进程的虚拟地址空间,因此该值在这里应该为NULL

线程运行函数的逻辑修改——就绪队列的初始化

当PCB和线程栈的运行信息初始化后,就需要开始着手启动线程,但这是之前的逻辑

但在这里,我们需要统一调度逻辑,因此,我们需要将准备好的PCB插入就绪队列和全队列中,将来由调度器从就绪队列中选择线程然后上CPU运行

如下所示

 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
/*根据线程栈的运行信息开始运行线程函数*/
struct task_struct *thread_start(char *name, int prio, thread_func function, void *func_args)
{
    /*
    1.分配一页的空间给线程作为线程执行的栈空间
    */
    struct task_struct *thread = get_kernel_pages(1);
    /*
    2.初始化PCB,PCB里存放了线程的基本信息以及线程栈的栈顶指针
    */
    init_thread(thread, name, prio);
    /*
    3.根据线程栈的栈顶指针,初始化线程栈,也就是初始化线程的运行信息
    比如线程要执行的函数,以及函数参数
    */
    thread_create(thread, function, func_args);

    /*
    4.将准备好PCB和运行信息的线程插入到就绪队列和存储所有任务的队列
    */
    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);

    // /*5.上述准备好线程运行时的栈信息后,即可运行执行函数了*/
    // asm volatile("movl %0,%%esp;    \
    //             pop %%ebp;          \
    //             pop %%ebx;          \
    //             pop %%edi;          \
    //             pop %%esi;          \
    //             ret"
    //              :
    //              : "g"(thread->self_kstack)
    //              : "memory");
    return thread;
}

以上我们对初始化逻辑进行一个总结

  1. 初始化PCB
  2. 初始化线程栈的运行信息
  3. 将初始化所有信息的线程PCB插入就绪队列和全队列,将来由调度统一选择调度

在正式实现轮转调度之前,因为main线程的特殊性,我们还需要对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
/*获取当前正在运行的线程的PCB*/
struct task_struct *running_thread(void)
{
    uint32_t esp;
    // 获取当前栈顶指针地址
    asm("mov %%esp,%0" : "=g"(esp));
    /*
    由于栈顶指针是在一页内存空间内,而PCB存储在这一页空间的起始地址处
    因此栈顶指针地址取整得到的这一页空间的起始地址就是PCB的起始地址
    */
    return (struct task_struct *)(esp & 0xfffff000);
}
static void make_main_thread(void)
{
    /* 因为main线程早已运行,咱们在loader.S中进入内核时的mov esp,0xc009f000,
就是为其预留了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 thread_init(void)
{
    put_str("thread_init start\n");
    list_init(&thread_ready_list);
    list_init(&thread_all_list);
    make_main_thread();
    put_str("thread_init done\n");
}

其初始化逻辑与之前是相同的,也就是对PCB和线程栈运行信息的初始化,但是由于main线程已经在运行,因此不需要对线程栈的运行信息进行初始化,也不需要将其添加到就绪队列,只需添加到全队列即可

轮转调度

接下来我们开始实现轮转调度

首先是轮转调度的基本逻辑,如前所述

  • 每个线程在运行之前都被分配一个时间片,然后线程每执行一个时钟周期(每发生一次时钟中断)时间片就减一,直到减为0的时候就开始进行轮转调度
  • 而我们知道,时钟中断无时无刻都在发生,每发生一次时钟中断都要触发一次时钟中断的中断处理程序,因此我们只需要修改时钟中断的中断处理程序,让其每次被调用的时候就将当前正在运行的线程可用时间(ticks)减去一,直到减为0的时候就调用调度函数

如下所示

/device/timer.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
uint32_t ticks; // ticks是内核自中断开启以来总共的嘀嗒数
/* 时钟的中断处理函数 */
static void intr_timer_handler(void)
{
   struct task_struct *cur_thread = running_thread();
   // 检查栈是否溢出
   ASSERT(cur_thread->stack_magic == 0x20241221);
   // 记录此线程占用的cpu时间嘀
   cur_thread->elapsed_ticks++;
   // 从内核第一次处理时间中断后开始至今的滴哒数,内核态和用户态总共的嘀哒数
   ticks++;

   if (cur_thread->ticks == 0)
   { // 若进程时间片用完就开始调度新的进程上cpu
      schedule();
   }
   else
   { // 将当前进程的时间片-1
      cur_thread->ticks--;
   }
}

当然,我们要把上述中断处理程序注册到中断描述符表IDT中,如下所示,其中0x20是时钟中断的中断向量号

1
2
3
4
5
6
7
void timer_init()
{
   put_str("timer_init start\n");
   /* 设置8253的定时周期,也就是发中断的周期 */
   register_handler(0x20, intr_timer_handler);
   put_str("timer_init done\n");
}

具体的注册实现register_handler如下所示

/kernel/interrupt.c

1
2
3
4
5
6
/* 在中断处理程序数组第vector_no个元素中注册安装中断处理程序function */
void register_handler(uint8_t vector_no, intr_handler function) {
/* idt_table数组中的函数是在进入中断后根据中断向量号调用的,
 * 见kernel/kernel.S的call [idt_table + %1*4] */
   idt_table[vector_no] = function; 
}

image-20241223001758173

上述由时钟中断触发的中断处理程序,最终当发生轮转调度时的处理如下所示

  • 把当前时间片已经用完的线程换到就绪队列尾
  • 然后从就绪队列头取出一个新的线程换上执行新的时间片

/thread/thread.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
/*时间片轮转调度函数*/
void schedule(void)
{
    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
    {
        /* 若此线程需要某事件发生后才能继续上cpu运行,
          不需要将其加入队列,因为当前线程不在就绪队列中。*/
    }

    /* 将thread_ready_list队列中的第一个就绪线程弹出,准备将其调度上cpu. */
    ASSERT(!list_empty(&thread_ready_list));
    thread_tag = NULL; // thread_tag清空
    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);
}

终于到了最后一步,将从就绪队列中拿到的新线程上cpu运行

  • 将当前正在运行的栈顶指针(esp)和寄存器映像保存到旧PCB中
  • 将新PCB中的栈顶指针取出赋值给esp准备执行
  • 借用ret指令弹出新线程的可执行函数地址,赋值给指令寄存器eip,正式运行新线程的执行函数
 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
[bits 32]
section .text
global switch_to
switch_to:
    ; 栈中此处是返回地址
    push esi
    push edi
    push ebx
    push ebp
    ; 取出栈中的参数cur,cur = [esp+20]
    ; 即当前已经时间片运行结束的线程PCB
    mov eax,[esp+20]
    ; 保存栈顶指针esp到当前时间片运行结束的线程PCB的self_kstack字段,
    ; self_kstack在task_struct中的偏移为0,
	; 所以直接往thread开头处存4字节便可。
    mov [eax],esp

    ; 取出栈中的参数next, next = [esp+24]
    ; 即要被换上CPU的线程PCB
    mov eax,[esp+24]
    ;让esp指向新线程PCB中的运行栈的栈顶指针
    mov esp,[eax]

    pop ebp
    pop ebx
    pop edi
    pop esi
    ; 返回到上面switch_to下面的那句注释的返回地址,
    ; 未由中断进入,第一次执行时会返回到kernel_thread
    ret		 				 					

编译运行

 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
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

#将main函数与print函数进行链接
ld -m elf_i386 -Ttext 0xc0001500 -e main -o $(pwd)/bin/kernel.bin $(pwd)/bin/main.o $(pwd)/bin/thread.o $(pwd)/bin/switch.o  $(pwd)/bin/list.o $(pwd)/bin/print.o $(pwd)/bin/init.o $(pwd)/bin/timer.o $(pwd)/bin/interrupt.o $(pwd)/bin/kernel.o $(pwd)/bin/memory.o $(pwd)/bin/bitmap.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-20241223002618286

image-20241223002633979

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