在現代操作系統里,同一時間可能有多個內核執行流在執行,因此內核其實象多進程多線程編程一樣也需要一些同步機制來同步各執行單元對共享數據的訪問。尤其是在多處理器系統上,更需要一些同步機制來同步不同處理器上的執行單元對共享的數據的訪問。
在主流的Linux內核中包含了幾乎所有現代的操作系統具有的同步機制,這些同步機制包括:原子操作、信號量(semaphore)、讀寫信號量(rw_semaphore)、spinlock、BKL(Big Kernel Lock)、rwlock、brlock(只包含在2.4內核中)、RCU(只包含在2.6內核中)和seqlock(只包含在2.6內核中)。
比較經典的有原子操作、spin_lock(忙等待的鎖)、mutex(互斥鎖)、semaphore(信號量)等。并且它們幾乎都有對應的rw_XXX(讀寫鎖),以便在能夠區分讀與寫的情況下,讓讀操作相互不互斥(讀寫、寫寫依然互斥)。而seqlock和rcu應該可以不算在經典之列,它們是兩種比較有意思的同步機制。
原子操作
原子操作就是指某一個操作在執行過程中不可以被打斷,要么全部執行,要不就一點也不執行。原子操作需要硬件的支持,與體系結構相關,使用匯編語言實現。原子操作主要用于實現資源計數,很多引用計數就是通過原子操作實現。Linux中提供了兩種原子操作接口,分別是原子整數操作和原子位操作。
原子整數操作只對atomic_t類型的數據進行操作,不能對C語言的int進行操作,使用atomic_t只能將其作為24位數據處理,主要是在SPARC體系結構中int的低8為中設置了一個鎖,避免對原子類型數據的并發訪問。
原子位操作是針對由指針變量指定的任意一塊內存區域的位序列的某一位進行操作。它只是針對普通指針的操作,不需要定義一個與該操作相對應的數據類型。
原子類型定義如下:
typedefstruct { volatile int counter; }atomic_t;
volatile修飾字段告訴gcc不要對該類型的數據做優化處理,對它的訪問都是對內存的訪問,而不是對寄存器的訪問。
原子操作API包括:
atomic_read(atomic_t* v);
該函數對原子類型的變量進行原子讀操作,它返回原子類型的變量v的值。
atomic_set(atomic_t* v, int i);
該函數設置原子類型的變量v的值為i。
voidatomic_add(int i, atomic_t *v);
該函數給原子類型的變量v增加值i。
atomic_sub(inti, atomic_t *v);
該函數從原子類型的變量v中減去i。
intatomic_sub_and_test(int i, atomic_t *v);
該函數從原子類型的變量v中減去i,并判斷結果是否為0,如果為0,返回真,否則返回假。
voidatomic_inc(atomic_t *v);
該函數對原子類型變量v原子地增加1。
voidatomic_dec(atomic_t *v);
該函數對原子類型的變量v原子地減1。
intatomic_dec_and_test(atomic_t *v);
該函數對原子類型的變量v原子地減1,并判斷結果是否為0,如果為0,返回真,否則返回假。
intatomic_inc_and_test(atomic_t *v);
該函數對原子類型的變量v原子地增加1,并判斷結果是否為0,如果為0,返回真,否則返回假。
intatomic_add_negative(int i, atomic_t*v);
該函數對原子類型的變量v原子地增加I,并判斷結果是否為負數,如果是,返回真,否則返回假。
intatomic_add_return(int i, atomic_t *v);
該函數對原子類型的變量v原子地增加i,并且返回指向v的指針。
intatomic_sub_return(int i, atomic_t *v);
該函數從原子類型的變量v中減去i,并且返回指向v的指針。
intatomic_inc_return(atomic_t * v);
該函數對原子類型的變量v原子地增加1并且返回指向v的指針。
intatomic_dec_return(atomic_t * v);
該函數對原子類型的變量v原子地減1并且返回指向v的指針。
原子操作通常用于實現資源的引用計數,在TCP/IP協議棧的IP碎片處理中,就使用了引用計數,碎片隊列結構structipq描述了一個IP碎片,字段refcnt就是引用計數器,它的類型為atomic_t,當創建IP碎片時(在函數ip_frag_create中),使用atomic_set函數把它設置為1,當引用該IP碎片時,就使用函數atomic_inc把引用計數加1,當不需要引用該IP碎片時,就使用函數ipq_put來釋放該IP碎片,ipq_put使用函數atomic_dec_and_test把引用計數減1并判斷引用計數是否為0,如果是就釋放Ip碎片。函數ipq_kill把IP碎片從ipq隊列中刪除,并把該刪除的IP碎片的引用計數減1(通過使用函數atomic_dec實現)。
自旋鎖
Linux自旋鎖保證了任意時刻只能有一個執行線程進入臨界區,其他試圖進入臨界區的線程將一直進行嘗試(即自旋),直到獲得該鎖。自旋鎖主要應用在加鎖時間不長并且不會睡眠的情況。
自旋鎖的本質是對內存區域的一個整數的操作,任何線程進入臨界區之前都必須檢查該整數,可用則進入,都則一直忙循環等待。
自旋鎖機制讓試圖獲得該鎖的線程一直進行忙循環(占用CPU),因此自旋鎖適合于斷時間內進行輕量級加鎖。而且自旋鎖絕對不可以遞歸使用,否則會被自己鎖死。
Linux自旋鎖主要應用與多核處理器中,單CPU中不會進行自旋鎖操作。
linux上的自旋鎖有三種實現:
a. 在單cpu,不可搶占內核中,自旋鎖為空操作。
b. 在單cpu,可搶占內核中,自旋鎖實現為“禁止內核搶占”,并不實現“自旋”。
c. 在多cpu,可搶占內核中,自旋鎖實現為“禁止內核搶占” + “自旋”。其中,禁止內核搶占只是關閉“可搶占標志”,而不是禁止進程切換。顯式使用schedule或進程阻塞(此也會導致調用schedule)時,還是會發生進程調度的。
使用自旋鎖需要注意有可能造成的死鎖情況:
static DEFINE_SPINLOCK(xxx_lock);
unsigned long flags;
spin_lock_irqsave(&xxx_lock, flags);
。。。 critical section here 。。
spin_unlock_irqrestore(&xxx_lock, flags);
代碼中spin_lock_irqsave會禁止本地cpu中斷的搶占。以上代碼在任何情況下都是安全的。但問題是關中斷的代價太大。
如果把spin_lock_irqsave/spin_unlock_irqrestore換成spin_lock/spin_unlock會有什么問題嗎?
答案是,如果中斷中調用了spin_lock,可能會引起死鎖!
例如:
spin_lock(&lock);
。。。
《- interrupt comes in:
spin_lock(&lock);
值得注意的是,如果產生中斷的cpu和進程中調用spin_lock的cpu不是同一個,則不會有問題。這也是irq版本的spin_lock函數實現時只需要禁止本地cpu中斷的原因。
結論:要想在進程中用spin_lock代替spin_lock_irqsave,條件是中斷中不會使用相應的spin_lock
何時使用自旋鎖?
不允許睡眠的上下文且臨界區操作較短時使用自旋鎖。
?
? ? 讀/寫自旋鎖
Linux中規定,讀/寫自旋鎖允許多個線程同時以只讀的方式訪問臨界資源,只有當一個線程想更新數據時,才會互斥訪問資源。
讀寫自旋鎖包括一個24位讀者計數和一個解鎖標記來實現的。
注意:讀寫鎖需要比spinlocks更多的訪問原子內存操作,如果讀臨界區不是很大,最好別使用讀寫鎖。
讀寫鎖代碼:
點擊(此處)折疊或打開rwlock_t xxx_lock = __RW_LOCK_UNLOCKED(xxx_lock);
unsigned long flags;
read_lock_irqsave(&xxx_lock, flags);
。。 critical section that only reads the info 。。。
read_unlock_irqrestore(&xxx_lock, flags);
write_lock_irqsave(&xxx_lock, flags);
。。 read and write exclusive access to the info 。。。
write_unlock_irqrestore(&xxx_lock, flags);
讀寫鎖比較適合鏈表等數據結構,特別是查找遠多于修改的情況。
另外,可以靈活的使用read-write和irq版本的自旋鎖。例如,如果中斷中只是用了讀鎖,進程中就可以使用non-irq版本的讀鎖和irq版本的寫鎖。
注意:RCU比讀寫鎖更適合遍歷list,但需要更關注細節。目前kernel社區正在努力用RCU代替讀寫鎖。
評論
查看更多