1.概念:併發:多個執行單元同時發生;注意:執行單元包括硬件中斷、軟件中斷、多進程(進程的搶佔通過中斷實現),
竟態:併發的多個執行單元同時訪問共享資源,引起的競爭狀態
形成竟態條件:1一定要有併發情況2一定要有共享資源 硬件資源(小到寄存器的而某個bit位)軟件上的全局變量,例如open_cnt3併發的多個執行單元要同時訪問共享資源
互斥訪問:當多個執行單元對共享資源進行訪問時,只能允許一個執行單元對共享資源進行訪問,其他執行單元被禁止訪問!
互斥:指多個進程不能同時使用同一個資源;
死鎖:指多個進程互不相讓,都得不到足夠的資源;
飢餓:指一個進程一直得不到資源(其他進程可能輪流佔用資源)
臨界資源:系統中某些資源一次只允許一個進程使用,稱這樣的資源爲臨界資源或互斥資源或共享變量
臨界區:訪問共享資源的代碼區域,所以互斥訪問就是對臨界區的互斥訪問!進程的同步(直接制約):synchronism
指系統中一些進程需要相互合作,共同完成一項任務。具體說,一個進程運行到某一點時要求另一夥伴進程爲它提供消息,在未獲得消息之前,該進程處於等待狀態,獲得消息後被喚醒進入就緒態。同步是指在互斥的基礎上(大多數情況),通過其它機制實現訪問者對資源的有序訪問。在大多數情況下,同步已經實現了互斥,特別是所有寫入資源的情況必定是互斥的。少數情況是指可以允許多個訪問者同時訪問資源。
進程的互斥(間接制約)mutual exclusion
由於各進程要求共享資源,而有些資源需要互斥使用,因此各進程間競爭使用這些資源,進程的這種關係爲進程的互斥。某一資源同時只允許一個訪問者對其進行訪問,具有唯一性和排它性。但互斥無法限制訪問者對資源的訪問順序,即訪問是無序的。
例如:
cpu讀取數據到寄存器,修改數據,寫回數據
static int open_cnt=1;
int uart_open(struct inode*inode,struct file *file){
unsigned long flags;
local_irq_save(flags); //屏蔽中斷
if(--open_cnt!=0)
{
printk("已經被打開!\n");
open_cnt++;
//local_irq_retore(flags);//使能中斷
rerturn -EBUSY;
}
local_irq_retore(flags);//使能中斷
printk("打開成功\n");
return 0;
}
由於linux內核支持進程之間的搶佔,當A在執行以上臨界區時,一定要進行互斥訪問,不讓B進程發生搶佔情況,保證A的執行路徑不被打斷!
總結:互斥訪問本質上其實就是讓臨界區的而執行路徑具有原子性(不可再發-不能被打斷)。
問題:如何做到一個執行單元在訪問臨界區。其他執行單元不被打斷正在訪問臨界區的執行單元的路徑呢?強調:執行單元:中斷和進程。
2.linux內核產生竟態的情形
第一種是多核(多cpu),多個cpu他們共享系統總線,共享內存,外存,系統io單隻竟態;
第二種是單cpu的進程之間的搶佔(必須具備搶佔,搶佔的原因是進程能夠指定優先級),
由於linux內核支持進程的搶佔,多個進程訪問共享資源,並且有搶佔,也會產生竟態;
第三種情形是中斷(硬件中斷,軟中斷)和進程也會形成竟態(中斷的優先級高於進程)
第四種是中斷和中斷.硬件中斷優先級高於軟中斷,軟中斷又分優先級。
3.linux內核解決竟態的方法:
這些方法的本質目的就是讓臨界區的訪問具有原子性。
1.中斷屏蔽 2.原子操作 3.自旋鎖 4.信號量
1.中斷操作:屏蔽的硬件中斷和軟中斷
能夠解決竟態情形:1進程與進程之間的搶佔(由於linux進程的調度,搶佔都是基於軟中斷實現)2中斷和進程 3中斷和中斷 中斷髮生時會向多個cpu發送中斷信號.
linux內核提供相關的中斷屏蔽的方法:#include <asm/system.h>
1.屏蔽中斷
unsigned long flsgs;
local_irq_disable();//屏蔽中斷
local_irq_save(floags);//屏蔽中斷並且保存中斷狀態到flags中
2.使能中斷
unsigned long flags;
local_irq_enable();//使能中斷
local_irq_restore(flags); //使能中斷,並且從flags中恢復屏蔽中斷前保存的中斷狀態
linux內核中斷屏蔽的使用方法:
1.臨界區之前屏蔽中斷
2.執行臨界區,中斷不能被打斷,進程的搶佔也不能發生
3.臨界區之後恢復中斷
2.原子操作:
筆試題:請實現將一個數的某個bit置1或者清0;
第一個:
int data =0x1234;data |=(1<<5);data &=~(1<<5);
第二個
void set_bit(int nbit,int *data){
.....
}
第三個:
void set_bit(int nbit,void *data){
....
}
第四個:
#define SET_BIT(nr,data)......
基於linux系統的參考答案(GNU C)--析取c++的優秀代碼:
inline void set_bit(int nr,void *data){
.....
}
linux內核原子操作:
原子操作能夠解決所有竟態問題:
原子操作分爲:位原子操作 整形原子操作
1位原子操作:如果以後驅動中對共享資源進行位操作,並且爲了避免竟態問題,一定要使用內核提供的位原子操作的方法,保證位操作的方法,保證位操作的過程是原子的,不能自己去實現位操作,例如:
static int data; //全局變量,共享資源
//臨界區
data |=(1<<5); //這個代碼不是原子的,有可能被別的任務打斷!
如果不考慮多核引起的竟態,還有一種通過中斷屏蔽(屏蔽只是單核)來解決以上代碼的竟態問題:
insigned long flags;
local_irq_save(flags);
data |=(1<<5);
local_irq_restore(flags);
以上代碼無法表面多核引起的竟態!
內核提供的位原子操作的方法:
#include <asm/bitops.h>
set_bit/clear_bit/change_bit/test_bit組合函數
Tset_and_set_bit/test_and_clear_bit/test_and_change_bit
對於data進行位操作的正確方法
static int data; //將data數據的第5位設置爲1
set_bit(5,&data); //這個代碼時原子的,不能被別的任務打斷
注意:以上函數在多核cpu情況下,會使用兩條arm的原子指令:ldrex,strex,這個兩條保證在cpu那一級別能夠避免竟態,以上函數都是採用c的內嵌彙編來實現,如果用c語言來實現,編譯器肯定用ldr,str,但這個兩條指令不能避免竟態!
案例:利用位原子操作將0x5555->0xaaaa,不允許使用change_bit函數!取反操作
2整形原子操作:
如果以後驅動程序中,涉及的共享資源是整型數,就是原型要定義爲char short int
long型的數據,並且他們是共享資源,爲了避免竟態,可以考慮使用內核提供的整型原子
操作機制來避免竟態問題。說白了就是將原先的char short int long型換成atomic_t數據類型即可,然後配合內核提供的整型原子操作的函數對整型變量進行數學運算。
整形原子變量數據類型定義#include <asm/atomic.h>定義在arch/arm/include/asm/atomic.h
typedef struct{
volatile int counter;
}atomic_t;
如何使用:分配整型原子變量atomic_t v;
進行對整型變量的操作:
atomic_set/atomic_read/atomic_add/atomi_sub/atomic_inc/atomic_dec、/atomic_inc_and_test。。
對整型變量的操作一定要使用以上的函數進行,保證具有原子性。不能使用如下代碼:
static int data; //全局變量,共享資源
//臨界區
data++;//不是原子的!有可能被打斷
解決的方法:如果不考慮多核:
unsigned long flags;
local_irq_save(flags);
data++;
loacl_irq_restore(flags);
如果考慮多核:atomic data;atomic_inc(&data);
注意:以上整型原子操作的函數,如果在多核情況下,他們的實現都是c的內嵌彙編
來實現的,都調用了ldrex,strex來避免竟態.
案例;實現led燈驅動,要求這個設備只能被一個應用程序打開
分析:明確:app會調用open開發設備,close關閉設備
驅動:一定要提供對應的底層open,close的實現,注意不能省略
底層的這兩個函數,因爲需要在底層的open,close函數中做一些用戶需求的代碼(設備只能被一個應用程序打開)
方案:static int open_cnt; //可以採用中斷
方案;static atomic_t open_cnt; //利用整型原子操作 top查看進程狀態
實驗步驟:
1.insmod led_drv.ko
2.cat /proc/devices //查看申請的主設備號
3.cat /sys/class/myleds/myled/uevent //查看創建設備文件的原材料
4.ls /dev/myled //查看設備文件
5../led_test &、、啓動a進程,讓其後臺運行,a進程進入休眠
6.ps //查看a進程的pid
7.top //查看a進程的狀態和cpu的利用率,內存使用率
8../led_test //啓動b進程
9.kill a進程的pid //殺死a進程
*******************************************************************************
自旋鎖:等於“自旋” + “鎖”
自旋鎖特點:
1.自旋鎖一般要附加在共享資源上;類似光有自行車鎖沒有自行車是沒有意義的!
2.自旋鎖的“自旋”不是鎖在自旋,意思是想獲取的自旋鎖的執行的單元,在沒有獲取自旋鎖的情況下,原地打轉,忙着等待獲取自旋鎖;
3.一旦一個執行單元獲取了自旋鎖,在執行臨界區時,不要進行休眠操作。 “不夠意思”
4.自旋鎖也是讓臨界區的訪問具有原子性!
linux內核如何描述一個自旋鎖#include <linux/spinlock.h>數據類型:spinlock_t
Typedef struct {
Raw_spinlock_t lock;
}spinlock_t;
Typedef struct {
Volatile unsigned int lock;
}raw_spinlock_t;
如何使用自旋鎖來對臨界區進行互斥訪問:
static int open_cnt; //全局變量,共享資源
1.分配自旋鎖spinlock_t lock
2.初始化自旋鎖spin_lock_init(&lock);
3.訪問臨界區之前獲取自旋鎖,進行鎖定spin_lock(&lock); //如何執行單元獲取自旋鎖,函數立即返回,如果執行單元沒有獲取鎖,執行單元不會返回,而是原地打轉!處於忙等待,直到持有自旋鎖的執行單元釋放自旋鎖或者:
spin_trylock(&lock); //如果執行單元獲取自旋鎖,函數返回true,如果沒有獲取諮詢所,返回false,不會原地打轉!
4.執行臨界區的代碼
if(--opencnt!=0){
.......
}
這個過程其他cpu或者本cpu的搶佔進程就無法執行臨界區,但是還會被中斷鎖打斷!如果考慮中斷的因素,要使用衍生自旋鎖。
5.釋放自旋鎖
spin_unlock(&lock); //獲取鎖的執行單元釋放鎖,然後等待獲取鎖的執行單元停止原地打轉而是獲取自旋鎖,然後開始對臨界資源的訪問。
注意;以上自旋鎖的操作只能解決多cpu和本cpu的進程搶佔引起的竟態,但是無法處理中斷(中斷和中斷底半部)引起的競態,如果考慮到中斷,必須採用衍生自旋鎖!
衍生自旋鎖本質上其實就是在普通的自旋鎖的基礎上進行屏蔽中斷和使能中斷的動作。
衍生自旋鎖的使用:static int open_cnt; //全局變量,共享資源
1.分配自旋鎖spinlock_t lock
2.初始化自旋鎖spin_lock_init(&lock);
3.訪問臨界區之前獲取自旋鎖,進行鎖定
spin_lock_irq(&lock); //屏蔽中斷,獲取自旋鎖
=local_irq_disable() + spin_lock()
或者spin_lock_irqsave(&lock,flags); //屏蔽中斷,保存中斷狀態,獲取自旋鎖
= lock_irq_save() + spin_lock();
4.執行臨界區的代碼
if(--opencnt!=0){
.......
}
5.釋放自旋鎖spin_unlock_irq(*lock); //釋放自旋鎖,使能中斷
Local_irq_disable() + spin_lock();
或者spin_unlock_irqrestore(&lock,flags); //釋放自旋鎖,使能中斷,保存中斷狀態= spin_unlock() + local_irq_restore();
注意:衍生自旋鎖能夠解決所有的竟態問題。
自旋鎖使用的注意事項:
1. 一旦獲取自旋鎖,臨界區的執行速度要快,不能做休眠動作。由於獲取鎖是一直等待,所以臨界區較大或有共享設備的時候,使用自旋鎖會降低系統性能.
2. 自旋鎖可能導致死鎖:--遞歸調用,也就是已經擁有自旋鎖的CPU想要第二次獲取鎖
--獲取自旋鎖之後再被阻塞,所以,再自旋鎖佔有期間,不能調用可能引起阻塞的函數:如kammoc(),copy_from_uesr()等.
案例:利用自旋鎖,來實現一個設備只能被一個應用程序打開
******************************************************************
linux系統進程的狀態:三個狀態
1.進程的運行狀態,linux系統描述運行中的進程通過TASK_RUNNING宏來表示!
2.進程的準備就緒狀態,linux系統描述準備就緒用TASK_READY來表示.
3.進程的休眠狀態,進程的休眠狀態又分可中斷的休眠狀態和不可休眠的。
3.1不可中斷休眠狀態,linux系統用TASK_UNINTERRUPTIBLE來表示,睡眠期間如果
接受到了信號,不會立即處理信號,但是喚醒以後會判斷是否接受到信號,有,處理信號。
3.2可中斷的休眠狀態,linux系統用TASK_INTERRUPTIBLE,在休眠期間,如果接受到了信號,會被信號喚醒,並且立即處理信號。
信號量--不可以在中斷上下文使用(因爲會導致休眠)
由於自旋鎖在使用的時候,要求臨界區不能做休眠操作,但是在某些場合需要在臨界區做休眠操作,又要考慮竟態問題,此時可以使用信號量來保護臨界區。
信號量的特定:1.又叫”睡眠鎖“,內核的信號量在概念和原理上與用戶態的信號量是一樣的,但是它不能再內核之外使用;
2.如果一個執行但願你想要獲取信號量,如果信號量已經被別的執行單元所持有,那麼這個執行單元將會進入休眠狀態;直到持有信號量的執行單元釋放信號量爲止。
3.已經獲取信號量的執行單元在執行臨界區的代碼時,也可以進行休眠操作!
4.明確信號量能讓進程放入一個等待隊列中,然後讓其進行休眠操作,
linux內核描述信號量的數據類型:#include <linux/semaphore.h>
struct semaphore{
spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
一般使用方法:DECLARE_MUTEX(semm);//定義一個互斥信號量
..
Down(&semm);//獲取信號量,保護臨界區
Critical section code; //執行臨界區代碼
Up(&semm); //釋放信號量
如何使用信號量:1.分配信號量struct semaphore sema;
2.初始化信號量爲互斥信號量init_MUTEX(&sema,1);
3.在訪問臨界區之前獲取信號量,對臨界區進行鎖定
dowm(&sema); //獲取信號量 ,如果信號量已經被別的任務給持有,d爲不可中斷的信號狀態那麼,進程將進入不可中斷的休眠狀態;不能使用在中斷上下文。
或者:dowm_interruptible(&sema); //獲取信號量,如果信號量已經被別的任務給持有,那麼進程將進入可中斷的休眠狀態;一般在使用的時候一定要對這個函數的返回值進行判斷,如果函數返回0,表明進程正常獲取信號量,然後訪問臨界區;如果函數返回非0,表明進程是由於接受到了信號引起的喚醒:
if(dowm_interruptible(&sema)){
printk("進程被喚醒的原因是接受到了信號\n");
return -EINTR;
}
else {
printk("正常獲取信號量引起的喚醒");
printk("進程可以訪問臨界區");
} 或者:
dowm_trylock(&sema); //獲取信號量,如果沒有獲取信號量,返回false,
如果獲取信號量,返回true。所以對返回值也要做判斷
if(dowm_trylock(&sema)){
printk("無法獲取信號量");
return -EBUSY;
}else {
printk("獲取信號量");
printk("訪問臨界區");
}
4.訪問臨界區
5.釋放信號量up(&sema); //一方面釋放信號量,另一方面還要喚醒之前休眠的進程
案例;採用信號量,實現一個設備只能被一個應用程序打開; 一般設置爲可中斷的狀態
對於這個案例
gpc0_3
gpio_config{
//gpio共享資源 互斥訪問
unsigned long flags;
local_irq_save(flags);
gpio_set_vluae(0);
udelay(5);
gpio_set_vluae(1);
udelay(10);
gpio_set_vluae(0);
udelay(15);
........
local_irq_restore(flags);
}