1. 程式人生 > 其它 >Linux核心同步和非同步

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_lockspin_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的函式是不允許使用休眠的。