OS-课程设计 Project 1-threads Report
-Mission 1:重新实现timer_sleep()函数
用到的相关文件:pintos-anon/src/devices目录下的:timer.c
pintos-anon/src/threads目录下的:thread.h,thread.c
我们需要修改timer.c,thread.h,thread.c这三个文件内容使得线程休眠函数来保证pintos不会在一个线程休眠时忙等待。
修改成功后alarm-single,alarm-multiple,alarm-simultaneous,alarm-zero,alarm-negative可以Pass。
问题描述:
线程不断地在CPU的就绪队列和运行队列之间来回,CPU资源被占用, 需要修改函数来重新实现唤醒机制。
实现思路:
在线程结构体thread中,加入新的变量ticks_blocked,代替原来的ticks,若想要线程保持ready的时间,根据操作系统自身的时钟实现每中断一次就对ticks_blocked-1,并且检测线程的状态。直到ticks_blocked时间为0,运行的线程实现alarm。这样就无需将线程一遍遍的从运行状态中拖到就绪队列,减少CPU资源的消耗。
数据结构:
给线程结构体thread加入新的变量ticks_blocked,来记录保持就绪的时间。ticks_blocked的类型为int64_t,在初始化时使它为0。
struct thread{ /*在线程的结构体上增添ticks_blocked成员:*/ int64_t ticks_blocked; /*表示线程应该睡眠的时间(初始化时值设为0)*/ };
具体实现:
①在/home/pintos/pintos-anon/src/devices目录中,修改timer.c中的timer_sleep函数。在调用timer_sleep的时候直接把线程阻塞掉,线程结构体中的ticks_blocked记录了这个线程被sleep了多少时间, 然后利用操作系统自身的时钟中断(每个tick会执行一次)加入对线程状态的检测, 每次检测将ticks_blocked减1, 如果减到0就唤醒这个线程。
timer_sleep (int64_t ticks)` { if(ticks <= 0) /*sleep时间*/` { return; } ASSERT (intr_get_level () == INTR_ON);/*intr_get_level()返回当前中断处于哪个状态*/ enum intr_level old_level = intr_disable ();/*关中断CLI*/ struct thread *cur=thread_current();/*当前线程*/ cur->ticks_blocked=ticks; thread_block();/*阻塞*/ intr_set_level (old_level);/*开中断*/ }
②在/home/pintos/pintos-anon/src/devices目录中,修改timer.c中的timer_interrupt时钟中断处理函数,增加 thread_foreach (check_if_blocked_thread, NULL)函数,检测哪些进程使用block状态并且含有剩余的休眠时间。thread_foreach对每个线程都执行check_if_blocked_thread函数(函数需要自己添加到thread中),用来判断ticks_blocked的状态。修改如下:
static void timer_interrupt (struct intr_frame *args UNUSED) { ticks++; thread_foreach(check_if_blocked_thread,NULL); thread_tick (); }
③check_if_blocked_thread函数编写再thread.c中,作用是检测该线程的ticks_blocked是否已经减为0,若不为0,则减1;若为0,则调用thread_unblock函数使线程放入就绪队列中。实现如下所示:
void /*添加方法check_if_blocked_thread*/ check_if_blocked_thread(struct thread *t,void *aux UNUSED) { if(t->status == THREAD_BLOCKED && t->ticks_blocked > 0) { t->ticks_blocked--; if (t->ticks_blocked == 0) { thread_unblock(t);/*把线程放入就绪队列*/ } } }
这样timer_sleep函数唤醒机制实现完成。
-Mission 2:重新实现优先级调度
用到的相关文件:/home/pintos/pintos-anon/src/threads目录下的:thread.h,thread.c,synch.h ,synch.c。
我们需要实现优先级调度,即当一个线程被添加到具有比当前运行的线程更高优先级的就绪列表时,当前线程就应该马上将处理器放到新线程中去。
分为四个部分解决。优先队列问题,线程优先级改变和抢占式调度问题,线程同步问题,优先级捐赠问题。
1.优先队列问题
解决思路:
核心思想是维持就绪队列为一个优先级队列,即我们需要在插入线程到就绪队列的时候保证这个队列是一个优先级队列。
具体实现:
thread_unblock函数中使用 的list_push_back是直接扔到队尾,但是线程调度的时候下一个thread是直接取队头。修改这句代码,使用原来有编写好的的list_insert_ordered函数帮助实现优先级队列。
所以,修改thread_unblock函数,将list_push_back 替换为list_insert_ordered语句;同时,还需要对thread_yield和thread_init里的list_push_back作同样的修改:
/*例:thread_unblock修改*/ void thread_unblock (struct thread *t) { /*list_push_back (&ready_list, &t->elem);*/ //修改该语句 list_insert_ordered(&ready_list,&t->elem,(list_less_func *)&thread_cmp_priority,NULL); }
thread_cmp_priority函数是比较优先级,需要在thread.c中编写添加:
bool thread_cmp_priority(const struct list_elem *a,const struct list_elem *b,void *aux UNUSED) { return list_entry(a, struct thread, elem)->priority > list_entry(b, struct thread, elem)->priority; /*将指向列表元素列表的指针装换为指向嵌入列表元素的结构的指针,再比较优先级*/ }
至此,可以解决alarm_priority。
2.线程优先级改变和抢占式调度问题
问题描述:
高优先级对于低优先级的中断和抢占。
解决思路:
在创建一个线程的时候, 如果线程高于当前线程就先执行创建的线程,重新安排执行顺序。
具体实现:
①在线程设置完优先级之后立刻重新调度,将CPU让出,即增加thread_yield。所以修改thread_set_priority函数。
void thread_set_priority (int new_priority) {/*线程设置优先级*/ thread_current ()->priority = new_priority; thread_yield(); }
②若新创建的线程比主线程优先级高,则调用thread_yield。所以,修改thread_create函数,在thread_create函数最后在创建的线程并unblock后增加代码:
tid_t thread_create (const char *name, int priority,thread_func *function, void *aux) { ... thread_unblock (t); /*增加的代码*/ if (thread_current ()->priority < priority) { thread_yield (); } /********/ return tid; }
③如果新线程的优先级高于当前线程优先级,调用thread_yield()函数。在thread_unblock 唤醒进程函数中添加:
void thread_unblock (struct thread *t) { if (thread_current () ->priority < priority) thread_yield(); }
priority-fifo,priority_change和priority_preempt三个test可以成功Pass.
3.线程同步问题
问题描述:
相关的测试创建了多个不同优先级的进程,每个线程调用sema_down函数,其他得不到信号量的线程都得阻塞。而每次运行的线程释放信号量时必须确保优先级最高的线程继续执行。
用到的文件:修改home/pintos/pintos-anon/src/threads中的synch.c
数据结构:
查看semaphore 结构体,其中的waiters为阻塞队列,我们需要在waiters中取出优先级最高的thread线程,不需要修改该结构体:
struct semaphore { unsigned value; struct list waiters; /* 阻塞队列 */ };
具体实现:
原有的sema_up设计:只是把waiters最前面的线程取出来加入到ready_list。修改方法:在waiters中取出优先级最高的thread,并yield()。
修改sema_up函数:
void sema_up (struct semaphore *sema) { enum intr_level old_level; ASSERT (sema != NULL); old_level = intr_disable (); if (!list_empty (&sema->waiters)) { /*新增*/ list_sort(&sema->waiters, thread_cmp_priority,NULL); thread_unblock (list_entry (list_pop_front (&sema->waiters),struct thread, elem)); } sema->value++; thread_yield();/*增加*/ intr_set_level (old_level); }
同时,条件变量也维护了一个waiters用于存储等待接受条件变量的线程,修改cond_signal()函数使它可以唤醒优先级最高的线程,再重新获取锁。
增加cond_sema_cmp_priority函数,将队列中的线程按照优先级降序作为比较返回值,cond_sema_cmp_priority函数增加在synch.c中:
bool cond_sema_cmp_priority(const struct list_elem *a,const struct list_elem *b,void *aux UNUSED) { struct semaphore_elem *sa = list_entry (a, struct semaphore_elem, elem); struct semaphore_elem *sb = list_entry (b, struct semaphore_elem, elem); return list_entry(list_front(&sa->semaphore.waiters), struct thread, elem)->priority > list_entry(list_front(&sb->semaphore.waiters), struct thread, elem)->priority; }
修改cond_signal()函数,当等待cond的线程队列不为空的时候,利用上面实现的cond_sema_cmp_priority先对其排序:
void cond_signal (struct condition *cond, struct lock *lock UNUSED) { .... /*修改*/ if (!list_empty (&cond->waiters)) { list_sort(&cond->waiters, cond_sema_cmp_priority,NULL); sema_up (&list_entry (list_pop_front (&cond->waiters), struct semaphore_elem, elem)->semaphore); } }
可以Pass两个test: priority-sema 和priority-condvar。
4.优先级捐赠问题
涉及的这几个test的意义如下:
-
priority-donate-one:
一个线程获取一个锁的时候, 如果拥有这个锁的线程优先级比自己低就提高它的优先级,并且如果这个锁还被别的锁锁着, 将会递归地捐赠优先级, 然后在这个线程释放掉这个锁之后恢复未捐赠逻辑下的优先级。
-
priority-donate-multiple:
一个线程可能有多个锁,多个其他线程会因为这个线程而阻塞,用于多锁情况下优先级逻辑的正确性。
-
priority-donate-multiple2:
优先级的逻辑问题,和priority-donate-multiple差别不大。
-
priority-donate-nest:
优先级嵌套问题,优先级提升具有连环效应。
-
priority-donate-chain:
链式优先级捐赠, 测试多层优先级捐赠逻辑的正确性。
-
priority-donate-sema
信号量和锁混合触发。
-
priority-donate-lower
当修改一个被捐赠的线程优先级的时,测试行为正确性。
问题描述:
低优先级的线程对高优先级的线程需要访问的临界资源拥有线程锁,导致比高优先级的线程更低优先级的线程(不需要访问该临界资源)先运行,产生优先级翻转问题。
解决思路:
关键的问题是优先级嵌套,和因为互斥锁而导致的线程阻塞问题。当发现高优先级的任务因为低优先级任务占用资源而阻塞时,就将低优先级的优先级提升到等待它所占有的资源的最高优先级任务的优先级。
逻辑:
-
在一个线程获取一个锁的时候, 如果拥有这个锁的线程优先级比自己低就提高它的优先级,并且如果这个锁还被别的锁锁着, 将会递归地捐赠优先级, 然后在这个线程释放掉这个锁之后恢复未捐赠逻辑下的优先级。
-
如果一个线程被多个线程捐赠, 维持当前优先级为捐赠优先级中的最大值(acquire和release之时)。
-
在对一个线程进行优先级设置的时候, 如果这个线程处于被捐赠状态, 则对original_priority进行设置, 然后如果设置的优先级大于当前优先级, 则改变当前优先级, 否则在捐赠状态取消的时候恢复original_priority。
-
在释放锁对一个锁优先级有改变的时候应考虑其余被捐赠优先级和当前优先级。
-
释放锁的时候若优先级改变则可以发生抢占。
数据结构:
释放一个锁的时候,将该锁的拥有者改为该线程被捐赠的第二优先级,若没有其余捐赠者, 则恢复原始优先级。需要新的数据结构来记录所有对这个线程有捐赠行为的线程。
在thread.h中的thread数据结构中,添加属性,分别为:当前优先级、拥有的锁、等待的锁,添加如下:
struct thread {... int base_priority;/*基本优先级*/ struct list locks;/*线程所持有的锁*/ struct lock *lock_waiting;/*线程等待的锁*/ }
在init_thread中加入这三个变量的初始化
static void init_thread (struct thread *t, const char *name, int priority) { t->base_priority = priority; list_init (&t->locks); t->lock_waiting = NULL; }
在synch.h中的lock数据结构中,添加了两个属性,分别是对锁的捐赠队列、最大的优先级捐赠,添加如下:
struct lock { ... struct list_elem elem;/*用于优先级捐赠*/ int max_priority;/*最高的线程优先级*/ };
具体实现:
修改lock_acquire函数,在P操作之前递归地实现优先级捐赠, 然后在被唤醒之后(此时这个线程已经拥有了这个锁),成为这个锁的拥有者。:
void lock_acquire (struct lock *lock) { /*增加*/ struct thread *cur=thread_current(); struct lock *l; enum intr_level old_level; ASSERT (lock != NULL); ASSERT (!intr_context ()); ASSERT (!lock_held_by_current_thread (lock)); /*增加*/ if(lock->holder!=NULL && !thread_mlfqs) { cur->lock_waiting=lock; l=lock; /*P操作前递归实现优先级捐赠,再唤醒,该线程成为这个锁的拥有者*/ while(l&&cur->priority>l->max_priority) { l->max_priority=cur->priority; thread_donate_priority(l->holder); l=l->holder->lock_waiting; } } sema_down (&lock->semaphore); old_level=intr_disable(); cur=thread_current(); if(!thread_mlfqs) { cur->lock_waiting=NULL; lock->max_priority=cur->priority; list_insert_ordered(&thread_current()->locks,&lock->elem ,lock_cmp_priority,NULL); } lock->holder = thread_current (); intr_set_level(old_level); }
在thread.c中增加thread_donate_priority和thread_hold_the_lock函数,实现方法不再展开。
void thread_hold_the_lock(struct lock *lock); void thread_donate_priority(struct thread *t);
实现锁队列排序函数,在synch.c中增加lock_cmp_priority函数。
bool lock_cmp_priority(const struct list_elem *a,const struct list_elem *b,void *aux) { return list_entry (a, struct lock, elem)->max_priority > list_entry (b, struct lock, elem)->max_priority; }
在lock_release函数加入以下语句:
void lock_release (struct lock *lock) { if (!thread_mlfqs) thread_releaseAndRemove_lock(lock); }
添加thread_releaseAndRemove_lock函数,在thread.c中添加。
void thread_releaseAndRemove_lock(struct lock *lock) { enum intr_level old_level = intr_disable (); list_remove (&lock->elem); thread_update_priority (thread_current ()); intr_set_level (old_level); }
当释放一个锁时,当前线程的优先级可能会发生变化,需要在thread.c中添加thread_update_priority函数处理。即实现为如果这个线程还有锁, 就先获取这个线程拥有锁的最大优先级(可能被更高级线程捐赠), 然后如果这个优先级比base_priority大的话更新的应该是被捐赠的优先级。thread_update_priority函数实现如下:
void thread_update_priority(struct thread *t) { enum intr_level old_level = intr_disable(); int priority=t->base_priority; if (!list_empty (&t->locks)) { list_sort (&t->locks, lock_cmp_priority, NULL); int lock_priority = list_entry (list_front (&t->locks), struct lock, elem)->max_priority; if (lock_priority > priority) priority = lock_priority; } t->priority = priority; intr_set_level (old_level); }
-Mission 3:实现多级队列反馈调度
用到的相关文件:/home/pintos/pintos-anon/src/threads目录下的:thread.h,thread.c,synch.h ,synch.c。
实现多级反馈队列调度程序,减少在系统上运行作业的平均响应时间。用于减少系统平均响应时间
解决思路:
通过公式计算来计算出线程当前的优先级,系统调度的时候会从高优先级队列开始选择线程执行,这里线程的优先级随着操作系统的运转数据而动态改变。
由于pintos本身没有浮点数运行逻辑,还需要人为地添加计算方法。
数据结构:
在线程结构体重增加nice和 recent_cpu。
nice参数可以衡量当前线程同其他线程之间的好环程度:当nice=0时将不会影响线程的优先级;若nice为正整数,则nice越大,线程优先级越低;若nice为负,则nice越小,线程优先级越高。
recent_cpu则是用于测量当前每一个进程占用了多少cpu时间。
在线程初始化时使之为0;
struct thread { int nice;/*mlfqs中和线程优先级相关的变量,记录线程的友好程度*/ fixed_t recent_cpu;/*最近使用CPU的时间*/ }
t->nice = 0; t->recent_cpu = FP_CONST (0);
增加fixed-point.h,用于支持浮点数的运算,浮点数:阶符+阶码+数符+尾数;浮点运算逻辑实现在fixed-point.h:
#ifndef THREADS_FIXED_POINT_H #define THREADS_FIXED_POINT_H typedef int fixed_t; #define FP_SHIFT_AMOUNT 12 #define FP_CONST(A) ((fixed_t)(A<<FP_SHIFT_AMOUNT)) #define FP_ADD(A,B) (A + B) #define FP_ADD_MIX(A,B) (A+(B<<FP_SHIFT_AMOUNT)) #define FP_SUB(A,B) (A-B) #define FP_SUB_MIX(A,B) (A-(B<<FP_SHIFT_AMOUNT)) #define FP_MULT_MIX(A,B) (A*B) #define FP_DIV_MIX(A,B) (A/B) #define FP_MULT(A,B) ((fixed_t)(((int64_t) A)*B>>FP_SHIFT_AMOUNT)) #define FP_DIV(A,B) ((fixed_t)((((int64_t) A)<<FP_SHIFT_AMOUNT)/B)) #define FP_INT_PART(A) (A >> FP_SHIFT_AMOUNT) #define FP_ROUND(A) (A>=0 ? ((A+(1<<(FP_SHIFT_AMOUNT-1)))>> FP_SHIFT_AMOUNT) : ((A-(1<<(FP_SHIFT_AMOUNT-1)))>>FP_SHIFT_AMOUNT)) #endif /* threads/fixed-point.h */
在thread.c中增加全局变量 load_avg,在系统启动时对全局变量load_avg进行初始化。
/*浮点数运算逻辑*/ fixed_t load_avg;
具体实现:
1.synch.c中修改lock_acquire()函数,当进行mlfqs测试时,禁止修改线程优先级。在修改线程优先级之前,先判断当前的线程是否为mlfqs,如果不是才可以进行,否则就直接执行下面。
void lock_acquire (struct lock *lock) { struct thread *current_thread = thread_current (); struct lock *l; enum intr_level old_level; ..... current_thread = thread_current (); if (!thread_mlfqs) /*进行mlfqs测试时,需要禁止修改线程优先级*/ { current_thread->lock_waiting = NULL; lock->max_priority = current_thread->priority; thread_hold_the_lock (lock); } ..... }
2.修改lock_try_acquire()函数,当进行mlfqs测试时,禁止修改线程优先级。
3.修改lock_release()函数,当进行mlfqs测试时,禁止修改线程优先级。
synch.c:修改后的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); }
4.在thread.c中增加关键的实现函数。
thread_mlfqs_update_priority函数用来更新线程优先级,通过过对队列的排序之后,选取最高的优先级来更新线程的优先级。
thread_mlfqs_increase_recent_cpu函数用来更新recent_cpu,具体实现为对原来的recent_cpu进行浮点加一。
thread_mlfqs_update_load_avg_and_recent_cpu函数用来更新load_avg和recent_cpu,先通过公式计算得到recent_cpu,再调用thread_update_priority函数实现了线程优先级的更新操作。
void thread_mlfqs_increase_recent_cpu (void); void thread_mlfqs_update_load_avg_and_recent_cpu (void); void thread_mlfqs_update_priority (struct thread *t); void thread_update_recent_cpu (struct thread *t, void *aux);
5.修改timer_interrupt()函数。
每一秒更新一次系统load_avg和所有线程的recent_cpu,每四个timer_ticks更新一次线程优先级, 每个timer_tick running线程的recent_cpu加一。
即在timer_interrupt()中增加:
.... if(thread_mlfqs) { thread_increase_recent_cpu();/*每个ticks更新*/ if(ticks % TIMER_FREQ == 0)/*每秒更新*/ { thread_recalculate_load_avg(); thread_foreach(&thread_recalculate_recent_cpu,NULL); } if(ticks %4 == 0)/*每4个ticks更新*/ thread_foreach(&thread_recalculate_priority,NULL); } .....
6.实现thread.c中的四个空函数。
thread_set_nice (int nice)
thread_get_nice (int nice)
thread_get_load_avg (void)
thread_get_recent_cpu (void)
void thread_set_nice (int nice UNUSED) { /* . */ thread_current()->nice=nice; thread_mlfqs_update_priority (thread_current ()); thread_yield(); } int thread_get_nice (void) { /* Not yet implemented. */ return thread_current ()->nice; } /* Returns 100 times the system load average. */ int thread_get_load_avg (void) { /* Not yet implemented. */ return FP_ROUND (FP_MULT_MIX (load_avg, 100)); } /* Returns 100 times the current thread's recent_cpu value. */ int thread_get_recent_cpu (void) { /* Not yet implemented. */ return FP_ROUND (FP_MULT_MIX (thread_current ()->recent_cpu, 100)); }
Project1-threads总结
在配置pintos的环境上花了很多的时间,也重新安装了很多次,按照网上的方法配置好之后,我这边是27个test全都fail的状态。第一个mission比较地好改,逻辑也大概可以理解,但是越到后边越难改,尤其是优先级的捐赠问题和多级队列反馈调度问题,这部分知识的内容不太好理解,平时在操作系统这门课程中接触比较少,上手修改非常慢。在修改时遇到了二值信号量,操作系统课程中涉及二值信号量的内容不多,了解到pintos使用二值信号量来实现锁semaphore结构体,同时我深刻地理解了线程是如何切换的,如何调度的。
OS-课程设计 Project 2-userprog Report
要通过userprog的test,需要先在userprog/build目录下创建用户磁盘。这里的project1和project2可以独立修改,所以分成两个pintos文件编写修改。
-Task 1: Argument Passing
用到的文件:<process.c>文件,<syscall.c>文件.主要修改process.c并处理字符串,需要在syscall.c添加函数在验证算法的正确性。
问题描述:
以特定的顺序将参数分配给特定的堆栈。
实现思路:
拆分命令名和其他参数,并将它们传递给特定的函数,然后以正确的顺序将它们添加到堆栈中。
数据结构:
无
具体实现:
先调用process_execute函数创建线程,并分离参数,同时把参数传递给start_process函数,使得新线程执行start_process函数。start_process再将参数继续传递给load函数,load函数为用户程序分配地址空间,同时又将参数传递给setup_stack函数。setup_stack创建了用户栈并返回到load,load返回到start_process。最后,在start_process中调用push_argument将用户程序所需的参数argc,argv及地址入栈。
①原先的process_execute函数不支持传递参数给一个新的进程,所以需要拓展process_execute函数的功能来实现将参数传递给函数 start _ process,load 和 setup _ stack。并以此为线程名创建一个新线程,然后新线程转去执行start_process函数。若子进程加载可执行文件的过程没有问题,则返回新建线程的tid.在这之前,父进程无法返回。更改process.c中的process_execute函数:
tid_t process_execute (const char *file_name) { tid_t tid; char *fn_copy = malloc(strlen(file_name)+1); char *fn_copy2 = malloc(strlen(file_name)+1); /*把file_name 复制2份,PGSIZE为页大小*/ strlcpy (fn_copy, file_name, strlen(file_name)+1); strlcpy (fn_copy2, file_name, strlen(file_name)+1); /*避免caller和load的冲突*/ char * save_ptr; /*通过strtok_r分解上面函数返回的file_name,得到thread_name*/ fn_copy2 = strtok_r (fn_copy2, " ", &save_ptr); /*利用得到的thread_name创建线程*/ tid = thread_create (fn_copy2, PRI_DEFAULT, start_process, fn_copy); free (fn_copy2); /*释放*/ if (tid == TID_ERROR){ free (fn_copy); return tid; } /*降低父进程的信号量,等待子进程结束*/ sema_down(&thread_current()->sema); /*子进程加载可执行文件失败报错 */ if (!thread_current()->success) return TID_ERROR; /* 返回创建出的子线程的tid*/ return tid; }
②更改start_process函数,对于刚刚新创建的线程,先初始化中断帧,再调用load函数。如果调用成功,分配好了地址空间并创建完成用户栈,则调用push_argument把argv数组压入栈顶,否则就退出当前的线程。使可以完成参数的切割并压入栈顶,依次存放入参数argv数组(靠参数分离得到)、argv的地址和argc的地址。
static void start_process (void *file_name_) { /*增加增加*/ char *file_name = file_name_; struct intr_frame if_; bool success; char *fn_copy=malloc(strlen(file_name)+1); strlcpy(fn_copy,file_name,strlen(file_name)+1); /*增加增加*/ memset (&if_, 0, sizeof if_); if_.gs = if_.fs = if_.es = if_.ds = if_.ss = SEL_UDSEG; if_.cs = SEL_UCSEG; if_.eflags = FLAG_IF | FLAG_MBS; char *token, *save_ptr; /*获得线程名*/ file_name = strtok_r (file_name, " ", &save_ptr); /*调用load函数,判断其是否成功load*/ success = load (file_name, &if_.eip, &if_.esp); if (success){ int argc = 0; int argv[50]; /*token是命令行输入的参数分离后的数组,包含了argv*/ for (token = strtok_r (fn_copy, " ", &save_ptr); token != NULL; token = strtok_r (NULL, " ", &save_ptr)){ if_.esp -= (strlen(token)+1); memcpy (if_.esp, token, strlen(token)+1); argv[argc++] = (int) if_.esp; } /*通过 argv 拆分参数*/ push_argument (&if_.esp, argc, argv); thread_current ()->parent->success = true; sema_up (&thread_current ()->parent->sema); } else{ /*失败则保存父进程的执行状态为执行失败并退出*/ thread_current ()->parent->success = false; sema_up (&thread_current ()->parent->sema); thread_exit (); } asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (&if_) : "memory"); NOT_REACHED (); }
③ push _ argument函数 在 start _ process 中通过 argv 拆分参数,据argc的大小将argv数组压入栈的操作。
增加push_argument函数到process.c中:
void push_argument (void **esp, int argc, int argv[]){ *esp = (int)*esp & 0xfffffffc; *esp -= 4;/*四位对齐(word-align)下压uint8_t大小*/ *(int *) *esp = 0; /*按照argc的大小,循环压入argv数组*/ for (int i = argc - 1; i >= 0; i--) { *esp -= 4; *(int *) *esp = argv[i]; } /*压入argv[0]的地址*/ *esp -= 4; *(int *) *esp = (int) *esp + 4; *esp -= 4; *(int *) *esp = argc; *esp -= 4; *(int *) *esp = 0; }
④修改process_wait (tid_t child_tid)函数:
int process_wait (tid_t child_tid UNUSED) { /* Find the child's ID that the current thread waits for and sema down the child's semaphore */ struct list *l = &thread_current()->childs; struct list_elem *temp; temp = list_begin (l); struct child *temp2 = NULL; while (temp != list_end (l)) { temp2 = list_entry (temp, struct child, child_elem); if (temp2->tid == child_tid) { if (!temp2->isrun) { temp2->isrun = true; sema_down (&temp2->sema); break; } else { return -1; } } temp = list_next (temp); } if (temp == list_end (l)) { return -1; } list_remove (temp); return temp2->store_exit; }
⑤在<syscall.c>增加sys_write函数:
/* Do system write, Do writing in stdout and write in files */ void sys_write (struct intr_frame* f) { uint32_t *user_ptr = f->esp; check_ptr2 (user_ptr + 7); check_ptr2 (*(user_ptr + 6)); *user_ptr++; int temp2 = *user_ptr; const char * buffer = (const char *)*(user_ptr+1); off_t size = *(user_ptr+2); if (temp2 == 1) { /* Use putbuf to do testing */ putbuf(buffer,size); f->eax = size; } else { struct thread_file * thread_file_temp = find_file_id (*user_ptr); if (thread_file_temp) { acquire_lock_f (); f->eax = file_write (thread_file_temp->file, buffer, size); release_lock_f (); } else { f->eax = 0; } } }
在thread.c和thread.h文件中加入文件锁filesys_lock
/*Do file lock operation*/ void acquire_lock_f(){ lock_acquire(&lock_f); } void release_lock_f(){ lock_release(&lock_f); }
-Task 2: Process Control Syscalls
问题描述:
在进程的执行过程中,如果进程失败,execute会返回-1,不能返回。
解决思路:
在 thread中结构体中添加success来记录线程是否执行成功。使用 parent 来获取它的父进程,并根据加载的结果设置它的状态。 将子进程的执行结果记录在父进程中。使用信号量来实现“父进程”等待“子进程”。 当一个子进程被创建时,阻塞父进程。 当子进程完成并结束时,唤醒该子进程的父进程。
数据结构:
<thread.h>中添加child结构,即子进程信息,用以记录子进程的各类信息以方便对其控制:
struct child { tid_t tid; /* tid of the thread */ bool isrun;/*子线程是否运行成功*/ struct list_elem child_elem; /* list of children */ struct semaphore sema; /* 控制等待*/ int store_exit; /* the exit status of child thread */ };
在thread结构体中加入一些属性:
struct list childs; /* The list of childs */ struct child * thread_child; /* Store the child of this thread */ int st_exit; /* Exit status */ struct semaphore sema; /*控制子进程的逻辑,完成父进程对子进程的等待*/ bool success; /* 判断子线程是否成功执行*/ struct thread* parent; /* 父进程 */
具体实现:
在执行系统调用之前,要检查该地址是否指向了有效地址。检查地址是否低于PHYS_BASE,地址是否被映射,或者是否为空。如果地址无效,需要释放内存页面并在退出之前释放该进程的所有锁或信号量。
在syscall.c中增加get_user 函数,增加如下:
/* Method in document to handle special situation */ static int get_user (const uint8_t *uaddr) { int result; asm ("movl $1f, %0; movzbl %1, %0; 1:" : "=&a" (result) : "m" (*uaddr)); return result; }
修改syscall_handler 函数,系统调用被调用时,中断就会自动使用这个函数进行处理,修改如下:
/* Smplify the code to maintain the code more efficiently */ static void syscall_handler (struct intr_frame *f UNUSED) { /* For Task2 practice, just add 1 to its first argument, and print its result */ int * p = f->esp; check_ptr2 (p + 1); int type = * (int *)f->esp; if(type <= 0 || type >= max_syscall){ exit_special (); } syscalls[type](f); }
修改syscall_init 函数,修改如下:
void syscall_init (void) { intr_register_int (0x30, 3, INTR_ON, syscall_handler, "syscall"); /* Our implementation for Task2: initialize halt,exit,exec */ syscalls[SYS_HALT] = &sys_halt; syscalls[SYS_EXIT] = &sys_exit; syscalls[SYS_EXEC] = &sys_exec; }
添加以下函数:
-
sys_halt
调用shutdown_power_off函数。
-
sys_exit
结束当前的用户程序,并返回状态给内核kernel,如果当前进程的父进程正在等待该进程,则这个状态就是应该返回的状态。传统来说,状态0代表返回成功而一个非0值代表有错误。
-
sys_exec
首先检查file_name引用的文件是否有效(指向内存地址的指针,page和page的内容是否有效)。如果无效,则返回 -1,否则调用函数 process_execute完成修改。
-
sys_write
等待子进程,检查该子进程的退出状态。
-
sys_wait
首先检查它传递的参数是否有效。如果无效,返回-1,否则调用任务1中实现的函数process_wait。
void sys_halt (struct intr_frame* f) { shutdown_power_off(); } void sys_exit (struct intr_frame* f) { uint32_t *user_ptr = f->esp; check_ptr2 (user_ptr + 1); *user_ptr++; /* record the exit status of the process */ thread_current()->st_exit = *user_ptr; thread_exit (); } void sys_exec (struct intr_frame* f) { uint32_t *user_ptr = f->esp; check_ptr2 (user_ptr + 1); check_ptr2 (*(user_ptr + 1)); *user_ptr++; f->eax = process_execute((char*)* user_ptr); } //修改修改增添增添 void sys_write (struct intr_frame* f) { uint32_t *user_ptr = f->esp; check_ptr2 (user_ptr + 7); check_ptr2 (*(user_ptr + 6)); *user_ptr++; int temp2 = *user_ptr; const char * buffer = (const char *)*(user_ptr+1); off_t size = *(user_ptr+2); if (temp2 == 1) { /* Use putbuf to do testing */ putbuf(buffer,size); f->eax = size; } else { /* Write to Files */ struct thread_file * thread_file_temp = find_file_id (*user_ptr); if (thread_file_temp) { acquire_lock_f (); f->eax = file_write (thread_file_temp->file, buffer, size); release_lock_f (); } else { f->eax = 0; } } } void sys_wait (struct intr_frame* f) { uint32_t *user_ptr = f->esp; check_ptr2 (user_ptr + 1); *user_ptr++; f->eax = process_wait(*user_ptr); }
-Task 3 File Operation Syscalls
用到的文件:<syscall.c>文件。
问题描述:
当用户程序运行时,必须确保没有人可以修改它在磁盘上的可执行文件。
解决思路:
需要提供在用户虚拟地址空间中读取和写入数据的方法,而且是在获得系统调用编号之前,因为系统调用编号位于用户的虚拟地址空间中的用户堆栈上。需要通过终止用户进程来处理无效指针。需要创建新的结构体thread_file,并添加thread 结构体的属性。对文件操作进行上锁,以防止出现竞争状态。
数据结构:
在thread结构体中加入属性:
struct list files; /* List of opened files */ int file_fd; /* File's descriptor */ struct file * file_owned; /* 保存线程打开的文件 */
在<thread.h>创建新的结构体thread_file,即文件句柄,用以标识文件:
struct thread_file { int fd; struct file* file; struct list_elem file_elem; };
在thread.h>中新建一个static struct lock确保线程的安全性,对使用到的文件进行加锁以预防读写冲突。
/*Use a lock to lock process when do file operation*/ static struct lock lock_f;
在<thread.c> 加入关于文件锁的函数:
void acquire_lock_f () { lock_acquire(&lock_f); } void release_lock_f () { lock_release(&lock_f); }
具体实现:
简要流程为:用户操作产生中断->中断识别->参数入栈->弹出栈顶参数->识别系统调用类型->实现相应的系统调用。
在syscall.c中修改syscall_handler 函数,弹出用户栈参数。可以通过识别数组的序号来决定调用哪一个系统:
/* Smplify the code to maintain the code more efficiently */ static void syscall_handler (struct intr_frame *f UNUSED) { /* */ int * p = f->esp; /*检查有效性*/ check_ptr2 (p + 1); /*记录在栈顶的系统调用类型type*/ int type = * (int *)f->esp; /*类型错误,则退出*/ if(type <= 0 || type >= max_syscall){ exit_special (); } /*正确则查找数组调用对应系统调用并调用执行*/ syscalls[type](f); }
修改syscall_init函数,初始化系统调用,通过syscall数组来存储13个系统调用,在syscall_handler里通过识别数组的序号决定调用哪一个系统调用,修改如下:
void syscall_init (void) { intr_register_int (0x30, 3, INTR_ON, syscall_handler, "syscall"); /* Our implementation for Task2: initialize halt,exit,exec */ syscalls[SYS_HALT] = &sys_halt; syscalls[SYS_EXIT] = &sys_exit; syscalls[SYS_EXEC] = &sys_exec; /* Our implementation for Task3: initialize create, remove, open, filesize, read, write, seek, tell, and close */ syscalls[SYS_WAIT] = &sys_wait; syscalls[SYS_CREATE] = &sys_create; syscalls[SYS_REMOVE] = &sys_remove; syscalls[SYS_OPEN] = &sys_open; syscalls[SYS_WRITE] = &sys_write; syscalls[SYS_SEEK] = &sys_seek; syscalls[SYS_TELL] = &sys_tell; syscalls[SYS_CLOSE] =&sys_close; syscalls[SYS_READ] = &sys_read; syscalls[SYS_FILESIZE] = &sys_filesize; }
新增is_valid_pointer函数,检查是否有效,增加如下:
/* Check is the user pointer is valid */ bool is_valid_pointer (void* esp,uint8_t argc){ for (uint8_t i = 0; i < argc; ++i) { if((!is_user_vaddr (esp)) || (pagedir_get_page (thread_current()->pagedir, esp)==NULL)){ return false; } } return true; }
新增find_file_id函数,通过file的 ID 查找file,增加如下:
/* Find file by the file's ID */ struct thread_file * find_file_id (int file_id) { struct list_elem *e; struct thread_file * thread_file_temp = NULL; struct list *files = &thread_current ()->files; for (e = list_begin (files); e != list_end (files); e = list_next (e)){ thread_file_temp = list_entry (e, struct thread_file, file_elem); if (file_id == thread_file_temp->fd) return thread_file_temp; } return false; }
增加check_ptr2函数,检查地址和页面的有效性,来确保系统调用时各种操作的合法性,增加如下:
/* New method to check the address and pages to pass test sc-bad-boundary2, execute */ void * check_ptr2(const void *vaddr) { /*检查指针是否无效 */ if (!is_user_vaddr(vaddr)) { exit_special (); } /* 检查页面是否无效 */ void *ptr = pagedir_get_page (thread_current()->pagedir, vaddr); if (!ptr) { exit_special (); } /* 检查页面的内容是否无效 */ uint8_t *check_byteptr = (uint8_t *) vaddr; for (uint8_t i = 0; i < 4; i++) { /*无效则退出*/ if (get_user(check_byteptr + i) == -1) { exit_special (); } } return ptr; }
新增的一些函数如下所示:
/*检查地址和页面的有效性,获得文件的锁,创建文件*/ void sys_create(struct intr_frame* f); /* syscall create */ /*检查地址和页面的有效性,获得文件的锁,删除文件*/ void sys_remove(struct intr_frame* f); /* syscall remove */ /*打开栈顶指向的文件*/ void sys_open(struct intr_frame* f);/* syscall open */ void sys_wait(struct intr_frame* f); /*syscall wait */ /*通过find_file_id获取文件标识符。之后调用file_length返回以文件标识符fd指代的文件的大小*/ void sys_filesize(struct intr_frame* f);/* syscall filesize */ void sys_read(struct intr_frame* f); /* syscall read */ /*往缓冲区写入数据或往文件中写入数据*/ void sys_write(struct intr_frame* f); /* syscall write */ /*根据传入的参数,把下一个要读入或写入的字节跳转到指定文件的指定位置*/ void sys_seek(struct intr_frame* f); /* syscall seek */ /*返回下一个在已打开文件fd中即将被读入或写入的字节的位置*/ void sys_tell(struct intr_frame* f); /* syscall tell */ /*关闭文件,并把这个文件从线程的文件list中移除并释放资源*/ void sys_close(struct intr_frame* f); /* syscall close */
Project2-userprog总结:
Pintos 中的虚拟内存分为两个区域:用户虚拟内存和内核虚拟内存。用户程序只能访问自己的用户虚拟内存。访问内核虚拟内存的尝试导致页面错误;内核虚拟内存是全局的。无论用户进程或内核线程正在运行什么,它总是以相同的方式映射。userprog这个project的修改集中在syscall.c文件中,修改的幅度也比较大,主要是增加了很多的新函数。在修改中,我发现函数定义顺序的问题会影响该project的执行,需要确保函数的顶下顺序没有问题,所以,c语言函数最好是在开头就先定义好。
总结:
1)对专业知识基本概念、基本理论和典型方法的理解。
基本概念:
操作系统是控制和管理整个计算机系统的硬件和软件资源,并合理地组织调度计算机的工作和资源分配,以提供给用户和其他软件方便的接口和环境的软件集合。pintos也是一个有一定难度的操作系统,为了实现基本的功能,存在很多的嵌套函数,分析起来有一定的难度,但是它的基本概念和操作系统是一致的。
基本理论:
从关键代码处下手,着重分析好底层的实现,为顶层的实现打基础。数据结构也非常重要,要逐步实现对线程,CPU和用户之间的良好切换。
典型方法:
确定好数据结构,使得数据结构完整,易理解。再编写函数,精简函数,使得操作系统运行正常,不出错。
2)怎么建立模型。
1.以对系统本身的规律进行分析,根据事物的机理来建模;
2.通过对系统的实验或统计数据的处理,并根据关于系统的已有的知识和经验来建模。
我们应该通过pintos的test中的实现机理来找出关键文件,从需要实现的功能来探索如何去实现。
3)如何利用基本原理解决复杂工程问题。
1.先将一个复杂的工程问题分解成许多小问题,按照问题的性质和相似性分解。比如第一个project-threads就比较好分解,主要分为3个task,第一个task主要和线程休眠问题,可以看到解决了线程休眠问题后,可以PASS掉这部分相关的所有test,因为它们的实现原理类似。
2.分析问题产生的原因。分析各个test的实现原理,对症下药,充分利用已有的函数,在已有的函数基础上改写,添加需要的函数,直至逻辑上能够顺通,可以适当画一些流程图帮助自己理解。
3.充分利用已学的知识,从关键部分下手,着重分析好底层的实现,为顶层的实现打基础。
4)具有实验方案设计的能力。
通过分析存在的问题,理解实现逻辑,手动绘制流程图帮助自己理解操作系统的实现,再到最后顺通解决方案,需要耗费大量的时间和精力。我在完成project1和project2的过程中,都会提炼出问题描述和解决问题的核心思想,这都是实验方案设计的一部分,对实验方案设计有很大的推动作用,我感受到我的实验方案设计的能力提升了很多。
5)如何对环境和社会的可持续发展。
提高操作系统的CPU的效率,可以有效利用CPU资源,在有效的资源的时间内实现更多更好的功能,同时用户的时间也可以节省,体验就会更好。提高效率之后,也会促进社会的可持续发展。