Linux內核分析(七)----並發與競態


Linux內核分析(七)

這兩天家里的事好多,我們今天繼續接着上一次的內容學習,上次我們完善了字符設備控制方法,並深入分析了系統調用的實質,今天我們主要來了解一下並發和競態。

今天我們會分析到以下內容:

1.      並發和競態簡介

2.      競態解決辦法

3.      為我們的虛擬設備增加並發控制

 

在前幾次博文我們已經實現了簡單的字符設備,看似完美但我們忽視了一個很嚴重的問題,即並發問題,那么什么是並發,又如何解決並發呢,我們下面進行分析。

 

l  並發和競態簡介

1.       並發與競態概念

1.        何為並發:並發是指多個執行單元同時、並行被執行。

2.        何為競態:並發的執行單元對共享資源(硬件資源和軟件上的全局變量,靜態變量等)的訪問容易發生競態。

3.        我們虛擬設備的缺陷:對於我們前期的虛擬設備驅動個,假設一個執行單元A對其寫入300個字符‘a’,而另一個執行單元B對其寫入300個字符‘b’,第三個執行單元讀取所有字符。如果AB順序執行那么C讀出的則不會出錯,但如果AB並發執行,那結果則是我們不可料想的。

2.       競態發生的情況

1.        對稱多處理器(SMP)的多個CPUSMP是一種緊耦合、共享存儲的系統模型,它的特點是多個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/,轉載請注明出處。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM