轉載請注明出處。
前言:
本實驗來自斯坦福大學cs140課程,只限於教學用途,以下是他們對於Pintos系統的介紹:
Pintos is a simple operating system framework for the 80x86 architecture. It supports kernel threads, loading and running user programs, and a file system, but it implements all of these in a very simple way. In the Pintos projects, you and your project team will strengthen its support in all three of these areas. You will also add a virtual memory implementation.
Pintos實驗主要分成四部分,如下所示:
- 實驗一:Thread
- 實驗二:User Programs
- 實驗三:Virtual Memory
- 實驗四:File System
實驗原理:
通過 bochs 加載 pintos 操作系統, 該操作系統會根據 pintos 的實現打印運行結果,通過比較標准輸出文檔和實際輸出,來判斷 pintos 實現是否符合要求。
環境配置:
參考: http://www.stanford.edu/class/cs140/projects/pintos/pintos_12.html#SEC166
實驗實現代碼地址:
https://github.com/laiy/Pintos/tree/master/src
實驗一 THREAD:
我們試驗一的最終任務就是在threads/中跑make check的時候, 27個test全pass。
Mission1:
重新實現timer_sleep
函數(2.2.2)
(注意, 博主以下用了包括代碼在內大概7000字的說明從每一個底層細節解析了這個函數的執行, 雖然很長但是讓我們對pintos這個操作系統的各種機制和實現有更深刻的理解, 如果嫌長請直接跳到函數重新實現)
timer_sleep
函數在devices/timer.c
。系統現在是使用busy wait實現的,即線程不停地循環,直到時間片耗盡。更改timer_sleep
的實現方式。
我們先來看一下devices目錄下timer.c中的timer_sleep實現:
1 /* Sleeps for approximately TICKS timer ticks. Interrupts must 2 be turned on. */ 3 void 4 timer_sleep (int64_t ticks) 5 { 6 int64_t start = timer_ticks (); 7 ASSERT (intr_get_level () == INTR_ON); 8 while (timer_elapsed (start) < ticks) 9 thread_yield(); 10 }
讓我們一行一行解析:
第6行: 調用了timer_ticks函數, 讓我們來看看這個函數做了什么。
1 /* Returns the number of timer ticks since the OS booted. */ 2 int64_t 3 timer_ticks (void) 4 { 5 enum intr_level old_level = intr_disable (); 6 int64_t t = ticks; 7 intr_set_level (old_level); 8 return t; 9 }
然后我們注意到這里有個intr_level的東西通過intr_disable返回了一個東西,沒關系,我們繼續往下找。
1 /* Interrupts on or off? */ 2 enum intr_level 3 { 4 INTR_OFF, /* Interrupts disabled. */ 5 INTR_ON /* Interrupts enabled. */ 6 };
1 /* Disables interrupts and returns the previous interrupt status. */ 2 enum intr_level 3 intr_disable (void) 4 { 5 enum intr_level old_level = intr_get_level (); 6 7 /* Disable interrupts by clearing the interrupt flag. 8 See [IA32-v2b] "CLI" and [IA32-v3a] 5.8.1 "Masking Maskable 9 Hardware Interrupts". */ 10 asm volatile ("cli" : : : "memory"); 11 12 return old_level; 13 }
這里很明顯,intr_level代表能否被中斷,而intr_disable做了兩件事情:1. 調用intr_get_level() 2. 直接執行匯編代碼,調用匯編指令來保證這個線程不能被中斷。
注意: 這個asm volatile是在C語言中內嵌了匯編語言,調用了CLI指令,CLI指令不是command line interface, 而是clear interrupt, 作用是將標志寄存器的IF(interrupt flag)位置為0, IF=0時將不響應可屏蔽中斷。
好,讓我們繼續來看intr_get_level又做了什么鬼。
1 /* Returns the current interrupt status. */ 2 enum intr_level 3 intr_get_level (void) 4 { 5 uint32_t flags; 6 7 /* Push the flags register on the processor stack, then pop the 8 value off the stack into `flags'. See [IA32-v2b] "PUSHF" 9 and "POP" and [IA32-v3a] 5.8.1 "Masking Maskable Hardware 10 Interrupts". */ 11 asm volatile ("pushfl; popl %0" : "=g" (flags)); 12 13 return flags & FLAG_IF ? INTR_ON : INTR_OFF; 14 }
這里就是intr_disable函數調用的最深的地方了!
這個函數一樣是調用了匯編指令,把標志寄存器的東西放到處理器棧上,然后把值pop到flags(代表標志寄存器IF位)上,通過判斷flags來返回當前終端狀態(intr_level)。
好, 到這里。 函數嵌套了這么多層, 我們整理一下邏輯:
1. intr_get_level返回了intr_level的值
2. intr_disable獲取了當前的中斷狀態, 然后將當前中斷狀態改為不能被中斷, 然后返回執行之前的中斷狀態。
有以上結論我們可以知道: timer_ticks第五行做了這么一件事情: 禁止當前行為被中斷, 保存禁止被中斷前的中斷狀態(用old_level儲存)。
讓我們再來看timer_ticks剩下的做了什么, 剩下的就是用t獲取了一個全局變量ticks, 然后返回, 其中調用了set_level函數。
1 /* Enables or disables interrupts as specified by LEVEL and 2 returns the previous interrupt status. */ 3 enum intr_level 4 intr_set_level (enum intr_level level) 5 { 6 return level == INTR_ON ? intr_enable () : intr_disable (); 7 }
有了之前的基礎,這個函數就很容易看了, 如果之前是允許中斷的(INTR_ON)則enable否則就disable.
而intr_enable正如你們所想,實現和之前基本一致:
1 /* Enables interrupts and returns the previous interrupt status. */ 2 enum intr_level 3 intr_enable (void) 4 { 5 enum intr_level old_level = intr_get_level (); 6 ASSERT (!intr_context ()); 7 8 /* Enable interrupts by setting the interrupt flag. 9 10 See [IA32-v2b] "STI" and [IA32-v3a] 5.8.1 "Masking Maskable 11 Hardware Interrupts". */ 12 asm volatile ("sti"); 13 14 return old_level; 15 }
說明一下, sti指令就是cli指令的反面,將IF位置為1。
然后有個ASSERT斷言了intr_context函數返回結果的false。
再來看intr_context
1 /* Returns true during processing of an external interrupt 2 and false at all other times. */ 3 bool 4 intr_context (void) 5 { 6 return in_external_intr; 7 }
這里直接返回了是否外中斷的標志in_external_intr, 就是說ASSERT斷言這個中斷不是外中斷(IO等, 也稱為硬中斷)而是操作系統正常線程切換流程里的內中斷(也稱為軟中斷)。
好的, 至此, 我們總結一下:
這么多分析其實分析出了pintos操作系統如何利用中斷機制來確保一個原子性的操作的。
我們來看, 我們已經分析完了timer_ticks這個函數, 它其實就是獲取ticks的當前值返回而已, 而第5行和第7行做的其實只是確保這個過程是不能被中斷的而已。
那么我們來達成一個共識, 被以下兩個語句包裹的內容目的是為了保證這個過程不被中斷。
1 enum intr_level old_level = intr_disable (); 2 ... 3 intr_set_level (old_level);
好的, 那么ticks又是什么? 來看ticks定義。
1 /* Number of timer ticks since OS booted. */ 2 static int64_t ticks;
從pintos被啟動開始, ticks就一直在計時, 代表着操作系統執行單位時間的前進計量。
好, 現在回過來看timer_sleep這個函數, start獲取了起始時間, 然后斷言必須可以被中斷, 不然會一直死循環下去, 然后就是一個循環
1 while (timer_elapsed (start) < ticks) 2 thread_yield();
注意這個ticks是函數的形參不是全局變量, 然后看一下這兩個函數:
1 /* Returns the number of timer ticks elapsed since THEN, which 2 should be a value once returned by timer_ticks(). */ 3 int64_t 4 timer_elapsed (int64_t then) 5 { 6 return timer_ticks () - then; 7 }
很明顯timer_elapsed返回了當前時間距離then的時間間隔, 那么這個循環實質就是在ticks的時間內不斷執行thread_yield。
那么我們最后來看thread_yield是什么就可以了:
1 /* Yields the CPU. The current thread is not put to sleep and 2 may be scheduled again immediately at the scheduler's whim. */ 3 void 4 thread_yield (void) 5 { 6 struct thread *cur = thread_current (); 7 enum intr_level old_level; 8 9 ASSERT (!intr_context ()); 10 11 old_level = intr_disable (); 12 if (cur != idle_thread) 13 list_push_back (&ready_list, &cur->elem); 14 cur->status = THREAD_READY; 15 schedule (); 16 intr_set_level (old_level); 17 }
第6行thread_current函數做的事情已經可以顧名思義了, 不過具有鑽研精神和強迫症的你還是要確定它的具體實現:
1 /* Returns the running thread. 2 This is running_thread() plus a couple of sanity checks. 3 See the big comment at the top of thread.h for details. */ 4 struct thread * 5 thread_current (void) 6 { 7 struct thread *t = running_thread (); 8 9 /* Make sure T is really a thread. 10 If either of these assertions fire, then your thread may 11 have overflowed its stack. Each thread has less than 4 kB 12 of stack, so a few big automatic arrays or moderate 13 recursion can cause stack overflow. */ 14 ASSERT (is_thread (t)); 15 ASSERT (t->status == THREAD_RUNNING); 16 17 return t; 18 }
1 /* Returns the running thread. */ 2 struct thread * 3 running_thread (void) 4 { 5 uint32_t *esp; 6 7 /* Copy the CPU's stack pointer into `esp', and then round that 8 down to the start of a page. Because `struct thread' is 9 always at the beginning of a page and the stack pointer is 10 somewhere in the middle, this locates the curent thread. */ 11 asm ("mov %%esp, %0" : "=g" (esp)); 12 return pg_round_down (esp); 13 }
1 /* Returns true if T appears to point to a valid thread. */ 2 static bool 3 is_thread (struct thread *t) 4 { 5 return t != NULL && t->magic == THREAD_MAGIC; 6 }
先來看thread_current調用的running_thread, 把CPU棧的指針復制到esp中, 然后調用pg_round_down
1 /* Round down to nearest page boundary. */ 2 static inline void *pg_round_down (const void *va) { 3 return (void *) ((uintptr_t) va & ~PGMASK); 4 }
好,這里又涉及到這個操作系統是怎么設計頁面的了:
1 /* Page offset (bits 0:12). */ 2 #define PGSHIFT 0 /* Index of first offset bit. */ 3 #define PGBITS 12 /* Number of offset bits. */ 4 #define PGSIZE (1 << PGBITS) /* Bytes in a page. */ 5 #define PGMASK BITMASK(PGSHIFT, PGBITS) /* Page offset bits (0:12). */
1 /* Functions and macros for working with virtual addresses. 2 3 See pte.h for functions and macros specifically for x86 4 hardware page tables. */ 5 6 #define BITMASK(SHIFT, CNT) (((1ul << (CNT)) - 1) << (SHIFT))
一個頁面12位, PGMASK調用BITMASK其實就是一個頁面全部位都是1的這么個MASK, 注意1ul的意思是unsigned long的1。
然后來看pg_round_down, 對PGMASK取反的結果就是一個頁面大小全部為0的這么個數, 然后和傳過來的指針做與操作的結果就是清0指針的靠右12位。
這里有什么效果呢? 我們知道一個頁面12位, 而struct thread是在一個頁面的最開始的, 所以對任何一個頁面的指針做pg_round_down的結果就是返回到這個頁面最開始線程結構體的位置。
好, 我們現在分析出了pg_round_down其實就是返回了這個頁面線程的最開始指針, 那么running_thread的結果返回當前線程起始指針。
再來看thread_current里最后的兩個斷言, 一個斷言t指針是一個線程, 一個斷言這個線程處於THREAD_RUNNING狀態。
然后is_thread用的t->magic其實是用於檢測時候有棧溢出的這么個元素。
1 /* Owned by thread.c. */ 2 unsigned magic; /* Detects stack overflow. */
好, 現在thread_current分析完了, 這個就是返回當前線程起始指針位置。
我們繼續看thread_yield, 然后剩下的很多東西其實我們已經分析過了, 在分析的過程其實是對這個操作系統工作過程的剖析, 很多地方都是相通的。
第9斷言這是個軟中斷, 第11和16包裹起來的就是我們之前分析的線程機制保證的一個原子性操作。
然后我們來看12-15做了什么:
1 if (cur != idle_thread) 2 list_push_back (&ready_list, &cur->elem); 3 cur->status = THREAD_READY; 4 schedule ();
如何當前線程不是空閑的線程就調用list_push_back把當前線程的元素扔到就緒隊列里面, 並把線程改成THREAD_READY狀態。
關於隊列list的相關操作mission2會涉及到, 這里先不作解釋, 顧名思義即可。
然后再調用schedule:
1 /* Schedules a new process. At entry, interrupts must be off and 2 the running process's state must have been changed from 3 running to some other state. This function finds another 4 thread to run and switches to it. 5 6 It's not safe to call printf() until thread_schedule_tail() 7 has completed. */ 8 static void 9 schedule (void) 10 { 11 struct thread *cur = running_thread (); 12 struct thread *next = next_thread_to_run (); 13 struct thread *prev = NULL; 14 15 ASSERT (intr_get_level () == INTR_OFF); 16 ASSERT (cur->status != THREAD_RUNNING); 17 ASSERT (is_thread (next)); 18 19 if (cur != next) 20 prev = switch_threads (cur, next); 21 thread_schedule_tail (prev); 22 }
首先獲取當前線程cur和調用next_thread_to_run獲取下一個要run的線程:
1 /* Chooses and returns the next thread to be scheduled. Should 2 return a thread from the run queue, unless the run queue is 3 empty. (If the running thread can continue running, then it 4 will be in the run queue.) If the run queue is empty, return 5 idle_thread. */ 6 static struct thread * 7 next_thread_to_run (void) 8 { 9 if (list_empty (&ready_list)) 10 return idle_thread; 11 else 12 return list_entry (list_pop_front (&ready_list), struct thread, elem); 13 }
如果就緒隊列空閑直接返回一個空閑線程指針, 否則拿就緒隊列第一個線程出來返回。
然后3個斷言之前講過就不多說了, 確保不能被中斷, 當前線程是RUNNING_THREAD等。
如果當前線程和下一個要跑的線程不是同一個的話調用switch_threads返回給prev。
1 /* Switches from CUR, which must be the running thread, to NEXT, 2 which must also be running switch_threads(), returning CUR in 3 NEXT's context. */ 4 struct thread *switch_threads (struct thread *cur, struct thread *next);
注意, 這個函數實現是用匯編語言實現的在threads/switch.S里:
1 #### struct thread *switch_threads (struct thread *cur, struct thread *next); 2 #### 3 #### Switches from CUR, which must be the running thread, to NEXT, 4 #### which must also be running switch_threads(), returning CUR in 5 #### NEXT's context. 6 #### 7 #### This function works by assuming that the thread we're switching 8 #### into is also running switch_threads(). Thus, all it has to do is 9 #### preserve a few registers on the stack, then switch stacks and 10 #### restore the registers. As part of switching stacks we record the 11 #### current stack pointer in CUR's thread structure. 12 13 .globl switch_threads 14 .func switch_threads 15 switch_threads: 16 # Save caller's register state. 17 # 18 # Note that the SVR4 ABI allows us to destroy %eax, %ecx, %edx, 19 # but requires us to preserve %ebx, %ebp, %esi, %edi. See 20 # [SysV-ABI-386] pages 3-11 and 3-12 for details. 21 # 22 # This stack frame must match the one set up by thread_create() 23 # in size. 24 pushl %ebx 25 pushl %ebp 26 pushl %esi 27 pushl %edi 28 29 # Get offsetof (struct thread, stack). 30 .globl thread_stack_ofs 31 mov thread_stack_ofs, %edx 32 33 # Save current stack pointer to old thread's stack, if any. 34 movl SWITCH_CUR(%esp), %eax 35 movl %esp, (%eax,%edx,1) 36 37 # Restore stack pointer from new thread's stack. 38 movl SWITCH_NEXT(%esp), %ecx 39 movl (%ecx,%edx,1), %esp 40 41 # Restore caller's register state. 42 popl %edi 43 popl %esi 44 popl %ebp 45 popl %ebx 46 ret 47 .endfunc
分析一下這個匯編代碼: 先4個寄存器壓棧保存寄存器狀態(保護作用), 這4個寄存器是switch_threads_frame的成員:
1 /* switch_thread()'s stack frame. */ 2 struct switch_threads_frame 3 { 4 uint32_t edi; /* 0: Saved %edi. */ 5 uint32_t esi; /* 4: Saved %esi. */ 6 uint32_t ebp; /* 8: Saved %ebp. */ 7 uint32_t ebx; /* 12: Saved %ebx. */ 8 void (*eip) (void); /* 16: Return address. */ 9 struct thread *cur; /* 20: switch_threads()'s CUR argument. */ 10 struct thread *next; /* 24: switch_threads()'s NEXT argument. */ 11 };
然后全局變量thread_stack_ofs記錄線程和棧之間的間隙, 我們都知道線程切換有個保存現場的過程,
來看34,35行, 先把當前的線程指針放到eax中, 並把線程指針保存在相對基地址偏移量為edx的地址中。
38,39: 切換到下一個線程的線程棧指針, 保存在ecx中, 再把這個線程相對基地址偏移量edx地址(上一次保存現場的時候存放的)放到esp當中繼續執行。
這里ecx, eax起容器的作用, edx指向當前現場保存的地址偏移量。
簡單來說就是保存當前線程狀態, 恢復新線程之前保存的線程狀態。
然后再把4個寄存器拿出來, 這個是硬件設計要求的, 必須保護switch_threads_frame里面的寄存器才可以destroy掉eax, edx, ecx。
然后注意到現在eax(函數返回值是eax)就是被切換的線程棧指針。
我們由此得到一個結論, schedule先把當前線程丟到就緒隊列,然后把線程切換如果下一個線程和當前線程不一樣的話。
然后再看shedule最后一行的函數thread_schedule_tail做了什么鬼, 這里參數prev是NULL或者在下一個線程的上下文中的當前線程指針。
1 /* Completes a thread switch by activating the new thread's page 2 tables, and, if the previous thread is dying, destroying it. 3 4 At this function's invocation, we just switched from thread 5 PREV, the new thread is already running, and interrupts are 6 still disabled. This function is normally invoked by 7 thread_schedule() as its final action before returning, but 8 the first time a thread is scheduled it is called by 9 switch_entry() (see switch.S). 10 11 It's not safe to call printf() until the thread switch is 12 complete. In practice that means that printf()s should be 13 added at the end of the function. 14 15 After this function and its caller returns, the thread switch 16 is complete. */ 17 void 18 thread_schedule_tail (struct thread *prev) 19 { 20 struct thread *cur = running_thread (); 21 22 ASSERT (intr_get_level () == INTR_OFF); 23 24 /* Mark us as running. */ 25 cur->status = THREAD_RUNNING; 26 27 /* Start new time slice. */ 28 thread_ticks = 0; 29 30 #ifdef USERPROG 31 /* Activate the new address space. */ 32 process_activate (); 33 #endif 34 35 /* If the thread we switched from is dying, destroy its struct 36 thread. This must happen late so that thread_exit() doesn't 37 pull out the rug under itself. (We don't free 38 initial_thread because its memory was not obtained via 39 palloc().) */ 40 if (prev != NULL && prev->status == THREAD_DYING && prev != initial_thread) 41 { 42 ASSERT (prev != cur); 43 palloc_free_page (prev); 44 } 45 }
先是獲得當前線程cur, 注意此時是已經切換過的線程了(或者還是之前run的線程, 因為ready隊列為空)。
然后把線程狀態改成THREAD_RUNNING, 然后thread_ticks清零開始新的線程切換時間片。
然后調用process_activate觸發新的地址空間。
1 /* Sets up the CPU for running user code in the current 2 thread. 3 This function is called on every context switch. */ 4 void 5 process_activate (void) 6 { 7 struct thread *t = thread_current (); 8 9 /* Activate thread's page tables. */ 10 pagedir_activate (t->pagedir); 11 12 /* Set thread's kernel stack for use in processing 13 interrupts. */ 14 tss_update (); 15 }
這里先是拿到當前線程, 調用pagedir_activate:
1 /* Loads page directory PD into the CPU's page directory base 2 register. */ 3 void 4 pagedir_activate (uint32_t *pd) 5 { 6 if (pd == NULL) 7 pd = init_page_dir; 8 9 /* Store the physical address of the page directory into CR3 10 aka PDBR (page directory base register). This activates our 11 new page tables immediately. See [IA32-v2a] "MOV--Move 12 to/from Control Registers" and [IA32-v3a] 3.7.5 "Base 13 Address of the Page Directory". */ 14 asm volatile ("movl %0, %%cr3" : : "r" (vtop (pd)) : "memory"); 15 }
這個匯編指令將當前線程的頁目錄指針存儲到CR3(頁目錄表物理內存基地址寄存器)中,也就是說這個函數更新了現在的頁目錄表。
最后來看tss_update:
1 /* Sets the ring 0 stack pointer in the TSS to point to the end 2 of the thread stack. */ 3 void 4 tss_update (void) 5 { 6 ASSERT (tss != NULL); 7 tss->esp0 = (uint8_t *) thread_current () + PGSIZE; 8 }
首先要弄清楚tss是什么, tss是task state segment, 叫任務狀態段, 任務(進程)切換時的任務現場信息。
這里其實是把TSS的一個棧指針指向了當前線程棧的尾部, 也就是更新了任務現場的信息和狀態。
好, 到現在process_activate分析完了, 總結一下: 其實就是做了2件事情: 1.更新頁目錄表 2.更新任務現場信息(TSS)
我們現在繼續來看thread_schedule_tail, 最后是這4行:
1 /* If the thread we switched from is dying, destroy its struct 2 thread. This must happen late so that thread_exit() doesn't 3 pull out the rug under itself. (We don't free 4 initial_thread because its memory was not obtained via 5 palloc().) */ 6 if (prev != NULL && prev->status == THREAD_DYING && prev != initial_thread) 7 { 8 ASSERT (prev != cur); 9 palloc_free_page (prev); 10 }
這里是如果我們切換的線程狀態是THREAD_DYING(代表欲要銷毀的線程)的話, 調用palloc_free_page:
1 /* Frees the page at PAGE. */ 2 void 3 palloc_free_page (void *page) 4 { 5 palloc_free_multiple (page, 1); 6 }
1 /* Frees the PAGE_CNT pages starting at PAGES. */ 2 void 3 palloc_free_multiple (void *pages, size_t page_cnt) 4 { 5 struct pool *pool; 6 size_t page_idx; 7 8 ASSERT (pg_ofs (pages) == 0); 9 if (pages == NULL || page_cnt == 0) 10 return; 11 12 if (page_from_pool (&kernel_pool, pages)) 13 pool = &kernel_pool; 14 else if (page_from_pool (&user_pool, pages)) 15 pool = &user_pool; 16 else 17 NOT_REACHED (); 18 19 page_idx = pg_no (pages) - pg_no (pool->base); 20 21 #ifndef NDEBUG 22 memset (pages, 0xcc, PGSIZE * page_cnt); 23 #endif 24 25 ASSERT (bitmap_all (pool->used_map, page_idx, page_cnt)); 26 bitmap_set_multiple (pool->used_map, page_idx, page_cnt, false); 27 }
這里創建了一個pool的結構體:
1 /* A memory pool. */ 2 struct pool 3 { 4 struct lock lock; /* Mutual exclusion. */ 5 struct bitmap *used_map; /* Bitmap of free pages. */ 6 uint8_t *base; /* Base of pool. */ 7 };
首先palloc實現的是一個頁分配器, 這里pool的角色就是記憶分配的內容。 這里結構體用位圖記錄空的頁, 關鍵是這里又有一個操作系統很重要的知識概念出現了,就是lock:
1 /* Lock. */ 2 struct lock 3 { 4 struct thread *holder; /* Thread holding lock (for debugging). */ 5 struct semaphore semaphore; /* Binary semaphore controlling access. */ 6 };
然后鎖其實是由二值信號量實現的:
1 /* A counting semaphore. */ 2 struct semaphore 3 { 4 unsigned value; /* Current value. */ 5 struct list waiters; /* List of waiting threads. */ 6 };
具體信號量方法實現在threads/synch.c中, 這里不作更多講解了, 畢竟函數分析還沒涉及到這里。
繼續看palloc_free_multiple, 第8行其實就是截取后12位, 即獲得當前頁偏差量, 斷言為0就是說頁指針應該指向線程結構體
1 /* Offset within a page. */ 2 static inline unsigned pg_ofs (const void *va) { 3 return (uintptr_t) va & PGMASK; 4 }
然后分析12-17行, 這里要弄清楚一點是系統memory分成2個池, 一個是kernel pool, 一個是user pool, user pool是提供給用戶頁的, 別的都是kernel pool。
然后看下這里調用的page_from_pool函數:
1 /* Returns true if PAGE was allocated from POOL, 2 false otherwise. */ 3 static bool 4 page_from_pool (const struct pool *pool, void *page) 5 { 6 size_t page_no = pg_no (page); 7 size_t start_page = pg_no (pool->base); 8 size_t end_page = start_page + bitmap_size (pool->used_map); 9 10 return page_no >= start_page && page_no < end_page; 11 }
pg_no是獲取虛擬頁數的, 方法其實就是直接指針右移12位就行了:
1 /* Virtual page number. */ 2 static inline uintptr_t pg_no (const void *va) { 3 return (uintptr_t) va >> PGBITS; 4 }
然后這里獲取當前池中的的起始頁和結束頁位置, 然后判斷頁面時候在這個池的Number范圍之類來判斷時候屬於某個池。
再看NOT_REACHED函數,這個函數博主找了半天, 最后用全文件搜索才找着在哪,在lib/debug.h中:
1 /* This is outside the header guard so that debug.h may be 2 included multiple times with different settings of NDEBUG. */ 3 #undef ASSERT 4 #undef NOT_REACHED 5 6 #ifndef NDEBUG 7 #define ASSERT(CONDITION) \ 8 if (CONDITION) { } else { \ 9 PANIC ("assertion `%s' failed.", #CONDITION); \ 10 } 11 #define NOT_REACHED() PANIC ("executed an unreachable statement"); 12 #else 13 #define ASSERT(CONDITION) ((void) 0) 14 #define NOT_REACHED() for (;;) 15 #endif /* lib/debug.h */
1 /* GCC lets us add "attributes" to functions, function 2 parameters, etc. to indicate their properties. 3 See the GCC manual for details. */ 4 #define UNUSED __attribute__ ((unused)) 5 #define NO_RETURN __attribute__ ((noreturn)) 6 #define NO_INLINE __attribute__ ((noinline)) 7 #define PRINTF_FORMAT(FMT, FIRST) __attribute__ ((format (printf, FMT, FIRST))) 8 9 /* Halts the OS, printing the source file name, line number, and 10 function name, plus a user-specific message. */ 11 #define PANIC(...) debug_panic (__FILE__, __LINE__, __func__, __VA_ARGS__) 12 13 void debug_panic (const char *file, int line, const char *function, 14 const char *message, ...) PRINTF_FORMAT (4, 5) NO_RETURN;
這里根據NDEBUG狀態分兩種define, 一個是ASSERT空函數, NOT_REACHED執行死循環, 一個是如果ASSERT參數CONDITION為false的話就調用PANIC輸出文件,行數,函數名和用戶信息, NOT_REACHED也會輸出信息。
有些童鞋在跑測試的時候會出現卡在一個地方不動的狀態, 其實不是因為你電腦的問題, 而是當一些錯誤觸發NOT_REACHED之類的問題的時候, 因為非debug環境就一直執行死循環了, 反映出來的行為就是命令行卡住不動沒有輸出。
注意這里的語法類似__attribute__和((format(printf, m , n)))是面向gcc編譯器處理的寫法, 這里做的事情其實是參數聲明和調用匹配性檢查。
好, 繼續來看palloc_free_multiple, 用page_idx保存了計算出來了頁id, 清空了頁指針, 然后還剩下最后兩行:
1 ASSERT (bitmap_all (pool->used_map, page_idx, page_cnt)); 2 bitmap_set_multiple (pool->used_map, page_idx, page_cnt, false);
第一個斷言:
1 /* Returns true if every bit in B between START and START + CNT, 2 exclusive, is set to true, and false otherwise. */ 3 bool 4 bitmap_all (const struct bitmap *b, size_t start, size_t cnt) 5 { 6 return !bitmap_contains (b, start, cnt, false); 7 }
1 /* Returns true if any bits in B between START and START + CNT, 2 exclusive, are set to VALUE, and false otherwise. */ 3 bool 4 bitmap_contains (const struct bitmap *b, size_t start, size_t cnt, bool value) 5 { 6 size_t i; 7 8 ASSERT (b != NULL); 9 ASSERT (start <= b->bit_cnt); 10 ASSERT (start + cnt <= b->bit_cnt); 11 12 for (i = 0; i < cnt; i++) 13 if (bitmap_test (b, start + i) == value) 14 return true; 15 return false; 16 }
bitmap_contains首先做斷言對參數正確性確認, 然后如果所有位處於start到start+cnt都是value的話, 別的都是~value的話, 返回true, 從我們的函數調用來看就是斷言位圖全是0。
1 /* Returns the value of the bit numbered IDX in B. */ 2 bool 3 bitmap_test (const struct bitmap *b, size_t idx) 4 { 5 ASSERT (b != NULL); 6 ASSERT (idx < b->bit_cnt); 7 return (b->bits[elem_idx (idx)] & bit_mask (idx)) != 0; 8 } 9
1 /* Returns the index of the element that contains the bit 2 numbered BIT_IDX. */ 3 static inline size_t 4 elem_idx (size_t bit_idx) 5 { 6 return bit_idx / ELEM_BITS; 7 } 8 9 /* Returns an elem_type where only the bit corresponding to 10 BIT_IDX is turned on. */ 11 static inline elem_type 12 bit_mask (size_t bit_idx) 13 { 14 return (elem_type) 1 << (bit_idx % ELEM_BITS); 15 }
來看bit_test的實現, 這里直接返回某一位的具體值。
這里直接用elem_idx獲取idx對應的index取出位, 然后和bit_mask做與操作, bit_mask就是返回了一個只有idx位是1其他都是0的一個數, 也就是說idx必須為1才返回true對bit_test來說, 否則false。
好, 至此, 對palloc_free_multiple只剩一行了:
1 bitmap_set_multiple (pool->used_map, page_idx, page_cnt, false);
/* Sets the CNT bits starting at START in B to VALUE. */ void bitmap_set_multiple (struct bitmap *b, size_t start, size_t cnt, bool value) { size_t i; ASSERT (b != NULL); ASSERT (start <= b->bit_cnt); ASSERT (start + cnt <= b->bit_cnt); for (i = 0; i < cnt; i++) bitmap_set (b, start + i, value); }
這里對位圖所有位都做了bitmap_set設置:
1 /* Atomically sets the bit numbered IDX in B to VALUE. */ 2 void 3 bitmap_set (struct bitmap *b, size_t idx, bool value) 4 { 5 ASSERT (b != NULL); 6 ASSERT (idx < b->bit_cnt); 7 if (value) 8 bitmap_mark (b, idx); 9 else 10 bitmap_reset (b, idx); 11 }
很明顯這里mark就是設為1, reset就是置為0。
來看一下實現:
1 /* Atomically sets the bit numbered BIT_IDX in B to true. */ 2 void 3 bitmap_mark (struct bitmap *b, size_t bit_idx) 4 { 5 size_t idx = elem_idx (bit_idx); 6 elem_type mask = bit_mask (bit_idx); 7 8 /* This is equivalent to `b->bits[idx] |= mask' except that it 9 is guaranteed to be atomic on a uniprocessor machine. See 10 the description of the OR instruction in [IA32-v2b]. */ 11 asm ("orl %1, %0" : "=m" (b->bits[idx]) : "r" (mask) : "cc"); 12 } 13 14 /* Atomically sets the bit numbered BIT_IDX in B to false. */ 15 void 16 bitmap_reset (struct bitmap *b, size_t bit_idx) 17 { 18 size_t idx = elem_idx (bit_idx); 19 elem_type mask = bit_mask (bit_idx); 20 21 /* This is equivalent to `b->bits[idx] &= ~mask' except that it 22 is guaranteed to be atomic on a uniprocessor machine. See 23 the description of the AND instruction in [IA32-v2a]. */ 24 asm ("andl %1, %0" : "=m" (b->bits[idx]) : "r" (~mask) : "cc"); 25 }
一樣, 最底層的實現依然是用匯編語言實現的, 兩個匯編語言實現的就是兩個邏輯: 1. b->bits[idx] |= mask 2. b->bits[idx] &= ~mask, 這里mask都是只有idx位為1, 其他為0的mask。
好, 到現在位置palloc_free_multiple已經分析完了, 整理一下邏輯:
其實就是把頁的位圖全部清0了, 清0代表這這個頁表的所有頁都是free的, 等於清空了頁目錄表中的所有頁面。
邏輯繼續向上回溯:
thread_schedule_tail其實就是獲取當前線程, 分配恢復之前執行的狀態和現場, 如果當前線程死了就清空資源。
schedule其實就是拿下一個線程切換過來繼續run。
thread_yield其實就是把當前線程扔到就緒隊列里, 然后重新schedule, 注意這里如果ready隊列為空的話當前線程會繼續在cpu執行。
最后回溯到我們最頂層的函數邏輯: timer_sleep就是在ticks時間內, 如果線程處於running狀態就不斷把他扔到就緒隊列不讓他執行。
好的, 至此我們對原來的timer_sleep的實現方式有了十分清楚的理解了, 我們也很清楚的看到了它的缺點:
線程依然不斷在cpu就緒隊列和running隊列之間來回, 占用了cpu資源, 這並不是我們想要的, 我們希望用一種喚醒機制來實現這個函數。
實現思路: 調用timer_sleep的時候直接把線程阻塞掉,然后給線程結構體加一個成員ticks_blocked來記錄這個線程被sleep了多少時間, 然后利用操作系統自身的時鍾中斷(每個tick會執行一次)加入對線程狀態的檢測, 每次檢測將ticks_blocked減1, 如果減到0就喚醒這個線程。
具體代碼:
1 /* Sleeps for approximately TICKS timer ticks. Interrupts must 2 be turned on. */ 3 void 4 timer_sleep (int64_t ticks) 5 { 6 if (ticks <= 0) 7 { 8 return; 9 } 10 ASSERT (intr_get_level () == INTR_ON); 11 enum intr_level old_level = intr_disable (); 12 struct thread *current_thread = thread_current (); 13 current_thread->ticks_blocked = ticks; 14 thread_block (); 15 intr_set_level (old_level); 16 }
注意這里調用的thread_block:
1 /* Puts the current thread to sleep. It will not be scheduled 2 again until awoken by thread_unblock(). 3 4 This function must be called with interrupts turned off. It 5 is usually a better idea to use one of the synchronization 6 primitives in synch.h. */ 7 void 8 thread_block (void) 9 { 10 ASSERT (!intr_context ()); 11 ASSERT (intr_get_level () == INTR_OFF); 12 13 thread_current ()->status = THREAD_BLOCKED; 14 schedule (); 15 }
線程的各種原理之前分析都已經剖析過了, 不作過多解釋。
給線程的結構體加上我們的ticks_blocked成員:
1 /* Record the time the thread has been blocked. */ 2 int64_t ticks_blocked;
然后在線程被創建的時候初始化ticks_blocked為0, 加在thread_create函數內:
1 t->ticks_blocked = 0;
然后修改時鍾中斷處理函數, 加入線程sleep時間的檢測, 加在timer_interrupt內:
1 thread_foreach (blocked_thread_check, NULL);
這里的thread_foreach就是對每個線程都執行blocked_thread_check這個函數:
1 /* Invoke function 'func' on all threads, passing along 'aux'. 2 This function must be called with interrupts off. */ 3 void 4 thread_foreach (thread_action_func *func, void *aux) 5 { 6 struct list_elem *e; 7 8 ASSERT (intr_get_level () == INTR_OFF); 9 10 for (e = list_begin (&all_list); e != list_end (&all_list); 11 e = list_next (e)) 12 { 13 struct thread *t = list_entry (e, struct thread, allelem); 14 func (t, aux); 15 } 16 }
aux就是傳給這個函數的參數。
然后, 給thread添加一個方法blocked_thread_check即可:
thread.h中聲明:
1 void blocked_thread_check (struct thread *t, void *aux UNUSED);
thread.c:
1 /* Check the blocked thread */ 2 void 3 blocked_thread_check (struct thread *t, void *aux UNUSED) 4 { 5 if (t->status == THREAD_BLOCKED && t->ticks_blocked > 0) 6 { 7 t->ticks_blocked--; 8 if (t->ticks_blocked == 0) 9 { 10 thread_unblock(t); 11 } 12 } 13 }
thread_unblock就是把線程丟到就緒隊列里繼續跑:
1 /* Transitions a blocked thread T to the ready-to-run state. 2 This is an error if T is not blocked. (Use thread_yield() to 3 make the running thread ready.) 4 5 This function does not preempt the running thread. This can 6 be important: if the caller had disabled interrupts itself, 7 it may expect that it can atomically unblock a thread and 8 update other data. */ 9 void 10 thread_unblock (struct thread *t) 11 { 12 enum intr_level old_level; 13 14 ASSERT (is_thread (t)); 15 16 old_level = intr_disable (); 17 ASSERT (t->status == THREAD_BLOCKED); 18 list_push_back (&ready_list, &t->elem); 19 t->status = THREAD_READY; 20 intr_set_level (old_level); 21 }
好的, 這樣timer_sleep函數喚醒機制就實現了。
然后跑測試結果會是這樣的:
好, 我們還需要pass掉alarm_priority我們這個實驗一就算完成了,繼續搞~
Mission2:
實現優先級調度(2.2.3)
先來分析一下線程, 線程成員本身就有一個priority。
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 /* Owned by thread.c. */ 20 unsigned magic; /* Detects stack overflow. */ 21 22 /* Record the time the thread has been blocked. */ 23 int64_t ticks_blocked; 24 };
然后priority的約束:
1 /* Thread priorities. */ 2 #define PRI_MIN 0 /* Lowest priority. */ 3 #define PRI_DEFAULT 31 /* Default priority. */ 4 #define PRI_MAX 63 /* Highest priority. */
我們之前分析timer_sleep的時候其實已經對線程的調度有了非常深刻的剖析了,這里實現優先級調度的核心思想就是: 維持就緒隊列為一個優先級隊列。
換一種說法: 我們在插入線程到就緒隊列的時候保證這個隊列是一個優先級隊列即可。
那么我們在什么時候會把一個線程丟到就緒隊列中呢?
1. thread_unblock
2. init_thread
3. thread_yield
那么我們只要在扔的時候維持這個就緒隊列是優先級隊列即可。
我們來看: thread_unblock現在丟進隊列里是這么干的:
1 list_push_back (&ready_list, &t->elem);
這個是直接扔到隊列尾部了, 我們並不希望這么做, 於是我們只要改一下這個扔的動作就可以了,因為調度的時候下一個thread是直接取隊頭的。
那么我們先來研究一下pintos的隊列是如何搞的,在/lib/kernel/:
1 /* List element. */ 2 struct list_elem 3 { 4 struct list_elem *prev; /* Previous list element. */ 5 struct list_elem *next; /* Next list element. */ 6 }; 7 8 /* List. */ 9 struct list 10 { 11 struct list_elem head; /* List head. */ 12 struct list_elem tail; /* List tail. */ 13 };
很常見的隊列數據結構, 繼續研究一下現在隊列有哪些有用的函數可以幫助我們實現優先級隊列:
然后喜大普奔地發現了一些神奇的函數:
1 /* Operations on lists with ordered elements. */ 2 void list_sort (struct list *, 3 list_less_func *, void *aux); 4 void list_insert_ordered (struct list *, struct list_elem *, 5 list_less_func *, void *aux); 6 void list_unique (struct list *, struct list *duplicates, 7 list_less_func *, void *aux);
list_insert_ordered不就是為我們這里的實現量身訂造的么!搞起!!
1 /* Inserts ELEM in the proper position in LIST, which must be 2 sorted according to LESS given auxiliary data AUX. 3 Runs in O(n) average case in the number of elements in LIST. */ 4 void 5 list_insert_ordered (struct list *list, struct list_elem *elem, 6 list_less_func *less, void *aux) 7 { 8 struct list_elem *e; 9 10 ASSERT (list != NULL); 11 ASSERT (elem != NULL); 12 ASSERT (less != NULL); 13 14 for (e = list_begin (list); e != list_end (list); e = list_next (e)) 15 if (less (elem, e, aux)) 16 break; 17 return list_insert (e, elem); 18 }
直接修改thread_unblock函數把list_push_back改成:
1 list_insert_ordered (&ready_list, &t->elem, (list_less_func *) &thread_cmp_priority, NULL);
然后實現一下比較函數thread_cmp_priority:
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 }
然后對thread_yield和thread_init里的list_push_back作同樣的修改:
init_thread:
1 list_insert_ordered (&all_list, &t->allelem, (list_less_func *) &thread_cmp_priority, NULL);
thread_yield:
1 list_insert_ordered (&ready_list, &cur->elem, (list_less_func *) &thread_cmp_priority, NULL);
做完這些工作之后, 就興高采烈地發現alarm_priority這個測試pass了。
好的, 我們實現了一個優先級隊列, 但是因為這個優先級機制還會引出一系列的問題, 就是我們接下來要解決的問題。
下面讓我們來pass這兩個test:
priority-change
priority-preempt
我們直接TDD吧, 測試驅動開發, 來看測試做了什么:
1 void 2 test_priority_preempt (void) 3 { 4 /* This test does not work with the MLFQS. */ 5 ASSERT (!thread_mlfqs); 6 7 /* Make sure our priority is the default. */ 8 ASSERT (thread_get_priority () == PRI_DEFAULT); 9 10 thread_create ("high-priority", PRI_DEFAULT + 1, simple_thread_func, NULL); 11 msg ("The high-priority thread should have already completed."); 12 } 13 14 static void 15 simple_thread_func (void *aux UNUSED) 16 { 17 int i; 18 19 for (i = 0; i < 5; i++) 20 { 21 msg ("Thread %s iteration %d", thread_name (), i); 22 thread_yield (); 23 } 24 msg ("Thread %s done!", thread_name ()); 25 }
先分析一下搶占式調度的測試, 其實就是在創建一個線程的時候, 如果線程高於當前線程就先執行創建的線程。
然后下面這個測試是基於搶占式調度的基礎之上做的測試:
1 void 2 test_priority_change (void) 3 { 4 /* This test does not work with the MLFQS. */ 5 ASSERT (!thread_mlfqs); 6 7 msg ("Creating a high-priority thread 2."); 8 thread_create ("thread 2", PRI_DEFAULT + 1, changing_thread, NULL); 9 msg ("Thread 2 should have just lowered its priority."); 10 thread_set_priority (PRI_DEFAULT - 2); 11 msg ("Thread 2 should have just exited."); 12 } 13 14 static void 15 changing_thread (void *aux UNUSED) 16 { 17 msg ("Thread 2 now lowering priority."); 18 thread_set_priority (PRI_DEFAULT - 1); 19 msg ("Thread 2 exiting."); 20 }
而測試希望的輸出結果是這樣的:
所以我們得出一個結論就是: 測試線程(我們稱為thread1)創建了一個PRI_DEFAULT+1優先級的內核線程thread2,然后由於thread2優先級高,
所以線程執行直接切換到thread2, thread1阻塞, 然后thread2執行的時候調用的是changing_thread, 又把自身優先級調為PRI_DEFAULT-1,
這個時候thread1的優先級就大於thread2了, 此時thread2阻塞於最后一個msg輸出, 線程切換到thread1, 然后thread1又把自己優先級改成PRI_DEFAULT-2,
這個時候thread2又高於thread1了, 所以執行thread2, 然后在輸出thread1的msg, 於是整個過程就有了圖中的測試輸出結果。
分析這個測試行為我們得出的結論就是: 在設置一個線程優先級要立即重新考慮所有線程執行順序, 重新安排執行順序。
好, 弄清楚思路實現其實就非常簡單了, 直接在線程設置優先級的時候調用thread_yield即可, 這樣就把當前線程重新丟到就緒隊列中繼續執行, 保證了執行順序。
此外, 還有在創建線程的時候, 如果新創建的線程比主線程優先級高的話也要調用thread_yield。
實現代碼:
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 (); 7 }
然后在thread_create最后把創建的線程unblock了之后加上這些代碼:
1 if (thread_current ()->priority < priority) 2 { 3 thread_yield (); 4 }
然后就搞定了,總共就加了5行代碼。
可能有人會想2個test總共才加了5行代碼,這個會不會太簡單了。
其實不會,實現這個解決方案的前提是對這個調度機制有清楚的認識,對測試有充分完整的分析。
來看測試結果:
然后就興高采烈的把priority_change和priority_preempt給pass了,好我們來繼續搞別的測試~
接下來我們來把priority-priority-*給過了,這個測試是優先級捐贈的測試,下面我們來分析一下:
這個是一個優先級翻轉(priority inversion)問題:
線程 A,B,C 分別具有 1,2,3 優先級(數字越大說明優先級越高), 線程 A,B 目前在就緒隊列中等待調度, 線程 A 對一個互斥資源擁有線程鎖。而此時, 高優先級的線程 C 也想要訪問這個互斥資源, 線程 C 只好在這個資源上等待,不能進入就緒隊列。當調度器開始調度時, 它只能從 A 和 B 中進行選擇,根據優先級調度原理,線程 B 將會首先運行。
這時就產生了一個問題, 即本來線程 C 優先級比線程 B 高, 但是線程 B 卻先運行了,從而產生了優先級翻轉問題。(這里復制了課程TA的原話)
怎么解決這個問題?
當發現高優先級的任務因為低優先級任務占用資源而阻塞時,就將低優先級任務的優先級提升到等待它所占有的資源的最高優先級任務的優先級。
繼續TDD走起, 先來看priority-donate-one測試代碼:
1 void 2 test_priority_donate_one (void) 3 { 4 struct lock lock; 5 6 /* This test does not work with the MLFQS. */ 7 ASSERT (!thread_mlfqs); 8 9 /* Make sure our priority is the default. */ 10 ASSERT (thread_get_priority () == PRI_DEFAULT); 11 12 lock_init (&lock); 13 lock_acquire (&lock); 14 thread_create ("acquire1", PRI_DEFAULT + 1, acquire1_thread_func, &lock); 15 msg ("This thread should have priority %d. Actual priority: %d.", 16 PRI_DEFAULT + 1, thread_get_priority ()); 17 thread_create ("acquire2", PRI_DEFAULT + 2, acquire2_thread_func, &lock); 18 msg ("This thread should have priority %d. Actual priority: %d.", 19 PRI_DEFAULT + 2, thread_get_priority ()); 20 lock_release (&lock); 21 msg ("acquire2, acquire1 must already have finished, in that order."); 22 msg ("This should be the last line before finishing this test."); 23 } 24 25 static void 26 acquire1_thread_func (void *lock_) 27 { 28 struct lock *lock = lock_; 29 30 lock_acquire (lock); 31 msg ("acquire1: got the lock"); 32 lock_release (lock); 33 msg ("acquire1: done"); 34 } 35 36 static void 37 acquire2_thread_func (void *lock_) 38 { 39 struct lock *lock = lock_; 40 41 lock_acquire (lock); 42 msg ("acquire2: got the lock"); 43 lock_release (lock); 44 msg ("acquire2: done"); 45 }
分析: 首先當前線程(稱為original_thread)是一個優先級為PRI_DEFAULT的線程, 然后第4行創建了一個鎖, 接着創建一個線程acquire1,優先級為PRI_DEFAULT+1, 傳了一個參數為這個鎖的函數過去(線程acquire1執行的時候會調用)。
好, 我們之前實現的搶占式調度會讓acquire1馬上執行, 來看acquire1_thread_func干了什么, 這里直接獲取了這個鎖, 來看lock_acquire函數:
1 /* Acquires LOCK, sleeping until it becomes available if 2 necessary. The lock must not already be held by the current 3 thread. 4 5 This function may sleep, so it must not be called within an 6 interrupt handler. This function may be called with 7 interrupts disabled, but interrupts will be turned back on if 8 we need to sleep. */ 9 void 10 lock_acquire (struct lock *lock) 11 { 12 ASSERT (lock != NULL); 13 ASSERT (!intr_context ()); 14 ASSERT (!lock_held_by_current_thread (lock)); 15 16 sema_down (&lock->semaphore); 17 lock->holder = thread_current (); 18 }
這里如我們所想, 直接調用信號量PV操作中的P操作, 來看P操作sema_down:
1 /* Down or "P" operation on a semaphore. Waits for SEMA's value 2 to become positive and then atomically decrements it. 3 4 This function may sleep, so it must not be called within an 5 interrupt handler. This function may be called with 6 interrupts disabled, but if it sleeps then the next scheduled 7 thread will probably turn interrupts back on. */ 8 void 9 sema_down (struct semaphore *sema) 10 { 11 enum intr_level old_level; 12 13 ASSERT (sema != NULL); 14 ASSERT (!intr_context ()); 15 16 old_level = intr_disable (); 17 while (sema->value == 0) 18 { 19 list_push_back (&sema->waiters, &thread_current ()->elem); 20 thread_block (); 21 } 22 sema->value--; 23 intr_set_level (old_level); 24 }
和課上描述的一致, 把線程丟到這個信號量的隊列waiters里, 阻塞該線程等待喚醒, value--。
注意, 這里acquire1_thread_func阻塞了, msg這個時候並不會輸出, 這時會繼續執行original_thread, 然后輸出msg, 輸出當前線程應該的優先級和實際的優先級。
然后繼續創建一個線程acquire2, 優先級為PRI_DEFAULT+2, 這里調用和acquire1一致, 然后original_thread繼續輸出msg。
好, 然后original_thread釋放了這個鎖(V操作), 釋放的過程會觸發被鎖着的線程acquire1, acquire2, 然后根據優先級調度, 先執行acquire2, 再acquire1, 最后再執行original_thread。
那么這里應該是acquire2, acquire1分別釋放鎖然后輸出msg, 最后original_thread再輸出msg。
好, 我們已經把這個測試程序分析完了, 我們來看它希望的輸出:
1 # -*- perl -*- 2 use strict; 3 use warnings; 4 use tests::tests; 5 check_expected ([<<'EOF']); 6 (priority-donate-one) begin 7 (priority-donate-one) This thread should have priority 32. Actual priority: 32. 8 (priority-donate-one) This thread should have priority 33. Actual priority: 33. 9 (priority-donate-one) acquire2: got the lock 10 (priority-donate-one) acquire2: done 11 (priority-donate-one) acquire1: got the lock 12 (priority-donate-one) acquire1: done 13 (priority-donate-one) acquire2, acquire1 must already have finished, in that order. 14 (priority-donate-one) This should be the last line before finishing this test. 15 (priority-donate-one) end 16 EOF 17 pass;
輸出行為和我們分析的一致, 來看7,8行, original_thread的優先級分別變成了PRI_DEFAULT+1和PRI_DEFAULT+2。
我們來根據這個結果分析一下優先級捐贈行為:
original_thread擁有的鎖被acquire1獲取之后, 因為acquire1線程被阻塞於這個鎖, 那么acquire1的執行必須要original_thread繼續執行釋放這個鎖, 從優先級的角度來說, original_thread的優先級應該提升到acquire1的優先級,
因為original_thread本身的執行包含了acquire1執行的阻塞, 所以此時acquire1對original_thread做了捐贈, 優先級提到PRI_DEFAULT+1, acquire2行為類似。
好, 支持priority-donate-one分析結束, 我們來分析一下實現:
具體行為肯定是被鎖定在了鎖的獲取和釋放上了, 我們的實現思路是:
在一個線程獲取一個鎖的時候, 如果擁有這個鎖的線程優先級比自己低就提高它的優先級,然后在這個線程釋放掉這個鎖之后把原來擁有這個鎖的線程改回原來的優先級。
好, 這里先不急着寫代碼, 繼續來分析其他測試, 全部分析完了再寫代碼, 因為這些測試都是優先級捐獻相關的行為, 我們全部分析完了再寫就避免了走彎路了。
來分析測試priority-donte-multiple:
1 void 2 test_priority_donate_multiple (void) 3 { 4 struct lock a, b; 5 6 /* This test does not work with the MLFQS. */ 7 ASSERT (!thread_mlfqs); 8 9 /* Make sure our priority is the default. */ 10 ASSERT (thread_get_priority () == PRI_DEFAULT); 11 12 lock_init (&a); 13 lock_init (&b); 14 15 lock_acquire (&a); 16 lock_acquire (&b); 17 18 thread_create ("a", PRI_DEFAULT + 1, a_thread_func, &a); 19 msg ("Main thread should have priority %d. Actual priority: %d.", 20 PRI_DEFAULT + 1, thread_get_priority ()); 21 22 thread_create ("b", PRI_DEFAULT + 2, b_thread_func, &b); 23 msg ("Main thread should have priority %d. Actual priority: %d.", 24 PRI_DEFAULT + 2, thread_get_priority ()); 25 26 lock_release (&b); 27 msg ("Thread b should have just finished."); 28 msg ("Main thread should have priority %d. Actual priority: %d.", 29 PRI_DEFAULT + 1, thread_get_priority ()); 30 31 lock_release (&a); 32 msg ("Thread a should have just finished."); 33 msg ("Main thread should have priority %d. Actual priority: %d.", 34 PRI_DEFAULT, thread_get_priority ()); 35 } 36 37 static void 38 a_thread_func (void *lock_) 39 { 40 struct lock *lock = lock_; 41 42 lock_acquire (lock); 43 msg ("Thread a acquired lock a."); 44 lock_release (lock); 45 msg ("Thread a finished."); 46 } 47 48 static void 49 b_thread_func (void *lock_) 50 { 51 struct lock *lock = lock_; 52 53 lock_acquire (lock); 54 msg ("Thread b acquired lock b."); 55 lock_release (lock); 56 msg ("Thread b finished."); 57 }
一樣, original_thread是優先級為PRI_DEFAULT的線程, 然后創建2個鎖, 接着創建優先級為PRI_DEFAULT+1的線程a, 把鎖a丟給這個線程的執行函數。
這時候線程a搶占式地調用a_thread_func, 獲取了a這個鎖, 阻塞。
然后original_thread輸出線程優先級的msg。
然后再創建一個線程優先級為PRI_DEFAULT+2的線程b, 和a一樣做同樣的操作。
好, 然后original_thread釋放掉了鎖b, 此時線程b被喚醒, 搶占式執行b_thread_func。
然后original再輸出msg, a同上, 此時我們來看一下測試希望的輸出是什么:
1 # -*- perl -*- 2 use strict; 3 use warnings; 4 use tests::tests; 5 check_expected ([<<'EOF']); 6 (priority-donate-multiple) begin 7 (priority-donate-multiple) Main thread should have priority 32. Actual priority: 32. 8 (priority-donate-multiple) Main thread should have priority 33. Actual priority: 33. 9 (priority-donate-multiple) Thread b acquired lock b. 10 (priority-donate-multiple) Thread b finished. 11 (priority-donate-multiple) Thread b should have just finished. 12 (priority-donate-multiple) Main thread should have priority 32. Actual priority: 32. 13 (priority-donate-multiple) Thread a acquired lock a. 14 (priority-donate-multiple) Thread a finished. 15 (priority-donate-multiple) Thread a should have just finished. 16 (priority-donate-multiple) Main thread should have priority 31. Actual priority: 31. 17 (priority-donate-multiple) end 18 EOF 19 pass;
好, 這里輸出和我們的分析依然是一致的。 重點在於original_thread的優先級變化, 第一次輸出是正常的, priority-donate-one已經測試了這個邏輯。
這里特別的是original_thread擁有兩把鎖分別給a, b兩個線程占有了。
后面是釋放了b之后, original_thread的優先級恢復到32, 即當前線程的優先級還是被a的優先級所捐贈着的,最后釋放了a之后才回到原來的優先級。
這里測試的行為實際是: 多鎖情況下優先級邏輯的正確性。
那么我們對應的實現思路是: 釋放一個鎖的時候, 將該鎖的擁有者改為該線程被捐贈的第二優先級,若沒有其余捐贈者, 則恢復原始優先級。
那么我們的線程必然需要一個數據結構來記錄所有對這個線程有捐贈行為的線程。
繼續來看priority-donate-multiple2這個測試:
1 void 2 test_priority_donate_multiple2 (void) 3 { 4 struct lock a, b; 5 6 /* This test does not work with the MLFQS. */ 7 ASSERT (!thread_mlfqs); 8 9 /* Make sure our priority is the default. */ 10 ASSERT (thread_get_priority () == PRI_DEFAULT); 11 12 lock_init (&a); 13 lock_init (&b); 14 15 lock_acquire (&a); 16 lock_acquire (&b); 17 18 thread_create ("a", PRI_DEFAULT + 3, a_thread_func, &a); 19 msg ("Main thread should have priority %d. Actual priority: %d.", 20 PRI_DEFAULT + 3, thread_get_priority ()); 21 22 thread_create ("c", PRI_DEFAULT + 1, c_thread_func, NULL); 23 24 thread_create ("b", PRI_DEFAULT + 5, b_thread_func, &b); 25 msg ("Main thread should have priority %d. Actual priority: %d.", 26 PRI_DEFAULT + 5, thread_get_priority ()); 27 28 lock_release (&a); 29 msg ("Main thread should have priority %d. Actual priority: %d.", 30 PRI_DEFAULT + 5, thread_get_priority ()); 31 32 lock_release (&b); 33 msg ("Threads b, a, c should have just finished, in that order."); 34 msg ("Main thread should have priority %d. Actual priority: %d.", 35 PRI_DEFAULT, thread_get_priority ()); 36 } 37 38 static void 39 a_thread_func (void *lock_) 40 { 41 struct lock *lock = lock_; 42 43 lock_acquire (lock); 44 msg ("Thread a acquired lock a."); 45 lock_release (lock); 46 msg ("Thread a finished."); 47 } 48 49 static void 50 b_thread_func (void *lock_) 51 { 52 struct lock *lock = lock_; 53 54 lock_acquire (lock); 55 msg ("Thread b acquired lock b."); 56 lock_release (lock); 57 msg ("Thread b finished."); 58 } 59 60 static void 61 c_thread_func (void *a_ UNUSED) 62 { 63 msg ("Thread c finished."); 64 }
有了之前的分析這里簡單說一下: original_thread擁有2個鎖, 然后創建PRI_DEFAULT+3的線程a去拿a這個鎖, PRI_DEFAULT+5的線程b去拿b這個鎖, 中間創建了一個PRI_DEFAULT+1的c線程, 但是因為創建的時候當前線程的優先級已經被a線程捐贈了所以搶占調度並沒有發生。 然后分別釋放掉a和b, 釋放a的時候線程a被喚醒, 但是優先級依然不如當前線程, 此時當前線程優先級仍然被b捐贈着, 優先級最高繼續執行, 然后釋放掉b, 釋放掉b之后,original_thread的優先級降到初始,應該最后被調用, 線程b搶占調度, 然后線程a, 再是線程c, 最后才original_thread輸出msg。
於是就有了以下的輸出斷言:
1 # -*- perl -*- 2 use strict; 3 use warnings; 4 use tests::tests; 5 check_expected ([<<'EOF']); 6 (priority-donate-multiple2) begin 7 (priority-donate-multiple2) Main thread should have priority 34. Actual priority: 34. 8 (priority-donate-multiple2) Main thread should have priority 36. Actual priority: 36. 9 (priority-donate-multiple2) Main thread should have priority 36. Actual priority: 36. 10 (priority-donate-multiple2) Thread b acquired lock b. 11 (priority-donate-multiple2) Thread b finished. 12 (priority-donate-multiple2) Thread a acquired lock a. 13 (priority-donate-multiple2) Thread a finished. 14 (priority-donate-multiple2) Thread c finished. 15 (priority-donate-multiple2) Threads b, a, c should have just finished, in that order. 16 (priority-donate-multiple2) Main thread should have priority 31. Actual priority: 31. 17 (priority-donate-multiple2) end 18 EOF 19 pass;
這里依然測試的是多鎖情況下優先級邏輯的正確性。
再來看測試priority-donate-nest:
1 void 2 test_priority_donate_nest (void) 3 { 4 struct lock a, b; 5 struct locks locks; 6 7 /* This test does not work with the MLFQS. */ 8 ASSERT (!thread_mlfqs); 9 10 /* Make sure our priority is the default. */ 11 ASSERT (thread_get_priority () == PRI_DEFAULT); 12 13 lock_init (&a); 14 lock_init (&b); 15 16 lock_acquire (&a); 17 18 locks.a = &a; 19 locks.b = &b; 20 thread_create ("medium", PRI_DEFAULT + 1, medium_thread_func, &locks); 21 thread_yield (); 22 msg ("Low thread should have priority %d. Actual priority: %d.", 23 PRI_DEFAULT + 1, thread_get_priority ()); 24 25 thread_create ("high", PRI_DEFAULT + 2, high_thread_func, &b); 26 thread_yield (); 27 msg ("Low thread should have priority %d. Actual priority: %d.", 28 PRI_DEFAULT + 2, thread_get_priority ()); 29 30 lock_release (&a); 31 thread_yield (); 32 msg ("Medium thread should just have finished."); 33 msg ("Low thread should have priority %d. Actual priority: %d.", 34 PRI_DEFAULT, thread_get_priority ()); 35 } 36 37 static void 38 medium_thread_func (void *locks_) 39 { 40 struct locks *locks = locks_; 41 42 lock_acquire (locks->b); 43 lock_acquire (locks->a); 44 45 msg ("Medium thread should have priority %d. Actual priority: %d.", 46 PRI_DEFAULT + 2, thread_get_priority ()); 47 msg ("Medium thread got the lock."); 48 49 lock_release (locks->a); 50 thread_yield (); 51 52 lock_release (locks->b); 53 thread_yield (); 54 55 msg ("High thread should have just finished."); 56 msg ("Middle thread finished."); 57 } 58 59 static void 60 high_thread_func (void *lock_) 61 { 62 struct lock *lock = lock_; 63 64 lock_acquire (lock); 65 msg ("High thread got the lock."); 66 lock_release (lock); 67 msg ("High thread finished."); 68 }
注意, original_thread只獲取了鎖a, 它並不擁有鎖b。
這里創建了一個locks的結構體裝着2個鎖a和b, 然后創建medium線程, 優先級為PRI_DEFAULT+1, 把這個locks作為線程medium執行函數的參數。
然后搶占調用medium_thread_func, 此時這個函數獲取b這個鎖, 此時medium成為鎖b的擁有者, 並不阻塞, 繼續執行, 然后medium在獲取鎖a的時候阻塞了。
此時original_thread繼續跑, 它的優先級被medium提到了PRI_DEFAULT+1, 輸出優先級Msg。
然后創建優先級為PRI_DEFAULT+2的high線程, 搶占調用high_thread_func, 然后這里high拿到了b這個鎖, 而b的擁有者是medium, 阻塞, 注意, 這里medium被high捐贈了, 優先級到PRI_DEFAULT+2, 此時original_thread也應該一樣提到同樣優先級。
然后original_thread輸出一下優先級msg之后釋放掉鎖a, 釋放出發了medium_thread_func搶占調用, 輸出此時優先級為PRI_DEFAULT+2, 然后medium釋放掉a, 釋放掉b, 釋放b的時候被high_thread_func搶占, high輸出完之后medium繼續run, 輸出兩句之后再到original_thread, 輸出兩句msg完事。
按照這個邏輯, 它的希望輸出也是和我們分析的過程一樣:
1 # -*- perl -*- 2 use strict; 3 use warnings; 4 use tests::tests; 5 check_expected ([<<'EOF']); 6 (priority-donate-nest) begin 7 (priority-donate-nest) Low thread should have priority 32. Actual priority: 32. 8 (priority-donate-nest) Low thread should have priority 33. Actual priority: 33. 9 (priority-donate-nest) Medium thread should have priority 33. Actual priority: 33. 10 (priority-donate-nest) Medium thread got the lock. 11 (priority-donate-nest) High thread got the lock. 12 (priority-donate-nest) High thread finished. 13 (priority-donate-nest) High thread should have just finished. 14 (priority-donate-nest) Middle thread finished. 15 (priority-donate-nest) Medium thread should just have finished. 16 (priority-donate-nest) Low thread should have priority 31. Actual priority: 31. 17 (priority-donate-nest) end 18 EOF 19 pass;
這個測試是一個優先級嵌套問題, 重點在於medium擁有的鎖被low阻塞, 在這個前提下high再去獲取medium的說阻塞的話, 優先級提升具有連環效應, 就是medium被提升了, 此時它被鎖捆綁的low線程應該跟着一起提升。
從實現的角度來說, 我們線程又需要加一個數據結構, 我們需要獲取這個線程被鎖於哪個線程。
好的, 再看下一個測試priority-donate-sema:
1 void 2 test_priority_donate_sema (void) 3 { 4 struct lock_and_sema ls; 5 6 /* This test does not work with the MLFQS. */ 7 ASSERT (!thread_mlfqs); 8 9 /* Make sure our priority is the default. */ 10 ASSERT (thread_get_priority () == PRI_DEFAULT); 11 12 lock_init (&ls.lock); 13 sema_init (&ls.sema, 0); 14 thread_create ("low", PRI_DEFAULT + 1, l_thread_func, &ls); 15 thread_create ("med", PRI_DEFAULT + 3, m_thread_func, &ls); 16 thread_create ("high", PRI_DEFAULT + 5, h_thread_func, &ls); 17 sema_up (&ls.sema); 18 msg ("Main thread finished."); 19 } 20 21 static void 22 l_thread_func (void *ls_) 23 { 24 struct lock_and_sema *ls = ls_; 25 26 lock_acquire (&ls->lock); 27 msg ("Thread L acquired lock."); 28 sema_down (&ls->sema); 29 msg ("Thread L downed semaphore."); 30 lock_release (&ls->lock); 31 msg ("Thread L finished."); 32 } 33 34 static void 35 m_thread_func (void *ls_) 36 { 37 struct lock_and_sema *ls = ls_; 38 39 sema_down (&ls->sema); 40 msg ("Thread M finished."); 41 } 42 43 static void 44 h_thread_func (void *ls_) 45 { 46 struct lock_and_sema *ls = ls_; 47 48 lock_acquire (&ls->lock); 49 msg ("Thread H acquired lock."); 50 51 sema_up (&ls->sema); 52 lock_release (&ls->lock); 53 msg ("Thread H finished."); 54 }
lock_and_sema是包含一個鎖和一個信號量的結構體, 初始化信號量為0, 然后創建PRI_DEFAULT+1的線程low, 獲取ls內的鎖成為擁有者, 然后sema_down(P)阻塞。
然后創建PRI_DEFAULT+3的線程med, 這里也直接調用sema_down(P)阻塞了。
最后創建PRI_DEFAULT+5的線程high, 這里獲取鎖, 阻塞。
然后回到original_thread, 調用V操作, 此時喚醒了l_thread_func, 因為low被high捐獻了優先級高於med, 然后l_thread_func跑, 釋放掉了鎖。
此時直接觸發h_thread_func, 輸出, 然后V操作, 釋放掉鎖, 由於優先級最高所以V操作之后不會被搶占, 這個函數跑完之后m_thread_func開始跑(被V喚醒), 然后輸出一句完事, 再到l_thread_func中輸出最后一句回到original_thread。
這里包含了信號量和鎖混合觸發, 實際上還是信號量在起作用, 因為鎖是由信號量實現的。
所以輸出是這樣的:
1 # -*- perl -*- 2 use strict; 3 use warnings; 4 use tests::tests; 5 check_expected ([<<'EOF']); 6 (priority-donate-sema) begin 7 (priority-donate-sema) Thread L acquired lock. 8 (priority-donate-sema) Thread L downed semaphore. 9 (priority-donate-sema) Thread H acquired lock. 10 (priority-donate-sema) Thread H finished. 11 (priority-donate-sema) Thread M finished. 12 (priority-donate-sema) Thread L finished. 13 (priority-donate-sema) Main thread finished. 14 (priority-donate-sema) end 15 EOF 16 pass;
再來看priority-donate-lower:
1 void 2 test_priority_donate_lower (void) 3 { 4 struct lock lock; 5 6 /* This test does not work with the MLFQS. */ 7 ASSERT (!thread_mlfqs); 8 9 /* Make sure our priority is the default. */ 10 ASSERT (thread_get_priority () == PRI_DEFAULT); 11 12 lock_init (&lock); 13 lock_acquire (&lock); 14 thread_create ("acquire", PRI_DEFAULT + 10, acquire_thread_func, &lock); 15 msg ("Main thread should have priority %d. Actual priority: %d.", 16 PRI_DEFAULT + 10, thread_get_priority ()); 17 18 msg ("Lowering base priority..."); 19 thread_set_priority (PRI_DEFAULT - 10); 20 msg ("Main thread should have priority %d. Actual priority: %d.", 21 PRI_DEFAULT + 10, thread_get_priority ()); 22 lock_release (&lock); 23 msg ("acquire must already have finished."); 24 msg ("Main thread should have priority %d. Actual priority: %d.", 25 PRI_DEFAULT - 10, thread_get_priority ()); 26 } 27 28 static void 29 acquire_thread_func (void *lock_) 30 { 31 struct lock *lock = lock_; 32 33 lock_acquire (lock); 34 msg ("acquire: got the lock"); 35 lock_release (lock); 36 msg ("acquire: done"); 37 }
這里當前線程有一個鎖, 然后創建acquire搶占式獲取了這個鎖阻塞, 然后此時original_thread優先級為PRI_DEFAULT+10, 然后這里調用thread_set_priority, 此時當前線程的優先級應該沒有改變, 但是它以后如果恢復優先級時候其實是有改變的, 就是說, 我們如果用original_priority來記錄他的話, 如果這個線程處於被捐贈狀態的話則直接修改original_priority來完成邏輯, 此時函數過后優先級還是PRI_DEFAULT+10, 然后釋放掉鎖, acquire搶占輸出和釋放, 然后original_thread的優先級應該變成了PRI_DEFAULT-10。
這里測試的邏輯是當修改一個被捐贈的線程優先級的時候的行為正確性。
還剩下最后3個測試分析, 先看priority-sema:
1 void 2 test_priority_sema (void) 3 { 4 int i; 5 6 /* This test does not work with the MLFQS. */ 7 ASSERT (!thread_mlfqs); 8 9 sema_init (&sema, 0); 10 thread_set_priority (PRI_MIN); 11 for (i = 0; i < 10; i++) 12 { 13 int priority = PRI_DEFAULT - (i + 3) % 10 - 1; 14 char name[16]; 15 snprintf (name, sizeof name, "priority %d", priority); 16 thread_create (name, priority, priority_sema_thread, NULL); 17 } 18 19 for (i = 0; i < 10; i++) 20 { 21 sema_up (&sema); 22 msg ("Back in main thread."); 23 } 24 } 25 26 static void 27 priority_sema_thread (void *aux UNUSED) 28 { 29 sema_down (&sema); 30 msg ("Thread %s woke up.", thread_name ()); 31 }
這里創建10個線程阻塞於P操作, 然后用一個循環V操作, 然后看結果:
1 # -*- perl -*- 2 use strict; 3 use warnings; 4 use tests::tests; 5 check_expected ([<<'EOF']); 6 (priority-sema) begin 7 (priority-sema) Thread priority 30 woke up. 8 (priority-sema) Back in main thread. 9 (priority-sema) Thread priority 29 woke up. 10 (priority-sema) Back in main thread. 11 (priority-sema) Thread priority 28 woke up. 12 (priority-sema) Back in main thread. 13 (priority-sema) Thread priority 27 woke up. 14 (priority-sema) Back in main thread. 15 (priority-sema) Thread priority 26 woke up. 16 (priority-sema) Back in main thread. 17 (priority-sema) Thread priority 25 woke up. 18 (priority-sema) Back in main thread. 19 (priority-sema) Thread priority 24 woke up. 20 (priority-sema) Back in main thread. 21 (priority-sema) Thread priority 23 woke up. 22 (priority-sema) Back in main thread. 23 (priority-sema) Thread priority 22 woke up. 24 (priority-sema) Back in main thread. 25 (priority-sema) Thread priority 21 woke up. 26 (priority-sema) Back in main thread. 27 (priority-sema) end 28 EOF 29 pass;
好, 也就是說V喚醒的時候也是優先級高的先喚醒, 換句話說, 信號量的等待隊列是優先級隊列。
再看priority-condvar測試:
1 void 2 test_priority_condvar (void) 3 { 4 int i; 5 6 /* This test does not work with the MLFQS. */ 7 ASSERT (!thread_mlfqs); 8 9 lock_init (&lock); 10 cond_init (&condition); 11 12 thread_set_priority (PRI_MIN); 13 for (i = 0; i < 10; i++) 14 { 15 int priority = PRI_DEFAULT - (i + 7) % 10 - 1; 16 char name[16]; 17 snprintf (name, sizeof name, "priority %d", priority); 18 thread_create (name, priority, priority_condvar_thread, NULL); 19 } 20 21 for (i = 0; i < 10; i++) 22 { 23 lock_acquire (&lock); 24 msg ("Signaling..."); 25 cond_signal (&condition, &lock); 26 lock_release (&lock); 27 } 28 } 29 30 static void 31 priority_condvar_thread (void *aux UNUSED) 32 { 33 msg ("Thread %s starting.", thread_name ()); 34 lock_acquire (&lock); 35 cond_wait (&condition, &lock); 36 msg ("Thread %s woke up.", thread_name ()); 37 lock_release (&lock); 38 }
這里condition里面裝的是一個waiters隊列, 看一下con_wait和cond_signal函數:
1 /* Atomically releases LOCK and waits for COND to be signaled by 2 some other piece of code. After COND is signaled, LOCK is 3 reacquired before returning. LOCK must be held before calling 4 this function. 5 6 The monitor implemented by this function is "Mesa" style, not 7 "Hoare" style, that is, sending and receiving a signal are not 8 an atomic operation. Thus, typically the caller must recheck 9 the condition after the wait completes and, if necessary, wait 10 again. 11 12 A given condition variable is associated with only a single 13 lock, but one lock may be associated with any number of 14 condition variables. That is, there is a one-to-many mapping 15 from locks to condition variables. 16 17 This function may sleep, so it must not be called within an 18 interrupt handler. This function may be called with 19 interrupts disabled, but interrupts will be turned back on if 20 we need to sleep. */ 21 void 22 cond_wait (struct condition *cond, struct lock *lock) 23 { 24 struct semaphore_elem waiter; 25 26 ASSERT (cond != NULL); 27 ASSERT (lock != NULL); 28 ASSERT (!intr_context ()); 29 ASSERT (lock_held_by_current_thread (lock)); 30 31 sema_init (&waiter.semaphore, 0); 32 list_push_back (&cond->waiters, &waiter.elem); 33 lock_release (lock); 34 sema_down (&waiter.semaphore); 35 lock_acquire (lock); 36 } 37 38 /* If any threads are waiting on COND (protected by LOCK), then 39 this function signals one of them to wake up from its wait. 40 LOCK must be held before calling this function. 41 42 An interrupt handler cannot acquire a lock, so it does not 43 make sense to try to signal a condition variable within an 44 interrupt handler. */ 45 void 46 cond_signal (struct condition *cond, struct lock *lock UNUSED) 47 { 48 ASSERT (cond != NULL); 49 ASSERT (lock != NULL); 50 ASSERT (!intr_context ()); 51 ASSERT (lock_held_by_current_thread (lock)); 52 53 if (!list_empty (&cond->waiters)) 54 sema_up (&list_entry (list_pop_front (&cond->waiters), 55 struct semaphore_elem, elem)->semaphore); 56 }
分析: cond_wait和cond_signal就是釋放掉鎖, 等待signal喚醒, 然后再重新獲取鎖。
這里的代碼邏輯是: 創建10個線程, 每個線程調用的時候獲取鎖, 然后調用cond_wait把鎖釋放阻塞於cond_signal喚醒, 然后連續10次循環調用cond_signal。
來看輸出:
1 # -*- perl -*- 2 use strict; 3 use warnings; 4 use tests::tests; 5 check_expected ([<<'EOF']); 6 (priority-condvar) begin 7 (priority-condvar) Thread priority 23 starting. 8 (priority-condvar) Thread priority 22 starting. 9 (priority-condvar) Thread priority 21 starting. 10 (priority-condvar) Thread priority 30 starting. 11 (priority-condvar) Thread priority 29 starting. 12 (priority-condvar) Thread priority 28 starting. 13 (priority-condvar) Thread priority 27 starting. 14 (priority-condvar) Thread priority 26 starting. 15 (priority-condvar) Thread priority 25 starting. 16 (priority-condvar) Thread priority 24 starting. 17 (priority-condvar) Signaling... 18 (priority-condvar) Thread priority 30 woke up. 19 (priority-condvar) Signaling... 20 (priority-condvar) Thread priority 29 woke up. 21 (priority-condvar) Signaling... 22 (priority-condvar) Thread priority 28 woke up. 23 (priority-condvar) Signaling... 24 (priority-condvar) Thread priority 27 woke up. 25 (priority-condvar) Signaling... 26 (priority-condvar) Thread priority 26 woke up. 27 (priority-condvar) Signaling... 28 (priority-condvar) Thread priority 25 woke up. 29 (priority-condvar) Signaling... 30 (priority-condvar) Thread priority 24 woke up. 31 (priority-condvar) Signaling... 32 (priority-condvar) Thread priority 23 woke up. 33 (priority-condvar) Signaling... 34 (priority-condvar) Thread priority 22 woke up. 35 (priority-condvar) Signaling... 36 (priority-condvar) Thread priority 21 woke up. 37 (priority-condvar) end 38 EOF 39 pass;
從結果來看, 這里要求的實質就是: condition的waiters隊列是優先級隊列
好, 來看最后一個測試priority-donate-chain:
1 void 2 test_priority_donate_chain (void) 3 { 4 int i; 5 struct lock locks[NESTING_DEPTH - 1]; 6 struct lock_pair lock_pairs[NESTING_DEPTH]; 7 8 /* This test does not work with the MLFQS. */ 9 ASSERT (!thread_mlfqs); 10 11 thread_set_priority (PRI_MIN); 12 13 for (i = 0; i < NESTING_DEPTH - 1; i++) 14 lock_init (&locks[i]); 15 16 lock_acquire (&locks[0]); 17 msg ("%s got lock.", thread_name ()); 18 19 for (i = 1; i < NESTING_DEPTH; i++) 20 { 21 char name[16]; 22 int thread_priority; 23 24 snprintf (name, sizeof name, "thread %d", i); 25 thread_priority = PRI_MIN + i * 3; 26 lock_pairs[i].first = i < NESTING_DEPTH - 1 ? locks + i: NULL; 27 lock_pairs[i].second = locks + i - 1; 28 29 thread_create (name, thread_priority, donor_thread_func, lock_pairs + i); 30 msg ("%s should have priority %d. Actual priority: %d.", 31 thread_name (), thread_priority, thread_get_priority ()); 32 33 snprintf (name, sizeof name, "interloper %d", i); 34 thread_create (name, thread_priority - 1, interloper_thread_func, NULL); 35 } 36 37 lock_release (&locks[0]); 38 msg ("%s finishing with priority %d.", thread_name (), 39 thread_get_priority ()); 40 } 41 42 static void 43 donor_thread_func (void *locks_) 44 { 45 struct lock_pair *locks = locks_; 46 47 if (locks->first) 48 lock_acquire (locks->first); 49 50 lock_acquire (locks->second); 51 msg ("%s got lock", thread_name ()); 52 53 lock_release (locks->second); 54 msg ("%s should have priority %d. Actual priority: %d", 55 thread_name (), (NESTING_DEPTH - 1) * 3, 56 thread_get_priority ()); 57 58 if (locks->first) 59 lock_release (locks->first); 60 61 msg ("%s finishing with priority %d.", thread_name (), 62 thread_get_priority ()); 63 } 64 65 static void 66 interloper_thread_func (void *arg_ UNUSED) 67 { 68 msg ("%s finished.", thread_name ()); 69 }
首先lock_pair是包含兩個lock指針的結構體, 然后將當前線程優先級設為PRI_MIN, 然后這里有個locks數組, 容量為7, 然后lock_pairs數組用來裝lock_pair, 容量也是7。
然后當前線程獲取locks[0]這個鎖, 接着跳到7次循環里, 每次循環thread_priority為PRI_MIN+i*3, 也就是3,6,9,12... 然后對應的lock_pairs[i]的first記錄locks[i]的指針, second記錄locks[i-1]指針,
然后創建線程, 優先級為thread_priority, 執行參數傳的是&lock_pairs[i], 注意這里由於優先級每次都底層, 所以每次循環都會搶占調用donor_thread_func, 然后分別獲取lock_pairs[i]里裝的鎖, 然后每次循環先獲取first, 即locks[i], 然后獲取second, 由於second是前一個, 而前一個的擁有者一定是前一次循環創建的線程, 第一次拿得的是locks[0], 最后一次循環first為NULL, second為locks[6], 即最后一個線程不擁有鎖, 但是阻塞於前一個創建的線程, 這里還會輸出信息, 即創建的線程阻塞之后會輸出當前線程的優先級msg, 當然這里必然是每一次都提升了的, 所以每次都是thread_priority。
然后每次循環最后還創建了1個線程, 優先級為thread_priority-1, 但是這里由於上一個線程創建和阻塞的過程中優先級捐獻已經發生, 所以這里並不發生搶占, 只是創建出來了而已。
然后original_thread釋放掉locks[0], 釋放掉這個之后thread1得到了喚醒, 輸出信息, 釋放掉這個鎖, 然后輸出當前優先級, 由於這個線程還是被后面最高優先級的線程說捐贈的, 所以每次往后優先級都是21, 然后釋放掉first, 這里又觸發下一個線程繼續跑, 注意當后面的全部跑完的時候當前線程的優先級其實是不被捐贈的, 這里就變成了原來的優先級, 但是是所有線程都釋放了之后才依次返回輸出結束msg。
這個測試其實就是一個鏈式優先級捐贈, 本質測試的還是多層優先級捐贈邏輯的正確性。
需要注意的是一個邏輯: 釋放掉一個鎖之后, 如果當前線程不被捐贈即馬上改為原來的優先級, 搶占式調度。
好, 我們把所有priority相關的測試都分析了個遍, 現在再來寫代碼實現這些測試邏輯就是很簡單的事情了, 來搞~
先總結一下所有測試整合的邏輯:
1. 在一個線程獲取一個鎖的時候, 如果擁有這個鎖的線程優先級比自己低就提高它的優先級,並且如果這個鎖還被別的鎖鎖着, 將會遞歸地捐贈優先級, 然后在這個線程釋放掉這個鎖之后恢復未捐贈邏輯下的優先級。
2. 如果一個線程被多個線程捐贈, 維持當前優先級為捐贈優先級中的最大值(acquire和release之時)。
3. 在對一個線程進行優先級設置的時候, 如果這個線程處於被捐贈狀態, 則對original_priority進行設置, 然后如果設置的優先級大於當前優先級, 則改變當前優先級, 否則在捐贈狀態取消的時候恢復original_priority。
4. 在釋放鎖對一個鎖優先級有改變的時候應考慮其余被捐贈優先級和當前優先級。
5. 將信號量的等待隊列實現為優先級隊列。
6. 將condition的waiters隊列實現為優先級隊列。
7. 釋放鎖的時候若優先級改變則可以發生搶占。
具體代碼實現:
先修改thread數據結構, 加入以下成員:
1 int base_priority; /* Base priority. */ 2 struct list locks; /* Locks that the thread is holding. */ 3 struct lock *lock_waiting; /* The lock that the thread is waiting for. */
然后給lock加一下成員:
1 struct list_elem elem; /* List element for priority donation. */ 2 int max_priority; /* Max priority among the threads acquiring the lock. */
好, 數據結構只需要增加這些就可以滿足我們的需要了, 開寫。
先修改lock_acquire函數:
1 void 2 lock_acquire (struct lock *lock) 3 { 4 struct thread *current_thread = thread_current (); 5 struct lock *l; 6 enum intr_level old_level; 7 8 ASSERT (lock != NULL); 9 ASSERT (!intr_context ()); 10 ASSERT (!lock_held_by_current_thread (lock)); 11 12 if (lock->holder != NULL && !thread_mlfqs) 13 { 14 current_thread->lock_waiting = lock; 15 l = lock; 16 while (l && current_thread->priority > l->max_priority) 17 { 18 l->max_priority = current_thread->priority; 19 thread_donate_priority (l->holder); 20 l = l->holder->lock_waiting; 21 } 22 } 23 24 sema_down (&lock->semaphore); 25 26 old_level = intr_disable (); 27 28 current_thread = thread_current (); 29 if (!thread_mlfqs) 30 { 31 current_thread->lock_waiting = NULL; 32 lock->max_priority = current_thread->priority; 33 thread_hold_the_lock (lock); 34 } 35 lock->holder = current_thread; 36 37 intr_set_level (old_level); 38 }
在P操作之前遞歸地實現優先級捐贈, 然后在被喚醒之后(此時這個線程已經擁有了這個鎖),成為這個鎖的擁有者。
這里thread_donate_priority和thread_hold_the_lock封裝成函數,注意一下這里優先級捐贈是通過直接修改鎖的最高優先級, 然后調用update的時候把現成優先級更新實現的,update下面會寫, 實現如下:
1 /* Let thread hold a lock */ 2 void 3 thread_hold_the_lock(struct lock *lock) 4 { 5 enum intr_level old_level = intr_disable (); 6 list_insert_ordered (&thread_current ()->locks, &lock->elem, lock_cmp_priority, NULL); 7 8 if (lock->max_priority > thread_current ()->priority) 9 { 10 thread_current ()->priority = lock->max_priority; 11 thread_yield (); 12 } 13 14 intr_set_level (old_level); 15 }
1 /* Donate current priority to thread t. */ 2 void 3 thread_donate_priority (struct thread *t) 4 { 5 enum intr_level old_level = intr_disable (); 6 thread_update_priority (t); 7 8 if (t->status == THREAD_READY) 9 { 10 list_remove (&t->elem); 11 list_insert_ordered (&ready_list, &t->elem, thread_cmp_priority, NULL); 12 } 13 intr_set_level (old_level); 14 }
鎖隊列排序函數lock_cmp_priority:
1 /* lock comparation function */ 2 bool 3 lock_cmp_priority (const struct list_elem *a, const struct list_elem *b, void *aux UNUSED) 4 { 5 return list_entry (a, struct lock, elem)->max_priority > list_entry (b, struct lock, elem)->max_priority; 6 }
然后在lock_release函數加入以下語句:
1 if (!thread_mlfqs) 2 thread_remove_lock (lock);
thread_remove_lock實現如下:
1 /* Remove a lock. */ 2 void 3 thread_remove_lock (struct lock *lock) 4 { 5 enum intr_level old_level = intr_disable (); 6 list_remove (&lock->elem); 7 thread_update_priority (thread_current ()); 8 intr_set_level (old_level); 9 }
當釋放掉一個鎖的時候, 當前線程的優先級可能發生變化, 我們用thread_update_priority來處理這個邏輯:
1 /* Update priority. */ 2 void 3 thread_update_priority (struct thread *t) 4 { 5 enum intr_level old_level = intr_disable (); 6 int max_priority = t->base_priority; 7 int lock_priority; 8 9 if (!list_empty (&t->locks)) 10 { 11 list_sort (&t->locks, lock_cmp_priority, NULL); 12 lock_priority = list_entry (list_front (&t->locks), struct lock, elem)->max_priority; 13 if (lock_priority > max_priority) 14 max_priority = lock_priority; 15 } 16 17 t->priority = max_priority; 18 intr_set_level (old_level); 19 }
這里如果這個線程還有鎖, 就先獲取這個線程擁有鎖的最大優先級(可能被更高級線程捐贈), 然后如果這個優先級比base_priority大的話更新的應該是被捐贈的優先級。
然后在init_thread中加入初始化:
1 t->base_priority = priority; 2 list_init (&t->locks); 3 t->lock_waiting = NULL;
修改一下thread_set_priority:
1 void 2 thread_set_priority (int new_priority) 3 { 4 if (thread_mlfqs) 5 return; 6 7 enum intr_level old_level = intr_disable (); 8 9 struct thread *current_thread = thread_current (); 10 int old_priority = current_thread->priority; 11 current_thread->base_priority = new_priority; 12 13 if (list_empty (¤t_thread->locks) || new_priority > old_priority) 14 { 15 current_thread->priority = new_priority; 16 thread_yield (); 17 } 18 19 intr_set_level (old_level); 20 }
好, 至此整個捐贈的邏輯都寫完了, 還差兩個優先級隊列的實現, 搞起~
然后把condition的隊列改成優先級隊列, 修改如下, 修改cond_signal函數:
1 void 2 cond_signal (struct condition *cond, struct lock *lock UNUSED) 3 { 4 ASSERT (cond != NULL); 5 ASSERT (lock != NULL); 6 ASSERT (!intr_context ()); 7 ASSERT (lock_held_by_current_thread (lock)); 8 9 if (!list_empty (&cond->waiters)) 10 { 11 list_sort (&cond->waiters, cond_sema_cmp_priority, NULL); 12 sema_up (&list_entry (list_pop_front (&cond->waiters), struct semaphore_elem, elem)->semaphore); 13 } 14 }
比較函數:
1 /* cond sema comparation function */ 2 bool 3 cond_sema_cmp_priority (const struct list_elem *a, const struct list_elem *b, void *aux UNUSED) 4 { 5 struct semaphore_elem *sa = list_entry (a, struct semaphore_elem, elem); 6 struct semaphore_elem *sb = list_entry (b, struct semaphore_elem, elem); 7 return list_entry(list_front(&sa->semaphore.waiters), struct thread, elem)->priority > list_entry(list_front(&sb->semaphore.waiters), struct thread, elem)->priority; 8 }
然后把信號量的等待隊列實現為優先級隊列:
修改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 { 11 list_sort (&sema->waiters, thread_cmp_priority, NULL); 12 thread_unblock (list_entry (list_pop_front (&sema->waiters), struct thread, elem)); 13 } 14 15 sema->value++; 16 thread_yield (); 17 intr_set_level (old_level); 18 }
修改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) 11 { 12 list_insert_ordered (&sema->waiters, &thread_current ()->elem, thread_cmp_priority, NULL); 13 thread_block (); 14 } 15 sema->value--; 16 intr_set_level (old_level); 17 }
好, 做完這些其實是一氣呵成的, 因為之前多測試需求有足夠多的分析了, 來看測試結果:
毫無懸念地過了, 至此mission2完成,撒個花, 看個B站繼續搞mission3~
Mission3:
實現多級反饋調度(2.2.4)(Advanced Scheduler or 4.4BSD Scheduler)
作用: 減少系統平均響應時間。
在這個mission中我們需要把mlfqs*給過了, 完成之后實驗一的所有測試就pass了~
實驗具體描述(必讀): http://www.ccs.neu.edu/home/amislove/teaching/cs5600/fall10/pintos/pintos_7.html
Every thread has a nice value between -20 and 20 directly under its control. Each thread also has a priority, between 0 (
PRI_MIN
) through 63 (PRI_MAX
), which is recalculated using the following formula every fourth tick:
priority = PRI_MAX
- (recent_cpu / 4) - (nice * 2).
recent_cpu measures the amount of CPU time a thread has received "recently." On each timer tick, the running thread's recent_cpu is incremented by 1. Once per second, every thread's recent_cpu is updated this way:
recent_cpu = (2*load_avg)/(2*load_avg + 1) * recent_cpu + nice.
load_avg estimates the average number of threads ready to run over the past minute. It is initialized to 0 at boot and recalculated once per second as follows:
load_avg = (59/60)*load_avg + (1/60)*ready_threads.
where ready_threads is the number of threads that are either running or ready to run at time of update (not including the idle thread).
簡單來說這里是維持了64個隊列, 每個隊列對應一個優先級, 從PRI_MIN到PRI_MAX。
然后通過一些公式計算來計算出線程當前的優先級, 系統調度的時候會從高優先級隊列開始選擇線程執行, 這里線程的優先級隨着操作系統的運轉數據而動態改變。
然后這個計算又涉及到了浮點數運算的問題, pintos本身並沒有實現這個, 需要我們自己來搞。
The following table summarizes how fixed-point arithmetic operations can be implemented in C. In the table,
x
andy
are fixed-point numbers,n
is an integer, fixed-point numbers are in signed p.q format where p + q = 31, andf
is1 << q:
Convert n
to fixed point:n * f
Convert x
to integer (rounding toward zero):x / f
Convert x
to integer (rounding to nearest):(x + f / 2) / f
ifx >= 0
,(x - f / 2) / f
ifx <= 0
.Add x
andy
:x + y
Subtract y
fromx
:x - y
Add x
andn
:x + n * f
Subtract n
fromx
:x - n * f
Multiply x
byy
:((int64_t) x) * y / f
Multiply x
byn
:x * n
Divide x
byy
:((int64_t) x) * f / y
Divide x
byn
:x / n
實現思路: 在timer_interrupt中固定一段時間計算更新線程的優先級,這里是每TIMER_FREQ時間更新一次系統load_avg和所有線程的recent_cpu, 每4個timer_ticks更新一次線程優先級, 每個timer_tick running線程的recent_cpu加一, 雖然這里說的是維持64個優先級隊列調度, 其本質還是優先級調度, 我們保留之前寫的優先級調度代碼即可, 去掉優先級捐贈(之前donate相關代碼已經對需要的地方加了thread_mlfqs的判斷了)。
浮點運算邏輯實現在fixed_point.h中:
1 #ifndef __THREAD_FIXED_POINT_H 2 #define __THREAD_FIXED_POINT_H 3 4 /* Basic definitions of fixed point. */ 5 typedef int fixed_t; 6 /* 16 LSB used for fractional part. */ 7 #define FP_SHIFT_AMOUNT 16 8 /* Convert a value to fixed-point value. */ 9 #define FP_CONST(A) ((fixed_t)(A << FP_SHIFT_AMOUNT)) 10 /* Add two fixed-point value. */ 11 #define FP_ADD(A,B) (A + B) 12 /* Add a fixed-point value A and an int value B. */ 13 #define FP_ADD_MIX(A,B) (A + (B << FP_SHIFT_AMOUNT)) 14 /* Substract two fixed-point value. */ 15 #define FP_SUB(A,B) (A - B) 16 /* Substract an int value B from a fixed-point value A */ 17 #define FP_SUB_MIX(A,B) (A - (B << FP_SHIFT_AMOUNT)) 18 /* Multiply a fixed-point value A by an int value B. */ 19 #define FP_MULT_MIX(A,B) (A * B) 20 /* Divide a fixed-point value A by an int value B. */ 21 #define FP_DIV_MIX(A,B) (A / B) 22 /* Multiply two fixed-point value. */ 23 #define FP_MULT(A,B) ((fixed_t)(((int64_t) A) * B >> FP_SHIFT_AMOUNT)) 24 /* Divide two fixed-point value. */ 25 #define FP_DIV(A,B) ((fixed_t)((((int64_t) A) << FP_SHIFT_AMOUNT) / B)) 26 /* Get integer part of a fixed-point value. */ 27 #define FP_INT_PART(A) (A >> FP_SHIFT_AMOUNT) 28 /* Get rounded integer of a fixed-point value. */ 29 #define FP_ROUND(A) (A >= 0 ? ((A + (1 << (FP_SHIFT_AMOUNT - 1))) >> FP_SHIFT_AMOUNT) \ 30 : ((A - (1 << (FP_SHIFT_AMOUNT - 1))) >> FP_SHIFT_AMOUNT)) 31 32 #endif /* thread/fixed_point.h */
注意一下這里的實現, 這里用16位數(FP_SHIFT_AMOUNT)作為浮點數的小數部分, 理解了這點就很容易看了, 注意一點是無論什么運算一定要維持整數部分從第17位開始就行。
先實現timer_interrupt的邏輯,加入以下代碼:
1 if (thread_mlfqs) 2 { 3 thread_mlfqs_increase_recent_cpu_by_one (); 4 if (ticks % TIMER_FREQ == 0) 5 thread_mlfqs_update_load_avg_and_recent_cpu (); 6 else if (ticks % 4 == 0) 7 thread_mlfqs_update_priority (thread_current ()); 8 }
然后填一下這里挖的坑:
1 /* Increase recent_cpu by 1. */ 2 void 3 thread_mlfqs_increase_recent_cpu_by_one (void) 4 { 5 ASSERT (thread_mlfqs); 6 ASSERT (intr_context ()); 7 8 struct thread *current_thread = thread_current (); 9 if (current_thread == idle_thread) 10 return; 11 current_thread->recent_cpu = FP_ADD_MIX (current_thread->recent_cpu, 1); 12 }
注意這里調用的運算都是浮點運算。
1 /* Every per second to refresh load_avg and recent_cpu of all threads. */ 2 void 3 thread_mlfqs_update_load_avg_and_recent_cpu (void) 4 { 5 ASSERT (thread_mlfqs); 6 ASSERT (intr_context ()); 7 8 size_t ready_threads = list_size (&ready_list); 9 if (thread_current () != idle_thread) 10 ready_threads++; 11 load_avg = FP_ADD (FP_DIV_MIX (FP_MULT_MIX (load_avg, 59), 60), FP_DIV_MIX (FP_CONST (ready_threads), 60)); 12 13 struct thread *t; 14 struct list_elem *e = list_begin (&all_list); 15 for (; e != list_end (&all_list); e = list_next (e)) 16 { 17 t = list_entry(e, struct thread, allelem); 18 if (t != idle_thread) 19 { 20 t->recent_cpu = FP_ADD_MIX (FP_MULT (FP_DIV (FP_MULT_MIX (load_avg, 2), FP_ADD_MIX (FP_MULT_MIX (load_avg, 2), 1)), t->recent_cpu), t->nice); 21 thread_mlfqs_update_priority (t); 22 } 23 } 24 }
這里的大部分邏輯都是Project給好的算數運算邏輯, 真正的行為邏輯並不復雜, 再填最后一個坑:
1 /* Update priority. */ 2 void 3 thread_mlfqs_update_priority (struct thread *t) 4 { 5 if (t == idle_thread) 6 return; 7 8 ASSERT (thread_mlfqs); 9 ASSERT (t != idle_thread); 10 11 t->priority = FP_INT_PART (FP_SUB_MIX (FP_SUB (FP_CONST (PRI_MAX), FP_DIV_MIX (t->recent_cpu, 4)), 2 * t->nice)); 12 t->priority = t->priority < PRI_MIN ? PRI_MIN : t->priority; 13 t->priority = t->priority > PRI_MAX ? PRI_MAX : t->priority; 14 }
好, 把這個3個坑給填了之后我們mission3的主體邏輯就已經完成了。
然后處理一些細節改動, thread結構體加入以下成員:
1 int nice; /* Niceness. */ 2 fixed_t recent_cpu; /* Recent CPU. */
線程初始化的時候初始化這兩個新的成員, 在init_thread中加入這些代碼:
1 t->nice = 0; 2 t->recent_cpu = FP_CONST (0);
然后填一下這個系統給你挖好的坑:
1 /* Sets the current thread's nice value to NICE. */ 2 void 3 thread_set_nice (int nice) 4 { 5 thread_current ()->nice = nice; 6 thread_mlfqs_update_priority (thread_current ()); 7 thread_yield (); 8 } 9 10 /* Returns the current thread's nice value. */ 11 int 12 thread_get_nice (void) 13 { 14 return thread_current ()->nice; 15 } 16 17 /* Returns 100 times the system load average. */ 18 int 19 thread_get_load_avg (void) 20 { 21 return FP_ROUND (FP_MULT_MIX (load_avg, 100)); 22 } 23 24 /* Returns 100 times the current thread's recent_cpu value. */ 25 int 26 thread_get_recent_cpu (void) 27 { 28 return FP_ROUND (FP_MULT_MIX (thread_current ()->recent_cpu, 100)); 29 } 30
然后在thread.c中加入全局變量:
1 fixed_t load_avg;
並在thread_start中初始化:
1 load_avg = FP_CONST (0);
好, 所有的坑都填完了, 跑一下測試:
看到這 “All 27 tests passed.” 還是挺激動的哈哈~
ok, 至此這個project1就全部完成了, 難度還是有的, 特別在優先級調度這個mission上。
歷時差不多一周, 從開始做這個東西。
完結撒花。
謝謝。