【內核&驅動】併發和競態【1】

1.導致併發執行的原因
    硬件中斷服務
    SMP(對稱多處理)
    內核搶佔
    schedule()

2.scull的缺陷

  1. if (!dptr->data[s_pos]) {
  2.         dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
  3.         if (!dptr->data[s_pos])
  4.             goto out;
  5.     }

    假定有兩個進程,正在獨立嘗試向一個scull設備的相同偏移量寫入數據,
    而且兩個進程在同一時刻達到上述代碼阿中的第一個if判斷語句。如果代碼
    涉及的指針是NULL,則這兩個進程會同時分配空間,而每個進程會同時將結束
    指針賦值給dptr->data[s_pos],然而,只有一個進程會賦值成功。而另一個
    進程分配的空間則永遠也不會返回到系統中。

    以上事件描述的就是競態,競態會導致對共享數據的非控制訪問,產生非預期結果。

3.併發及其管理
    內核代碼是可搶佔的,所以我們的驅動程序代碼會在任何時候丟失對處理器的獨佔
    而擁有處理器的進程可能正在調用我們的驅動程序代碼
    設備中斷是異步事件,也會導致代碼的併發執行。
    內核中提供了許多可以延遲代碼執行的機制。
        workqueue    工作隊列
        tasklet        小任務
        timer        定時器
    這些機制可以使得代碼在任何時刻執行,而不管當前進程在作什麼

    對競態的避免也是一種脅迫性的任務,大部分競態可以通過使用內核的併發控制原語,
    並應用幾個基本原理來控制

    競態通常作爲對資源的共享訪問結果而產生。
    當兩個執行線程需要訪問相同數據結構時,混合的可能性就永遠存在
    因此,應該避免資源的共享。但是這種類型的共享通常是必須的。硬件資源在
    本質上就是共享的資源,軟件資源經常需要對其他執行線程可用。全局變量並不是
    共享數據的唯一途徑,只要我們將代碼的一個指針傳遞給內核的其他部分,一個
    新的共享就可能建立。共享是現實的生活

    資源個共享的硬規則:
        在單個執行線程之外共享硬件或軟件資源的任何時候,因爲另一個線程可能
        產生對該資源不一致的觀察,因此必須顯示的管理對該資源的訪問。

        訪問管理的常見技術稱爲“鎖定”或者“互斥”————確保一次只有一個執行線程
        可操作其共享資源
        當內核代碼創建了一個可能和其他內核部分共享的對象時,該對象必須在
        還有其他組件引用自己時保持存在,也就是說,在對象尚不能工作時,不能
        將其對內核引用
4.信號量和互斥體
    我們的目的是對scull數據結構的操作是原子的,這就意味着在涉及到其他執行線程之前,
    整個操作就已經結束了。爲此,我們需要建立臨界區(在任意給定的時刻,代碼只能被
    一個線程執行)
    並不是所有的臨界區都是一樣的,因此內核爲不同的需求提供了不同的原語
    在之前的scull程序,每個發生在進程的上下文的對scull數據結構的訪問都被認爲是一個
    直接的用戶請求,這就意味着,當scull驅動程序在等待訪問數據結構而進入休眠時,不
    需要考慮其他內核組件

    在這個上下文中“進入休眠”是一個具有明確定義的術語。
    當一個Linux進程達到某個時間點,此時它不能進行任何處理,將進入休眠(或阻塞)狀態
    這將把處理器讓給其他執行線程直到將來它能夠繼續完成自己的處理爲止。
    因此,我們可以使用一種鎖定機制,當進程在等待對臨界區訪問時,此機制可讓進程進入
    休眠狀態

    在可能出現休眠的情況下,並不是所有的鎖機制都可用(還有一些不能休眠的鎖機制)。
    而目前,對於我們來說最適合的機制是信號量(semaphore)

    信號量:
    一個信號量本質上是一個整數值,它和一對函數聯合使用,這一對函數通常成爲P和V。希望
    進入臨界區的進程將在相關信號量上調用P;如果信號量大於零,則該值會減小一,而進程
    可以繼續。相反,如果信號量的值爲零,進程必須等待直到其他人釋放該信號量。對信號量的
    解鎖通過調用V完成;該函數增加信號量的值,並在必要時喚醒等待的進程。

    當信號量用於互斥時,信號量的值應初始化爲2,這種信號量在給定的時候只能由單個進程或者
    線程使用,在這種模式下,一個信號量有時也稱爲一個互斥體(mutex),它是互斥的簡稱。
    Linux內核中幾乎所有的信號量均用於互斥

5.Linux信號量的實現
    要使用信號量,內核代碼必須包括<asm/esmaphore.h>
    相關類型是struct semaphore
    直接創建信號量
      
  1. void sema_init(struct semaphore *sem, int val);
            val是賦予一個信號量的初始值
    內核提供了一組輔助的函數和宏
     
  1. DECLARE_MUTEX(name);
  2. /*一個稱爲name的信號量被初始化爲1*/
  3. DECLARE_MYTEX_LOCKED(name);
  4. /*一個稱爲name的信號量被初始化爲0,互斥體的初始狀態是鎖定的*/
    如果互斥體必須在運行時被初始化:

  1. void init_MUTEX(struct semaphore *sem);
  2. void init_MUTEX_LOCKED(struct semaphore *sem);



    在linux世界裏,P操作稱之爲down,down指的是減小信號量,它也許會將調用者置於休眠狀態
    ,然後等信號量變得可用,之後授予調用者對被保護資源的訪問:
       

  1. void down(struct semaphore *sem);
  2. /*減小信號量的值,並在必要時一直等待*/
  3. int down_interruptible(struct semaphore *sem);
  4. /*減小信號量的值,並在必要時一直等待,但是可被中斷打斷*/
  5. int down_trylock(struct semaphore *sem);
  6. /*永遠不會休眠,當信號量在調用的時候不可獲得,down_trylock會立即返回一個非零值*/


    當一個線程成功調用上述操作時,就稱爲該線程“擁有”(“拿到”,“獲得”)了該信號量。這樣,
    該線程就被賦予訪問該信號量保護的臨界區的權力。當互斥操作完成後,必須返回該信號量。
    linux中等價於V的操作是up:

  1. void up(struct semaphore *sem);
  2. /*調用up後,調用者不再擁有該信號量*/


6.在scull中使用信號量
    信號量機制可以避免在訪問scull_dev結構時產生競態
    正確使用鎖的關鍵是:明確指定需要保護的資源,並確保每一個對這些資源的訪問使用正確的鎖定。

    該結構體定義如下:
      

  1. struct scull_dev {
  2.             strucy scull_qset *data; /* 指向第一個量子集的指針 */
  3.             int quantum; /* 當前量子的大小 */
  4.             int qset; /* 當前數組的大小 */
  5.             unsigned long size; /* 保存在其中的數據的總量 */
  6.             unsigned int access_key; /* 由sculluid和scullpriv使用 */
  7.             struct semaphore sem; /* 互斥信號量 */
  8.             struct cdev cdev; /* 字符設備結構 */
  9.         };


    初始化信號量:
      

  1. for (i = 0; i < scull_nr_devs; i++) {
  2.             scull_devices[i].quantum = scull_quantum;
  3.             scull_devices[i].qset = scull_qset;
  4.             init_MUTEX(&scull_devices[i].sem);
  5.             scull_setup_cdev(&scull_devices[i], i);
  6.         }


        信號量必須在scull設備對系統其他部分可用前被初始化,所以在
        scull_setup_cdev()之前調用了init_MUTEX()
    在scull_write的開始處包含以下代碼:
       

  1. if (down_interruptible(&dev->sem)) {
  2.             return -ERESTARTSYS;
  3.         }


        down_interruptible()返回非0值說明操作被中斷
    不管scull_write能否成功,都需要釋放信號量:
       

  1. out:
  2.             up(&dev->sem);
  3.             return retval;


            
7.讀取者/寫入者信號量
許多任務可以劃分兩種不同的工作類型
    一些任務只需要讀取受保護的數據結構
    其他的則必須做出修改
爲了提高性能,併發執行讀操作是可行的
linux內核爲這種情形提供了一種特殊的信號量類型,稱爲“rwsem”
(或者“reader/writer semaphore, 讀取者/寫入者信號量”)

<linux/rwsem.h>
struct rw_semaphore;
初始化:

  1. void init_rwsem(struct rw_semaphore *sem);
對於只讀訪問,可用的接口如下:
  

  1. void down_read(struct rw_semaphore *sem);
  2. /* 提供了對受保護資源的只讀訪問,可與其他的讀取者併發訪問
  3.  * 可能會將調用進程置於不可中斷的休眠 */
  4. int down_read_trylock(struct rw_semaphore *sem);
  5. /* 不會在讀取訪問不可獲得時等待,他在授予訪問時返回非零,其他情況下返回零 */
  6. void up_read(struct rw_semaphore *sem);

針對寫入者的接口類似於讀取者接口
 

  1. void down_write(struct rw_semaphore *sem);
  2. void down_read_trylock(struct rw_semaphore *sem);
  3. void up_write(struct rw_semaphore *sem);
  4. void downgrade_write(struct rw_semaphore *sem);
  5. /* 當某個快速改變獲得了一個寫者鎖,而其後是更長時間的只讀訪問的話,
  6.  * 我們可以在結束脩改之後調用downgrade_write來允許其他讀取者訪問*/



     一個rwsem可允許一個寫入者和無限的讀取者擁有該信號量, 寫入者具有更高的優先級,
     當某個給定的寫入者試圖進入臨界區時,在所有寫入者完成其工作之前,不會允許讀取者
     獲得訪問。如果有大量的寫入者競爭該信號量,則這種實現會導致讀取者“餓死”,即可能會
     長期拒絕讀取者的訪問。爲此,最好在很少需要寫訪問且寫入者只會短期擁有信號量時使用rwsem
8.completion
內核中常見一種模式,在當前線程之外初始化某個活動,然後等待活動的結束。在這種情況下,我們可以
使用該信號量來同步這兩個任務:
  

  1. struct semaphore sem;
  2. init_MUTEX_LOCKED(&sem);
  3. start_external_task(&sem);
  4. down(&sem);

當外部任務完成其工作的時候調用:
   

  1. up(&sem);
completion(完成)接口,是一種輕量級的機制,它允許一個線程告訴另外一個線程某個工作已經完成。
<linux/completion.h>
創建接口:
 

  1. DECLARE_COMPLETION(my_completion);
動態的創建和初始化copletion:

  1. struct completion my_completion;
  2.     ...
  3. init_completion(&my_completion);


要等待completion,可進行如下調用
  

  1. void wait_for_completion(struct completion *c);
  2. /* 該函數執行一個不可殺的進程,如果調用了wait_for_completion且
  3.  * 沒有人會完成該任務,則將產生一個不可殺的進程*/
出發completion事件:
   

  1. void complete(struct completion *c);
  2. void complete_all(struct completion *c);


一個completiion通常是一個單次設備,如果沒有使用complete_all,則我們可以重複使用一個completion結構
但是如果使用了complete_all,則在重複使用的時候重新初始化它:

  1. INIT_COMPLETION(struct completion c)

實例程序:
    任何試圖從該設備讀取的進程都將等待,直到其他進程寫入該設備爲止
   

  1. DECLARE_COMPLETION(comp);
  2. ssize_t complete_read(struct file * filp, char __user *buf,
  3.                        size_t count, loff_t *pos)
  4. {
  5.     printk(KERN_DEBUG "process %i (%s) going to sleep\n",
  6.            current->pid, current->comm);
  7.     wait_for_completion(&comp);
  8.     printk(KERN_DEBUG "awoken %i (%s)\n", current->pid, current->comm);
  9.     return 0;
  10. }
  11. ssize_t complete_write(struct file * filp, const char __user *buf,     
  12.             size_t count, loff_t *pos)
  13. {
  14.     printk(KERN_DEBUG "process %i (%s) awakening the readers...\n",
  15.             current->pid, current->comm);
  16.     complete(&comp);
  17.     return count;
  18. }


completion機制的典型使用是模塊退出時的內核線程終止,在這種原型中,某些驅動程序的內部工作由一個內核線程在
while(1)循環中完成,當內核準備清除該模塊時,exit函數會告訴該線程退出並等待completion。爲了實現這個目的,內核
包含了可用於這種線程的一個特殊函數:

  1. void complete_and_exit(struct completion *c, long retval);


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章