原子操做能夠保證指令以原子的方式執行----執行過程不被打斷。linux
針對整數的原子操做只能對atomic_t類型的數據進行處理。安全
atomic_t類型定義在文件<linux/types.h> 中數據結構
typedef struct { volatile int counter; } atomic_t;
使用原子整型操做須要的聲明都在<asm/atomic.h>文件中。併發
定義一個atomic_t類型的數據方法很日常,還能夠在定義時給它設定初值:ide
atomic_t v; /* 定義v */ atomic_t u = ATOMIC_INIT(0); /* 定義u並把它初始化爲0 */ atomic_set(&v, 4); /* v = 4 */ atomic_add(2, &v); /* v = v + 2 = 6 */ atomic_inc(&v); /* v = v + 1 = 7 */
若是須要atomic_t轉換成int,則須要atomic_read()來完成。函數
還能夠用原子整數操做原子的執行一個操做並檢查結果。優化
printk("%d\n", atomic_read(&v)); /* 會打印"7" */ int atomic_dec_and_test(atomic_t *v)
某種特定的體系結構上實現的全部操做能夠在文件<asm/atomic.h>中找到atom
ATOMIC_INIT(int i); /* 在聲明一個atomic_t變量時,將它初始化爲i */ int atomic_read(atomic_t *v); /* 原子地讀取整數變量v */ void atomic_set(atomic_t *v, int i); /* 原子地設置v值爲i */ void atomic_add(int i, atomic_t *v) /* 原子地給v加i */ void atomic_sub(int i, atomic_t *v) /* 原子地從v減i */ void atomic_inc(atomic_t *v) /* 原子地給v加1 */ void atomic_dec(atomic_t *v) /* 原子地從v減1 */ int atomic_sub_and_test(int i, atomic_t *v) /* 原子地從v減i,若是結果等於0,返回真,不然返回假 */ int atomic_add_negative(int i, atomic_t *v) /* 原子地給v加i,若是結果是負數,返回真,不然返回假 */ int atomic_add_return(int i, atomic_t *v) /* 原子地給v加i,且返回結果 */ int atomic_sub_return(int i,atomic_t *v) /* 原子地從v減i,且返回結果 */ int atomic_inc_return(int i, atomic_t *v) /* 原子地給v加1,且返回結果 */ int atomic_dec_return(int i, atomic_t *v) /* 原子地給v減1,且返回結果 */ int atomic_dec_and_test(atomic_t *v) /* 原子地從v減1,若是結果等於0,返回真,不然返回假 */ int atomic_inc_and_test(atomic_t *v) /* 原子地給v加1,若是結果等於0,返回真,不然返回假 */
在編寫代碼時,能使用原子操做時,就儘可能不要使用複雜的加鎖機制。spa
隨着64位操做系統的普及,由於atomic_t便令沒法在體系結構之間改變。因此atomic_t類型即使在64位下也是32位的,須要使用64位的原子變量操作系統
atomic64_t類型,其功能和32位原子操做無異,不一樣的只有整型變量大小從32位變成了64位,atomic64_t類型實際上是對長整型的一個簡單封裝類。
ATOMIC64_INIT(int i); /* 在聲明一個atomic_t變量時,將它初始化爲i */ int atomic64_read(atomic_t *v); /* 原子地讀取整數變量v */ void atomic64_set(atomic_t *v, int i); /* 原子地設置v值爲i */ void atomic64_add(int i, atomic_t *v) /* 原子地給v加i */ void atomic64_sub(int i, atomic_t *v) /* 原子地從v減i */ void atomic64_inc(atomic_t *v) /* 原子地給v加1 */ void atomic64_dec(atomic_t *v) /* 原子地從v減1 */ int atomic64_sub_and_test(int i, atomic_t *v) /* 原子地從v減i,若是結果等於0,返回真,不然返回假 */ int atomic64_add_negative(int i, atomic_t *v) /* 原子地給v加i,若是結果是負數,返回真,不然返回假 */ int atomic64_add_return(int i, atomic_t *v) /* 原子地給v加i,且返回結果 */ int atomic64_sub_return(int i,atomic_t *v) /* 原子地從v減i,且返回結果 */ int atomic64_inc_return(int i, atomic_t *v) /* 原子地給v加1,且返回結果 */ int atomic64_dec_return(int i, atomic_t *v) /* 原子地給v減1,且返回結果 */ int atomic64_dec_and_test(atomic_t *v) /* 原子地從v減1,若是結果等於0,返回真,不然返回假 */ int atomic64_inc_and_test(atomic_t *v) /* 原子地給v加1,若是結果等於0,返回真,不然返回假 */
定義在文件<asm/bitops.h>中,例子:
unsigned long word = 0; set_bit(0, &word); /* 第0位被設置(原子地) */ set_bit(1, &work); /* 第1位被設置(原子地) */ printk("%u1\n", word); /* 打印3 */ clear_bit(1, &word); /* 清空第1位 */ change_bit(0, &word); /* 反轉第0位的值,這裏它被清空 */ /* 原子地設置第0位而且返回設置前的值(0) */ if(test_and_set_bit(0, &word) { /* 永遠不爲真 */ } /* 下面的語句是合法的,你能夠把原子位指令與通常的C語言混在一塊兒 */ word = 7;
標準原子位操做列表:
void set_bit(int nr, void *addr) /* 原子地設置addr所指對象的第nr位 */ void clear_bit(int nr, void *addr) /* 原子地清空addr所指對象的第nr位 */ void change_bit(int nr, void *addr) /* 原子地翻轉addr所指對象的第nr位 */ int test_and_set_bit(int nr, void *addr) /* 原子地翻轉addr所指對象的第nr位,並返回原先的值 */ int test_and_clear_bit(int nr, void *addr) /* 原子地清空addr所指對象的第nr位,並返回原先的值 */ int test_and_change_bit(int nr, void *addr) /* 原子地翻轉addr所指對象的第nr位,並返回原先的值 */ int test_bit(int nr, void *addr) /* 原子地返回addr所指對象的第nr位 */
內核還提供了一組與上述操做對應的非原子位函數,非原子位函數與原子位函數的操做徹底相同。
可是前者不保證原子性,其名字前綴多兩個下劃線。 好比test_bit()和__test_bit()
內核還提供了兩個例程用來指定的地址開始搜索第一個被設置的位。
int find_first_bit(unsigned long *addr, unsigned int size) int find_first_zero_bit(unsigned long *addr, unsgined int size) /* 第一參數是一個指針,第二個參數是要搜索的總位數 */
Linux內核中最多見的鎖是自旋鎖。自旋鎖最多隻能被一個可執行線程持有。
自旋鎖的要點:被爭用的自旋鎖使得請求它的線程在等待鎖從新可用時自選(特別浪費處理器時間),因此自旋鎖不該該被長時間持有。
這也是自旋鎖的初衷:在短期內進行輕量級加鎖。持有自旋鎖的時間最好小於完成兩次上下文切換的耗時。
相關的體系結構代碼在文件<asm/spinlock.h>中,實際須要用到的接口定義在文件<linux/spinlock.h>中。自旋鎖的基本使用形式以下:
DEFINE_SPINLOCK(mr_lock); spin_lock(&mr_lock); /* 臨界區 */ spin_unlock(&mr_lock);
在中斷處理程序中使用自旋鎖時,必定要在獲取鎖以前,首先禁止本地中斷。不然,中斷處理程序就會打斷持有鎖的內核代碼,爭用已經被持有的鎖。這樣就會自旋,變成雙重請求死鎖。
警告:自旋鎖是不可遞歸的
內核進制中斷同時請求鎖的接口,以下:
DEFINE_SPINLOCK(mr_lock); unsigned long flags; /* 臨界區 */ spin_lock_irqsave(&mr_lock, flags); spin_unlock_irqrestore(&mr_lock, flags);
spin_lock_irqsave()保存中斷的當前狀態,並禁止本地中斷,而後再去獲取指定的鎖。反過來spin_unlock_irqrestore()對指定的鎖解鎖,而後讓中斷恢復到加鎖前的狀態。
因此計時中斷最初是被禁止的,代碼也不會錯誤地激活他們,相反,會繼續讓他們禁止。
鎖的大原則:針對代碼加鎖會使得程序難以理解,而且容易引起競爭條件,正確的作法應該是對數據而不是代碼加鎖。
若是你能肯定中斷在加鎖前是激活的,那就不須要在解鎖後恢復中斷之前的狀態了。 你能夠無條件地在解鎖時激活中斷。
這時使用spin_lock_irq()和spin_unlock_irq()會更好點,
DEFINE_SPINLOCK(mr_lock); spin_lock_irq(&mr_lock); /* 關鍵節 */ spin_unlock_irq(&mr_lock);
可使用spin_lock_init()方法初始化動態建立的自旋鎖,spin_try_lock()試圖得到某個特定自旋鎖,若是已被爭用,那麼該防範會馬上返回一個非0值,而不會等待自旋鎖釋放。
若是成功得到了這個鎖,返回0。同理spin_is_lock(),用於檢查特定鎖是否被佔用。佔用非0,不然返回0
spin_lock() /* 獲取指定的自旋鎖 */ spin_lock_irq() /* 進制本地中斷並獲取指定的鎖 */ spin_lock_irqsave() /* 保存本地中斷的當前狀態,禁止本地中斷,並獲取指定的鎖 */ spin_unlock() /* 釋放指定的鎖 */ spin_unlock_irqrestore() /* 釋放指定的鎖,並讓本地中斷恢復到之前狀態 */ spin_lock_init() /* 動態初始化指定的spinlock_t */ spin_trylock() /* 試圖獲取指定的鎖,若是未獲取,則返回非0 */ spin_is_locked() /* 若是指定的鎖當前正在被獲取,則返回非0,不然返回0 */
與下半部配合使用時,必須當心使用鎖機制。函數spin_lock_bh()用於獲取指定鎖,同時會禁止全部下半部的執行。
相應的spin_unlock_bh()函數執行相反的操做
有時鎖的用途能夠明確的分爲讀取和寫入兩個場景。
當對某個數據結構的操做能夠劃分爲 讀/寫或者消費者/生產者兩種類別時,相似讀/寫鎖這樣的機制就頗有幫助了。
/* 讀/寫自旋鎖的初始化 */ DEFINE_RWLOCK(mr_rwlock); read_lock(&mr_rwlock); /* 臨界區(只讀)··· */ read_unlock(&mr_rwlock); write_lock(&mr_rwlock); /* 臨界區(讀寫)··· */ write_unlock(&mr_rwlock); /* 注意:不能把一個讀鎖升級爲寫鎖 */ read_lock(&mr_rwlock); write_lock(&mr_rwlock);
下面列出了針對讀-寫自旋鎖的全部操做:
read_lock() /* 得到指定的讀鎖 */ read_lock_irq() /* 禁止本地中斷並得到指定讀鎖 */ read_lock_irqsave() /* 存儲本地中斷的當前狀態,禁止本地中斷並得到指定讀鎖 */ read_unlock() /* 釋放指定的讀鎖 */ read_unlock_irqrestore() /* 釋放指定的讀鎖並將本地中斷恢復到指定的前狀態 */ write_lock() /* 得到指定的寫鎖 */ write_lock_irq() /* 禁止本地中斷並得到指定寫鎖 */ write_lock_irqsave() /* 儲存本地中斷的當前狀態,進制本地中斷並得到指定寫鎖 */ write_unlock() /* 釋放指定的寫鎖 */ write_unlock_irq() /* 釋放指定的寫鎖並激活本地中斷 */ write_unlock_irqrestore() /* 釋放指定的寫鎖並將本地中斷恢復到指定的前狀態 */ write_trylock() /* 試圖得到指定的寫鎖,若是寫鎖不可用,返回非0值 */ rwlock_init() /* 初始化指定的rwlock_t */
讀寫鎖會照顧讀比照顧寫要多一點,因此大量讀者一定會使掛起的寫着處於飢餓狀態
Linux信號量是一種睡眠鎖,若是有一個任務試圖得到一個不可用的信號量時,信號量會將其推動一個等待隊列,讓後讓其睡眠。
這時處理器能重獲自由,從而去執行其餘代碼。當持有信號量可用後,處於等待隊列中的那個任務將被喚醒。
信號量比自旋鎖提供了更好的處理器利用率,由於沒有把時間花費在忙等待上,可是,信號量比自旋鎖有更大的開銷。
若是須要在自旋鎖和信號量中作選擇,應該根據鎖被持有的時間長短作判斷。
信號量同時容許的持有者數量能夠在聲明信號量時指定。這個值稱爲使用者數量(usage count)或簡單地叫數量(count)。
一般狀況下,信號量和自旋鎖同樣,一個時刻容許一個持有者。這樣的的信號量被稱爲二值信號量或者稱爲互斥信號量。
若是初始化時爲大於1的非0值,信號量被稱爲計數信號量,容許一個時刻至多有count個鎖持有者。
信號量有兩個down()和up()操做,down操做經過信號量計數減1來請求得到一個信號量,若是結果是0或大於0,得到信號量。不然就進入等待隊列。
up()操做用來釋放信號量,也被稱做提高,由於會增長信號量計數值。若是在該信號量上的等待隊列不爲空,那麼等待隊列的任務會被喚醒同時得到信號量。
在頭文件<asm/semaphore.h>中,能夠經過靜態地聲明信號量
struct semaphore name; sema_init(&name, count); /* name是信號量變量名,count是信號量的使用數量 */ static DECLARE_MUTEX(name); /* 更爲普通的信號量的建立 */ sema_inti(sem, count); /* 動態建立,sem是指針,count是信號量的使用者數量 */ init_MUTEX(sem);
函數down_interruptible()試圖獲取指定的信號量,若是信號量不可用,它將把調用進程置成TASK_INTERRUPTIBLE狀態----進入睡眠。
sema_init(struct semaphore *, int) /* 以指定的計數值初始化動態建立的信號量 */ init_MUTEX(struct semaphore *) /* 以計數值1初始化動態建立的信號量 */ init_MUTEX_LOCKED(struct semaphore *) /* 以計數值0初始化動態建立的信號量,初始爲加鎖狀態 */ down_interruptible(struct semaphore *) /* 以試圖得到指定的信號量,若是信號量已被爭用, 則進入不可中斷睡眠狀態 */ down(struct semaphore *) /* 以試圖得到指定的信號量,若是信號量已被爭用, 則進入不可中斷睡眠狀態 */ down_trylock(struct semaphore *) /* 以試圖得到指定的信號量,若是信號量已被爭用, 則當即返回非0值 */ up(struct semaphore *) /* 以釋放指定的信號量,若是睡眠隊列不空, 則喚醒其中一個任務 */
定義在文件<linux/rwsem.h>中,經過如下語句能夠建立靜態聲明
static DECLARE_RWSEM(name); /* 靜態建立,name是新信號量名 */ init_rwsem(struct rw_semaphore *sem) /* 動態建立, */
全部的讀寫信號量都是互斥信號量,在引用計數等於1,它們只對寫着互斥,不對讀者。例如:
static DECLARE_RWSEM(mr_rwsem); /* 試圖獲取信號量用於讀... */ down_read(&mr_rwsem); /* 臨界區(只讀)... */ /* 釋放信號量 */ up_read(&mr_rwsem); /* ... */ /* 試圖獲取信號量用於寫 ... */ down_write(&mr_rwsem); /* 臨界區(讀和寫) ... */ /* 釋放信號量 */ up_write(&mr_sem);
多數時候信號量只使用計數1,信號量適合用於哪些較複雜的、未明狀況下的互斥訪問。
爲了找到要給更簡單睡眠鎖,內核引入了互斥體(mutex),指的是能夠睡眠的強制互斥鎖。
DEFINE_MUTEX(name); /* 靜態定義mutex */ mutex_init(&mutex); /* 動態初始化mutex */ /* 互斥鎖鎖定和解鎖並不難 */ mutex_lock(&mutex); /* 臨界區 */ mutex_unlock(&mutex);
下面是基本的mutex操做列表:
mutex_lock(struct mutex *) /* 爲指定的mutex上鎖,若是鎖不可用則睡眠 */ mutex_unlock(struct mutex *) /* 爲指定的mutex解鎖 */ mutex_trylock(struct mutex *) /* 試圖獲取指定的mutex,若是成功則返回1; 不然鎖被獲取,返回值是0 */ mutex_is_locked(struct mutex *) /* 若是鎖已被爭用,則返回1;不然返回0 */
mutex更要個的要求了:
打開內核配置選項CONFIG_DEBUG_MUTEXES後,就會有多種檢測來確保這些約束得以遵照。
在信號量和互斥體的選中中,首選mutex。除非mutex的某個約束妨礙你使用,不然相比信號量要有限使用mutex。
在中斷上下文中只能使用自旋鎖,而在任務睡眠時只能使用互斥體
需求 | 建議的加鎖方法 |
低開銷加鎖 | 優先自旋鎖 |
短時間鎖定 | 優先自旋鎖 |
長期加鎖 | 優先互斥體 |
中斷上下文中加鎖 | 使用自旋鎖 |
持有鎖須要睡眠 | 使用互斥體 |
若是內核中一個任務須要發出信號通知另外一個任務發生了某個特定事件,利用完成變量。
完成變量由結構completion表示,定義在<linux/completion.h>中。
DECLARE_COMPLETION(mr_comp); /* 靜態建立 */ init_completion(&mr_comp); /* 動態建立 */
在一個指定的完成變量上,須要等待的任務調用wait_for_completion()來等待特定事件。
init_completions(struct completion *) /* 初始化指定的動態建立的完成變量 */ wait_for_completions(struct completion *) /* 等待指定的完成變量接收信號 */ complete(struct completion *) /* 發信號喚醒任何等待任務 */
使用完成變量的例子能夠參考kernel/sched.c和kernel/fork.c。
完成變量的一般用法是:將完成變量做爲數據結構中的一項動態建立,而完成數據結構初始化工做的內核代碼將調用wait_for_completion()進行等待。
初始化完成後,初始化函數調用completion()喚醒在等待的內核任務。
BKL(大內核鎖)是一個全局自旋鎖,使用它主要是爲了方便實現從Linux最初的SMP過分到細粒度加鎖機制。
BKL的使用方式和自旋鎖相似,函數lock_kernel()請求鎖,unlock_kernel()釋放鎖。
函數kernel_locker()檢測鎖當前是否被持有,有返回非0值,不然返回0。頭文件在<linux/smp_lock.h>中。
lock_kernel(); /* 臨界區,對全部其餘的BLK用戶進行同步······ * 注意,你能夠安全地在此睡眠,鎖會悄無聲息的被釋放 * 當你的任務被從新調度時,鎖又會被悄無聲息地獲取 * 這意味着你不會處於死鎖狀態,可是,若是你須要鎖保護這裏的數據 * 你仍是不須要睡眠 */ unlock_kernel();
BLK函數列表
lock_kernel() /* 得到BKL */ unlock_kernel() /* 釋放BKL */ kernel_locked() /* 若是鎖被持有返回非0值,不然返回0 */
2.6版本引入的新型鎖,簡稱seq鎖。實現這種鎖主要依靠一個序列計數器。當有疑義的數據被寫入時,會獲得一個鎖,而且序列值會增長。
seqlock_t mr_seq_lock = DEFINE_SEQLOCK(mr_sq_lock); /* 定義一個seq鎖 */ write_seqlock(&mr_seq_lock); /* 寫鎖被獲取 ... */ write_sequnlock(&mr_seq_lock); /* 和自旋鎖相似,不一樣狀況發生在讀時,而且與自旋鎖有很大不一樣 */ unsigned long seq; do { seq = read_seqbegin(&mr_seq_lock); /* 讀這裏的數據 ... */ } while(read_seqretry(&mr_seq_lock, seq);
seq鎖有助於提供一種很是輕量級和具備可擴展性的外觀。可是seq鎖對寫着更有利,只要沒有其餘寫者,寫鎖老是可以被成功得到。讀者不會影響寫鎖。
以下狀況用seq鎖比較理想:
jiffies使用的就是seq鎖。
u64 get_jiffies_64(void) { unsigned long seq; u64 ret; do { seq = read_seqbegin(&xtime_lock); ret = jiffies_64; } while (read_seqretry(&xtime_lock, seq)); return ret; }
若是想要進一步瞭解jiffies和內核時間管理,在kernel/timer.c與kernel/time/tick-common.c文件
因爲內核是搶佔性的,內核中的進程在任什麼時候刻均可能停下來以便另外一個具備更高優先權的進程運行。
若是一個自旋鎖被持有,內核便不能進行搶佔。由於內核搶佔和SMP面對相同的併發問題,而且內核已是SMP安全的,因此,這種簡單的變化使得內核也是搶佔安全的。
有些時候咱們不須要自旋鎖,可是任然須要關閉內核搶佔。爲了解決這個問題,能夠經過preemt_disable()禁止內核搶佔。每次調用都必須有一個相應的preemt_enable()調用。
當最後一次preemt_enable()被調用後,內核搶佔才從新啓用。例如:
preempt_disable() /* 搶佔被禁止 ... */ preempt_enable();
搶佔計數存放着被持有鎖的數量和preempt_disable()的調用次數,若是計數是0,那麼內核能夠進行搶佔。若是爲1或更大的值,那麼,內核就不會進行搶佔。
他是一種對原子操做和睡眠頗有效的調試方法。
preempt_disable() /* 增長搶佔計數值,從而進制內核搶佔 */ preempt_enable() /* 減小搶佔計數,並當該值降爲0時檢查和執行被掛起的需調度的任務 */ preempt_enable_no_resched() /* 激活內核搶佔但再也不檢查任何被掛起的需調度任務 */ preempt_count() /* 返回搶佔計數 */
爲了更簡潔的方法解決每一個處理器上的數據訪問問題,能夠經過get_cpu()得到處理器編號。這個函數在返回當前處理器號前首先會關閉內核搶佔。
int cpu; /* 禁止內核搶佔,並將CPU設置爲當前處理器 */ cpu = get_cpu(); /* 對每一個處理器的數據進行操做 */ /* 再給與內核搶佔性,"CPU"可改變故它再也不有效 */ put_cpu();
當處理多處理器之間或硬件設備之間的同步問題時,有時須要在你的程序代碼中以指定的順序發出讀內存(讀入)和寫內存(存儲)指令。
編譯器和處理器爲了提升效率,可能對讀和寫從新排序,這樣無疑使問題複雜化了。
一樣也能夠指示編譯器不要對給定點周圍的指令序列進行從新排序,這些確保順序的指令稱做屏障。
rmb()方法提供了一個"讀"內存屏障,它確保跨越rmb()的載入動做不會發生重排序。也就是說在rmb()以前的載入操做不會被從新排序在該調用以後,一樣的,在rmb()以後的載入操做不會被重排在該調用以前
wmb()方法提供了一個"寫"內存屏障,和rmb()相似,區別是它針對存儲而非載入
mb()方法及提供了讀屏障也提供寫屏障。
read_barrier_depends()是rmb()的變種,它提供了讀屏障,可是僅僅針對後續讀操做所依靠的哪些載入。
barrier()方法能夠防止編譯器跨屏障對載入或存儲操做進行優化。
rmb() /* 阻止跨越屏障的載入動做發生重排序 */ read_barrier_depends() /* 組織跨越屏障的具備數據依賴關係的載入動做重排序 */ wmb() /* 組織跨越屏障的存儲動做發生重排序 */ mb() /* 阻止跨越屏障的載入和存儲動做從新排序 */ smp_rmb() /* 在SMP上提供rmb()功能,在UP上提供barrier()功能 */ smp_read_barrier_depends() /* 在SMP上提供read_barrier_depends()功能,在UP上提供barrier()功能 */ smp_wmb() /* 在SMP上提供wmb()功能,在UP上提供barrier()功能 */ smp_mb() /* 在SMP上提供mb()功能,在UP上提供barrier()功能 */ barrier() /* 組織編譯器跨屏障對載入或存儲操做進行優化 */