這兩天家裏的事好多,咱們今天繼續接着上一次的內容學習,上次咱們完善了字符設備控制方法,並深刻分析了系統調用的實質,今天咱們主要來了解一下併發和競態。git
今天咱們會分析到如下內容:github
1. 併發和競態簡介網絡
2. 競態解決辦法架構
3. 爲咱們的虛擬設備增長併發控制併發
在前幾回博文咱們已經實現了簡單的字符設備,看似完美但咱們忽視了一個很嚴重的問題,即併發問題,那麼什麼是併發,又如何解決併發呢,咱們下面進行分析。異步
1. 併發與競態概念oop
1. 何爲併發:併發是指多個執行單元同時、並行被執行。性能
2. 何爲競態:併發的執行單元對共享資源(硬件資源和軟件上的全局變量,靜態變量等)的訪問容易發生競態。
3. 咱們虛擬設備的缺陷:對於咱們前期的虛擬設備驅動個,假設一個執行單元A對其寫入300個字符‘a’,而另外一個執行單元B對其寫入300個字符‘b’,第三個執行單元讀取全部字符。若是A、B被順序執行那麼C讀出的則不會出錯,但若是A、B併發執行,那結果則是咱們不可料想的。
2. 競態發生的狀況
1. 對稱多處理器(SMP)的多個CPU:SMP是一種緊耦合、共享存儲的系統模型,它的特色是多個CPU使用共同的系統總線,所以能夠訪問共同的外設和存儲器。
2. 單CPU內進程與搶佔它的進程:2.6的內核支持搶佔調度,一個進程在內核執行的時候可能被另外一高優先級進程打斷。
3. 中斷(硬中斷、軟中斷、tasklet、低半部)與進程之間:中斷能夠打斷正在執行的進程,處理中斷的程序和被打斷的進程間也可能發生競態。
3. 競態的解決辦法
解決競態問題的途徑是保證對共享資源的互斥訪問。訪問共享資源的代碼區域稱爲臨界區,臨界區要互斥機制保護。Linux設備驅動中常見的互斥機制有如下方式:中斷屏蔽、原子操做、自旋鎖和信號量等。
l 競態解決辦法
上面咱們已經分析了競態產生的緣由、發生的狀況以及解決辦法,下面咱們對常見的解決辦法一一分析。
1. 中斷屏蔽
1. 基本概念:在單CPU中避免競態的一種簡單方法是在進入臨界區以前屏蔽系統的中斷。因爲linux的異步I/O、進程調度等不少內容都依靠中斷,因此咱們應該儘快的執行完臨界區的代碼,換句話就是臨界區代碼應該儘可能少。
2. 具體操做:linux內核提供了下面具體方法
Local_irq_disable();//屏蔽中斷
Local_irq_enable();//打開中斷
Local_irq_save(flags);//禁止中斷並保存當前cpu的中斷位信息
2. 原子操做
1. 基本概念:原子操做指在執行過程當中不會被別的代碼中斷的操做。
2. 具體操做:linux內核提供了一系列的函數來實現內核中的原子操做,這些操做分爲兩類,一類是整型原子操做,另外一類是位原子操做,其都依賴底層CPU的原子操做實現,因此這些函數與CPU架構有密切關係。
1) 整型原子操做
a) 設置原子變量的值
atomic_t v = ATOMIC_INIT(0);//定義原子變量v並初始化爲0
void atomic_set(atomic_t *v, int i);//設置原子變量值爲i
b) 獲取原子變量的值
atomic_read(atomic_t *v);//返回原子變量v的值
c) 原子變量加、減操做
void atomic_add(int i, atomic_t *v);//原子變量v增長i
void atomic_sub(int I, atomic_t *v);//原子變量v減小i
d) 原子變量自增、自減
void atomic_inc(atomic_t *v);//原子變量v自增1
void atomic_dec(atomic_t *v);//原子變量v自減1
e) 操做並測試
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);
/*上述三個函數對原子變量v自增、自減和減操做(沒有加)後測試其是否爲0,若是爲0返回true,不然返回false*/
f) 操做並返回
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);
/*上述函數對原子變量v進行自增、自減、加、減操做,並返回新的值*/
2) 位原子操做
a) 設置位
void set_bit(nr,void *addr);//設置addr地址的第nr位,即向該位寫入1。
b) 清除位
void clear_bit(nr,void *addr);//清除addr地址的第nr位,即向該位寫入0。
c) 改變位
void change_bit(nr,void *addr);//對addr地址的第nr取反
d) 測試位
int test_bit(nr,void *addr);//返回addr地址的第nr位
e) 測試並操做位
int test_and_set_bit(nr,void *addr);
int test_and_clear_bit(nr,void *addr);
int test_and_change_bit(nr,void *addr);
/*上述函數等同於執行test_bit後,再執行xxx_bit函數*/
3. 自旋鎖
1. 基本概念:自旋鎖是一種對臨界資源進行互斥訪問的手段。
2. 工做原理:爲得到自旋鎖,在某CPU上運行的代碼需先執行一個原子操做,該操做測試並設置某個內存變量,因爲其爲原子操做,因此在該操做完成以前其餘執行單元不可能訪問這個內存變量,若是測試結果代表已經空閒,則程序得到這個自旋鎖並繼續執行,若是測試結果代表該鎖仍被佔用,程序將在一個小的循環內重複這個「測試並設置」操做,即進行所謂的「自旋」,通俗的說就是在「原地打轉」。
3. 具體操做:linux內核中與自旋鎖相關的操做主要有:
1) 定義自旋鎖
spinlock_t lock;
2) 初始自旋鎖
spin_lock_init(lock);
3) 得到自旋鎖
spin_lock(lock);//得到自旋鎖lock
spin_trylock(lock);//嘗試獲取lock若是不能得到鎖,返回假值,不在原地打轉。
4) 釋放自旋鎖
spin_unlock(lock);//釋放自旋鎖
爲保證咱們執行臨界區代碼的時候不被中斷等影響咱們的自旋鎖又衍生了下面的內容
5) 自旋鎖衍生
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_disable()
4. 使用注意事項:
1) 自旋鎖實質是忙等鎖,所以在佔用鎖時間極短的狀況下,使用鎖纔是合理的,反之則會影響系統性能。
2) 自旋鎖可能致使系統死鎖。
3) 自旋鎖鎖按期間不能調用可能引發進程調度的函數。
4. 讀寫自旋鎖
1. 基本概念:爲解決自旋鎖中不能容許多個單元併發讀的操做,衍生出了讀寫自旋鎖,其不容許寫操做併發,但容許讀操做併發。
2. 具體操做:linux內核中與讀寫自旋鎖相關的操做主要有:
1) 定義和初始化讀寫自旋鎖
rwlock_t my_rwlock = RW_LOCK_UNLOCKED;//靜態初始化
rwlock_t my_rwlock;
rwlock_init(&my_rwlock);//動態初始化
2) 讀鎖定
read_lock();
read_lock_irqsave();
read_lock_irq();
read_lock_bh();
3) 讀解鎖
read_unlock();
read_unlock_irqrestore();
read_unlock_irq();
read_unlock_bh();
4) 寫鎖定
write_lock();
write_lock_irqsave();
write_lock_irq();
write_lock_bh();
write_trylock();
5) 寫解鎖
write_unlock();
write_unlock_irqrestore();
write_unlock_irq();
write_unlock_bh();
5. 順序鎖
1. 基本概念:順序鎖是對讀寫鎖的一種優化,若是使用順序鎖,讀執行單元在寫執行單元對被順序鎖保護的共享資源進行寫操做時仍然能夠繼續讀,沒必要等待寫執行單元的完成,寫執行單元也不需等待讀執行單元完成在進行寫操做。
2. 注意事項:順序鎖保護的共享資源不含有指針,由於在寫執行單元可能使得指針失效,但讀執行單元若是此時訪問該指針,將致使oops。
3. 具體操做:linux內核中與順序鎖相關的操做主要有:
1) 寫執行單元得到順序鎖
write_seqlock();
write_tryseqlock();
write_seqlock_irqsave();
write_seqlock_irq();
write_seqlock_bh();
2) 寫執行單元釋放順序鎖
write_sequnlock();
write_sequnlock_irqrestore();
write_sequnlock_irq();
write_sequnlock_bh();
3) 讀執行單元開始
read_seqbegin();
read_seqbegin_irqsave();//local_irq_save + read_seqbegin
4) 讀執行單元重讀
read_seqretry ();
read_seqretry_irqrestore ();
6. RCU(讀—拷貝—更新)
1. 基本概念:RCU能夠看作是讀寫鎖的高性能版本,相比讀寫鎖,RCU的優勢在於即容許多個讀執行單元同時訪問被保護數據,又容許多個讀執行單元和多個寫執行單元同時訪問被保護的數據。
2. 注意事項:RCU不能代替讀寫鎖。
3. 具體操做:linux內核中與RCU相關的操做主要有:
1) 讀鎖定
rcu_read_lock ();
rcu_read_lock_bh ();
2) 讀解鎖
rcu_read_unlock ();
rcu_read_unlock_bh ();
3) 同步RCU
synchronize_rcu ();//由RCU寫執行單元調用
synchronize_sched();//能夠保證中斷處理函數處理完畢,不能保證軟中斷處理結束
4) 掛接回調
call_rcu ();
call_rcu_bh ();
有關RCU的操做還有不少,你們能夠參考網絡。
7. 信號量
1. 基本概念:信號量用於保護臨界區的經常使用方法與自旋鎖相似,但不一樣的是當獲取不到信號量時,進程不會原地打轉而是進入休眠等待狀態。
2. 具體操做:linux內核中與信號量相關的操做主要有:
1) 定義信號量
Struct semaphore sem;
2) 初始化信號量
void sema_init(struct semaphore *sem, int val);//初始化sem爲val,固然還有系統定義的其餘宏初始化,這裏不列舉
3) 得到信號量
void down(struct semaphore *sem);//得到信號量sem,其會致使睡眠,並不能被信號打斷
int down_interruptible(struct semaphore *sem);//進入睡眠能夠被信號打斷
int down_trylock(struct semaphore *sem);//不會睡眠
4) 釋放信號量
void up(struct semaphore *sem);//釋放信號量,喚醒等待進程
注:當信號量被初始爲0時,其能夠用於同步。
8. Completion用於同步
1. 基本概念:linux中的同步機制。
2. 具體操做:linux內核中與Completion相關的操做主要有:
1) 定義Completion
struct completion *my_completion;
2) 初始化Completion
void init_completion(struct completion *x);
3) 等待Completion
void wait_for_completion(struct completion *);
4) 喚醒Completion
void complete(struct completion *);//喚醒一個
void complete_all(struct completion *);//喚醒該Completion的全部執行單元
9. 讀寫信號量
1. 基本概念:與自旋鎖和讀寫自旋鎖的關係相似
2. 具體操做:linux內核中與讀寫信號量相關的操做主要有:
1) 定義和初始化讀寫自旋鎖
struct rw_semaphore sem;
init_rwsem(&sem);
2) 讀信號量獲取
down_read ();
down_read_trylock();
3) 讀信號量釋放
up_read ();
4) 寫信號量獲取
down_write ();
down_write_trylock ();
5) 寫信號量釋放
up_write();
10. 互斥體
1. 基本概念:用來實現互斥操做
2. 具體操做:linux內核中與互斥體相關的操做主要有:
1) 定義和初始化互斥體
struct mutex lock;
mutex_init(&lock);
2) 獲取互斥體
void mutex_lock(struct mutex *lock);
int mutex_lock_interruptible(struct mutex *lock);
int mutex_lock_killable(struct mutex *lock);
3) 釋放互斥體
void mutex_unlock(struct mutex *lock);
上面咱們介紹了linux內核中爲了解決競態所提供的方法,咱們下面使用信號量爲咱們的虛擬設備增長併發控制。
l 爲咱們的虛擬設備增長併發控制
咱們增長了併發控制後的代碼以下,詳細代碼參考https://github.com/wrjvszq/myblongs
1 struct mem_dev{ 2 struct cdev cdev; 3 int mem[MEM_SIZE];//全局內存4k 4 dev_t devno; 5 struct semaphore sem;//併發控制所使用的信號量 6 }; 7 static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos){ 8 unsigned long p = *ppos; 9 unsigned int count = size; 10 int ret = 0; 11 int *pbase = filp -> private_data; 12 13 if(p >= MEM_SIZE) 14 return 0; 15 if(count > MEM_SIZE - p) 16 count = MEM_SIZE - p; 17 18 if(down_interruptible(&my_dev.sem))//獲取信號量 19 return - ERESTARTSYS; 20 21 if(copy_from_user(pbase + p,buf,count)){ 22 ret = - EFAULT; 23 }else{ 24 *ppos += count; 25 ret = count; 26 } 27 28 up(&my_dev.sem);//釋放信號量 29 30 return ret; 31 } 32 static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos){ 33 int * pbase = filp -> private_data;/*獲取數據地址*/ 34 unsigned long p = *ppos;/*讀的偏移*/ 35 unsigned int count = size;/*讀數據的大小*/ 36 int ret = 0; 37 38 if(p >= MEM_SIZE)/*合法性判斷*/ 39 return 0; 40 if(count > MEM_SIZE - p)/*讀取大小修正*/ 41 count = MEM_SIZE - p; 42 43 if(down_interruptible(&my_dev.sem))//獲取信號量 44 return - ERESTARTSYS; 45 46 if(copy_to_user(buf,pbase + p,size)){ 47 ret = - EFAULT; 48 }else{ 49 *ppos += count; 50 ret = count; 51 } 52 53 up(&my_dev.sem);//釋放信號量 54 55 return ret; 56 }
至此咱們今天的工做完成,快過年了家裏好多事,沒有太多時間,還請你們見諒,提早祝你們新年快樂。
做者:wrjvsz 來源於:http://www.cnblogs.com/wrjvszq/,轉載請註明出處。