〇、前言
本文將會結合源代碼談論 sleep、wakeup 這兩個系統調用。
一、sleep()系統調用
以下是sleep()
函數源碼:
// Atomically release lock and sleep on chan.
// Reacquires lock when awakened.
void
sleep(void *chan, struct spinlock *lk)
{struct proc *p = myproc();// Must acquire p->lock in order to// change p->state and then call sched.// Once we hold p->lock, we can be// guaranteed that we won't miss any wakeup// (wakeup locks p->lock),// so it's okay to release lk.acquire(&p->lock); //DOC: sleeplock1release(lk);// Go to sleep.p->chan = chan;p->state = SLEEPING;sched();// Tidy up.p->chan = 0;// Reacquire original lock.release(&p->lock);acquire(lk);
}
先來看看 lost wakeup 問題。當一個進程在 sleep()
時,如果 sleep()
了一半,狀態還沒來得及修改為SLEEPING,這時候發生了中斷,并且被某些進程調用了 wakeup()
,那么這個 wakeup()
肯定不能把這個進程喚醒。而且,在被中斷恢復后,它將永遠等不到喚醒,因為喚醒已經錯過。所以,在這里必須要正不可中斷性和操作先后性。因為在下面就會看到 wakeup()
只喚醒狀態為 SLEEPING 的進程。
因此我們必須保證,sleep()
是一個原子操作,在 sleep()
執行過程中,要么執行完全,要么沒有被執行。所以這里必須加一個進程鎖。所以在下面就會看到 wakeup()
中也會嘗試獲取休眠的進程鎖。
在持有進程鎖的時候,將進程的狀態設置為 SLEEPING 并記錄sleep channel,之后再調用 sched()
函數,這個函數中會再調用 swtch()
函數(而這會返回到 scheduler()
函數中),此時 sleep()
函數中仍然持有了進程的鎖,wakeup()
仍然不能做任何事情。
因此在 sleep()之后,這個鎖必須釋放。我們來看看細節:
void
scheduler(void)
{...swtch(&c->context, &p->context);// Process is done running for now.// It should have changed its p->state before coming back.c->proc = 0; // 返回的位置,此刻繼續執行}release(&p->lock);...
}
在這里,它會繼續執行上一次執行到的位置,即 c->proc = 0
,然后執行 release(&p->lock)
,也就是釋放鎖,而且釋放的是 sleep()
中的當前進程的鎖。(這一點不是很好理解,可以理解為用的上一個進程的代碼釋放當前進程的鎖?總之,這些代碼就冰冷冷的放在內存里,被 pc 不斷地指一遍又一遍)。更有意思的是,在 sched() 函數返回之后,繼續運行:
void
sleep(void *chan, struct spinlock *lk)
{...sched();// Tidy up.p->chan = 0; // 就緒執行的位置// Reacquire original lock.release(&p->lock);acquire(lk);
}
這里 release(&p->lock)
實際上釋放的是 scheduler()
中選中的進程的鎖。
所以在調度器線程釋放進程鎖之后,wakeup()
才能終于獲取進程的鎖,發現它正在 SLEEPING狀態,并喚醒它。
這里的效果是由之前定義的一些規則確保的,這些規則包括了:
- 調用 sleep 時需要持有condition lock,這樣 sleep 函數才能知道相應的鎖;
- sleep函數只有在獲取到進程的鎖
p->lock
之后,才能釋放 condition lock; - wakeup需要同時持有兩個鎖才能查看進程。
二、wakeup()調用
以下是 wakeup()
的源碼:
// Wake up all processes sleeping on chan.
// Must be called without any p->lock.
void
wakeup(void *chan)
{struct proc *p;for(p = proc; p < &proc[NPROC]; p++) {if(p != myproc()){acquire(&p->lock);if(p->state == SLEEPING && p->chan == chan) {p->state = RUNNABLE;}release(&p->lock);}}
}
可以看到它的工作很簡單,檢查兩個條件之后,就修改進程的狀態為 RUNNABLE。
三、總結
這篇文章詳細地介紹了 xv6 操作系統中的 sleep()
和 wakeup()
系統調用的實現原理以及相關的內部工作機制。主要強調了在 sleep()
中的原子操作性,確保了操作的完整性,以及在 wakeup()
中喚醒休眠進程的方式。
關于 sleep()
:
強調了 sleep()
操作的原子性,使用進程鎖確保 sleep()
操作是一個原子操作,避免了 “lost wakeup” 問題的發生。
通過釋放持有的鎖,讓出 CPU 控制權,進入 SLEEPING 狀態,然后釋放進程鎖,使得其他進程能夠繼續運行。
調度器在合適的時機恢復了進程的執行,完成 sleep()
操作。
關于 wakeup()
:
wakeup()
通過遍歷進程列表,并獲取每個進程的鎖,查看處于 SLEEPING 狀態且 sleep channel 匹配的進程,將其狀態設置為 RUNNABLE,喚醒進程。
整體上,這篇文章清晰地解釋了 sleep()
和 wakeup()
這兩個關鍵系統調用的工作原理和實現細節,突出了在并發環境下確保原子性操作和避免死鎖的重要性。
全文完,感謝閱讀。