Pintos Project1

688 阅读8分钟

Pintos Project1

重写timer_sleep()函数

函数原型:void timer_sleep (int64_t ticks)
挂起调用线程的执行,直到时间至少提前x个计时器计时。除非系统处于空闲状态,否则线程不需要在恰好x个滴答声之后唤醒。在他们等待了适当的时间之后,把它放到准备好的队列中。

先来看函数的原实现:

/* Sleeps for approximately TICKS timer ticks.  Interrupts must
   be turned on. */
void
timer_sleep (int64_t ticks) 
{
  int64_t start = timer_ticks ();

  ASSERT (intr_get_level () == INTR_ON);
  while (timer_elapsed (start) < ticks) 
    thread_yield ();
}

可以看到函数的原实现是有"busy waiting"的问题的,即一个线程会在循环中不停等待,直到时间片耗尽。我们需要重新实现它来避免busy waiting。

原代码解析

先来看timer_ticks() 函数干了哪些工作:

/* Returns the number of timer ticks since the OS booted. */
int64_t
timer_ticks (void) 
{
  enum intr_level old_level = intr_disable ();
  int64_t t = ticks;
  intr_set_level (old_level);
  return t;
}

第一行调用了intr_disable()函数,其源代码是:

/* Disables interrupts and returns the previous interrupt status. */
enum intr_level
intr_disable (void) 
{
  enum intr_level old_level = intr_get_level ();

  /* Disable interrupts by clearing the interrupt flag.
     See [IA32-v2b] "CLI" and [IA32-v3a] 5.8.1 "Masking Maskable
     Hardware Interrupts". */
  asm volatile ("cli" : : : "memory");

  return old_level;
}

该函数的作用是禁用中断并返回之前的中断状态。

现在timer_ticks()的函数作用就很明确了,首先它调用intr_disable()函数禁用了中断,保证获取系统ticks时不被中断,再调用intr_set_level()函数恢复原来的状态,最后返回ticks。

接着,我们来看timer_elapsed()函数:

/* Returns the number of timer ticks elapsed since THEN, which
   should be a value once returned by timer_ticks(). */
int64_t
timer_elapsed (int64_t then) 
{
  return timer_ticks () - then;
}

该函数的作用是返回then之后经过的ticks数。

最后我们看thread_yield()函数:

/* Yields the CPU.  The current thread is not put to sleep and
   may be scheduled again immediately at the scheduler's whim. */
void
thread_yield (void) 
{
  struct thread *cur = thread_current ();
  enum intr_level old_level;
  
  ASSERT (!intr_context ());

  old_level = intr_disable ();
  if (cur != idle_thread) 
    list_push_back (&ready_list, &cur->elem);
  cur->status = THREAD_READY;
  schedule ();
  intr_set_level (old_level);
}

该函数先用一个指针保存了thread_current()返回的当前线程,在确保不是外中断后(intr_context()当外中断时返回true),禁用中断,如果当前线程不是idle的话(这里解释下idle,其就是在thread_start()里创建了之后放到list里后来启用中断的,之后就移除ready_list了,并且不会再次出现在其中),就将当前线程加入到ready_list中,之后将当前线程的状态调整为THREAD_READY后,调用schedule()函数。 故我们再来看一下schedule()函数:

/* Schedules a new process.  At entry, interrupts must be off and
   the running process's state must have been changed from
   running to some other state.  This function finds another
   thread to run and switches to it.

   It's not safe to call printf() until thread_schedule_tail()
   has completed. */
static void
schedule (void) 
{
  struct thread *cur = running_thread ();
  struct thread *next = next_thread_to_run ();
  struct thread *prev = NULL;

  ASSERT (intr_get_level () == INTR_OFF);
  ASSERT (cur->status != THREAD_RUNNING);
  ASSERT (is_thread (next));

  if (cur != next)
    prev = switch_threads (cur, next);
  thread_schedule_tail (prev);
}

该函数调度一个新进程。当然在调度开始时,必须确保中断关闭以确保其原子性,并且正在运行的线程要已经从运行态改变到其他状态了。如果当前线程不是下个要切换的线程,那么就调用switch_threads(),并用prev变量储存上一个线程,再对prev线程进行尾处理(不能在切换线程进行处理是因为线程切换需要之前线程的一些信息,如果这时候进行尾处理,如销毁线程等,会使线程切换出现问题)。

至此,我们简要分析了几个涉及到的函数作用,从整体上来看,timer_sleep()就是要当前线程循环等待ticks时间,然后通过thread_yield()切换到下一个线程。

至此,我们的解析可以看出这个函数的缺点也是我们需要改进的地方:当某个线程调用这个函数需要"sleep"时,它不会释放cpu资源,而是一直等待指定的ticks,才切换,这无疑会浪费cpu资源。

函数改写实现思路

总体思路:当一个函数需要sleep时,直接阻塞它,切换到下一个线程,直到到它需要唤醒的tick,唤醒它,把它放到就绪队列中。

具体实现:给线程结构体添加ticks_wakeup属性,以记录其需要被唤醒的tick,然后用thread_block()阻塞它,后在每个tick,若该线程被阻塞,且tick_wakeup不为零的话,则tick_wakeup--, 并检查一遍线程是否需要被唤醒,若需要,则用thread_unblock()唤醒该线程。

在thread.h里的thread结构体里加入tick_wakeup属性:

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. */

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

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

然后在thread_init()里初始化ticks_wakeup为0。 然后在thread.h和thread.c中分别声明和实现一个检查wakup属性的函数,实现:

void
thread_wakeup_check(struct thread* t, void* aux UNUSED)
{
  if(t->ticks_wakeup > 0 && t->status == THREAD_BLOCKED){
    t->ticks_wakeup--;
     if(t->ticks_wakeup <= 0){
      thread_unblock(t);
    }
  }
}

然后修改timer_interrupt()函数,使他每个tick检查一遍线程状态。 再修改timer_sleep()函数:

/* Sleeps for approximately TICKS timer ticks.  Interrupts must
   be turned on. */
void
timer_sleep (int64_t ticks) 
{
  if(ticks <= 0) {
    return;
  }
  int64_t start = timer_ticks ();
  struct thread* cur = thread_current();
  
  ASSERT (intr_get_level () == INTR_ON);
  enum intr_level old_level = intr_disable();
  cur->ticks_wakeup = ticks;
  thread_block();
  intr_set_level(old_level);
}

这里需要注意几点:

  • 将线程阻塞的过程代码需要保证原子性,不然可能会引发多重阻塞,ticks_wakeup读写错误等问题!
  • thread_block()后不必再次调用thread_yield()了,因为thread_yield()和thread_block()都做了同样的线程切换工作

实现优先级调度

要求:

  • 当优先级比当前高的线程就绪,应抢占当前线程
  • 当等待锁、信号量或条件变量时,应优先唤醒优先度高的线程
  • 线程可以随时提高或降低其优先度,当降低优先度使其不再具有最高优先级时,应立刻让出CPU资源(即调用thread_yield()函数)

注意事项:

  • 线程优先级的范围是从PRI_MAX(63)~PRI_MIN(0)之间的
  • 线程优先级作为创建线程的参数传递给thread_create()函数
  • 默认优先级为PRI_DEFAULT(31)

需要解决的问题:

  1. 优先级捐赠:当一个高优先级的线程需要等待一个低优先级的锁时,而又有一个中优先级的线程在等待队列里使低优先级线程得不到cpu资源,则我们需要把高优先级的优先度先暂时捐赠给低优先级,使他运行后将锁让给高优先级线程持有,再将优先级返还给原先高优先级线程。

先来解决优先级调度的问题

优先级调度本质上就是在每次schedule时都在就绪队列里挑选一个优先级最高的线程进行切换,那么我们只要把就绪队列改写为优先队列(最大堆)就行。

然而直接修改pintos原来用的双向链表工作量太大,我们可以用在list.h中定义的一个函数list_insert_ordered()来实现伪优先队列,再实现一个比较函数(高优先级的在前):

/* Compare thread by priority*/

bool 
priority_cmp_less(struct list_elem* a, struct list_elem* b , void* aux UNUSED){
  return list_entry(a, struct thread, elem) -> priority > list_entry(b, struct thread, elem) -> priority;
}

最后在thread_yield(),thread_unblock(),init_thread()中用到list_push_back的地方改为list_insert_ordered()即可。

再来解决优先级捐赠的问题

优先级捐赠问题是本人认为这个Project1中最繁琐的问题。 主要解决思路为高优先级线程将优先级捐赠给锁,锁再将优先级捐赠给当前持有者。
基本实现思路主要参考:Pintos-斯坦福大学操作系统Project详解-Project1 通过分析测试,我们可以得出优先级捐赠主要有以下要求:

  1. 在一个线程获取一个锁的时候, 如果拥有这个锁的线程优先级比自己低就提高它的优先级,并且如果这个锁还被别的锁锁着, 将会递归地捐赠优先级, 然后在这个线程释放掉这个锁之后恢复未捐赠逻辑下的优先级。

  2. 如果一个线程被多个线程捐赠, 维持当前优先级为捐赠优先级中的最大值(acquire和release之时)。

  3. 在对一个线程进行优先级设置的时候, 如果这个线程处于被捐赠状态, 则对original_priority进行设置, 然后如果设置的优先级大于当前优先级, 则改变当前优先级, 否则在捐赠状态取消的时候恢复original_priority。

  4. 在释放锁对一个锁优先级有改变的时候应考虑其余被捐赠优先级和当前优先级。

  5. 将信号量的等待队列实现为优先级队列。

  6. 将condition的waiters队列实现为优先级队列。

  7. 释放锁的时候若优先级改变则可以发生抢占。 首先给thread结构体中加入属性:

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. */

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

   int64_t ticks_wakeup;
   int base_priority;
   struct list lock_holding;
   struct lock* lock_waiting;
    /* Owned by thread.c. */
    unsigned magic;                     /* Detects stack overflow. */
  };

lock_holding为该线程持有的锁的链表,lock_waiting为当前线程等待的锁(为了实现递归捐赠)。 在init_thread()中加入上述值的初始化:

static void
init_thread (struct thread *t, const char *name, int priority)
{
  enum intr_level old_level;

  ASSERT (t != NULL);
  ASSERT (PRI_MIN <= priority && priority <= PRI_MAX);
  ASSERT (name != NULL);

  memset (t, 0, sizeof *t);
  t->status = THREAD_BLOCKED;
  strlcpy (t->name, name, sizeof t->name);
  t->stack = (uint8_t *) t + PGSIZE;
  t->priority = priority;
  t->base_priority = priority;
  list_init(&t->lock_holding);
  t->lock_waiting = NULL;
  t->magic = THREAD_MAGIC;

  old_level = intr_disable ();
  list_insert_ordered(&all_list, &t->allelem, priority_cmp_less,NULL); 
  intr_set_level (old_level);
}

再在锁结构体中加入属性:

struct lock 
  {
    struct thread *holder;      /* Thread holding lock (for debugging). */
    struct semaphore semaphore; /* Binary semaphore controlling access. */
    int have_thread_waiting;
    struct list_elem elem;
    int max_priority;
  };

初始化:

void
lock_init (struct lock *lock)
{
  ASSERT (lock != NULL);

  lock->holder = NULL;
  lock->have_thread_waiting = 0;
  lock->max_priority = PRI_MIN;
  sema_init (&lock->semaphore, 1);
}

优先级捐赠主要需要解决锁的问题,故我们先修改lock_acquire():

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

  struct lock* it;
  if(lock->holder != NULL && !thread_mlfqs){
    thread_current()->lock_waiting = lock;
    lock->have_thread_waiting++;
    it = lock;
    while(it && it->holder){
      thread_donate_priority(thread_current(), it);
      it = it->holder->lock_waiting;
    }
  }
  //intr_set_level(old_level);
  sema_down (&lock->semaphore);

   enum intr_level old_level = intr_disable();
   if(!thread_mlfqs){
    thread_current()->lock_waiting = NULL;
    thread_hold_lock(lock);
   }
  lock->holder = thread_current ();
  intr_set_level(old_level);
}

在sema_down()之前对锁进行递归捐赠,这里的thread_donate()实现如下:

void 
thread_donate_priority(struct thread* donator, struct lock* donatee){
  enum intr_level old_level = intr_disable();
  if(donator->priority > donatee->max_priority)
  {
    donatee->max_priority = donator->priority;
    thread_update_priority(donatee->holder);
  }
  intr_set_level(old_level);
}

如果捐赠者自身的优先级比锁拥有的最大优先级高, 那么就更新锁的最大优先级,并且更新锁持有者的优先级,这里的thread_update_priority()实现如下:

void 
thread_update_priority(struct thread* donatee)
{
  enum intr_level old_level = intr_disable();
  int max_priority =  donatee->base_priority;
  int max_priority_lock;
  if(!list_empty(&donatee->lock_holding))
  {
    list_sort(&donatee->lock_holding, lock_cmp_priority, NULL);
    max_priority_lock  = list_entry(list_front(&donatee->lock_holding),struct lock, elem)->max_priority;
    if(max_priority_lock >max_priority)
    {
      max_priority = max_priority_lock;
    }
  }
  donatee->priority = max_priority;
  if(thread_current()->priority < donatee->priority){
    thread_yield();
  }
  intr_set_level(old_level);
}

将线程优先级更新成当前持有锁的最高优先级,若没持有锁,则恢复成其自身基础优先级。线程优先级的改变导致某一线程优先级超过当前线程,则可以抢占。
再回到lock_acquire(), 在sema_down()后,这时锁的所有权已经回到当前线程, 通过thread_hold_lock()让当前线程持有锁:

void thread_hold_lock(struct lock* l){
  enum intr_level old_level = intr_disable();

  list_insert_ordered(&thread_current()->lock_holding, &l->elem, lock_cmp_priority, NULL);

  if(l->max_priority > thread_current()->priority){
    thread_current()->priority = l->max_priority; 
  }

  intr_set_level(old_level);
}

后面再来修改lock_release():

void
lock_release (struct lock *lock) 
{
  ASSERT (lock != NULL);
  ASSERT (lock_held_by_current_thread (lock));
  if(!thread_mlfqs) thread_remove_lock(lock);
  lock->holder = NULL;
  sema_up (&lock->semaphore);
}

可见,这里修改就加入了一个thread_remove_lock()函数:

void thread_remove_lock(struct lock* l){
  list_remove(&l->elem);
  if( l->have_thread_waiting == 0) return;
   enum intr_level old_level = intr_disable();
  thread_update_priority(thread_current());
  intr_set_level(old_level);
}

函数功能简单明了,就是将锁从链表中移除,并更新下当前线程的优先级。
最后我们再将sema->waiters,cond.waiters改成优先队列即可通过所有优先级捐赠的tests。

多级反馈队列

这个Mission很简单,根据文档提供的公式将load_avg, recent_cpu, priority, nice分别对应计算实现即可, 注意load_avg和recent_cpu每TIMER_FREQ更新一次,线程priority每4个ticks更新一次,当前线程的recent_cpu每个tick增加1。

在timer_interrupt()里对应实现就好。

最后,附上project1通关图:

2021-07-21 17-31-51 的屏幕截图.png

Project1完结撒花~~~