概述
這次實驗主要涉及多線程編程,和之前的實驗不太一樣,比較偏向於應用層面,除了任務一外都是在宿主機上編寫多線程程序,應該是xv6不支持系統級的多線程。
內容
Uthread: switching between threads
這個任務要求對一個程序填空,這個程序在用戶層面實現了多線程的調度。但實際上這個調度和xv6內核的進程調度非常相似。首先是對thread_schedule函數進行填空,只需要調用切換函數就行了,根據文檔的說明,這個切換函數是保存當前線程用到的所有callee-saved寄存器,然后將這些寄存器全部賦值為下一個線程里保存的值,聯想到xv6內核里的context結構體就是用來保存這些寄存器的,所以直接復制過來並作為thread結構體的一個屬性,那么調用切換函數就會像這樣:
thread_switch((uint64)(&t->context), (uint64)(¤t_thread->context));
thread_switch()在uthread_switch.S里實現,這里我不貼代碼了,因為和kernel文件夾里的swtch.S里的內容基本一樣。
注意文檔里的這句話:
One goal is ensure that when
thread_schedule()
runs a given thread for the first time, the thread executes the function passed tothread_create()
, on its own stack.
這里不是說在thread_schedule()中需要檢查線程是否第一次運行,如果是則轉入對應函數;而是說thread_schedule()在運行剛才加入的切換代碼后,自動開始運行對應函數。這就需要在thread_create初始化線程的時候,把它保存的ra寄存器的值賦成對應函數的入口地址,這樣在切換結束后運行匯編代碼中的ret就自動跳轉到ra指向的位置了;另一個需要初始化的是sp寄存器,因為每個線程都各自有一個棧,所以要讓自己的sp寄存器指向自己的棧底,由於棧地址從高向低增長,所以sp寄存器賦為棧數組的最高地址:
t->context.ra = (uint64)func;
t->context.sp = (uint64)t->stack + STACK_SIZE;
Using threads
這個任務要求解決一個程序中的競爭問題。通常兩個線程的競爭出現在同時寫的過程中,所以把put函數用鎖包起來,同時為了速度,一個桶弄一個鎖:
int i = key % NBUCKET;
// is the key already present?
pthread_mutex_lock(lock + i);
struct entry *e = 0;
for (e = table[i]; e != 0; e = e->next) {
if (e->key == key)
break;
}
if(e){
// update the existing key.
e->value = value;
} else {
// the new is new.
insert(key, value, &table[i], table[i]);
}
pthread_mutex_unlock(lock + i);
其實我個人覺得只有insert那個地方需要加鎖,因為兩個線程同時修改一個鍵的過程不管加不加鎖都是一個結果無法預測的行為(先后不明),所以程序肯定不會同時修改一個鍵。不過既然時限給的是1.25倍,文檔的意思應該是整個函數都得加鎖。
Barrier
這個任務要求解決一個程序中的同步問題。核心是理解pthread_cond_wait這個函數的功能,我沒理解好文檔中的注釋,結果遇到了各種奇怪的問題。pthread_cond_wait這個函數在調用時會釋放鎖,隱含的意思就是在執行這個函數前必須先鎖上;函數在阻塞結束被喚醒時會獲取鎖,隱含的意思就是在這個函數調用結束后需要釋放鎖:
pthread_mutex_lock(&bstate.barrier_mutex);
bstate.nthread++;
if (bstate.nthread == nthread) {
bstate.nthread = 0; bstate.round++;
pthread_cond_broadcast(&bstate.barrier_cond);
} else pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex);
pthread_mutex_unlock(&bstate.barrier_mutex);
另外一點就是pthread_mutex_lock的調用必須放在前面,如果只在else后面調用的話,可能會出現死鎖問題,即如果第一個線程加了nthread以后運行得比較慢,還沒有運行cond_wait的時候另一個線程跑得快,把cond_broadcast給運行了,那么第一個線程運行到cond_wait就卡死了,沒有線程去喚醒它,所以這整個if語句必須是個原子操作,不能並行。
總結一下,這次實驗基本沒怎么涉及操作系統的理論,重點還是並行程序的應用。不過不管是國內還是國外,操作系統課在進程(線程)這一章節教的也基本上都是競爭、同步、死鎖這些應用上的問題,可能進程(線程)的獨特之處就在這里,雖然是操作系統中的重點部分,但最值得關注的還是應用上的問題。