既然是依据优先级运行线程,那我们就来看看优先级在线程中是怎么存在的
1 struct thread 2 { 3 /* Owned by thread.c. */ 4 tid_t tid; /* Thread identifier. */ 5 enum thread_status status; /* Thread state. */ 6 char name[16]; /* Name (for debugging purposes). */ 7 uint8_t *stack; /* Saved stack pointer. */ 8 int priority; /* Priority. */ 9 struct list_elem allelem; /* List element for all threads list. */ 10 11 /* Shared between thread.c and synch.c. */ 12 struct list_elem elem; /* List element. */ 13 14 #ifdef USERPROG 15 /* Owned by userprog/process.c. */ 16 uint32_t *pagedir; /* Page directory. */ 17 #endif 18 19 20 int64_t sleep_ticks; /* # of timer ticks since blocked. */ 21 22 23 /* Owned by thread.c. */ 24 unsigned magic; /* Detects stack overflow. */ 25 };
在线程结构体里面有优先级这一成员,简简单单一个整型数据,那就不需要我们去定义了,教员讲了众多线程的组织形式是通用链表,当一个线程所有资源都准备好了之后就会放到一个就绪队列里面,当CUP空出来的时候,就会从就绪队列里面取一个来运行,至于取哪一个就要看定义的策略了
我们先来看看pintos现在是如何实现或者说如何维护就绪队列的
参考了前辈的经验得知在两个地方会涉及到将准备好的线程插入到就绪队列里面去:thread_unblock和thread_yield
首先来看thread_unblock
1 void 2 thread_unblock (struct thread *t) //解除阻塞态,线程A调用来解除B的阻塞态 3 { 4 enum intr_level old_level; 5 ASSERT (is_thread (t)); 6 old_level = intr_disable (); //关中断 7 ASSERT (t->status == THREAD_BLOCKED); 8 list_push_back (&ready_list, &t->elem); //放在就绪队列尾部 9 t->status = THREAD_READY; 10 intr_set_level (old_level); //开中断 11 }
thread_unblock会把被阻塞的线程解除阻塞态,然后放到就绪队列里面去等待CPU运行,我们跟进list_push_back看看具体是怎么放的
1 void 2 list_push_back (struct list *list, struct list_elem *elem) 3 { 4 list_insert (list_end (list), elem); 5 }
继续跟进
1 void 2 list_insert (struct list_elem *before, struct list_elem *elem) 3 { 4 ASSERT (is_interior (before) || is_tail (before)); 5 ASSERT (elem != NULL); 6 7 elem->prev = before->prev; //依据通用链表连接规则,将elem插入到before前面 8 elem->next = before; //即把他作为链表的最后一个元素 9 before->prev->next = elem; 10 before->prev = elem; 11 } 12 13 struct list_elem * //即取链表的尾部tail 14 list_end (struct list *list) 15 { 16 ASSERT (list != NULL); 17 return &list->tail; 18 }
分析下来,线程解除阻塞之后直接被放到了就绪队列的末尾
再看thread_yield,发现做法如出一辙
1 void 2 thread_yield (void) //让出CPU 3 { 4 struct thread *cur = thread_current (); //获取当前运行的线程 5 enum intr_level old_level; 6 7 ASSERT (!intr_context ()); 8 9 old_level = intr_disable (); //关中断 10 if (cur != idle_thread) 11 list_push_back (&ready_list, &cur->elem); //放在就绪队列尾部 12 cur->status = THREAD_READY; //就绪态 13 schedule (); //调度,从就绪队列里面取一个 14 intr_set_level (old_level); 15 }
接着看pintos是如何从就绪队列里面取出线程的
找来找去,只有发现schedule才会调度新的线程让CPU运行
1 static void 2 schedule (void) 3 { 4 struct thread *cur = running_thread (); //调用schedule之前被阻塞的线程 5 struct thread *next = next_thread_to_run (); //下一个要运行的线程 6 struct thread *prev = NULL; 7 8 ASSERT (intr_get_level () == INTR_OFF); 9 ASSERT (cur->status != THREAD_RUNNING); 10 ASSERT (is_thread (next)); 11 12 if (cur != next) 13 prev = switch_threads (cur, next); //线程切换 14 thread_schedule_tail (prev); //杀死线程 15 }
不难发现取线程的关键在于next_thread_to_run,跟进看一下
1 static struct thread * 2 next_thread_to_run (void) 3 { 4 if (list_empty (&ready_list)) 5 return idle_thread; 6 else 7 return list_entry (list_pop_front (&ready_list), struct thread, elem); 8 /*当就绪队列非空时使用宏定义(list_entry)取出其中的第一个线程*/ 9 } 10 11 struct list_elem * 12 list_pop_front (struct list *list) 13 { 14 struct list_elem *front = list_front (list); //取list里面的第一个元素 15 list_remove (front); //删除list的第一个元素 16 return front; //第一个元素作为返回值 17 }
到这里基本上可以形成两种思路:
- 在放入队列时即按照优先级的顺序插入,取的时候从头开始
- 放的时候直接放在尾巴上即可,取得时候拿优先级最高的
个人认为实现后者较为简单方便,这里就先实现第二种思路,主要对next_thread_to_run做手脚,将取第一个元素修改为取优先级最高的,即将lisi_pop_front修改为list_max_priority
在实践过程中,翻阅list.c里面的函数,发现了一个函数list_max
1 /* Returns the element in LIST with the largest value according 2 to LESS given auxiliary data AUX. If there is more than one 3 maximum, returns the one that appears earlier in the list. If 4 the list is empty, returns its tail. */ 5 struct list_elem * 6 list_max (struct list *list, list_less_func *less, void *aux) 7 { 8 struct list_elem *max = list_begin (list); 9 if (max != list_end (list)) 10 { 11 struct list_elem *e; 12 13 for (e = list_next (max); e != list_end (list); e = list_next (e)) 14 if (less (max, e, aux)) 15 max = e; 16 } 17 return max; 18 }
这就很nice了,先看less是什么
1 /* Compares the value of two list elements A and B, given 2 auxiliary data AUX. Returns true if A is less than B, or 3 false if A is greater than or equal to B. */ 4 typedef bool list_less_func (const struct list_elem *a, 5 const struct list_elem *b, 6 void *aux);
如果a<b,则返回true,那么list_max函数功能即遍历list,找出最不less的,也就是for循环里面的if,如果max<e,则max=e,注意这里的<不能带=,因为这里的max和e都是链表元素,而我们要比较的是线程的优先级,就需要我们通过线程的elem找到其结构体成员priority,我这里借用了一下laiy的优先级比较函数,返回值和less同步,即a<b时返回true
1 /* priority compare function. */ 2 bool 3 thread_cmp_priority (const struct list_elem *a, const struct list_elem *b, void *aux UNUSED) 4 { 5 return list_entry(a, struct thread, elem)->priority <
list_entry(b, struct thread, elem)->priority; 6 }
直接修改next_thread_to_run,并在thread.c中引用了头文件list.h
1 static struct thread * 2 next_thread_to_run (void) 3 { 4 if (list_empty (&ready_list)) 5 return idle_thread; 6 else 7 return list_entry (list_max (&ready_list,thread_cmp_priority,NULL), struct thread, elem); 8 }
完成后直接运行测试,发现测试陷入了死循环,久久不能跳出,对比list_max和list_pop_front发现,list_max在返回之前少了一句list_remove,这就使得就绪的线程运行完之后并不会被删掉,从而在下一轮继续运行,使测试程序陷入死循环,为了不影响整体代码的可扩展性,我这里手动复制了一下list_max,并更名为list_max_priority,直接放在next_thread_to_run上面
1 struct list_elem * 2 list_max_priority (struct list *list, list_less_func *less, void *aux) 3 { 4 struct list_elem *max = list_begin (list); 5 if (max != list_end (list)) 6 { 7 struct list_elem *e; 8 9 for (e = list_next(max); e != list_end (list); e = list_next(e)) 10 if (less (max, e, aux)) 11 max = e; 12 } 13 list_remove (max); 14 return max; 15 }
将list_max_priority和thread_cmp_priority的声明加在thread.c的头上
1 struct list_elem *list_max_priority (struct list *, list_less_func *, void *aux); 2 bool thread_cmp_priority (const struct list_elem *, const struct list_elem *, void *aux);
保存,测试,通过alarm-priority
至此,我们就有了一个利器,即切换线程时按照优先级降序执行,现在来分析priority-change,pintos里面的函数实现为thread-set-priority
1 /* Sets the current thread‘s priority to NEW_PRIORITY. */ 2 void 3 thread_set_priority (int new_priority) 4 { 5 thread_current ()->priority = new_priority; 6 }
这里就一个动作,即该调用函数后将当前线程的优先级修改为新的优先级,这时就牵扯到优先级高低和程序执行的问题了,如果修改后的优先级仍为就绪队列里面的第一名(或是并列第一),则继续运行该进程;反之,则调度新的第一名运行,而当前进程被放入就绪队列,此时自然而然想到函数thread_yield
1 void 2 thread_set_priority (int new_priority) 3 { 4 thread_current ()->priority = new_priority; 5 thread_yield (); 6 }
prioirty-preempt,优先级抢占式调度,即当创建一个新的进程时,如果新进程的优先级高于当前正在运行进程的优先级,则当前线程让出CPU,让新线程运行,那么我们只需要在创建线程之后加入一个条件语句即可,加在thread_unblock之后
1 if (thread_current ()->priority < priority) //抢占 2 thread_yield (); 3 return tid;
保存,测试,通过priority-change和priority-preempt
接下来是priority-sema,先来看看sema_down
1 void 2 sema_down (struct semaphore *sema) 3 { 4 enum intr_level old_level; 5 6 ASSERT (sema != NULL); 7 ASSERT (!intr_context ()); 8 9 old_level = intr_disable (); 10 while (sema->value == 0) //注意用while而不用if的原因 11 { 12 list_push_back (&sema->waiters, &thread_current ()->elem); //如果没资源了,就把当前线程放在等待队列的末尾 13 thread_block (); //阻塞该进程,被唤醒之后,继续从这里开始执行 14 } 15 sema->value--; 16 intr_set_level (old_level); 17 }
接着看看sema_up
1 void 2 sema_up (struct semaphore *sema) 3 { 4 enum intr_level old_level; 5 6 ASSERT (sema != NULL); 7 8 old_level = intr_disable (); //关中断 9 if (!list_empty (&sema->waiters)) //如果有进程在等待 10 thread_unblock (list_entry (list_pop_front (&sema->waiters), //则从等待队列里面去第一个放入就绪队列里面去 11 struct thread, elem)); //等待调度 12 sema->value++; 13 intr_set_level (old_level); 14 }
思路应该已经很清晰了,从等待队列里面取是应该拿优先级最大的,放入就绪队列之后应进行优先级调度,只需修改sema_up即可
1 void 2 sema_up (struct semaphore *sema) 3 { 4 enum intr_level old_level; 5 6 ASSERT (sema != NULL); 7 8 old_level = intr_disable (); 9 if (!list_empty (&sema->waiters)) 10 thread_unblock (list_entry (list_max_priority ((&sema->waiters),thread_cmp_priority,NULL), 11 struct thread, elem)); 12 sema->value++; 13 thread_yield(); 14 intr_set_level (old_level); 15 }
测试,通过!
至此,4项实验内容均已完成