Linux核心同步和非同步
介紹
kernel有很多的同步和非同步機制,做簡單整理,力求能夠熟練使用。
1.同步機制
併發
:多個執行單元同時被執行競態
:併發的執行單元對共享資源(硬體資源和軟體上的全域性變數等)的訪問導致競爭狀態。
併發與競態。
假設有2個程序試圖同時向一個裝置的相同位置寫入資料,就會造成資料混亂。處理併發常用的技術:加鎖
或者互斥
,即確保在任何時間只有一個執行單元可以操作共享資源。在Linux核心中主要通過semaphore
機制和spin_lock
機制實現。
1.1 訊號量
Linux核心訊號量在概念和原理上與使用者訊號量一樣的,但是它不能在核心之外使用,它是一種睡眠鎖
.
如果有一個任務想要獲得已經被佔用的訊號量時,訊號量會將這個程序放入一個等待佇列
- 訊號量在建立時需要
設定一個初始值
,表示允許幾個任務同時訪問該訊號量保護的共享資源。初始值為1就變成互斥鎖(Mutex),即同時只能有一個任務可以訪問訊號量保護的共享資源。 - 當任務訪問完被訊號量保護的共享資源後,必須釋放訊號量。釋放訊號量通常把訊號量的值加1實現,如果釋放後訊號量的值為非正數,表明有任務任務等待當前訊號量,因此要喚醒訊號量的任務。
訊號量的實現也是與體系結構相關的,定義在<asm/semaphore.h>
中,struct semaphore型別用類表示訊號量。
1.定義訊號量
struct semaphore sem;
2.初始化訊號量
void sema_init(struct semaphore*sem,int val)
該函式用於初始化訊號量設定訊號量的初值,它設定訊號量sem的值為val;
互斥鎖
void init_MUTEX(struct semaphore*sem);
該函式用於初始化一個互斥鎖,即它把訊號量sem的值設定為1.
void init_MUTEX_LOCKED(struct semaphore*sem);
該函式也用於初始化一個互斥鎖,但它把訊號量sem的值設定為0,即一開始就處於已鎖狀態。
定義與初始化工作可由如下巨集完成:
DECLARE_MUTEX(name)
定義一個訊號量name,並初始話它的值為1.DECLARE_MUTEXT_LOCKED(name)
定義一個訊號量name,但把它的初始值設定為0,即建立時就處於已鎖的狀態。
3.獲取訊號量
void down(struct semaphore*sem);
獲取訊號量sem,可能會導致程序睡眠,因此不能在中斷上下文使用該函式。 該函式將把sem的值減1:
- 如果訊號量的sem值為非負,就直接返回.
- 否則呼叫者將被掛起。直到別的任務釋放該訊號量才能繼續執行
int down_interruptible(struct semaphore*sem);
獲取訊號量sem.如果訊號量不可用,程序將被置為TASK_INTERRUPTIBLE
型別的睡眠狀態。該函式返回值來區分正常返回還是被訊號中斷返回:
- 如果返回0,表示獲得訊號量正常返回
- 如果被訊號打斷,返回
-EINTR
.
int dow_killable(struct semaphore*sem);
獲取訊號量sem,如果訊號量不可用,程序將被設定為TASK_KILLABLE
型別的睡眠狀態. 注:down()
函式已經不建議繼續使用。建議使用down_killable()
或down_interruptible()
函式。
4.釋放訊號量
void up(struct semaphore*sem);
該函式釋放訊號量sem,即把sem的值加1,如果sem的值為非正數,表明有任務等待該訊號量,因此喚醒這些等待者。
1.2 自旋鎖
自旋鎖最多隻能被一個可執行單元持有。自旋鎖不會引起呼叫者睡眠,如果一個執行難執行緒試圖獲得一個已經持有的自旋鎖,那麼執行緒就會一直進行忙迴圈,一直等待下去在那裡看是否該自旋鎖的保持者已經釋放了鎖,“自旋”就是這個意思。
1.初始化
spin_lock_init(x);
該巨集用於初始化自旋鎖x,自旋鎖在使用前必須先初始化。
2.獲取鎖
spin_lock(x)
獲取自旋鎖lock,如果成功,立即獲得鎖,並馬上返回,否則它將一直自旋在那裡,直到該自旋鎖的保持者釋放。
spin_trylock(x)
試圖獲取自旋鎖lock,如果能立即獲得鎖,並返回真,否則立即返回假。它不會一直等待釋放.
3.釋放鎖
spin_unlock(x)
釋放自旋鎖lock,它與spin_lock
或spin_trylock
配對。鎖用完要進行釋放
1.3 訊號量與自旋鎖比較
- 訊號量可能允許有多個持有者,而自旋鎖任何時候只能允許一個持有者.當然也有訊號量叫互斥訊號量(只能一個持有者),允許有多個持有者的訊號量叫計數訊號量
- 訊號量適合保持較長時間,而自旋鎖適合於保持時間非常短的情況,在實際應用中自旋鎖控制程式碼只有幾行,而持有自旋鎖的時間也不會超過兩次上下文切換的時間,因此執行緒一旦要進行切換,就至少花費出人兩次,自旋鎖的佔用時間如果遠遠長於兩次上下文切換,我們就應該選擇訊號量。
2.非同步
主要使用佇列來形成一種類似"緩衝",從而產生非同步。這個緩衝可以是資料,可以是"函式"。
2.1 等待佇列wait_queue
可以使用等待佇列來實現程序阻塞,在阻塞程序時,將程序放入等待佇列,當喚醒程序時,從等待佇列中取出程序。
1.定義和初始化
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
可以使用巨集來完成,定義和初始化過程
DECLARE_WAIT_QUEUE_HEAD(my_queue);
2.睡眠
a.有條件睡眠
wait_event(queue,condition)
:當condition為真時,立即返回;否則讓程序進入TASK_UNINTERRUPTIBLE
模式的睡眠,並掛在queue引數所指定的等待佇列上。wait_event_interruptible(queue,conditon)
:當condition為真時,立即返回;否則讓程序TASK_INTERRUPTIBLE
的睡眠,並掛起queue引數所指定的等待佇列int wait_event_killable(wait_queue_t queue,condition)
:當condition為真時,立即返回;否則讓程序進入TASK_KILLABLE
的睡眠,並掛在queue引數所指定的等待佇列上。
b.無條件睡眠(老版本,建議不再使用)
sleep_on(wait_queue_head_t *q)
:讓程序進入不可中斷的睡眠,並把它放入等待佇列q.interruptible_sleep_on(wait_queue_head_t *q)
:讓程序進入可中斷的睡眠,並把它放入等待佇列q。
3.喚醒
wake_up(wait_queue_t *q)
:從等待佇列q中喚醒狀態為TASK_UNINTERRUPTIBLE
,TASK_INTERRUPTIBLE
,TASK_KILLABLE
的所有程序。wake_up_interruptible(wait_queue_t*q)
:從等待佇列q中喚醒狀態為TASK_INTERRUPTIBLE
的程序。
2.2completion
核心程式設計中常見的一種模式是,在當前執行緒之外初始化某個活動,然後等待該活動的結束。核心中提供了另外一種機制——completion介面。Completion是一種輕量級的機制,他允許一個執行緒告訴另一個執行緒某個工作已經完成。實現基於等待佇列。
1.結構與初始化
struct completion {
unsigned int done; /*用於同步的原子量*/
wait_queue_head_t wait;/*等待事件佇列*/
} x;
函式
void init_completion(x);
巨集實現
DECLARE_COMPLETION(work)
2.等待
wait_for_completion(work);
3.完成
completion(work);
2.3 工作佇列work_struct
工作佇列一般用來做滯後的工作,比如在中斷裡面要做很多事,但是比較耗時,這時就可以把耗時的工作放到工作佇列。說白了就是系統延時排程
的一個自定義函式。
工作佇列的使用分兩種情況:
利用系統
共享的工作佇列來新增自己的工作,這種情況處理函式不能消耗太多時間
,這樣會影響共享佇列中其他任務的處理;- 另外一種是建立自己的工作佇列並新增工作。
2.3.1 使用系統
1.宣告工作函式
void my_func();
2.建立一個工作結構體變數,並將處理函式和引數的入口地址賦給這個工作結構體變數
//編譯時建立名為my_work的結構體變數
//並把 函式入口地址 和 引數地址 賦給它;
DECLARE_WORK(my_work,my_func,&data);
如果不想要在編譯時就用DECLARE_WORK()
建立並初始化工作結構體變數,也可以在程式執行時再用INIT_WORK()
建立:
//建立一個名為my_work的結構體變數,建立後才能使用INIT_WORK()
struct work_struct my_work;
//初始化已經建立的my_work,其實就是往這個結構體變數中新增處理函式的入口地址和data的地址
// 通常在驅動的open函式中完成
INIT_WORK(&my_work,my_func,&data);
3.將工作結構體變數新增入系統的共享工作佇列
schedule_work(&my_work); //新增入佇列的工作完成後會自動從佇列中刪除
2.3.2 建立自己的工作佇列來新增工作
1.宣告工作處理函式和一個指向工作佇列的指標
void my_func();
struct workqueue_struct *p_queue;
2.建立自己的工作佇列和工作結構體變數(通常在
open
函式中完成)
//建立一個名為my_queue的工作佇列並把工作佇列的入口地址賦給宣告的指標
p_queue=create_workqueue("my_queue");
//建立一個工作結構體變數並初始化,和第一種情況的方法一樣
struct work_struct my_work;
INIT_WORK(&my_work, my_func, &data);
3.將工作新增入自己建立的工作佇列等待執行
//作用與schedule_work()類似
//不同的是將工作新增入p_queue指標指向的工作佇列而不是系統共享的工作佇列
queue_work(p_queue, &my_work);
4.第四步:刪除自己的工作佇列
destroy_workqueue(p_queue); //一般是在close函式中刪除
2.4 tasklet
小程序,主要用於執行一些小任務,對這些任務使用全功能程序比較浪費。也稱為中斷下半部,在處理軟中斷時執行。
1.初始化和結構體
struct tasklet_struct{
struct tasklet_struct* next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
函式:tasklet_init(t,func,data)
- struct tasklet_struct 結構指標
- func:小任務函式
- data:傳遞給工作函式的實際引數
快捷巨集:DECLARE_TASKLET(name,func,data);
2.執行:
tasklet_schedule(t)
tasklet is scheduled for executation
兩種狀態:
- TASKLET_STATE_SCHED
- TASKLET_STATE_RUN //tasklet is running(SMP only)
3.銷燬:
tasklet_kill(struct tasklet_struct *t);
2.5 工作佇列和tasklet區別
Linux2.6核心使用了不少工作佇列來處理任務,他在使用上和tasklet最大的不同是工作佇列的函式可以使用休眠,而tasklet的函式是不允許使用休眠的。