Pintos-Lab1记录

这里主要讲解思路,代码可能展示得不会很详细
完整代码在Github

<0x01> Task 1: Alarm Clock

任务摘要

这个任务要求重新实现./devices/timer.ctimer_sleep()函数

原本的timer_sleep()实现是基于无限循环直到到达等待时间的忙等待
我们需要避免这个忙等待

怎么实现

参考5状态模型,线程运行部分主要有三个状态
分别是运行态、准备态和阻塞态
原本的忙实现仅在运行态和准备态之间切换
所以这里我们要做的就是把阻塞态用上

所以可以单独开一个线程队列,用来存放所有休眠的线程
然后在每次时钟中断时检查休眠线程队列,把到时间的线程放到准备队列中即可

代码实现

首先先看下原始代码

void timer_sleep(int64_t ticks)
{
	int64_t start = timer_ticks();
    enum intr_level old_level;
    ASSERT(intr_get_level() == INTR_ON);
    while (timer_elapsed(start) < ticks)
		thread_yield();
}

可以看到就是记录了开始的时间刻,然后无限循环
无限循环就是我们要删除的

添加休眠线程队列

./threads/thread.c的最上面,有定义ready_list的部分
按这个格式再定义一个sleep_list作为休眠线程队列

static struct list sleep_list;

定义完后往下找,会找到一个thread_init函数,这里初始化了ready_list
我们也跟着初始化一下

void thread_init(void)
{
    ASSERT(intr_get_level() == INTR_OFF);

    lock_init(&tid_lock);
    list_init(&ready_list);
    list_init(&all_list);
    // lab1 初始化休眠线程队列
    list_init(&sleep_list);

    /* Set up a thread structure for the running thread. */
    initial_thread = running_thread();
    init_thread(initial_thread, "main", PRI_DEFAULT);
    initial_thread->status = THREAD_RUNNING;
    initial_thread->tid = allocate_tid();
}

给线程加上苏醒时间信息

在tcb中加上线程的休眠时间信息,thread.h中添加

struct thread
{
    /* Owned by thread.c. */
    tid_t tid;                 /* Thread identifier. */
    enum thread_status status; /* Thread state. */
    char name[16];             /* Name (for debugging purposes). */
    uint8_t *stack;            /* Saved stack pointer. */
    int priority;              /* Priority. */
    struct list_elem allelem;  /* List element for all threads list. */

    int64_t wakeup_ticks; // lab1 添加苏醒时间刻

    /* Shared between thread.c and synch.c. */
    struct list_elem elem; /* List element. */

#ifdef USERPROG
    /* Owned by userprog/process.c. */
    uint32_t *pagedir; /* Page directory. */
#endif

    /* Owned by thread.c. */
    unsigned magic; /* Detects stack overflow. */
};

这个信息用来给内核检查是否到达对应的时间,然后做相应的操作

添加线程休眠逻辑

现在有休眠队列了,在线程需要休眠的时候将其添加到这个队列中

// 用来比较线程等待至目标时间刻的函数
bool thread_less_wakeup_tick(const struct list_elem *a, const struct list_elem *b, void *aux UNUSED)
{
    struct thread *p_thread_a = list_entry(a, struct thread, elem);
    struct thread *p_thread_b = list_entry(b, struct thread, elem);
    return p_thread_a->wakeup_ticks < p_thread_b->wakeup_ticks;
}
// 这个记得在thread.h中声明,要在timer.c中用的
void thread_sleep(int64_t target_ticks)
{
    // 获取当前线程
    struct thread *cur = thread_current();;
    // 给当前线程添加苏醒时间刻
    cur->wakeup_ticks = target_ticks;
    // 按苏醒时间刻从小到大插入休眠队列
    list_insert_ordered(&sleep_list, &cur->elem, thread_less_wakeup_tick, NULL);
    // 当前线程进入阻塞态
    thread_block();
}

然后在timer_sleep中调用即可

void timer_sleep(int64_t ticks)
{
    enum intr_level old_level;
    ASSERT(intr_get_level() == INTR_ON);
    old_level = intr_disable();

    thread_sleep(timer_ticks() + ticks);

    intr_set_level(old_level);
}

除了thread_sleep的代码跟中断处理有关系,我还没摸透

添加线程苏醒逻辑

现在线程会休眠了,但线程自己不会苏醒,需要内核操作
既然跟时间相关,那么就应该在时钟中断里面做点什么
也就是./devices/timer.ctimer_interrupt函数

static void timer_interrupt(struct intr_frame *args UNUSED)
{
    ticks++;
    thread_tick();
    // lab1 将到达休眠时间将线程移出休眠队列
    thread_wakeup();
}

这里的thread_wakeup是需要在./threads/thread.c中实现的函数

// 记得添加声明到thread.h中
void thread_wakeup(void)
{
    struct list_elem *p_elem;
    struct thread *p_thread;

    while (!list_empty(&sleep_list))
    {
        p_elem = list_front(&sleep_list);
        p_thread = list_entry(pe, struct thread, elem);
        // 休眠队列本身就是从小到大排序的,可以这样提前退出
        if (p_thread->wakeup_ticks > timer_ticks())
        {
            break;
        }
        // 移除出休眠队列
        list_remove(p_elem);
        // 让pt指向的线程退出阻塞态进入准备态
        thread_unblock(p_thread);
    }
}

运行结果

运行测试alarm-multiple

(alarm-multiple) begin
(alarm-multiple) Creating 5 threads to sleep 7 times each.
(alarm-multiple) Thread 0 sleeps 10 ticks each time,
(alarm-multiple) thread 1 sleeps 20 ticks each time, and so on.
(alarm-multiple) If successful, product of iteration count and
(alarm-multiple) sleep duration will appear in nondescending order.
(alarm-multiple) thread 0: duration=10, iteration=1, product=10
(alarm-multiple) thread 1: duration=20, iteration=1, product=20
(alarm-multiple) thread 0: duration=10, iteration=2, product=20
(alarm-multiple) thread 2: duration=30, iteration=1, product=30
(alarm-multiple) thread 0: duration=10, iteration=3, product=30
(alarm-multiple) thread 3: duration=40, iteration=1, product=40
(alarm-multiple) thread 1: duration=20, iteration=2, product=40
(alarm-multiple) thread 0: duration=10, iteration=4, product=40
(alarm-multiple) thread 4: duration=50, iteration=1, product=50
(alarm-multiple) thread 0: duration=10, iteration=5, product=50
(alarm-multiple) thread 2: duration=30, iteration=2, product=60
(alarm-multiple) thread 1: duration=20, iteration=3, product=60
(alarm-multiple) thread 0: duration=10, iteration=6, product=60
(alarm-multiple) thread 0: duration=10, iteration=7, product=70
(alarm-multiple) thread 3: duration=40, iteration=2, product=80
(alarm-multiple) thread 1: duration=20, iteration=4, product=80
(alarm-multiple) thread 2: duration=30, iteration=3, product=90
(alarm-multiple) thread 4: duration=50, iteration=2, product=100
(alarm-multiple) thread 1: duration=20, iteration=5, product=100
(alarm-multiple) thread 3: duration=40, iteration=3, product=120
(alarm-multiple) thread 2: duration=30, iteration=4, product=120
(alarm-multiple) thread 1: duration=20, iteration=6, product=120
(alarm-multiple) thread 1: duration=20, iteration=7, product=140
(alarm-multiple) thread 4: duration=50, iteration=3, product=150
(alarm-multiple) thread 2: duration=30, iteration=5, product=150
(alarm-multiple) thread 3: duration=40, iteration=4, product=160
(alarm-multiple) thread 2: duration=30, iteration=6, product=180
(alarm-multiple) thread 4: duration=50, iteration=4, product=200
(alarm-multiple) thread 3: duration=40, iteration=5, product=200
(alarm-multiple) thread 2: duration=30, iteration=7, product=210
(alarm-multiple) thread 3: duration=40, iteration=6, product=240
(alarm-multiple) thread 4: duration=50, iteration=5, product=250
(alarm-multiple) thread 3: duration=40, iteration=7, product=280
(alarm-multiple) thread 4: duration=50, iteration=6, product=300
(alarm-multiple) thread 4: duration=50, iteration=7, product=350
(alarm-multiple) end

当然,有人要说了,这样看不出来跟忙等待实现的差别啊
这里就可以在./threads/init.c的main函数中加点东西

// main函数,东西太多就不贴了
	// ...
    printf("Boot complete.\n");

    /* Run actions specified on kernel command line. */
    run_actions(argv);
    // 加上这句
    thread_print_stats();

    /* Finish up. */
    shutdown();
    thread_exit();
    // ...

然后执行,看最后的一句输出

忙等待实现是这样的

Thread: 0 idle ticks, 592 kernel ticks, 0 user ticks

休眠队列的实现是这样的

Thread: 550 idle ticks, 40 kernel ticks, 0 user ticks

可以看到忙等待实现不会跑idle

需要注意的是,虽然alarm-priority在这个部分,但这个测试目前是过不了的
要到task2写完才可以

<0x02> Task2: Priority Scheduling

测试目标为[Lab1] Default Scheduler Debug

任务摘要

优先级调度器不会在之后的项目中使用

Task2.1 实现优先级调度

当一个线程加入到就绪队列时,如果这个新线程的优先级高于当前线程,当前线程应该立即让步
同样的,如果线程正在等待锁、信号或者条件时,高优先级的线程应当最先唤醒
线程自己可以改变优先级,但如果优先级改变后,自己不具有最高优先级,应当立即让步

Task2.2

优先级反转问题

考虑三个线程,分别有高中低的优先级
低优先级线程占有一个锁,然后这时候高优先级线程也想占用这个锁
那么高优先级线程会被阻塞,等待低优先级线程释放这个锁
但如果后面中优先级的线程启动了,那么由于优先级确实比低优先级高
中优先级线程会在低优先级线程时钟中断后执行,并且直到处理完毕
本来应该最优先处理的高优先级线程实际上没有最优先处理
这个现象称为优先级反转

priority
   ↑
   │          ↓ 高优先级线程尝试获取锁,但会阻塞
   │         │─│                            │ high ──────────│
   │             ↓ 中优先级启动               ↑ 高优先级反而最后处理
   │             │ mid ─────────────│
   │  ↓ 低优先级启动
   │  │ low ─────│                  │ low ──│
   │             ↑ 低优先级时间片耗尽
   └────────────────────────────────────────────────────────────→ time

所以需要实现优先级借用机制来解决这个问题
(原词是priority donation,我觉得翻译成优先级借用比较合适)
优先级借用就是如果有高优先级线程尝试获取锁
那么原来低优先级线程暂时获得高优先级线程的优先级
还是前面例子,如果使用优先级借用机制,那么优先级与时间关系图将会时这样

priority
   ↑
   │          ↓ 尝试获取锁    ↓ 低优先级线程处理完毕后马上处理高优先级
   │         │─││ low ──────││ high ──────────│
   │            ↑ 低优先级线程优先级暂时提升      ↓ 中优先级启动
   │                                          │ mid ───────────│
   │  ↓ 低优先级启动
   │  │ low ─│
   │
   └────────────────────────────────────────────────────────────→ time

低优先级线程优先级会暂时提升到高优先级线程的水平
实现低优先级处理完就处理高优先级线程任务

你需要考虑所有的优先级借用的情况
必须实现锁的优先级借用,其他的同步结构的优先级借用不必实现
你需要实现所有情况的优先级调度
确保多个优先级借用时的工作情况

支持嵌套优先级借用

还是一样的优先级借用,但是需要考虑嵌套时的优先级借用的传递问题

Task2.3 实现优先级的获取与修改

这个任务需要你修改thread_set_prioritythread_get_priority 对于thread_set_priority,如果新优先级不具有最高的优先级,那么当前线程应当让步
对于thread_get_priority,返回当前优先级或者说借用的优先级

pintos默认实现的东西

pintos虽然没有实现优先级调度
但已经提供了一套优先级
范围0到63,线程默认优先级为31
调度没做高优先级优先调度的功能,是先进先出的

在信号量、锁、条件这些同步设施上,pintos的实现也是最基本的
只是能用,也都没做优先级处理的操作

代码实现

Task2.1

这部分还是简单的

先在./threads/thread.c实现一个线程优先级比较函数

// lab1 线程优先级比较函数
bool thread_more_priority(const struct list_elem *a, const struct list_elem *b, void *aux UNUSED)
{
    struct thread *p_thread_a = list_entry(a, struct thread, elem);
    struct thread *p_thread_b = list_entry(b, struct thread, elem);
    return p_thread_a->priority > p_thread_b->priority;
}

修改的是thread_create函数

Task2.2

这个可以说是这个lab最难的部分
如果lock的部分没写好,main线程初始化都会出问题,调试都不好调试
所以很多时候都不知道自己哪里错了

Task2.3

thread_get_priority非常简单,直接返回当前线程优先级

int thread_get_priority(void)
{
    return thread_current()->priority;
}

thread_set_priority也很简单,只是要处理一下锁的优先级借用问题

// lab1 当前线程是否持有锁
bool thread_is_holding_lock(void)
{
    struct thread *cur = thread_current();
    return !list_empty(&cur->lock_list);
}
void thread_set_priority(int new_priority)
{
    // 断言限制优先级范围
    ASSERT(PRI_MIN <= new_priority && new_priority <= PRI_MAX);

    struct thread *cur = thread_current();
    cur->base_priority = new_priority;
    if (!thread_is_holding_lock() || new_priority > cur->priority)
    {
        cur->priority = new_priority;
        thread_yield();
    }
}

如果没持有锁,那么直接更新就可以,如果有锁,那么还要判断下是否大于当前优先级
基础优先级是一定会更新的

<0x03> Task3: Advanced Scheduler

测试目标为[Lab1] Advanced Scheduler Debug

任务摘要

实现附录中的BSD调度

高级调度器不会在之后的项目中使用

BSD调度

那么什么是BSD调度呢
前面我们实现的调度是基于优先级的,线程需要根据自己的情况给内核一个优先级
但这样不够智能,更何况如果有恶意线程啥都不干但占用最高优先级,那不就完了

BSD调度算是一种优先级计算方法,它会评估系统总体的占用负载和线程的cpu使用率
然后给线程一个合理的优先级

整个BSD调度中线程需要新增两个量,nicenessrecent_cpu
niceness表示线程的好心程度,范围是-20~20,默认值为0
niceness越大,表示这个线程越好心,越愿意让步主动权
相反,如果这个值越小,表示这个线程不希望让步
recent_cpu表示这个线程的cpu使用频率,没啥好说的

BSD调度的优先级计算方法如下,每4个时钟中断更新一次
$$priority=PRI_MAX-\frac{recent_cpu}{4}-2niceness$$ 其中,recent_cpu的更新方法如下
每个时钟中断时,recent_cpu自增1
每过了物理时间1s,recent_cpu按下面公式重新计算
$$recent_cpu=\frac{2
load_avg}{2*load_avg+1}*recent_cpu+niceness$$ 这里出现的load_avg表示系统负载水平,计算公式如下,每物理时间1s更新一次
$$load_avg=\frac{59}{60}*load_avg+\frac{1}{60}*ready_threads$$ ready_threads表示不包括idle的运行线程和就绪线程的个数

(这里省略了很多公式推导的细节)

代码实现

这个还是比较简单的,基本上按着文档走就可以

实现模拟浮点库

pintos本身没有浮点类型,但BSD调度中会用到小数
好在文档的后面提供了一个方案,可以用模拟的浮点数
这个就按文档最后实现一下就好了
在我的代码中,实现的文件是./threads/fp.h./threads/fp.c
这里就不贴了,挺重复的

需要指出的是,我使用了typedef int fp;,提示自己是浮点数

禁用优先级借用机制

文档中有提到,BSD调度器不需要优先级借用机制
所以就把它禁用掉就可以了
在测试这个任务的代码时,会传入-mlfqs这个参数
这个参数会使thread_mlfqs变为true
通过这个值可以判断是否测试的是高级调度器

void lock_acquire(struct lock *lock)
{
    ASSERT(lock != NULL);
    ASSERT(!intr_context());
    ASSERT(!lock_held_by_current_thread(lock));

    // lab1 高级调度不需要优先级借用
    if (!thread_mlfqs)
    {
        lock_acquire_priority_donation(lock);
    }

    sema_down(&lock->semaphore);
    lock->holder = thread_current();
}
void lock_release(struct lock *lock)
{
    ASSERT(lock != NULL);
    ASSERT(lock_held_by_current_thread(lock));

    // lab1 高级调度不需要优先级借用
    if (!thread_mlfqs)
    {
        lock_release_priority_donation(lock);
    }

    lock->holder = NULL;
    sema_up(&lock->semaphore);
}

lock_acquire_priority_donationlock_release_priority_donation就是前面实现的优先级借用方法
只是把这两个封装成单独的函数而已

修改thread相关的代码

./threads/thread.h中添加niceness的范围与默认值定义

// lab1 高级调度 niceness部分
#define NICENESS_MIN -20
#define NICENESS_DEFAULT 0
#define NICENESS_MAX 20

添加load_avg的定义(在./threads/thread.c中也加一个)

// lab1 高级调度 系统负载
extern fp thread_load_avg;

给线程控制块加上nicenessrecent_cpu这两个值

struct thread
{
    // ...
    int niceness;   // lab1 高级调度 线程的好心程度
    fp recent_cpu;  // lab1 高级调度 线程使用的cpu时间
	// ...
};

实现BSD调度的公式计算

因为这个调度的值都是线程相关的,所以相关的计算我也就放在./threads/thread.c里面了
后面./devices/timer.c也会用到这几个函数,所以要添加声明到对应头文件中

// lab1 高级调度 基于nice与recent_cpu计算线程优先级
void thread_mlfqs_refresh_priority(struct thread *t)
{
    if (t == idle_thread)
    {
        return;
    }
    fp f_priority = fp_convert_from(PRI_MAX);
    // 1/4 * recent_cpu
    fp f_a = fp_div_int(t->recent_cpu, 4);
    // 2*niceness
    fp f_b = fp_convert_from(t->niceness);
    f_b = fp_mul_int(f_b, 2);
    // PRI_MAX - 1/4 * recent_cpu - 2 * niceness
    fp f_ans = fp_sub_fp(f_priority, f_a);
    f_ans = fp_sub_fp(f_ans, f_b);

    t->priority = fp_toint_round(f_ans);
}

// lab1 高级调度 当前线程recent_cpu自增1
void thread_mlfqs_increase_recent_cpu(void)
{
    struct thread *cur = thread_current();
    if (cur == idle_thread)
    {
        return;
    }
    cur->recent_cpu = fp_add_int(cur->recent_cpu, 1);
}

// lab1 高级调度 计算线程最近cpu使用率
void thread_mlfqs_refresh_recent_cpu(struct thread *t)
{
    if (t == idle_thread)
    {
        return;
    }
    // (2 * load_avg) / (2 * laod_avg + 1) * recent_cpu
    fp f_a = fp_mul_int(thread_load_avg, 2);
    f_a = fp_div_fp(f_a, fp_add_int(f_a, 1));
    f_a = fp_mul_fp(f_a, t->recent_cpu);
    fp f_niceness = fp_convert_from(t->niceness);
    // (2 * load_avg) / (2 * laod_avg + 1) * recent_cpu + niceness
    t->recent_cpu = fp_add_fp(f_a, f_niceness);
}

// lab1 高级调度 计算系统使用负载
void thread_mlfqs_refresh_load_avg(void)
{
    // 59/60 * load_avg
    fp f_a = fp_convert_from(59);
    f_a = fp_div_int(f_a, 60);
    f_a = fp_mul_fp(f_a, thread_load_avg);
    // 1/60 * ready_threads
    fp f_b = fp_convert_from(1);
    f_b = fp_div_int(f_b, 60);
    f_b = fp_mul_int(f_b, list_size(&all_list) - list_size(&sleep_list) - 1);
    // 59/60 * load_avg + 1/60 * ready_threads
    thread_load_avg = fp_add_fp(f_a, f_b);
}

修改时钟中断行为

毕竟是涉及到时间的操作,相关代码修改在./devices/timer.c中完成

static void timer_interrupt(struct intr_frame *args UNUSED)
{
    ticks++;
    thread_tick();
    // lab1 将到达休眠时间将线程移出休眠队列
    thread_wakeup();

    // lab1 高级调度
    if (thread_mlfqs)
    {
	    // 每tick增加当前线程recent_cpu
        thread_mlfqs_increase_recent_cpu();
        if (timer_ticks() % TIMER_FREQ == 0)
        {
	        // 每1s重新计算系统负载和所有线程的recent_cpu
            thread_mlfqs_refresh_load_avg();
            thread_foreach(thread_mlfqs_refresh_recent_cpu, NULL);
        }
        if (timer_ticks() % 4 == 0)
        {
	        // 每4个tick重新计算所有线程的优先级
            thread_foreach(thread_mlfqs_refresh_priority, NULL);
        }
    }
}

以上完成了所有修改,并且所有的测试也都可以过了
这部分的测试耗时很长,这个是正常的