您好,登錄后才能下訂單哦!
小編給大家分享一下linux驅動中并發與竟態的示例分析,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
首先什么是并發與竟態呢?并發(concurrency)指的是多個執行單元同時、并行被執行。而并發的執行單元對共享資源(硬件資源和軟件上的全局、靜態變量)的訪問則容易導致競態(race conditions)。可能導致并發和竟態的情況有:
SMP(Symmetric Multi-Processing),對稱多處理結構。SMP是一種緊耦合、共享存儲的系統模型,它的特點是多個CPU使用共同的系統總線,因此可訪問共同的外設和存儲器。
中斷。中斷可 打斷正在執行的進程,若中斷處理程序訪問進程正在訪問的資源,則競態也會發生。中斷也可能被新的更高優先級的中斷打斷,因此,多個中斷之間也可能引起并發而導致競態。
內核進程的搶占。linux是可搶占的,所以一個內核進程可能被另一個高優先級的內核進程搶占。如果兩個進程共同訪問共享資源,就會出現竟態。
以上三種情況只有SMP是真正意義上的并行,而其他都是宏觀上的并行,微觀上的串行。但其都會引發對臨界共享區的競爭問題。而解決競態問題的途徑是保證對共享資源的互斥訪問,即一個執行單元在訪問共享資源的時候,其他的執行單元被禁止訪問。那么linux內核中如何做到對對共享資源的互斥訪問呢?在linux驅動編程中,常用的解決并發與竟態的手段有信號量與互斥鎖,Completions 機制,自旋鎖(spin lock),以及一些其他的不使用鎖的實現方式。下面一一介紹。
信號量其實就是一個整型值,其核心是一個想進入臨界區的進程將在相關信號量上調用 P; 如果信號量的值大于零, 這個值遞減 1 并且進程繼續. 相反,,如果信號量的值是 0 ( 或更小 ), 進程必須等待直到別人釋放信號量. 解鎖一個信號量通過調用 V 完成; 這個函數遞增信號量的值,,并且, 如果需要, 喚醒等待的進程。而當信號量的初始值為1的時候,就變成了互斥鎖。
信號量的典型使用形式:
//聲明信號量 struct semaphore sem; //初始化信號量 void sema_init(struct semaphore *sem, int val) //常用下面兩種形式 #define init_MUTEX(sem) sema_init(sem, 1) #define init_MUTEX_LOCKED(sem) sema_init(sem, 0) //以下是初始化信號量的快捷方式,最常用的 DECLARE_MUTEX(name) //初始化name的信號量為1 DECLARE_MUTEX_LOCKED(name) //初始化信號量為0 //常用操作 DECLARE_MUTEX(mount_sem); down(&mount_sem); //獲取信號量 ... critical section //臨界區 ... up(&mount_sem); //釋放信號量
常用的down操作還有
// 類似down(),因為down()而進入休眠的進程不能被信號打斷,而因為down_interruptible()而進入休眠的進程能被信號打斷, // 信號也會導致該函數返回,此時返回值非0 int down_interruptible(struct semaphore *sem); // 嘗試獲得信號量sem,若立即獲得,它就獲得該信號量并返回0,否則,返回非0.它不會導致調用者睡眠,可在中斷上下文使用 int down_trylock(struct semaphore *sem);
完成量(completion)提供了一種比信號量更好的同步機制,它用于一個執行單元等待另一個執行單元執行完某事。
</pre></div><div><pre name="code" class="cpp">// 定義完成量 struct completion my_completion; // 初始化completion init_completion(&my_completion); // 定義和初始化快捷方式: DECLEAR_COMPLETION(my_completion); // 等待一個completion被喚醒 void wait_for_completion(struct completion *c); // 喚醒完成量 void cmplete(struct completion *c); void cmplete_all(struct completion *c);
若一個進程要訪問臨界資源,測試鎖空閑,則進程獲得這個鎖并繼續執行;若測試結果表明鎖扔被占用,進程將在一個小的循環內重復“測試并設置”操作,進行所謂的“自旋”,等待自旋鎖持有者釋放這個鎖。自旋鎖與互斥鎖類似,但是互斥鎖不能用在可能睡眠的代碼中,而自旋鎖可以用在可睡眠的代碼中,典型的應用是可以用在中斷處理函數中。自旋鎖的相關操作:
// 定義自旋鎖 spinlock_t spin; // 初始化自旋鎖 spin_lock_init(lock); // 獲得自旋鎖:若能立即獲得鎖,它獲得鎖并返回,否則,自旋,直到該鎖持有者釋放 spin_lock(lock); // 嘗試獲得自旋鎖:若能立即獲得鎖,它獲得并返回真,否則立即返回假,不再自旋 spin_trylock(lock); // 釋放自旋鎖: 與spin_lock(lock)和spin_trylock(lock)配對使用 spin_unlock(lock); 自旋鎖的使用: // 定義一個自旋鎖 spinlock_t lock; spin_lock_init(&lock); spin_lock(&lock); // 獲取自旋鎖,保護臨界區 ... // 臨界區 spin_unlock(); // 解鎖
自旋鎖持有期間內核的搶占將被禁止。自旋鎖可以保證臨界區不受別的CPU和本CPU內的搶占進程打擾,但是得到鎖的代碼路徑在執行臨界區的時候還可能受到中斷和底半部(BH)的影響。為防止這種影響,需要用到自旋鎖的衍生:
spin_lock_irq() = spin_lock() + local_irq_disable() spin_unlock_irq() = spin_unlock() + local_irq_enable() spin_lock_irqsave() = spin_lock() + local_irq_save() spin_unlock_irqrestore() = spin_unlock() + local_irq_restore() spin_lock_bh() = spin_lock() + local_bh_disable() spin_unlock_bh() = spin_unlock() + local_bh_enable()
以上是linux驅動編程中經常用到的鎖機制,下面講一些內核中其他的一些實現。
有時, 你可以重新打造你的算法來完全避免加鎖的需要.。許多讀者/寫者情況 -- 如果只有一個寫者 -- 常常能夠在這個方式下工作.。如果寫者小心使數據結構,由讀者所見的,是一直一致的,,有可能創建一個不加鎖的數據結構。在linux內核中就有一個通用的無鎖的環形緩沖實現,具體內容參考<linux/kfifo.h>。
原子操作指的是在執行過程中不會被別的代碼路徑所中斷的操作。原子變量與位操作都是原子操作。以下是其相關操作介紹。
// 設置原子變量的值 void atomic_set(atomic_t *v, int i); // 設置原子變量的值為i atomic_t v = ATOMIC_INIT(0); // 定義原子變量v,并初始化為0 // 獲取原子變量的值 atomic_read(atomic_t *v); // 返回原子變量的值 // 原子變量加/減 void atomic_add(int i, atomic_t *v); // 原子變量加i void atomic_sub(int i, atomic_t *v); // 原子變量減i // 原子變量自增/自減 void atomic_inc(atomic_t *v); // 原子變量增加1 void atomic_dec(atomic_t *v); // 原子變量減少1 // 操作并測試:對原子變量進行自增、自減和減操作后(沒有加)測試其是否為0,為0則返回true,否則返回false int atomic_inc_and_test(atomic_t *v); int atomic_dec_and_test(atomic_t *v); int atomic_sub_and_test(int i, atomic_t *v); // 操作并返回: 對原子變量進行加/減和自增/自減操作,并返回新的值 int atomic_add_return(int i, atomic_t *v); int atomic_sub_return(int i, atomic_t *v); int atomic_inc_return(atomic_t *v); int atomic_dec_return(atomic_t *v); 位原子操作: // 設置位 void set_bit(nr, void *addr); // 設置addr地址的第nr位,即將位寫1 // 清除位 void clear_bit(nr, void *addr); // 清除addr地址的第nr位,即將位寫0 // 改變位 void change_bit(nr, void *addr); // 對addr地址的第nr位取反 // 測試位 test_bit(nr, void *addr); // 返回addr地址的第nr位 // 測試并操作:等同于執行test_bit(nr, void *addr)后再執行xxx_bit(nr, void *addr) int test_and_set_bit(nr, void *addr); int test_and_clear_bit(nr, void *addr); int test_and_change_bit(nr, void *addr);
使用seqlock鎖,讀執行單元不會被寫執行單元阻塞,即讀執行單元可以在寫執行單元對被seqlock鎖保護的共享資源進行寫操作時仍然可以繼續讀,而不必等待寫執行單元完成寫操作,寫執行單元也不需要等待所有讀執行單元完成讀操作才去進行寫操作。寫執行單元之間仍是互斥的。若讀操作期間,發生了寫操作,必須重新讀取數據。seqlock鎖必須要求被保護的共享資源不含有指針。
// 獲得順序鎖 void write_seqlock(seqlock_t *sl); int write_tryseqlock(seqlock_t *sl); write_seqlock_irqsave(lock, flags) write_seqlock_irq(lock) write_seqlock_bh() // 釋放順序鎖 void write_sequnlock(seqlock_t *sl); write_sequnlock_irqrestore(lock, flags) write_sequnlock_irq(lock) write_sequnlock_bh() // 寫執行單元使用順序鎖的模式如下: write_seqlock(&seqlock_a); ... // 寫操作代碼塊 write_sequnlock(&seqlock_a); 讀執行單元操作: // 讀開始:返回順序鎖sl當前順序號 unsigned read_seqbegin(const seqlock_t *sl); read_seqbegin_irqsave(lock, flags) // 重讀:讀執行單元在訪問完被順序鎖sl保護的共享資源后需要調用該函數來檢查,在讀訪問期間是否有寫操作。若有寫操作,重讀 int read_seqretry(const seqlock_t *sl, unsigned iv); read_seqretry_irqrestore(lock, iv, flags) // 讀執行單元使用順序鎖的模式如下: do{ seqnum = read_seqbegin(&seqlock_a); // 讀操作代碼塊 ... }while(read_seqretry(&seqlock_a, seqnum));
讀取-拷貝-更新(RCU) 是一個高級的互斥方法,在合適的時候可以取得非常高的效率。RCU可以看作讀寫鎖的高性能版本,相比讀寫鎖,RCU的優點在于既允許多個讀執行單元同時訪問被保護的數據,又允許多個讀執行單元和多個寫執行單元同時訪問被保護的數據。但是RCU不能替代讀寫鎖,因為如果寫比較多時,對讀執行單元的性能提高不能彌補寫執行單元導致的損失。由于平時應用較少,所以不做多說。
以上是“linux驅動中并發與竟態的示例分析”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。