在Solaris上写内核模块总是会用到互斥锁(mutex)与条件变量(condvar), 光阴荏苒日月如梭弹指一挥间,Solaris的大船说沉就要沉了,此刻心情不是太好(Orz)。每次被年轻的有才华的同事们(比如Letty同学)问起mutex和cv怎么协同工作的,我总是不能给出一个非常清晰的解释。直到今天,看了cv_wait()的源代码之后,我终于可以给他们一个清楚明白的回答了。
Solaris的源码无法被公开粘贴出来,幸好还有OpenSolaris的继承者illumos。 先贴cv_wait()的源码,再讲互斥锁(mutex)与条件变量(condvar)的协同工作原理。
185 /* 186 * Block on the indicated condition variable and release the 187 * associated kmutex while blocked. 188 */ 189 void 190 cv_wait(kcondvar_t *cvp, kmutex_t *mp) 191 { 192 if (panicstr) 193 return; 194 ASSERT(!quiesce_active); 195 196 ASSERT(curthread->t_schedflag & TS_DONT_SWAP); 197 thread_lock(curthread); /* lock the thread */ 198 cv_block((condvar_impl_t *)cvp); 199 thread_unlock_nopreempt(curthread); /* unlock the waiters field */ 200 mutex_exit(mp); 201 swtch(); 202 mutex_enter(mp); 203 }
注意: 198, 200-202行,等会儿再解释。
首先,一个典型的使用mutex和cv的例子是这样子滴,
1 static kmutex_t mutex; 2 static kcondvar_t condv; 3 static unsigned int ready = 0; 4 5 /* 1. init mutex and cv */ 6 mutex_init(&mutex, NULL, MUTEX_DRIVER, NULL); 7 cv_init(&condv, NULL, CV_DRIVER, NULL); 8 9 /* 2. use mutex and cv */ 10 11 /* Thread 1 */ | /* Thread 2 */ 12 mutex_enter(&mutex); | mutex_enter(&mutex); 13 while (ready == 0) | ready = 1; 14 cv_wait(&condv, &mutex); | cv_signal(&condv); 15 mutex_exit(&mutex); | mutex_exit(&mutex); 16 17 /* 3. destroy mutex and cv */ 18 cv_destroy(&condv); 19 mutex_destroy(&mutex);
- 在Thread 1中,首先获得互斥锁,然后判断条件(ready==0)是否成立,如果成立,则调用cv_wait(&condv, &mutex)进入睡眠;
- 在Thread 2中,首先获得互斥锁,然后将ready赋值为1,调用cv_signal(&condv)唤醒正在睡眠的Thread1,同时释放持有的互斥锁;
- Thread1一旦醒来,会重新判断条件(ready==0)是否成立,如果不成立,则释放互斥锁。 (当然,如果成立,则将再次进入睡眠,等待下次被唤醒)
然后, 问题(Letty同学曾经问过我的)来了: 既然Thread 1在L12行获得了互斥锁然后睡过去了,那么Thread 2怎么可能获得互斥锁?
This is a good question, a really good question! (ps. 老美每次被问住了的时候都这么说)
在今天之前我无法回答,或者只能估摸着回答说"只能看具体实现了"。 好了,我今天就是真看完了具体实现。在cv_wait()的源代码中,
198 cv_block((condvar_impl_t *)cvp);
...
200 mutex_exit(mp); 201 swtch(); 202 mutex_enter(mp);
第198行将自己(currthread)加入睡眠队列,第200行将互斥锁释放,然后在第201行进入睡眠,等待被唤醒。一旦被唤醒,在第202行重新获得互斥锁。
也就是说,睡前释放互斥锁,醒来再获取互斥锁。这样别的线程就有机会获得互斥锁后干活,活干完后将睡眠的线程唤醒。
这也解释了为什么cv_wait()函数不仅仅只有一个参数kcondvar_t *cvp, 还包含参数kmutex_t *mp。
行文至此,我想用一句话作为总结,"The source code is the final world." 如果你想成为一个非常优秀的程序员,请记住RTFSC。
PS:
1. 如果想进一步弄懂为什么要将条件变量和互斥锁一起使用保证同步,请自行google或阅读OS相关的book。
2. 所有关于互斥锁和条件变量的协同工作原理应该是一致的,比如POSIX的pthread_mutex和pthread_cond,Linux内核mutex和completion variable等。