亚洲av成人无遮挡网站在线观看,少妇性bbb搡bbb爽爽爽,亚洲av日韩精品久久久久久,兔费看少妇性l交大片免费,无码少妇一区二区三区

Chinaunix

標(biāo)題: Unreliable Guide to Locking -by Rusty Russell-中文版 [打印本頁]

作者: albcamus    時間: 2005-11-25 17:45
標(biāo)題: Unreliable Guide to Locking -by Rusty Russell-中文版
Kernel Locking 中文版


Unreliable Guide To Locking
Rusty Russell

      <rusty@rustcorp.com.au>
翻譯:

      albcamus <albcamus@163.com>


Copyright © 2003 Rusty Russell

This documentation is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

For more details see the file COPYING in the source distribution of Linux.
作者: albcamus    時間: 2005-11-25 17:46
第1章. 介紹


歡迎進(jìn)入Rusty優(yōu)秀的《Unreliable Guide to Kernel Locking》細(xì)節(jié)。本文檔描述了Linux 2.6內(nèi)核的鎖系統(tǒng)。

隨著Linux內(nèi)核引入了超線程與 搶占 ,每一個Hacking內(nèi)核的人都應(yīng)該了解并發(fā)性與為SMP加鎖的基礎(chǔ)知識。
作者: albcamus    時間: 2005-11-25 17:46
第2章. 并發(fā)帶來的問題


(如果你已知道并發(fā)是什么,請略過本章).

在一個普通的程序里,你可以這樣為一個計數(shù)器增加計數(shù):


        very_important_count++;


下面是你期望的結(jié)果:

table21.png (14.68 KB, 下載次數(shù): 192)

期望的結(jié)果

期望的結(jié)果

作者: albcamus    時間: 2005-11-25 17:47
事情還可能這樣發(fā)生:

Table 2-2. 可能的結(jié)果

table22.png (15.04 KB, 下載次數(shù): 173)

可能的結(jié)果

可能的結(jié)果

作者: albcamus    時間: 2005-11-25 17:48
2.1. 競態(tài)條件與臨界區(qū)


這種情況,結(jié)果依賴于多個任務(wù)的相對執(zhí)行順序,叫做競態(tài)條件。包含并發(fā)問題的代碼,叫做臨界區(qū)。尤其是Linux開始支持SMP機(jī)器以來,競態(tài)條件和臨界區(qū)就成為內(nèi)核設(shè)計與實現(xiàn)的主要問題。

搶占具有相同的影響,即使只有一個CPU也是這樣的:搶占了正在臨界區(qū)中執(zhí)行的任務(wù),我們就重現(xiàn)了上面描述的情況。在這種情況下,搶占了其他代碼的線程必須在它的臨界區(qū)獨自運(yùn)行(排斥其它線程)。

解決的方法是:認(rèn)識到何時會發(fā)生同時訪問,使用鎖來保證在任意時刻只有一個實例能夠進(jìn)入臨界區(qū)。在Linux內(nèi)核中有很多友好的原語(primitives)來幫助你做到這點──也有不友好的原語,但我將當(dāng)作它們并不存在。
作者: albcamus    時間: 2005-11-25 17:49
第3章. 在Linux內(nèi)核中加鎖


我多么希望能給你一句忠告:不要與比你更不理性的人同眠;而如果是關(guān)于鎖的忠告,我會這樣給出:保持簡單。

不要試圖引入新類型的鎖。


真是夠奇怪的,最后一條竟然恰恰與我的忠告相反──因為這時你已經(jīng)與不理性的人同眠了,你需要考慮弄只看門狗。(譯者案:Watchdog是一種以NMI方式檢查CPU死鎖的機(jī)制)


3.1. 兩種主要類型的鎖:自旋鎖與信號量


內(nèi)核中主要有兩種類型的鎖。最基本的類型是自旋鎖(include/asm/spinlock.h),它只允許一個持有者;如果你獲取自旋鎖時不成功,你將一直在自旋中嘗試獲得它,直到成功。自旋鎖又小又快,可以在任何地方使用。

第二種類型是信號量(include/asm/spinlock.h),它可以在任何時刻被多個持有者同時持有(允許的持有者的數(shù)量是在信號量初試化時規(guī)定的),但是在通常情況下信號量只允許一個持有者(這時它也叫互斥鎖,mutex)。如果你獲取信號量時不成功,任務(wù)就會把自身放到一個隊列中睡眠,一直到信號量被釋放時才被喚醒。這意味著在你等待的時候,CPU將做其他的事情。但是,很多情況是不允許睡眠的(參考 第9章 ),這時你應(yīng)該用自旋鎖而不是信號量。

這兩種鎖的任何一種都是不允許遞歸的:參考 7.1小節(jié).


3.2. 單CPU內(nèi)核與鎖


在單CPU機(jī)器上,對于編譯時既沒打開CONFIG_SMP、也沒打開CONFIG_PREEMPT的內(nèi)核來說,自旋鎖根本不存在。這是一個出色的設(shè)計策略:既然沒有別人能在同時刻執(zhí)行,就沒理由加鎖。

(譯案:感謝max2005兄的指正)

如果編譯時同時打開了CONFIG_SMP和CONFIG_PREEMPT,自旋鎖的作用就僅僅是禁止搶占,這就足以防止任何競態(tài)了。多數(shù)情況下,我們可以把搶占等同于SMP來考慮其可能帶來的競態(tài)問題,不必單獨對付它。

當(dāng)測試加鎖代碼時,即使你沒有SMP測試環(huán)境,總應(yīng)該打開CONFIG_SMP和CONFIG_PREEMPT選項,因為這會重現(xiàn)關(guān)于鎖的某些類型的BUG。

信號量依然存在,因為它們之所以被引入,乃是為了同步用戶上下文。我們將馬上看到這一點。


3.3. 只在用戶上下文加鎖


如果你的數(shù)據(jù)結(jié)構(gòu)只可能被用戶上下文訪問,你可以用信號量(linux/asm/semaphore.h)來保護(hù)它。這是最簡單的情況了:把信號量初試化為可用資源的數(shù)量(通常是1),就可以調(diào)用down_interruptible()來獲取信號量,調(diào)用up()來釋放它。還有一個down()函數(shù),但應(yīng)該避免使用它,因為如果有信號到來它不會返回。

例如:linux/net/core/netfilter.c允許用nf_register_sockopt()注冊新的setsockopt()和getsockopt()調(diào)用。注冊和注銷只有在模塊加載與卸載時才會發(fā)生(是在引導(dǎo)時執(zhí)行的,沒有并發(fā)問題),注冊了的鏈表只有setsockopt()或getsockopt()系統(tǒng)調(diào)用才會查閱。因此,nf_sockopt_mutex 就可以完美的保護(hù)住這些,這是因為setsockopt和getsockopt允許睡眠。


3.4. 在用戶上下文與Softirqs之間加鎖


如果一個softirq與用戶上下文共享數(shù)據(jù),就有兩個問題:首先,當(dāng)前的用戶上下文可能被softirq中斷;其次,臨界區(qū)可能會在別的CPU進(jìn)入。這時spin_lock_bh() (include/linux/spinlock.h)就有了用武之地。它會在那個CPU上禁止softirqs,然后獲取鎖。spin_unlock_bh()做相反的工作。(由于歷史原因,后綴‘bh’成為對各種下半部的通稱,后者是software interrupts的舊稱。其實spin_lock_bh本應(yīng)叫作spin_lock_softirq才貼切)

注意這里你也可以用spin_lock_irq()或者spin_lock_irqsave(),這樣不單會禁止softirqs,還會禁止硬件中斷:參考第4章。

這在UP 上也能很好地工作:自旋鎖消失了,該宏變成了只是local_bh_disable() (include/linux/interrupt.h),它會禁止softirqs運(yùn)行。


3.5. 在用戶上下文與Tasklets之間加鎖


這種情況與上面的情況(譯者案,上面“在用戶上下文與softirqs之間加鎖”的情況)完全一致,因為tasklets本來就是作為一種softirq運(yùn)行的。


3.6. 在用戶上下文與Timers之間加鎖


這種情況也和上面情況完全一致,因為timers本來就是一種softirq(譯者案:Timer是時鐘中斷的下半部)。從加鎖觀點來看,tasklets和timers的地位是等同的。


3.7. 在Tasklets/Timers之間加鎖


有時一個tasklet或timer會與另一個tasklet或timer共享數(shù)據(jù)。


3.7.1. 同一個Tasklet/Timer


由于同一時刻,一個tasklet決不會在兩個CPU上執(zhí)行,即使在SMP機(jī)器上,你也不必?fù)?dān)心你的tasklet會發(fā)生重入問題(同時運(yùn)行了兩個實例)


3.7.2. 不同的Tasklets/Timers


如果你的tasklet或timer想要同另一個tasklet或timer共享數(shù)據(jù),你需要對它們二者都使用spin_lock()和spin_unlock()。沒必要使用spin_lock_bh(),因為你已經(jīng)處在tasklet中了,而同一個CPU不會同時再執(zhí)行其他的tasklet。


3.8. 在Softirqs之間加鎖


一個softirq經(jīng)常需要與自己或其它的tasklet/timer共享數(shù)據(jù)。


3.8.1. 同一個Softirq


同一個softirq可能在別的CPU上執(zhí)行:你可以使用per-CPU數(shù)據(jù)(參考 8.3小節(jié))以獲得更好的性能。如果你打算這樣使用softirq,通常是為了獲得可伸縮的高性能而帶來額外的復(fù)雜性。

你需要用spin_lock()和spin_unlock()保護(hù)共享數(shù)據(jù)。


3.8.2. 不同的 Softirqs


你需要使用spin_lock()和spin_unlock()保護(hù)共享數(shù)據(jù),不管是timer,tasklet,還是不同的/同一個/另一個的softirq:它們中的任何一個都可以在另一個CPU上運(yùn)行。

[ 本帖最后由 albcamus 于 2006-10-12 11:36 編輯 ]
作者: albcamus    時間: 2005-11-25 17:50
第4章. 硬中斷上下文


硬中斷通常與一個tasklet或softirq通信。這通常涉及到把一個任務(wù)放到某個隊列中,再由softirq取出來。


4.1. 在硬中斷與Softirqs/Tasklets之間加鎖


如果一個硬件中斷服務(wù)例程與一個softirq共享數(shù)據(jù),就有兩點需要考慮。第一,softirq的執(zhí)行過程可能會被硬件中斷打斷;第二,臨界區(qū)可能會被另一個CPU上的硬件中斷進(jìn)入。這正是spin_lock_irq()派上用場的地方。它在那個CPU上禁止中斷,然后獲取鎖。spin_unlock_irq()做相反的工作。

硬件中斷服務(wù)例程中不需要使用spin_lock_irq(),因為當(dāng)它在執(zhí)行的時候softirq是不可能執(zhí)行的:它可以使用spin_lock(),這個會更快一些。唯一的例外是另一個硬件中斷服務(wù)例程使用了相同的鎖:spin_lock_irq()會禁止那個硬件中斷。

這在UP機(jī)器上也能很好的工作:自旋鎖消失了,spin_lock_irq()變成了僅僅是local_irq_disable() (include/asm/smp.h),這將會禁止sofirq/tasklet/BH的運(yùn)行。

spin_lock_irqsave()(include/linux/spinlock.h)是spin_lock_irq()的變體,它把當(dāng)前的中斷開啟與否的狀態(tài)保存在一個狀態(tài)字中,用以將來傳遞給spin_unlock_restore()。這意味著同樣的代碼既可以在硬件中斷服務(wù)例程中(這時中斷已關(guān)閉)使用,也可以在softirqs中(這時需要主動禁止中斷)使用。

注意,softirqs(包括tasklets和timers)是在硬件中斷返回時得到運(yùn)行的,因此spin_lock_irq()同樣也會禁止掉它們。從這個意義上說,spin_lock_irqsave()是最通用和最強(qiáng)大的加鎖函數(shù)。


4.2. 在兩個硬中斷服務(wù)例程之間加鎖


很少有兩個硬件中斷服務(wù)例程共享數(shù)據(jù)的情況。如果你真的需要這樣做,可以使用spin_lock_irqsave() :在進(jìn)入中斷服務(wù)時是否自動關(guān)閉中斷,這件事是體系結(jié)構(gòu)相關(guān)的。
作者: albcamus    時間: 2005-11-25 17:50
第5章. 關(guān)于鎖的使用的圖表


Pete Zaitcev 提供了這樣的總結(jié):

    *

      如果你處在進(jìn)程上下文(任何系統(tǒng)調(diào)用),想把其他進(jìn)程排擠出臨界區(qū),使用信號量。你可以獲得信號量之后去睡眠(例如調(diào)用copy_from_user或kmalloc(x, GFP_KERNEL)之類的函數(shù))。
    *

      否則(亦即:數(shù)據(jù)可能被中斷訪問),使用spin_lock_irqsave()和spin_lock_irqrestore()。
    *

      避免任何持有自旋鎖超過5行代碼的情況,避免任何跨越函數(shù)調(diào)用的只有自旋鎖的情況。(有種情況例外,比方象readb這樣的原子訪問)

5.1. 加鎖最低要求表


下面的表列出了在不同上下文中加鎖時的最低要求。在一些情況下,同一個上下文在給定時刻只能在一個CPU上執(zhí)行,因此不需要鎖(例如,某個線程在給定時刻只可能在一個CPU上運(yùn)行,但如果它需要跟另一個線程共享數(shù)據(jù),就需要鎖了)

記住上面的建議:你可以總是只用spin_lock_irqsave(),它是其它加鎖原語的超集。


表 5-1. 加鎖的要求

table51.png (39.29 KB, 下載次數(shù): 173)

加鎖要求表

加鎖要求表

作者: albcamus    時間: 2005-11-25 17:53
第6章. 常見的例子


讓我們一步步看一下一個簡單的例子:一個對映射號的緩存(譯者案:原文“a cache of number to name mappings”,虛存子系統(tǒng)不熟,恐怕翻譯不確)。該緩存保存了對象的使用有多頻繁這個數(shù)據(jù),當(dāng)它滿了,拋出最少使用的那個。


6.1. 都在用戶上下文


在第一個例子中,我們假定所有的操作都發(fā)生在用戶上下文(也就是,都在系統(tǒng)調(diào)用中),所以允許睡眠。這意味著我們可以使用信號量來保護(hù)cache和其中的所有對象。下面是代碼:


  1. #include <linux/list.h>
  2. #include <linux/slab.h>
  3. #include <linux/string.h>
  4. #include <asm/semaphore.h>
  5. #include <asm/errno.h>

  6. struct object
  7. {
  8.         struct list_head list;
  9.         int id;
  10.         char name[32];
  11.         int popularity;
  12. };


  13. /* 保護(hù)緩存、緩存號和其中的對象 */
  14. static DECLARE_MUTEX(cache_lock);
  15. static LIST_HEAD(cache);
  16. static unsigned int cache_num = 0;
  17. #define MAX_CACHE_SIZE 10

  18. /* 必須持有cache_lock信號量 */
  19. static struct object *__cache_find(int id)
  20. {
  21.         struct object *i;

  22.         list_for_each_entry(i, &cache, list)
  23.                 if (i->id == id) {
  24.                         i->popularity++;
  25.                         return i;
  26.                 }
  27.         return NULL;
  28. }

  29. /* 必須持有cache_lock信號量 */
  30. static void __cache_delete(struct object *obj)
  31. {
  32.         BUG_ON(!obj);
  33.         list_del(&obj->list);
  34.         kfree(obj);
  35.         cache_num--;
  36. }


  37. /* 必須持有cache_lock信號量 */
  38. static void __cache_add(struct object *obj)
  39. {
  40.         list_add(&obj->list, &cache);
  41.         if (++cache_num > MAX_CACHE_SIZE) {
  42.                 struct object *i, *outcast = NULL;
  43.                 list_for_each_entry(i, &cache, list) {
  44.                         if (!outcast || i->popularity < outcast->popularity)
  45.                                 outcast = i;
  46.                 }
  47.                 __cache_delete(outcast);
  48.         }
  49. }

  50. /* 對上面函數(shù)的調(diào)用,這才是信號量發(fā)揮作用的地方──譯注 */
  51. int cache_add(int id, const char *name)
  52. {
  53.         struct object *obj;

  54.         if ((obj = kmalloc(sizeof(*obj), GFP_KERNEL)) == NULL)
  55.                 return -ENOMEM;

  56.         strlcpy(obj->name, name, sizeof(obj->name));
  57.         obj->id = id;
  58.         obj->popularity = 0;

  59.         down(&cache_lock);
  60.         __cache_add(obj);
  61.         up(&cache_lock);
  62.         return 0;
  63. }

  64. void cache_delete(int id)
  65. {
  66.         down(&cache_lock);
  67.         __cache_delete(__cache_find(id));
  68.         up(&cache_lock);
  69. }

  70. int cache_find(int id, char *name)
  71. {
  72.         struct object *obj;
  73.         int ret = -ENOENT;

  74.         down(&cache_lock);
  75.         obj = __cache_find(id);
  76.         if (obj) {
  77.                 ret = 0;
  78.                 strcpy(name, obj->name);
  79.         }
  80.         up(&cache_lock);

  81. return ret;

  82. }

復(fù)制代碼


注意我們總是保證在持有cache_lock信號量的情況下對緩存添加、刪除和查找操作:緩存結(jié)構(gòu)自身與其中的對象都被該信號量保護(hù)起來。這種情況很簡單,因為我們?yōu)橛脩艨截悢?shù)據(jù),而不允許用戶直接訪問對象。

這里有一個輕量(而且很常見)的優(yōu)化:在cache_add中,我們先設(shè)置對象的各個域,然后再獲取鎖。這是安全的,因為沒有人能夠在我們把對象加入到cache之前就訪問它。


6.2. 從中斷上下文中訪問


現(xiàn)在我們來考慮一下cache_find會在中斷上下文中被調(diào)用的情況:這個“中斷上下文”或者是硬件中斷,或者是softirq。我們使用一個timer來從cache中刪除對象。

改寫了的代碼在下面,用標(biāo)準(zhǔn)的補(bǔ)丁形式給出:以-開始的行是被刪除了的,以+開始的行是新添加了的。


  1. --- cache.c.usercontext 2003-12-09 13:58:54.000000000 +1100
  2. +++ cache.c.interrupt   2003-12-09 14:07:49.000000000 +1100
  3. @@ -12,7 +12,7 @@
  4.          int popularity;
  5. };

  6. -static DECLARE_MUTEX(cache_lock);
  7. +static spinlock_t cache_lock = SPIN_LOCK_UNLOCKED;
  8. static LIST_HEAD(cache);
  9. static unsigned int cache_num = 0;
  10. #define MAX_CACHE_SIZE 10
  11. @@ -55,6 +55,7 @@
  12. int cache_add(int id, const char *name)
  13. {
  14.          struct object *obj;
  15. +        unsigned long flags;

  16.          if ((obj = kmalloc(sizeof(*obj), GFP_KERNEL)) == NULL)
  17.                  return -ENOMEM;
  18. @@ -63,30 +64,33 @@
  19.          obj->id = id;
  20.          obj->popularity = 0;

  21. -        down(&cache_lock);
  22. +        spin_lock_irqsave(&cache_lock, flags);
  23.          __cache_add(obj);
  24. -        up(&cache_lock);
  25. +        spin_unlock_irqrestore(&cache_lock, flags);
  26.          return 0;
  27. }

  28. void cache_delete(int id)
  29. {
  30. -        down(&cache_lock);
  31. +        unsigned long flags;
  32. +
  33. +        spin_lock_irqsave(&cache_lock, flags);
  34.          __cache_delete(__cache_find(id));
  35. -        up(&cache_lock);
  36. +        spin_unlock_irqrestore(&cache_lock, flags);
  37. }

  38. int cache_find(int id, char *name)
  39. {
  40.          struct object *obj;
  41.          int ret = -ENOENT;
  42. +        unsigned long flags;

  43. -        down(&cache_lock);
  44. +        spin_lock_irqsave(&cache_lock, flags);
  45.          obj = __cache_find(id);
  46.          if (obj) {
  47.                  ret = 0;
  48.                  strcpy(name, obj->name);
  49.          }
  50. -        up(&cache_lock);
  51. +        spin_unlock_irqrestore(&cache_lock, flags);
  52.          return ret;
  53. }
復(fù)制代碼


注意一下,如果中斷原來是開啟著的,spin_lock_irqsave會關(guān)掉它們;否則(我們已經(jīng)處在中斷服務(wù)例程中)就什么也不做。這些函數(shù)可以安全地在任何上下文中調(diào)用。

不幸的是,cache_add調(diào)用了帶有GFP_KERNEL標(biāo)志的kmalloc,這只是在用戶上下文才是合法的。我假定cache_add仍然只會在用戶上下文中被調(diào)用,否則就應(yīng)該給cache_add添加參數(shù)了。(譯注:我覺得作者的意思是說,如果打算讓cache_add能在中斷和用戶兩種上下文中使用,應(yīng)該添加一個參數(shù)來表達(dá)究竟是在哪種上下文中調(diào)用的。FIXME)


6.3. 把對象暴露在文件之外


如果對象包含更多的信息,僅僅提供拷貝函數(shù)是不夠的:其它代碼可能會需要保持一個指向?qū)ο蟮闹羔,例如,不想每次訪問它都要先查找。這就產(chǎn)生了兩個問題。

第一個問題是,我們采用cache_lock來保護(hù)對象,我們必須把這些函數(shù)定義為非static的,這樣其它文件中的代碼才能使用。這使得鎖的使用更加技巧化,再也不是在同一地方了。

第二個問題是生命期問題。如果其它結(jié)構(gòu)保留了一個指向我們的對象的指針,它多半期望該指針總是有效的。很不幸,這只在你持有鎖時才會得到保證,否則其它人可能調(diào)用cache_delete來刪除對象──還可能更糟糕,在刪除之后又添加了另一個對象,還在原來的地址上。

但是由于只有一把鎖,你也不能總持有它,否則其它人就完全無法工作了。

該問題的解決是使用引用計數(shù):每個持有指向?qū)ο蟮闹羔樀娜耍谒麄兊谝淮潍@得該指針時,遞增一次引用計數(shù);當(dāng)他們使用完畢,遞減一次引用計數(shù)。誰把引用計數(shù)減到了零,就知道可以刪除了,于是執(zhí)行刪除操作。

下面是代碼:


  1. --- cache.c.interrupt   2003-12-09 14:25:43.000000000 +1100
  2. +++ cache.c.refcnt      2003-12-09 14:33:05.000000000 +1100
  3. @@ -7,6 +7,7 @@
  4. struct object
  5. {
  6.          struct list_head list;
  7. +        unsigned int refcnt;
  8.          int id;
  9.          char name[32];
  10.          int popularity;
  11. @@ -17,6 +18,35 @@
  12. static unsigned int cache_num = 0;
  13. #define MAX_CACHE_SIZE 10

  14. +static void __object_put(struct object *obj)
  15. +{
  16. +        if (--obj->refcnt == 0)
  17. +                kfree(obj);
  18. +}
  19. +
  20. +static void __object_get(struct object *obj)
  21. +{
  22. +        obj->refcnt++;
  23. +}
  24. +
  25. +void object_put(struct object *obj)
  26. +{
  27. +        unsigned long flags;
  28. +
  29. +        spin_lock_irqsave(&cache_lock, flags);
  30. +        __object_put(obj);
  31. +        spin_unlock_irqrestore(&cache_lock, flags);
  32. +}
  33. +
  34. +void object_get(struct object *obj)
  35. +{
  36. +        unsigned long flags;
  37. +
  38. +        spin_lock_irqsave(&cache_lock, flags);
  39. +        __object_get(obj);
  40. +        spin_unlock_irqrestore(&cache_lock, flags);
  41. +}
  42. +
  43. /* Must be holding cache_lock */
  44. static struct object *__cache_find(int id)
  45. {
  46. @@ -35,6 +65,7 @@
  47. {
  48.          BUG_ON(!obj);
  49.          list_del(&obj->list);
  50. +        __object_put(obj);
  51.          cache_num--;
  52. }

  53. @@ -63,6 +94,7 @@
  54.          strlcpy(obj->name, name, sizeof(obj->name));
  55.          obj->id = id;
  56.          obj->popularity = 0;
  57. +        obj->refcnt = 1; /* The cache holds a reference */

  58.          spin_lock_irqsave(&cache_lock, flags);
  59.          __cache_add(obj);
  60. @@ -79,18 +111,15 @@
  61.          spin_unlock_irqrestore(&cache_lock, flags);
  62. }

  63. -int cache_find(int id, char *name)
  64. +struct object *cache_find(int id)
  65. {
  66.          struct object *obj;
  67. -        int ret = -ENOENT;
  68.          unsigned long flags;

  69.          spin_lock_irqsave(&cache_lock, flags);
  70.          obj = __cache_find(id);
  71. -        if (obj) {
  72. -                ret = 0;
  73. -                strcpy(name, obj->name);
  74. -        }
  75. +        if (obj)
  76. +                __object_get(obj);
  77.          spin_unlock_irqrestore(&cache_lock, flags);
  78. -        return ret;
  79. +        return obj;
  80. }
復(fù)制代碼


我們把引用計數(shù)操作封裝進(jìn)標(biāo)準(zhǔn)的‘get’和‘put’函數(shù)中,F(xiàn)在我們可以用cache_find返回對象的指針了,它具有引用計數(shù)功能。這樣,持有對象指針的用戶就可以睡眠了(例如,調(diào)用copy_to_user把名字拷貝到用戶空間。)(譯注:最后一句的意思是說,持有對象指針的用戶不必象以前那樣為了保證指針的合法性就一直持有鎖了。引用計數(shù)本身跟鎖以及是否可以睡眠一點關(guān)系都沒有)

另一點要注意的是,我說“每個指向?qū)ο蟮闹羔樁紤?yīng)該有一個引用計數(shù)”──因此當(dāng)對象剛被插入到cache中時,其引用計數(shù)應(yīng)當(dāng)為1。有些不同版本的實現(xiàn)并不使用引用計數(shù),但它們更加復(fù)雜。


6.3.1. 為引用計數(shù)使用原子操作


實踐中,refcnt的類型應(yīng)該是atomic_t。在include/asm/atomic.h中定義了幾個原子操作:這些操作保證了從系統(tǒng)的所有CPU的角度看,都是原子的。因此不需要鎖。這種情況下,原子操作要比自旋鎖簡單的多,盡管復(fù)雜情況下使用自旋鎖要來得清晰。使用atomic_inc和atomic_dec_and_test來取代標(biāo)準(zhǔn)的加減操作符,再也不用為引用計數(shù)上鎖了。


  1. --- cache.c.refcnt      2003-12-09 15:00:35.000000000 +1100
  2. +++ cache.c.refcnt-atomic       2003-12-11 15:49:42.000000000 +1100
  3. @@ -7,7 +7,7 @@
  4. struct object
  5. {
  6.          struct list_head list;
  7. -        unsigned int refcnt;
  8. +        atomic_t refcnt;
  9.          int id;
  10.          char name[32];
  11.          int popularity;
  12. @@ -18,33 +18,15 @@
  13. static unsigned int cache_num = 0;
  14. #define MAX_CACHE_SIZE 10

  15. -static void __object_put(struct object *obj)
  16. -{
  17. -        if (--obj->refcnt == 0)
  18. -                kfree(obj);
  19. -}
  20. -
  21. -static void __object_get(struct object *obj)
  22. -{
  23. -        obj->refcnt++;
  24. -}
  25. -
  26. void object_put(struct object *obj)
  27. {
  28. -        unsigned long flags;
  29. -
  30. -        spin_lock_irqsave(&cache_lock, flags);
  31. -        __object_put(obj);
  32. -        spin_unlock_irqrestore(&cache_lock, flags);
  33. +        if (atomic_dec_and_test(&obj->refcnt))
  34. +                kfree(obj);
  35. }

  36. void object_get(struct object *obj)
  37. {
  38. -        unsigned long flags;
  39. -
  40. -        spin_lock_irqsave(&cache_lock, flags);
  41. -        __object_get(obj);
  42. -        spin_unlock_irqrestore(&cache_lock, flags);
  43. +        atomic_inc(&obj->refcnt);
  44. }

  45. /* Must be holding cache_lock */
  46. @@ -65,7 +47,7 @@
  47. {
  48.          BUG_ON(!obj);
  49.          list_del(&obj->list);
  50. -        __object_put(obj);
  51. +        object_put(obj);
  52.          cache_num--;
  53. }

  54. @@ -94,7 +76,7 @@
  55.          strlcpy(obj->name, name, sizeof(obj->name));
  56.          obj->id = id;
  57.          obj->popularity = 0;
  58. -        obj->refcnt = 1; /* The cache holds a reference */
  59. +        atomic_set(&obj->refcnt, 1); /* The cache holds a reference */

  60.          spin_lock_irqsave(&cache_lock, flags);
  61.          __cache_add(obj);
  62. @@ -119,7 +101,7 @@
  63.          spin_lock_irqsave(&cache_lock, flags);
  64.          obj = __cache_find(id);
  65.          if (obj)
  66. -                __object_get(obj);
  67. +                object_get(obj);
  68.          spin_unlock_irqrestore(&cache_lock, flags);
  69.          return obj;
  70. }
復(fù)制代碼


6.4. 保護(hù)對象自身


在上面的例子中,我們都假定對象(除了引用計數(shù))自從創(chuàng)建之后就不會被更改。如果我們希望允許改變name(譯者案,指struct object中的name域),有三種可能:

    *

      你可以把cache_lock變成非static的,告訴人們改變name之前先獲得鎖。
    *

      你也可以提供一個cache_obj_rename函數(shù),該函數(shù)先獲得鎖,然后為調(diào)用者改變name。你需要告訴每一個需要改變name的人使用這個函數(shù)。
    *

      你也可以用cache_lock只保護(hù)cache自身,而使用另一把鎖來保護(hù)name域。

理論上,你可以把鎖的使用細(xì)粒度化(fine-grained)到這樣的程度:為每個域維持一把鎖。實踐中,最通用的變體是:

    *

      一把用來保護(hù)基礎(chǔ)設(shè)施(infrastructure.本例中是cache鏈表)和所有對象的鎖。至今為止我們在示例中看到的便是這種情況。
    *

      一把用來保護(hù)基礎(chǔ)設(shè)施(包括對象結(jié)構(gòu)中的鏈表指針)的鎖,另一把放在對象內(nèi)部,用來保護(hù)對象的其它域.
    *

      多把鎖來保護(hù)基礎(chǔ)設(shè)施(例如,Hash表的每一個鏈都用一把鎖保護(hù)),可能配合使用每個對象一把的鎖。


下面是“每個對象一把的鎖”的實現(xiàn):


  1. --- cache.c.refcnt-atomic       2003-12-11 15:50:54.000000000 +1100
  2. +++ cache.c.perobjectlock       2003-12-11 17:15:03.000000000 +1100
  3. @@ -6,11 +6,17 @@

  4. struct object
  5. {
  6. +         /* 下面兩個域,由cache_lock來保護(hù) */
  7.          struct list_head list;
  8. +        int popularity;
  9. +
  10.          atomic_t refcnt;
  11. +
  12. +        /* Doesn't change once created. */
  13.          int id;
  14. +
  15. +        spinlock_t lock; /* Protects the name */
  16.          char name[32];
  17. -        int popularity;
  18. };

  19. static spinlock_t cache_lock = SPIN_LOCK_UNLOCKED;
  20. @@ -77,6 +84,7 @@
  21.          obj->id = id;
  22.          obj->popularity = 0;
  23.          atomic_set(&obj->refcnt, 1); /* cache本身占一個引用計數(shù) */
  24. +        spin_lock_init(&obj->lock);

  25.          spin_lock_irqsave(&cache_lock, flags);
  26.          __cache_add(obj);
復(fù)制代碼

注意我的策略是popularity域由cache_lock來保護(hù),而不是每個對象一把的鎖。這是因為它(就象對象內(nèi)部的struct list_head域一樣)在邏輯上是屬于基礎(chǔ)設(shè)施的一部分的。這樣,當(dāng)在__cache_add查找最少使用的對象時,我就不必去獲取每個對象一把的鎖了。

這個策略還有一處要注意:id域是不可更改的,這樣我就不必在__cache_find()時去獲取每個對象一把的鎖來保護(hù)它。每個對象一把的鎖,在這里,只是被調(diào)用者使用來保護(hù)name域的讀和寫。

另外,注意我在注釋里說明了哪些數(shù)據(jù)由哪些鎖來保護(hù)。這是非常重要的,因為它描述了代碼運(yùn)行時的行為,(倘若不加注釋)僅僅靠閱讀代碼是很難獲得認(rèn)識的。這正如Alan Cox所說,“鎖是用來保護(hù)數(shù)據(jù)的,而非代碼。”
作者: albcamus    時間: 2005-11-25 17:55
第7章. 常見問題


7.1. 死鎖: 簡單與高級


當(dāng)一段代碼試圖兩次獲取同一把自旋鎖時,就出現(xiàn)了BUG:它將永遠(yuǎn)自旋,等待著鎖被釋放(自旋鎖、讀寫自旋鎖和信號量在linux中皆是非遞歸的)。這很容易診斷:它并非是一個類似“呆上五夜來談清楚毛絨絨的兔子身上有多少根毛的編碼問題”(譯者案:原文是stay-up-five-nights-talk-to-fluffy-code-bunnies ,沒法翻譯啊)之類的問題。

試想一個稍微復(fù)雜一點的情況,假設(shè)你有一個softirq和用戶上下文共享一片區(qū)域。如果你使用了spin_lock()來保護(hù)它,很可能用戶上下文持有鎖時被這個softirq打斷,然后softirq嘗試獲得該鎖──但它永遠(yuǎn)也不會成功。

這些情形稱為死鎖。如同上面展示的那樣,它甚至?xí)趩蜟PU機(jī)器上發(fā)生(盡管不會在編譯時沒選上SMP的機(jī)器上發(fā)生。因為如果編譯時CONFIG_SMP=n,自旋鎖就消失了。這種情況下你會造成數(shù)據(jù)腐敗)。

這種死鎖容易診斷:在SMP機(jī)器上,看門狗(watchdog)timer或者編譯時加入DEBUG_SPINLOCKS (include/linux/spinlock.h)會在發(fā)生死鎖時立即顯示它。

更復(fù)雜的問題是一種叫做“致命擁抱”的死鎖,它涉及到兩把或更多的鎖。比方你有一個Hash表:表中的每一個項是一個自旋鎖和一個對象鏈表。在softirq處理函數(shù)中,你可能會想更改某個對象在Hash表中的位置,從某個位置到另一個:你獲得了舊的Hash鏈表的自旋鎖和新的鏈表的自旋鎖,然后從舊的鏈表中刪除對象,插入到新的鏈表中。

這里有兩個問題。第一,如果你的代碼試圖把對象移動到相同的鏈表中,它就會因兩次嘗試獲取同一把鎖而死鎖;第二,如果在別的CPU上運(yùn)行的同一個softirq試圖把另一個對象從你的目的鏈表移動到你的源鏈表中,下面的事情就發(fā)生了:


Table 7-1. 順序

CPU 1
       

CPU 2

獲取鎖 A -> OK
       

獲取鎖 B -> OK

試圖獲取 B -> spin
       

試圖獲取 A -> spin

這兩個CPU將永遠(yuǎn)自旋,等待著對方釋放自旋鎖。它會使系統(tǒng)看起來象崩潰了一樣。


7.2. 防備死鎖


教科書告訴你說:總是以相同的順序獲取鎖,你就不會造成死鎖了。而實踐則會告訴你,這種方法不具有擴(kuò)展性:當(dāng)我創(chuàng)建一把鎖的時候,我對內(nèi)核的理解還真不足以計算出在5000種鎖的層次中,哪一種合適。

最好的鎖是被封裝了的:它們不會暴露在頭文件中,不會被它所在文件之外的函數(shù)持有。你可以讀一下這種代碼,來看看為什么它永遠(yuǎn)不會死鎖。因為它持有一把鎖時,是決不會試圖去獲取另一把的。使用你的代碼的人甚至不需要知道你曾經(jīng)使用了鎖。

一個經(jīng)典問題是當(dāng)你提供了回調(diào)函數(shù)或鉤子函數(shù):如果你在持有鎖的情況下調(diào)用它們,你將冒死鎖的危險:簡單死鎖或致命擁抱(誰能預(yù)知回調(diào)函數(shù)將會干些什么?)。記住,別的程序員是極度想得到你的(原文:the other programmers are out to get you,實在不懂如何翻譯才好;-( ),所以不要這樣做。


7.2.1. 對死鎖的防備過當(dāng)


死鎖誠然會帶來問題,然而不如數(shù)據(jù)腐敗(data corruption)之甚。試想這樣的一段代碼,它獲取一把讀鎖,搜索一個鏈表,如果沒找到想要的數(shù)據(jù),就釋放掉讀鎖,然后獲取一把寫鎖,把對象插入到鏈表:這樣的代碼存在競態(tài)問題。

如果你看不出為什么,那就請離我的代碼他媽的遠(yuǎn)點兒。


7.3. 競態(tài)Timers: 一次內(nèi)核游戲


Timers會造成它們獨有的競態(tài)條件?紤]一個對象集合(鏈表,Hash表等),每個對象都有一個timer,該timer負(fù)責(zé)銷毀它所屬的對象。

如果你打算銷毀整個對象集合(例如模塊卸載的時候),你可能會這樣做:


  1.         /* 這段代碼太太太太太糟糕了:如果想更糟糕的話,你還可以使用“匈牙利命名法”
  2.          */
  3.         spin_lock_bh(&list_lock);

  4.         while (list) {
  5.                 struct foo *next = list->next;
  6.                 del_timer(&list->timer);
  7.                 kfree(list);
  8.                 list = next;
  9.         }

  10.         spin_unlock_bh(&list_lock);
復(fù)制代碼

在SMP機(jī)器上,這段代碼早晚要導(dǎo)致崩潰。因為一個timer可能在spin_lock_bh()之前已經(jīng)gone off了,它將只有在我們調(diào)用spin_unlock_bh()之后才成功的獲取鎖,然后試圖去釋放掉響應(yīng)的元素(而元素已經(jīng)釋放掉了!)

這個問題可以通過檢查del_timer()的返回值來預(yù)防:如果返回1,說明timer已經(jīng)被刪除了;如果返回0,說明(在這個例子中)它正在運(yùn)行。所以我們應(yīng)當(dāng)這樣:


  1.         retry:  
  2.                 spin_lock_bh(&list_lock);

  3.                 while (list) {
  4.                         struct foo *next = list->next;
  5.                         if (!del_timer(&list->timer)) {
  6.                                 /* Give timer a chance to delete this */
  7.                                 spin_unlock_bh(&list_lock);
  8.                                 goto retry;
  9.                         }
  10.                         kfree(list);
  11.                         list = next;
  12.                 }

  13.                 spin_unlock_bh(&list_lock);
  14.    
復(fù)制代碼

另一個常見問題是那些自啟動的(通過在timer的函數(shù)的最后調(diào)用add_timer()就會實現(xiàn)定時器的自啟動 )timers的刪除操作。這是一個很常見的易導(dǎo)致競態(tài)的情形,你該使用del_timer_sync() (include/linux/timer.h)來處理這種情況。該函數(shù)的返回值是:我們要刪除的timer最終被停止自啟動之前,刪除動作應(yīng)該執(zhí)行的次數(shù)。


7.4. 混亂的Sparc處理器


Alan Cox說:“在sparc機(jī)器上,中斷的禁止/激活動作處在寄存器窗口中”。Andi Kleen說“當(dāng)你從另一個函數(shù)用標(biāo)志(譯注:flag, 指如spin_lock_irqsave時的第二個參數(shù))恢復(fù)寄存器狀態(tài)時,就會弄亂所有的寄存器窗口”

所以永遠(yuǎn)不要把由spin_lock_irqsave()保存的標(biāo)志傳遞給另一個函數(shù)(除非它被聲明為inline的)。通常沒人會這么干,但你能記住這點最好。David Miller在直接方式下無法做任何事情。(我敢這么說,是因為我有印象他和一位PowverPC維護(hù)者在對待折衷點問題上態(tài)度是什么樣子的。)
作者: albcamus    時間: 2005-11-25 17:56
第8章. 加鎖速度


在考慮使用鎖的代碼的速度問題上,主要有三件事需要關(guān)注。首先是并發(fā)性:當(dāng)鎖被別人持有時,有多少事情是我們需要等待的。其次是,需要花多少時間來獲取與釋放一把無爭議的鎖。第三呢,是盡量使用更少的──或曰更靈活的──鎖。當(dāng)然,這里我已經(jīng)假定鎖的使用非常頻繁,否則你都不用擔(dān)心效率問題的。

并發(fā)性依賴于鎖被持有多長時間:你持有鎖的時間,就應(yīng)該同確實需要持有它的時間一樣長,不要更長。在上面那個關(guān)于cache的例子中,我們在創(chuàng)建對象時是不持有鎖的,只有在已經(jīng)準(zhǔn)備好把它插入到鏈表中時才去獲取鎖。

獲取鎖的時間依賴于加鎖操作對于管道線(pipeline stalls)做了多少破壞性工作,以及本CPU是最后一個試圖獲取該鎖的CPU(亦即:對本CPU來說,鎖是否為緩存密集的(cache-hot))的可能性有多大:在具有很多CPU的機(jī)器上,這種可能性迅速降低。考慮一個700MHz的Intel Pentium III處理器:一條指令大約花費(fèi)0.7納秒,一次原子增加操作大約花費(fèi)58納秒,一次緩存密集的的加鎖操作大約花費(fèi)160納秒,而從其他CPU上的一次緩存行轉(zhuǎn)換還要額外花費(fèi)170到360納秒。(這些數(shù)據(jù)來自于Paul McKenney的Linux Journal RCU article )

這兩個目標(biāo)會發(fā)生沖突:通過把鎖分成不同的的部分,可以減少持有鎖的時間(例如上面例子中“每個對象一把的鎖”),但是這增加了獲取鎖這一操作的數(shù)量,因而結(jié)果一般是比只有一把鎖的情況要慢一些。這也是推崇加鎖簡潔性的一個理由。

第三個考慮點放到了下面:的確存在一些方法能夠減少鎖的使用。


8.1. 讀/寫鎖變體


自旋鎖與信號量都具有讀/寫變體:rwlock_t和struct rw_semaphore。它們把使用者分成了兩類:讀者和寫者。如果你只需要讀數(shù)據(jù),你該獲取讀鎖;但如果你需要寫數(shù)據(jù),你需要獲取寫鎖?梢杂泻芏嗍褂谜咄瑫r獲取讀鎖,但寫鎖卻只能有單個使用者持有。

如果你的代碼可以簡潔的劃分出讀和寫的界限(就象我們上面的cache代碼那樣),而且讀者通常需要很長時間的持有讀鎖,使用讀寫鎖會更好。它們要比普通的鎖要稍微慢點兒,所以在實踐中rwlock_t通常并不很值得使用。


8.2. 避免鎖的使用:RCU


有一種特殊的讀/寫鎖定方法叫做RCU(Read Copy Update),讀者可以完全避免使用鎖:由于我們希望我們的cache被讀取遠(yuǎn)比被更新來的頻繁(否則的話,還引入cache干什么?),使用RCU就是一個可選擇的優(yōu)化。

如何移除讀鎖呢?移除讀鎖意味著寫者可能在存在讀者的情況下更新鏈表。這很簡單:要向鏈表中添加元素的代碼足夠小心,我們就可以在它添加的同時讀取鏈表。例如,把new元素添加到list鏈表中:


  1.         new->next = list->next;
  2.         wmb();
  3.         list->next = new;
  4.    
復(fù)制代碼

wmb()是一個寫內(nèi)存屏障。它保證了第一個操作(設(shè)置新元素的next指針)是完整的,而且立即為其它CPU所見,這發(fā)生在第二個操作(把新元素添加到鏈表中)之前。這是很重要的,因為現(xiàn)代CPU和編譯器可能會改變指令的執(zhí)行順序,除非明確告訴它們不要這樣:我們希望一個讀者要么完全看不到新元素的插入,要么能夠看到新元素插入之后的next指針正確指向了鏈表的剩余部分。

幸運(yùn)的是,有一個函數(shù)專門添加標(biāo)準(zhǔn)的struct list_head鏈表:list_add_rcu() (include/linux/list.h)。

從鏈表中刪除元素更簡單:假設(shè)我們要刪除的元素為A,我們讓指向A的指針重新指向A的后繼者,讀者要么完全看到了它,要么完全忽略了它。


        list->next = old->next;   

有一個list_del_rcu()函數(shù)(include/linux/list.h)來做該工作(而普通版本的刪除操作會毒害(poison)要移除的對象,這是我們不愿看到的)。

讀者也一定要小心:有些CPU會觀察next指針以便預(yù)取下一個元素,但這樣的話,如果next指針恰恰在這時候發(fā)生了變化,那么這些CPU預(yù)取的(pre-fetched)內(nèi)容就是錯誤的。還好,有一個list_fro_each_entry_rcu()函數(shù)(include/linux/list.h)可以幫助你。當(dāng)然,寫者可以只使用list_for_each_entry() (譯者案:這里的“只使用”,是說不用但是是否需要一個XXX_rcu()這樣的函數(shù)),因為不可能同時有兩個寫者。

我們的困難最終是:什么時候我們可以真正的刪除元素?記住,一個讀者可能正在訪問要被刪除的元素:如果我們釋放掉這個元素,而讓next指針指向新元素,讀者將立即訪問到垃圾內(nèi)存并崩潰。我們必須等待到所有正在遍歷鏈表的讀者都結(jié)束了遍歷,才能真正釋放掉元素。我們用call_rcu()函數(shù)注冊一個回調(diào)函數(shù),當(dāng)所有讀者結(jié)束遍歷時,它會真正銷毀對象。

但是,RCU如何得知什么時候讀者結(jié)束遍歷了呢?首先,讀者總是在rcu_read_lock()/rcu_read_unlock()之間執(zhí)行遍歷:這會禁止搶占(而且只是禁止搶占,其它什么都不做),因此讀者在遍歷鏈表期間不會睡眠。

RCU一直等待到其他的CPU至少睡眠了一次:因為讀者不會在讀取過程中睡眠,此時我們就知道我們等待其結(jié)束的那些讀者終于都結(jié)束了。于是回調(diào)函數(shù)被調(diào)用。真實的RCU代碼要比這里講述的更優(yōu)化些,但這里講的是它的基礎(chǔ)。


  1. --- cache.c.perobjectlock 2003-12-11 17:15:03.000000000 +1100
  2. +++ cache.c.rcupdate    2003-12-11 17:55:14.000000000 +1100
  3. @@ -1,15 +1,18 @@
  4. #include <linux/list.h>
  5. #include <linux/slab.h>
  6. #include <linux/string.h>
  7. +#include <linux/rcupdate.h>
  8. #include <asm/semaphore.h>
  9. #include <asm/errno.h>

  10. struct object
  11. {
  12. -        /* 下面兩個域由cache_lock保護(hù) */
  13. +        /* 下面由RCU保護(hù) */
  14.          struct list_head list;
  15.          int popularity;

  16. +        struct rcu_head rcu;
  17. +
  18.          atomic_t refcnt;

  19.          /* 一旦創(chuàng)建就不要更改. */
  20. @@ -40,7 +43,7 @@
  21. {
  22.          struct object *i;

  23. -        list_for_each_entry(i, &cache, list) {
  24. +        list_for_each_entry_rcu(i, &cache, list) {
  25.                  if (i->id == id) {
  26.                          i->popularity++;
  27.                          return i;
  28. @@ -49,19 +52,25 @@
  29.          return NULL;
  30. }

  31. +/* 一旦我們知道沒有讀者了,立即最終丟棄對象. */
  32. +static void cache_delete_rcu(void *arg)
  33. +{
  34. +        object_put(arg);
  35. +}
  36. +
  37. /* 必須在持有cache_lock的情況下調(diào)用 */
  38. static void __cache_delete(struct object *obj)
  39. {
  40.          BUG_ON(!obj);
  41. -        list_del(&obj->list);
  42. -        object_put(obj);
  43. +        list_del_rcu(&obj->list);
  44.          cache_num--;
  45. +        call_rcu(&obj->rcu, cache_delete_rcu, obj);
  46. }

  47. /* 必須在持有cache_lock的情況下調(diào)用 */
  48. static void __cache_add(struct object *obj)
  49. {
  50. -        list_add(&obj->list, &cache);
  51. +        list_add_rcu(&obj->list, &cache);
  52.          if (++cache_num > MAX_CACHE_SIZE) {
  53.                  struct object *i, *outcast = NULL;
  54.                  list_for_each_entry(i, &cache, list) {
  55. @@ -85,6 +94,7 @@
  56.          obj->popularity = 0;
  57.          atomic_set(&obj->refcnt, 1); /* cache本身占一個引用計數(shù) */
  58.          spin_lock_init(&obj->lock);
  59. +        INIT_RCU_HEAD(&obj->rcu);

  60.          spin_lock_irqsave(&cache_lock, flags);
  61.          __cache_add(obj);
  62. @@ -104,12 +114,11 @@
  63. struct object *cache_find(int id)
  64. {
  65.          struct object *obj;
  66. -        unsigned long flags;

  67. -        spin_lock_irqsave(&cache_lock, flags);
  68. +        rcu_read_lock();
  69.          obj = __cache_find(id);
  70.          if (obj)
  71.                  object_get(obj);
  72. -        spin_unlock_irqrestore(&cache_lock, flags);
  73. +        rcu_read_unlock();
  74.          return obj;
  75. }
復(fù)制代碼


注意讀者會在__cache_find()中改變popularity域的值,但是沒有使用鎖。我們的方案應(yīng)該把它變成atomic_t類型的,但是對本例來說我們不會導(dǎo)致競態(tài)條件:近似的結(jié)果就已經(jīng)不錯了,所以我沒做改變。

結(jié)果是,cache_find()函數(shù)并不需要與其它函數(shù)同步,因此它在SMP上幾乎跟在UP上一樣快。

此處還存在一個更縱深的可能的優(yōu)化:想一想我們最初的cache代碼,那時沒有引用計數(shù),無論何時調(diào)用者想訪問對象,就要持有鎖。這仍然是可能的:如果你持有鎖,就沒人能夠刪掉對象,因此你就不必讀取并寫回引用計數(shù)了。

由于RCU中的“讀鎖”會禁止搶占(而且只是禁止搶占,其它什么都不做),調(diào)用者如果調(diào)用cache_find()或object_put()之前已經(jīng)禁止搶占了,就并不需要讀取并寫回引用計數(shù):我們可以把__cache_find()變成非static的從而暴露給其它文件,這時候調(diào)用者就可以直接調(diào)用這些函數(shù)了。

此處的優(yōu)勢是:引用計數(shù)并不被寫入,對象并不被更改,這在SMP機(jī)器上會非常快──由于CPU的caching的原因。


8.3. Per-CPU數(shù)據(jù)


另一種使用相當(dāng)廣泛的避免鎖的技術(shù)是,為每個CPU使用數(shù)據(jù)的一份拷貝。例如,如果你想為一個普通的條件保持一個計數(shù),你可能會使用自旋鎖和一個計數(shù)器,這簡單而漂亮。

如果它表現(xiàn)的太慢了(通常不會,但是如果你的確測試到這種情況發(fā)生了),你就該改變一下策略:為每個CPU保持一個計數(shù)器,這樣,沒有一個計數(shù)器需要互斥的鎖來保護(hù)。參考DEFINE_PER_CPU(),get_cpu_var()和put_cpu_var() (include/linux/percpu.h)。

Per-CPU計數(shù)器的另一個用場是local_t數(shù)據(jù)類型,和cpu_local_inc()以及其它的相關(guān)函數(shù)。這在某些體系結(jié)構(gòu)上是比簡單的加鎖代碼更高效的。(include/asm/local.h)

注意,對計數(shù)器來說,根本不存在簡單而可靠的方法可以精確的得到它的值而不需要引入鎖。只不過某些情況下這不是問題罷了。


8.4. 中斷服務(wù)例程通常使用哪些數(shù)據(jù)


如果數(shù)據(jù)只是在中斷服務(wù)例程內(nèi)部訪問,你根本不需要鎖:內(nèi)核本身已經(jīng)保證了中斷服務(wù)例程不會同時在多個CPU上運(yùn)行。

Manfred Spraul指出,即使數(shù)據(jù)會偶爾被用戶上下文或softirqs/tasklets訪問,你仍然可以這樣。中斷服務(wù)例程自身不需要鎖,其它所有的訪問者這樣做:


  1.         spin_lock(&lock);
  2.         disable_irq(irq);
  3.         ...
  4.         enable_irq(irq);s
  5.         spin_unlock(&lock);
復(fù)制代碼

disable_irq()禁止了中斷服務(wù)例程的運(yùn)行(如果此刻已經(jīng)在別的CPU上運(yùn)行了,那就等待它的結(jié)束)。自旋鎖禁止了同時刻其它任何可能的訪問。自然,這比單個spin_lock_irq()調(diào)用要慢些,所以只有這種訪問很少發(fā)生時這種用法才是合理的。
作者: albcamus    時間: 2005-11-25 17:57
第9章. 中斷里調(diào)用哪些函數(shù)是安全的?


內(nèi)核中許多函數(shù)會導(dǎo)致睡眠(亦即,直接或間接地調(diào)用了scheduler()):當(dāng)持有鎖或禁止了搶占時,你不可以調(diào)用它們。這同樣意味著只有在用戶上下文才可以調(diào)用:在中斷里調(diào)用它們是非法的。


9.1. 一些可能睡眠的函數(shù)


下面列出了最常見的幾個,但通常來講,你需要閱讀代碼來判斷其它的函數(shù)是否可以在中斷里安全的調(diào)用。如果每一個人都是在可睡眠環(huán)境下調(diào)用某個函數(shù)的,那么多半你也要保證可睡眠的環(huán)境(譯者案:原文是“If everyone else who calls it can sleep, you probably need to be able to sleep, too.”) 特別地,注冊與注銷函數(shù)通常需要在用戶上下文中調(diào)用,它們可能睡眠。

    *

      訪問用戶空間:
          o

            copy_from_user()
          o

            copy_to_user()
          o

            get_user()
          o

            put_user()
    *

      kmalloc(GFP_KERNEL)
    *

      down_interruptible() 和 down()

      有一個down_trylock()函數(shù),它可以在中斷上下文中調(diào)用,不會睡眠。Up()決不會導(dǎo)致睡眠。

9.2. 一些不會睡眠的函數(shù)


有些函數(shù)可以在任何上下文中調(diào)用,可以持有任何的鎖。

    *

      printk()
    *

      kfree()
    *

      add_timer() 和 del_timer()
作者: albcamus    時間: 2005-11-25 17:57
第10章. 進(jìn)一步閱讀


    *

      Documentation/spinlock.txt: 這是Linus Torvalds的內(nèi)核源代碼中的自旋鎖教程
    *

      Unix Systems for Modern Architectures: Symmetric Multiprocessing and Caching for Kernel Programmers.

      Curt Schimmel的一部對內(nèi)核級加鎖的極好的著作(不是為Linux寫的,但幾乎都適用)。書可能貴了些,但對于理解SMP加鎖來說,它值這個價錢。[ISBN:0201633388]

      (譯者案:中文譯本《現(xiàn)代體系結(jié)構(gòu)上的Unix系統(tǒng):內(nèi)核程序員的SMP和Caching技術(shù)》,人民郵電出版社2003年4月出版,定價¥39,共289頁,翻譯非常不錯,是本值得閱讀的好書。)

第11章. 致謝


感謝Telsa Gwynne在DocBook化文檔上的幫助,整理與添加風(fēng)格。

感謝Martin Pool,Phlipp Rumpf,Stephen rothwell,Paul Mackerras,Ruedi Aschwanden,Alan Cox,Manfred Spraul,Time Waugh,Pete Zaitcev,James Morris,Robert Love,Paul McKennney,John Ashby,感謝他們的校對、勘誤、爭論和評注。

感謝小團(tuán)體(譯者案:原詞cabal,原意是陰謀團(tuán)體,似乎具有反諷意味,諷刺有些人把Linux內(nèi)核開發(fā)團(tuán)隊稱為cabal的FUD行為)沒有影響該文檔。
作者: albcamus    時間: 2005-11-25 17:58
術(shù)語

(譯者案:為了方便理解,本頁所有術(shù)語條目一律保留其英文)


搶占,preemption
    在2.5內(nèi)核之前,或編譯時CONFIG_PREEMPT未打開,內(nèi)核中用戶上下文的進(jìn)程不會彼此搶占(亦即:只要你占有CPU,你就一直占有,直到自己放棄或者發(fā)生硬件中斷)。在2.5.4以后且打開了CONFIG_PREEMPT,事情改變了:在用戶上下文,更高優(yōu)先級的任務(wù)可以搶占當(dāng)前任務(wù):自旋鎖為此改寫,以添加禁止搶占的效果,即使在UP上也是如此。

bh
    下半部:由于歷史原因,帶有'_bh'后綴的函數(shù)現(xiàn)在通常指所有的軟中斷。例如spin_lock_bh()將阻止任何軟中斷在本CPU上的運(yùn)行。原來的BH是不該繼續(xù)使用的,它終將被tasklets取代。在任一給定時刻,一個下半部只可能有一個實例在運(yùn)行。
    (譯者注:Linux內(nèi)核的各種下半部的命名混亂不堪,例如上面這句其實就不太嚴(yán)謹(jǐn)。tasklet/timer誠然是“任何時刻只可能有一個實例在運(yùn)行”,但是一個softirq就有可能同時在多個CPU上運(yùn)行。推薦閱讀Robert Love的Linux Kernel Development 一書來理清個中關(guān)系)

硬件中斷/硬中斷,Hardware Interrupt/Hardware IRQ
    硬件中斷請求。在硬件中斷處理函數(shù)中,in_irq()宏返回true。

中斷上下文,Interrupt Context
    非用戶上下文:正在處理一個硬件中斷或軟中斷。這種情況下,in_interrupt()宏返回true。

對稱多處理器,SMP
    對稱多處理器:為多CPU機(jī)器編譯的內(nèi)核。(CONFIG_SMP=y).

軟中斷/softirq,Software Interrupt/softirq
    軟中斷處理程序。in_irq()返回false;in_softirq()返回true。Tasklets和softirqs都屬于軟中斷。
    嚴(yán)格來講,一個softirq是一個最多能有32個枚舉類型的軟中斷,它可以同時運(yùn)行在多個CPU上。有時softirq一詞也用來指謂tasklets(也就是說,指謂所有的軟中斷)。

tasklet
    一種可動態(tài)注冊的軟中斷,保證了同一時刻只能有一個CPU在運(yùn)行它。

定時器,timer
    一種可動態(tài)注冊的軟中斷,它在給定時刻(或接近給定時刻)運(yùn)行。當(dāng)運(yùn)行時,它就象tasklet一樣(事實上它們是作為TIMER_SOFTIRQ被調(diào)用的)。

單處理器,UP
    單處理器:非SMP。(CONFIG_SMP=n)

用戶上下文,User Context
    內(nèi)核代表某個進(jìn)程(亦即,一次系統(tǒng)調(diào)用或陷阱)或內(nèi)核線程在運(yùn)行。你可以用current宏得知究竟是哪個進(jìn)程。注意不要與“用戶空間”一詞搞混。它可以被硬件或軟中斷打斷執(zhí)行。

用戶空間,Userspace
    進(jìn)程在內(nèi)核之外執(zhí)行自己的代碼。

翻譯后記

    由于本人水平所限,加上沒有SMP環(huán)境試驗一些理解正確與否,文檔誤譯之處肯定不少(有些不會翻譯的干脆就保留原文了,如您所見)。 無論您有什么觀點,請與albcamus@163.com聯(lián)系,歡迎批評指正,我會盡量跟蹤改進(jìn)本文檔的翻譯。如果您英文可以的話,還是看原版吧,作者的行文并不難懂。
    在翻譯過程中,alert7前輩為我提供了他的譯稿做參考,并指正了很多問題,在此向他表示感謝!──當(dāng)然,誤譯之處只由我自己負(fù)責(zé)。
    趁十一熬夜,05年10月05日凌晨6點
作者: albcamus    時間: 2005-11-25 18:01
HTML排版真是糟糕,論壇上更加無法整理。
制作了一個PDF文件,里面的超鏈接暫時不可用,等我改好了再另外上傳一份吧

kernel_locking_CN.pdf

816.28 KB, 下載次數(shù): 508

kernel_locking_CN.pdf


作者: albcamus    時間: 2005-11-28 16:25
傳上來3天了,一點回應(yīng)沒有,沒有人對互斥感興趣?
寫內(nèi)核態(tài)代碼,一不注意就會崩潰,沒人需要這個?
作者: bingosek    時間: 2005-11-28 17:13
不懂也是要支持一下啦

不然論壇里就只剩下“急急急,我的linux上不了網(wǎng)”之流的帖子啦
作者: fdrtnos    時間: 2005-11-29 13:23
感謝,感謝,最近正在看互斥的一些東西。。!
作者: albcamus    時間: 2005-12-23 09:28
這個帖子加個精華,方便別人查找──大家沒意見吧?
作者: zalem    時間: 2005-12-23 10:38
呵呵,老早就拜讀過了,不過只看了描述部分,涉及到linux的實現(xiàn)就全部略過了,以后再慢慢看...鎖真讓人頭痛...搞通了的人,都是機(jī)器人...
作者: albcamus    時間: 2005-12-23 10:40
原帖由 zalem 于 2005-12-23 10:38 發(fā)表
呵呵,老早就拜讀過了,不過只看了描述部分,涉及到linux的實現(xiàn)就全部略過了,以后再慢慢看...鎖真讓人頭痛...搞通了的人,都是機(jī)器人...


會用就行了,spin_lock的實現(xiàn),記得《情景分析》上有想當(dāng)詳細(xì)的講解,偶還讀過呢:)
作者: zalem    時間: 2005-12-23 11:00
...老大的《bsd內(nèi)核設(shè)計與實踐》多久看完的?
準(zhǔn)備兩個一起搞了,本來是說拿bsd做鋪墊的...
作者: albcamus    時間: 2005-12-23 11:04
我沒看BSD啊,那本4.4.BSD的書,我手頭有,但是不看,你要的話可以寄過去,放我手里浪費(fèi)了
作者: zalem    時間: 2005-12-23 11:24
呵呵,謝謝了,我買了本同系列的新版講《freebsd》的,不過它實在是非英文的討論和資料都太少了。本來說拿bsd的些基礎(chǔ)做linux的預(yù)備知識的...

不好意思,發(fā)現(xiàn)和主題無關(guān)了...不水了...
作者: 1jjk    時間: 2005-12-23 12:41
斑竹看看bsd源代碼!
可能會有更好的發(fā)現(xiàn)呢?!
你說的那本書是不是全是理論,很少用代碼實現(xiàn)的?
我去海圖時看到4.4bsd設(shè)計原理實踐了
作者: 1jjk    時間: 2005-12-23 12:43
這個主要是講互斥,我們課上老師也講過了!
很生動!呵呵
作者: jeffshia    時間: 2006-01-05 12:38
albcamus 老兄的譯作,欽佩!
作者: guotie    時間: 2006-01-08 18:02
7.2.1. 對死鎖的防備過當(dāng)


死鎖誠然會帶來問題,然而不如數(shù)據(jù)腐敗(data corruption)之甚。試想這樣的一段代碼,它獲取一把讀鎖,搜索一個鏈表,如果沒找到想要的數(shù)據(jù),就釋放掉讀鎖,然后獲取一把寫鎖,把對象插入到鏈表:這樣的代碼存在競態(tài)問題。

如果你看不出為什么,那就請離我的代碼他媽的遠(yuǎn)點兒。



不好意思,我悟性低,不明白為什么,能解釋下么?
作者: albcamus    時間: 2006-01-09 09:26
舉個例子來說:

在CPU1上,和CPU2上,兩段代碼都試圖獲取同一把鎖,CPU1先成功了,這時候CPU2在自旋,我們看看可能發(fā)生什么:

1. ==》CPU1獲取一把讀鎖,搜索一個鏈表,如果沒找到想要的數(shù)據(jù),//此時CPU2一直在自旋

2, ==》 CPU1就釋放掉讀鎖, //此時CPU2好不容易等到加鎖成功了

3, ==》 CPU1然后獲取一把寫鎖,把對象插入到鏈表 //由于上一步CPU2加鎖成功,它可能進(jìn)行了寫操作,改變了原來的數(shù)據(jù), 等到CPU1再獲取寫鎖成功, 早就不是它原來判斷的情況了。


LDK說,如果你覺得某段代碼中,你想先用讀鎖再用寫鎖,或者先用寫鎖再用讀鎖, 那么,你最好只用普通的自旋鎖。
作者: guotie    時間: 2006-01-09 10:50
感謝。。
作者: normalnotebook    時間: 2006-02-07 17:23
這篇文章非常不錯,感謝摟住的翻譯,辛苦了!
作者: brooks_shenzhen    時間: 2006-03-01 23:25
標(biāo)題: 豬頭樓主辛苦了
呵呵,俺也上了linux的賊船啦,對你敖敖欽佩
作者: hongfengyue    時間: 2006-03-02 08:26
非常不錯。謝謝了!
作者: richardhesidu    時間: 2006-03-04 14:17
翻譯的不錯啊,對理解內(nèi)核的同步機(jī)制很有幫助。雖然有一些小小的翻譯上的疏漏,不過不影響理解。
謝謝albcamus大哥!
作者: albcamus    時間: 2006-03-04 15:30
原帖由 richardhesidu 于 2006-3-4 14:17 發(fā)表
翻譯的不錯啊,對理解內(nèi)核的同步機(jī)制很有幫助。雖然有一些小小的翻譯上的疏漏,不過不影響理解。

翻譯上的疏漏應(yīng)該貼出來啊!! 我更新一下也免得誤導(dǎo)別人
作者: richardhesidu    時間: 2006-03-04 15:37
原帖由 albcamus 于 2006-3-4 15:30 發(fā)表

翻譯上的疏漏應(yīng)該貼出來啊!! 我更新一下也免得誤導(dǎo)別人


好的,等我看完了整理一下。
作者: max2005    時間: 2006-10-12 11:28
原帖由 albcamus 于 2006-3-4 15:30 發(fā)表

翻譯上的疏漏應(yīng)該貼出來啊!! 我更新一下也免得誤導(dǎo)別人


我來提一個質(zhì)疑:
----------------------------------
3.2. 單CPU內(nèi)核與鎖


在單CPU機(jī)器上,對于編譯時打開了CONFIG_SMP、但沒打開CONFIG_PREEMPT的內(nèi)核來說,自旋鎖根本不存在。這是一個出色的設(shè)計策略:既然沒有別人能在同時刻執(zhí)行,就沒理由加鎖。
----------------------------------

原文是:
For kernels compiled without CONFIG_SMP, and without CONFIG_PREEMPT spinlocks do not exist at all.
兩個都是without,也就是“沒有打開SMP,也沒有打開PREEMPT”。
你看我的理解對不。
作者: albcamus    時間: 2006-10-12 11:34
原帖由 max2005 于 2006-10-12 11:28 發(fā)表


我來提一個質(zhì)疑:
----------------------------------
3.2. 單CPU內(nèi)核與鎖


在單CPU機(jī)器上,對于編譯時打開了CONFIG_SMP、但沒打開CONFIG_PREEMPT的內(nèi)核來說,自旋 ...


是這樣的,謝謝!
作者: juanshuchun    時間: 2006-11-08 21:54
xiexie
作者: fineamy    時間: 2006-11-09 09:07
標(biāo)題: 謝謝,
又多了一份材料!




歡迎光臨 Chinaunix (http://72891.cn/) Powered by Discuz! X3.2