1. 程式人生 > 實用技巧 >Linux 驅動框架---驅動中的併發

Linux 驅動框架---驅動中的併發

併發指多個執行單元可以被同時、並行的執行,而併發執行的單元對共享資源的訪問就容易導致竟態。併發產生的情況分為單核(搶佔)和多核(並行)和中斷(打斷)。Linux為解決這一問題增加了一系列的介面來解決併發導致的竟態問題。其中原子操作是最基本的機制。

原子操作

  通常一句C程式碼在被翻譯成彙編時可能不止一句,如常見的使用一個全域性變數作為標誌位來標誌共享資源的使用情況這種機制的細節如下:

if(flags!= BUSY){
  flasg = BUSY;
  ops,,
  。。。

這種方式會有如下的風險,如果線上程級和中斷都使用一段共享資源,當執行緒執行了判斷之後因為執行或編譯亂序對共享資源的操作早於置起標誌位,然後中斷來了此時中斷也開始使用共享資源,執行完後執行緒繼續,但是對於執行緒操作而言是被中斷了的在有些情況下是不允許的所以需要一種單步(無法在分割為更小的執行步驟)就可以完成的原子操作來保證這種互斥所以有了原子操作,這是一種有硬體支援的操作所以肯定是彙編實現

分整形和位原子操作。

整形

1、建立和設定原子變數
void atomic_set(atomic_t *v,int i);設定原子變數
atomic_t v = ATOMIC_INIT(x);定義並初始化為x
2、獲取
atomic_read(atomic_t* v);
3、增加和減少
void atomic_add(in i ,atomic_t* v);v+=i
void atomic_sub(in i ,atomic_t* v);v-=i
4、自增和自減
void atomic_inc(atomic_t* v);//v++
void atomic_dec(atomic_t* v);//v--
5、操作並測試
int atomic_inc_and_test(atomic_t* v);//++v==0?


int atomic_add_and_test(in i ,atomic_t* v);v+=i;v==0?
int atomic_sub_and_test(in i ,atomic_t* v);v-=i;v==0?
6、操作並返回
int atomic_inc_and_return(atomic_t* v);//return ++v;
int atomic_dec_and_return(atomic_t* v);//return --v;
int atomic_add_and_return(in i ,atomic_t* v);return(v+=i);
int atomic_sub_and_return(in i ,atomic_t* v);return(v-=i);

如果是64位的平臺還支援64位的整形操作atomic64_xxx()。原子操作是後續其他有些互斥實現操作的基礎。

位原子

1、設定bit
void set_bit()
2、清除bit
void clear_bit()
3、取反對應bit
void change_bit()
4、測試bit
int test_bit()
5、測試和操作bit
int test_and_set()
int test_and_clear()
int test_and_change()

自旋鎖

  自旋鎖最初就是為了SMP系統設計的,實現在多處理器情況下保護臨界區。所以在SMP系統中,自旋鎖的實現是完整的本來面目。但是對於UP系統自旋鎖可以說是SMP版本的閹割版。因為只有在SMP系統中的自旋鎖才需要真正“自旋”。因為竟態產生有三種情況:

  • 單核支援搶佔的核心中的程序間搶佔
  • 單核不支援搶佔中斷和程序間的搶佔
  • 多核系統的真正併發執行

所以自旋鎖的目的就是在不同的場景下分別阻止其中一種或多種竟態產生,從而保證這個臨界區的內容不會同時被兩個執行單元訪問而造成資料額不同步或混亂。在核心中常常用來保護對核心資料結構的操作。執行的過程就是執行單元到達臨界區判斷臨界區是否可用如果可以獲取臨界資源則獲取資源繼續執行,此時若另一個執行單元執行到達這裡會發現資源被佔用則會原地執行檢查直到資源可用才可以繼續往下執行。說道這裡應該就會發現一個問題如果一個低優先順序的的執行單元先獲取了臨界資源還未使用完此時高優先順序任務來到了那麼高優先順序任務就會一直自旋並且,低優先順序無法獲取CPU時間無法繼續執行臨界資源無法釋放就會造成死鎖,同樣中斷也會出現類似情況所以,但單核系統中如果不支援搶佔則自旋鎖獲取鎖只需要關閉中斷就可以,如果還支援強佔則還需要關閉搶佔;最後再來看多核系統環境下上面的兩種情況的肯定都輝有除此之外還有就是多個核心程式碼的執行是並行的,要想A核心拿到共享資源後B核心不會拿到關閉中斷和搶佔是不夠的,多核系統之間記憶體訪問是相互可見的,所以在SMP平臺下的自旋鎖是需要操作記憶體從而做到和其他核心互斥訪問共享資源的,這就需要有一塊記憶體來標誌共享資源的狀態,綜上這就是自旋鎖的工作原理全部內容,其中搶佔和單核還是多核是在編譯構建核心時可以配置的所以配置好後自旋鎖對應的API介面的實現就已經支援的當前系統配置的自旋操作,而是否需要遮蔽中斷則是隻有程式開發者知道,因為共享資源的訪問會不會發生在中斷中這是應用邏輯的內容。接下來記錄一下自旋鎖的常用API介面:

不會在任何中斷例程中操作臨界區:
static inline void spin_lock(spinlock_t*lock)

static inline void spin_unlock(spinlock_t*lock)
如果在軟體中斷中操作臨界區:
static inline void spin_lock_bh(spinlock_t*lock)

static inline void spin_unlock_bh(spinlock_t*lock)
bh代表bottom half,也就是中斷中的底半部,因核心中斷的底半部一般通過軟體中斷(tasklet等)來處理而得名。 如果在硬體中斷中操作臨界區:
static inline void spin_lock_irq(spinlock_t*lock)

static inline void spin_unlock_irq(spinlock_t*lock)
如果在控制硬體中斷的時候需要同時儲存中斷狀態:
spin_lock_irqsave(lock, flags)

static inline void spin_unlock_irqrestore(spinlock_t*lock, unsigned long flags)

讀寫鎖

讀寫鎖的出現是為了優化自旋鎖的不足,因為常常存在這樣一種情況,有一個執行緒只會讀取共享資源而另一個程序則只會進行寫入,這種情況很常見但是如果使用自旋鎖系統的效率就很低,因為讀介面等寫介面使用完,所以為了解決這樣缺點Linux核心定義了讀寫鎖,即多個執行單元寫操作相互互斥而寫和讀單元也互斥,但是讀和讀之間就不需要互斥這樣的好處是明顯的讀取鎖可以多次獲取。API如下:

//定義
rwlock_t xxx_rwlock;
//初始化
rwlock_init(rwlock_t* lock);
//讀鎖定
read_lock(rwlock_t* lock);
read_lock_irqsave(rwlock_t* lock,unsigned long flags);//關中斷並記錄中斷之前的值後續恢復
read_lock_irq(rwlock_t* lock);//關中斷
read_lock_bh(rwlock_t* lock);//關底半部
//讀解鎖
read_unlock(rwlock_t* lock);
read_unlock_irqsave(rwlock_t* lock,unsigned long flags);//開中斷並恢復中斷之前的值後續
read_unlock_irq(rwlock_t* lock);//開中斷
read_unlock_bh(rwlock_t* lock);//開中斷
//寫鎖定
write_lock(rwlock_t* lock);
write_lock_irqsave(rwlock_t* lock,unsigned long flags);
write_lock_irq(rwlock_t* lock);
write_lock_bh(rwlock_t* lock);
//寫解鎖
write_unlock(rwlock_t* lock);
write_unlock_irqsave(rwlock_t* lock,unsigned long flags);
write_unlock_irq(rwlock_t* lock);
write_unlock_bh(rwlock_t* lock);

順序鎖

順序鎖又是對讀寫鎖效能的優化,增加了對讀和寫的併發支援,如果讀取過程有過寫的操作就需要重讀,這個機制是要有程式設計人員主動呼叫介面查詢的,讀取結束後通過介面查詢是否發生過寫如果發生過就需要重新讀取。API:

seqlock_init(x);       //動態初始化
DEFINE_SEQLOCK(x);     //靜態初始化
//獲取順序鎖
void write_seqlock(seqlock_t* sl);        //寫加鎖
int write_tryseqlock(seqlock_t* sl);      //嘗試寫加鎖
write_seqlock_irqsave(lock, flags);       //local_irq_save() + write_seqlock()
write_seqlock_irq(lock);                  //local_irq_disable() + write_seqlock()
write_seqlock_bh(lock);                  //local_bh_disable() + write_seqlock()
//釋放順序鎖
void write_sequnlock(seqlock_t* sl);         //寫解鎖
write_sequnlock_irqrestore(lock, flags);     //write_sequnlock() + local_irq_restore()
write_sequnlock_irq(lock);                   //write_sequnlock() + local_irq_enable()
write_sequnlock_bh(lock);                    //write_sequnlock() + local_bh_enable()

讀操作

//讀操作
unsigned int read_seqbegin(const seqlock_t* sl);
read_seqbegin_irqsave(lock, flags);          //local_irq_save() + read_seqbegin()

讀執行單元在訪問共享資源時要呼叫順序鎖的讀函式,返回順序鎖s1的順序號;該函式沒有任何獲得鎖和釋放鎖的開銷,只是簡單地返回順序鎖當前的序號;

重讀

int read_seqretry(const seqlock_t* sl, unsigned start);
read_seqretry_irqrestore(lock, iv, flags);

在順序鎖的一次讀操作結束之後,呼叫順序鎖的重讀函式,用於檢查是否有寫執行單元對共享資源進行過寫操作;如果有就會重新讀取共享資源;iv為順序鎖的id號;

訊號量

  訊號量是所有系統軟體中最典型的用於同步和互斥的軟體手段,程序執行前先獲取訊號量如果獲取成功則繼續執行如果獲取不到則會阻塞。並且會將當前程序掛接到該物件的等待佇列上,如果由程序釋放這個訊號量時就會喚醒這個佇列上的執行緒,順序是誰先阻塞先喚醒誰還是誰優先順序高先喚醒誰??。內容比較簡直接看API:

//定義
struct semaphore sem;
//初始化
void sema_init();
//獲取訊號量
void down();
//程序阻塞後可被訊號喚醒
void down_interruptible();
//獲取訊號量非阻塞版
void down_trylock();
//釋放訊號量
void up();

互斥體

  互斥體應該是比訊號量更加適合資源互斥的機制了,他和訊號量的最大區別就是訊號量的值可以大過1,而互斥訊號量只能是0和1。他的常用使用API:

//定義
struct mutex my_mutex;
//初始化
void mutex_init();
//獲取互斥
void mutex_lock();
//程序阻塞後可被訊號喚醒
void mutex_lock_interruptible();
//獲取互斥量非阻塞版
void mutex_lock_trylock();
//釋放
void mutex_unlock();
//例項
struct mutex my_mutex;

mutex_init(&my_mutex);
mutex_lock(&my_mutex);
.
.
.
void mutex_unlock(&my_mutex);

完成量

比較冷門暫時不記錄了,不用容易忘記。