並發是指多個 多個執行單元同時執行,而這對對共享的資源,比如硬件的資源、軟件的全局變量、靜態變量 的訪問,很容易導致競態,
在單核的 CPU 裡,避免競態的一個簡單有效的方法是,在進入臨界區之前,就屏蔽系統的中斷。也就是說,在進入臨界區之前,中斷被關閉,使得中斷與進程之間的並發不會發生,而且,因為進程的調度器是依賴於中斷來實現的,沒有了中斷,進程就不能被切換,保證了進程之間的並發不會發生。
方法:
local_irq_disable()
XXXXX 臨界區的代碼
local_irq_enable()
雖然 local_irq_disable() 和 local_irq_enable() 可以關閉本 CPU 的中斷,但是在 多核的 CPU裡面,並不能解決問題,因為 它們只能禁止和使能本 CPU 的中斷,並不能解決多 CPU 引發的競爭,所以在多個的裡面,一般是將關閉中斷的方法與 自旋鎖 結合使用。
如果指向禁止中斷的底部,在應該使用 locak_bh_disable() ,使能的是, local_bh_enable();
原子操作,可以保證對一個整型數據的修改的排他性,也就是說,原子操作是對一個數據進行操作,操作的過程,是不會被線程的調度機制給打斷,也就是說,一旦開始,是絕對不會睡眠和阻塞的,直到操作結束。對於整形數的操作,分別是對整型數和位進行原子的操作。
1.2.1、整型原子的操作
(1)設置原子變量的值
void atomic_set(atomic_t *v,int i); // 設置原子變量的值為 i
atomic_t v = ATOMIC_INIT(0) ; // 定義原子變量 V,並初始化為 0
(2)獲取原子變量的值
atomic_read(atomic_t *v) // 返回值就是原子變量的值
(3)原子的操作
void atomic_add(int i, atomic_t *v) // i + v
void atomic_sub(int i,atomic_t *v ) // v – i
(4)操作並測試
int atomic_inc_an_test(atomic_t *v) // 自加1,測試是否為0,使得話返回 true
int atomic_dec_and_test(atomic_t *v) // 自減 1,測試是否為 0,使得話,返回 true
(5)操作與返回
int atomic_add_reture(int i, atomic_t *v); // V + i,返回新的值
int atomic _sub_return(int i, atomic_t *v) // v – i ,返回新的值
int atomic_inc_return(atomic_t *v) // V + 1
int atomic_dec_return(atomic_t *v) // V – 1
(1)設置為
void set_bit(nr,void * addr) // 設置地址 addr 的 第 nr 位 為 1
(2)清除位
void clear_bit(nr, void *addr) // 設置地址 addr 的 第 nr 位 為 0
(3)改變位
void change_bit(nr, void *addr) // 設置地址 addr 的 第 nr 位 反轉
(4)檢測位
test_bit(nr, void *addr); // 返回 addr 第 nr 位的 值
(5)測試與操作為
int test_and_set_bit(nr, void *addr);
先進行測試,之後在進行位的設置。
原子操作的特性,就對原子數的操作的時候,絕對不會被線程的調度器進行打斷,也就說,這個時間段,是沒進程的上下文之間的切換,進一步說,就是絕對不會出現睡眠和阻塞。
自旋鎖 spin lock,是另外一種對臨界資源進行互斥訪問的手段,它的實現也是借助了 原子操作實現的。自旋鎖的機制是,進程對資源的訪問之前,需要對特性的內存進行讀取,當讀取到值,也就是獲得到鎖的時候,就有權限進入下一步的操作;而其他的進程沒有獲得到鎖的時候,就就如原地的等待,然後再次進行讀取,這個等待、讀取的過程,我們稱之為自旋。
(1)鎖的定義
spinlock_t lock;
(2)鎖的初始化
spin_lock_init(lock) // 完成初始化
(3)獲得鎖
spin_lock(lock)
嘗試去獲得鎖,如果獲得,就馬上返回,沒有獲得的話,就進入自旋的狀態,Linux 也提供非阻塞的方式:
spin_trylock(lock)
獲得鎖的時候,返回 true,沒有獲得的時候,就返回 false
(4)釋放鎖
spin_unlock(lock)
spinlock_t lock;
spin_init_lock(lock);
spin_lock(lock);
XXXX 臨界區代碼
spin_unlock(lock);
在單 CPU 和內核可被搶占的系統中,自旋鎖在獲得鎖的期間,內核的搶占就是被禁止的,因此,這個時候,是可以完全保證獲得鎖器件的臨界區操作代碼,不收到其他進程的打擾;但是在 多核的 CPU 中,其中的一個 CPU 獲得了自旋鎖,但是,只是在該 CPU 上的搶占調度被禁止了,其他核心的CPU 是並沒有被禁止搶占調度的,所以為了保證在多核的情況,臨界區不受到本 CPU 和其他的 CPU 搶占進程的打擾。
因為自旋鎖的底層是借助原子操作實現的,保證了獲得鎖的器件的操作是不會被被本 CPU 和其他 CPU 的進程調度所影響,而且,自旋鎖關閉了搶占系統,但是這些特性並不能保證在得到鎖的的時候,執行臨界區代碼的時候,不受到中斷和底部(BH)的影響,也就是這個時候,本 CPU 的中斷發生的時候,還是會反過來影響臨界區的代碼,所以一般是假如關閉中斷的操作,這個時候就借助了鎖的衍生機制
spin_lock_irq = spin_lock() + local_irq_disable() // 獲得鎖的同時,關閉中斷
spin_inlock_irq = spin_unlock() + local_irq_enable // 釋放鎖的同時,打開中斷
spin_lock_irqsave = spin_lock() + locak_irq_save() // 獲得鎖的同時,關閉中斷,且保存中斷的狀態
spin_lock_irqrestore() = spin_unlock() + locak_irq_restore() // 釋放鎖的同時,將中斷狀態進行回復
spin_lock_bh() = spin_lock() + local_bh_disable() ; // 獲得鎖的同時,關閉底部中斷
spin_unlock_bh() = spin_unlock() + local_bh_enable // 釋放鎖,且恢復底部中斷
當在自旋鎖內部加了中斷的時候,CPU0 獲得了自旋鎖,進入了原子的操而CPU 1 在 CPU0 還沒有釋放鎖的時候,CPU1 就只等原地等待(浪費
了 CPU);而且,中斷的話,就避免受到 本 CPU 的影響,保證了內核並發的可能。
1.3.4、自旋鎖注意事項
(1)因為在獲得了自旋鎖之後,CPU 就開始了原子的操作,其他的CPU 就會原地一直自旋,等待的過程,並不是睡眠,所以浪費了 N 多的資源,所以,一般上,進程擁有鎖的時間都是非常的短的,這樣才是比較的劃算;如果臨界區的代碼非常大的話,其他的 CPU 就一直原地等待,浪費了資源,導致了系統性能的降低。
(2)進程在擁有一個鎖的期間,不能再嘗試去獲取本鎖,否則導致死鎖。
(3)在獲得自旋鎖的器件,絕對不能出現進程調度的情況,也就是,絕對不能出現進程的睡眠和進程的阻塞,不能出現 copy_tousr或者copy_from_usr,kmalloc、msleep等函數,因為自旋鎖實現就是通過原子操作實現的,原子操作的特性,就是一旦開始執行,絕對不會出現進程的調度,否則會導致內核的崩潰。
信號量,是非常常用的一種同步互斥的手段,類似於燈,當等的個數不為零的時候,進程獲取資源的時候,就將燈拿走一個;當釋放資源的時候,就將燈放回去;當燈為零的時候,這個時候,進程將被掛起,進入睡眠的狀態,直到被喚醒。
1.4.1、API
(1)定義信號量
struct semaphore sem;
(2)初始化信號量
void sema_init(struct semaphore * sem, int val)
初始化信號量的值為 val
(3)獲得信號量
int down(struct semaphore * sem)
獲得信號量,和獲得鎖差不多,當沒有得到信號量的時候,就進入睡眠的狀態,也就是說,不能在中斷裡面被使用,因為中斷全部被關閉了,進程的調度器是依賴中斷實現的,沒有了中斷,當睡眠的進程,擁有沒有進程調度器去喚醒進程,導致程序用死停在那裡。內核也提供了可以被打斷的接口
int down_interruptible(struct semaphore *sem)
回去嘗試去獲取信號量,沒有獲得的時候,進入睡眠,但是可以被打斷,,
int dowun_trylock(struct semaphore *sem)
即使在沒有活動信號量的情況下,也不會導致睡眠,而是立即返回,所以可以在中斷的上下文進行使用
(4)釋放信號量
void up(struct semaphore *sem)
當信號量初始化為 1 的時候,實現的就是互斥鎖的功能
互斥鎖的使用方法,和信號量的幾乎一模一樣 ,只是接口名字不一樣而已。
(1)定義互斥鎖
struct mutex mymutex;
(2)初始化互斥鎖
mutex_init( struct mutex *mymutex)
(3)獲得互斥鎖
void mutex_lock( struct mutex mymutex;)
當然也有,void mutex_lock_interruptible( struct mutex mymutex;) 和 void mutex_trylock( struct mutex mymutex;)
(4)釋放互斥鎖
void mutex_unlock(struct mutex mymutex)
(1) 自旋鎖在沒有獲得鎖的時候,就只能是原地的等地,因此開銷就是其他進程獲得鎖執行的時間;而互斥鎖的話,在沒有獲得鎖,就會睡眠當前的進程,鎖帶來的開銷,是進程切換帶來的開銷。
(2)互斥鎖的過程,會帶來進程的睡眠 ,因此,互斥鎖是絕對不能出現在中斷的上下文;自旋鎖則不然,非常適合與中斷一起配合使用;自旋鎖保護的臨界區,絕對不能出現進程的阻塞以及睡眠。