天天看點

Linux中的各種鎖

自從各種任務不再順序執行的那一天起,自從多道程式設計開始上線的那天,程序就戴上了腳鐐。古老的作業系統的變體當然也接過了父親的狼牙棒,信号量杯證明是一種有效的互斥方式,可是它卻存在很多弊端。其實unix不喜歡混亂,是以unix創造了程序這個可被操縱系統核心強制管理的執行緒概念,unix幾乎給了所有可以執行的東西一個程序上下文,然後管理這些程序,unix的程序管理是很強大的,可是總有管不了的,就是中斷,中斷處理機制被視為一個硬體的無序性和軟體程序管理的有序性之間的協調接口,是以中斷處理程式并不屬于任何程序的上下文,是以在中斷中就不能像在别的程序上下文可以請求信号量,因為中斷進行中根本就沒有地方放床導緻任意程序上下文下沒有地方睡眠。中斷處理不能睡眠是不能在中斷處理用信号量的原因,是以必須提供一個中斷進行中的互斥方案,另外,即使在程序上下文,如果需要的信号量很多,那麼勢必會造成程序頻繁睡眠/被喚醒,這樣程序排程的開銷就過大,是以自旋鎖就出來了,不用睡眠,隻是自旋,很簡單,在核心中很高效。

可是自旋鎖是個平均的權衡結果,沒有考慮到資料的客觀特殊類型以及操作類型,自旋鎖太機制化了,沒有一點政策,其實想想便知道,讀請求根本就不用鎖也就是不用互斥,而寫才需要互斥,在這種特殊需求下,linux提出了讀寫鎖的概念,而且實作的十分藝術,如果看看很多學生寫的讀寫鎖再看看linux核心實作的讀寫鎖,你會認為學生們的作品就是垃圾,國産的教科書就是垃圾堆!我們欣賞一下這種藝術:

static inline int __raw_read_trylock(raw_rwlock_t *lock)

{

atomic_t *count = (atomic_t *)lock;

atomic_dec(count); //以小跨度遞減讀信号量

if (atomic_read(count) >= 0)

return 1;

atomic_inc(count);

return 0;

}

static inline int __raw_write_trylock(raw_rwlock_t *lock)

if (atomic_sub_and_test(RW_LOCK_BIAS, count)) //以大跨度遞減寫信号量

atomic_add(RW_LOCK_BIAS, count);

static inline void __raw_read_unlock(raw_rwlock_t *rw)

asm volatile(LOCK_PREFIX "incl %0" :"+m" (rw->lock) : : "memory");

static inline void __raw_write_unlock(raw_rwlock_t *rw)

asm volatile(LOCK_PREFIX "addl %1, %0" : "+m" (rw->lock) : "i" (RW_LOCK_BIAS) : "memory");

static inline void __raw_read_lock(raw_rwlock_t *rw)

asm volatile(LOCK_PREFIX " subl $1,(%0)/n/t"

"jns 1f/n"

"call __read_lock_failed/n/t"

"1:/n"

::LOCK_PTR_REG (rw) : "memory");

static inline void __raw_write_lock(raw_rwlock_t *rw)

asm volatile(LOCK_PREFIX " subl %1,(%0)/n/t"

"jz 1f/n"

"call __write_lock_failed/n/t"

::LOCK_PTR_REG (rw), "i" (RW_LOCK_BIAS) : "memory");

就這麼多嗎?是的,就這麼多!很簡單,很對稱,如果不看函數名和宏定義,你根本就看不出來哪個是讀鎖哪個是寫鎖,它們的形式是一樣的,也沒有隊列,也沒有複雜的資料結構,從本質上說,讀寫鎖是用自旋鎖的思想實作的,自旋鎖就是不斷判斷一個數字的大小,如果小于0就說明得不到鎖,如果為1就得到了鎖,釋放鎖就是将鎖設定為1就可以,近來又實作了ticket自旋鎖,使得鎖變得有序化了,照顧到了硬體緩存的緩存結果。自旋鎖就是不斷判斷一個數的大小,不管誰想得到鎖都要經過競争,也就是自旋,得不到鎖就是因為将那個數減1後,它小于0了,如果讓它不小于0不就可以得到鎖了嗎?我們的需求是讀之間可以随意,但是寫之間必須互斥,另外讀寫之間也要互斥,而在自旋鎖中,無論讀寫都要一樣的動作,那麼我們隻需要将讀寫分開成為不同的動作就可以了,目的就是要展現出讀和寫的不對稱性,先看看自旋鎖的實作,隻要請求鎖都要将鎖變量減去1,隻要釋放都要将鎖設定為1,那麼讀寫鎖中我們将讀和寫減去的數設定為不同就可以了,設定寫鎖請求時要減去一個很大的數N,而讀鎖隻需要減去1,隻要大于0就可以得到鎖,如果不是就忙等待,這樣的話,如果初始化的時候将鎖初始化為N,那麼隻要有一個讀,那麼所就是N-1,如果再讀,就是N-2,仍然大于0,如果寫,就是-2,小于0,忙等,等待兩個讀者釋放了鎖,就是0了,那麼寫可以進行,寫完後釋放鎖,就是将鎖加上N,于是鎖恢複N,其實這個實作和信号量有點類似,不同的是等不到鎖時不睡眠而是忙等,此實作給了讀很多的N個可用信号量,因為對于讀,信号量的跨度是1,而對于寫,隻有1個信号量,寫的跨度是N,這可以說是信号量的更新版,一個信号量支援兩個不同的跨度,不過在等待的意義上,它又像是自旋鎖,總之,linux核心的實作很是藝術吧。

說完了讀寫鎖,那麼看看順序鎖,順序鎖更具有創意,它基于一個事實,在讀寫鎖中,如果讀的時候恰好有一個寫者在寫,那麼讀者就要忙等,忙等是因為害怕讀到不一緻的資料,反過來如果寫者發現有讀者在讀,它也要忙等,忙等是因為怕讀者讀到不一緻的資料,這種互相照顧必然會影響效率,不管怎樣我們都可以賭一把,這種賭博需要很小的賭注就值得,沒有必要每次讀的時候都要請求鎖,而是随時都可以讀,一旦讀到不一緻的資料,那麼大不了再讀一次,這個設想應該是很不錯的,讀寫随時進行,誰也不管誰,自己注點意就可以了,比如以寫者為主,讀者自己進行資料一緻性的判斷,這樣效率會提高不少的。

分析到這裡可以想到,有兩個點要注意,一個就是讀開始,一個就是讀結束,這兩個點将時間分為了三個部分,讀前,讀中,讀後,讀前寫完成和讀後寫完成都不會影響讀操作,那麼隻有三類寫操作會污染讀者讀到的資料,一個就是寫在讀前開始在讀中結束,一個是在讀中開始在讀中結束,一個是在讀中開始,在讀後結束,于是我們隻需要監控寫開始和寫結束即可,按照最簡單的數學原理,如果設定一個變量,在寫開始和寫結束時都對它遞增,我們就可以知道寫的狀态,這個數隻要是偶數就說明沒有在寫中或者相反,取決于初始設定,那麼讀的時候可以盡量等到沒有在寫中的時候進行,然後在讀結束的時候判斷這個變量,如果被增加了就說明開始了一次寫已經結束或者開始了一次寫還沒有結束,總之我們的資料被污染了,那麼就需要再讀一次,即使再讀一次也不能保證沒有寫進行,但是我們可以再賭一把,這實際上是一個循環。這個順序鎖最起碼解放了寫操作,寫操作不必關心讀的情況,對于讀操作純粹是賭博,但是讀寫鎖對讀操作是有好處的,因為大多數情況下隻要沒有寫者,讀者都可以随意,總而言之,順序鎖還是比讀寫鎖好的。我們還是看一眼順序鎖吧:

static inline void write_sequnlock(seqlock_t *sl)

smp_wmb();

sl->sequence++; //遞增一個數字

spin_unlock(&sl->lock);

static inline void write_seqlock(seqlock_t *sl)

spin_lock(&sl->lock);

++sl->sequence; //釋放鎖也一樣要遞增一個數字

static __always_inline unsigned read_seqbegin(const seqlock_t *sl)

unsigned ret;

repeat:

ret = sl->sequence; //在讀之前,先取得這個數字

smp_rmb();

if (unlikely(ret & 1)) { //確定沒有在寫中

cpu_relax();

goto repeat;

return ret;

static __always_inline int read_seqretry(const seqlock_t *sl, unsigned start)

return (sl->sequence != start); //判斷這個數字是否改變

最後一類鎖就是經典的RCU鎖,這個鎖我就不說那麼多了,前面寫過兩篇文章說明這個鎖。RCU鎖基本沒有利用什麼鎖,可是除了真正的鎖之外,其它的特性它都用到了,比如RCU用到的就是cpu排程的周期,因為RCU确信自己不會破壞自己,隻有别人可能破壞自己,如果不想自己被破壞,那麼就不讓别人運作,于是傳統的RCU鎖就是禁用搶占,而最新的RCU鎖允許了搶占,但是Rcu的機制保護着rcu應該保護的資料。

 本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1273396

繼續閱讀