即時搶佔補丁概觀(待續)

來源:互聯網
上載者:User

A realtime preemption overview(2005-08-10/Paul McKenney)
即時搶佔補丁概觀

Yang Honggang<eagle.rtlinux@gmail.com>
ref: http://lwn.net/Articles/146861/
----------------------------------------

////PREEMPT_RT的思想

PREEMPT_RT補丁的核心是最小化(Linux)核心中不可搶佔部分的代碼,同時又將
為支援搶佔性必須要修改的代碼量最小化。

臨界區、中斷處理函數、關中斷等代碼序列通常是進行搶佔改進的。

PREEMPT_RT補丁利用Linux核心的SMP特性來進行可搶佔改進,這樣就避免了對
重寫整個Linux代碼。

從某種程度上講,我們可以簡單地認為搶佔是在系統中添加了一個新的CPU,
然後利用通常的鎖機制來對搶佔任務進行同步。

注意不要對上面的敘述死扣,比如在每次搶佔時PREEMPT_RT並沒有產生
CPU熱插拔事件。關鍵是底層SMP環境必須提供容忍自由搶佔的機制。
下面的章節給出來PREEMPT_RT的思想是怎樣實施的。

////PREEMPT_RT特性

1. 臨界區可搶佔
2. 中斷處理函數可搶佔
3. "關中斷"代碼序列可搶佔
4. 核心中的spinlock和semaphore支援優先順序繼承
5. 延遲操作
6. 降低延遲的措施

下面分別介紹:

/// 1. 臨界區可搶佔

在PREEMPT-RT中通常的spinlock(spinlock_t和rwlock_t)、RCU“讀部分”的臨界區
(rcu_read_lock()和rcu_read_unlock())都是可搶佔的。
Semaphore臨界區也是可以搶佔的(沒有打PREEMPT_RT補丁普通核心也是這樣的)。
該可搶佔性意味著當擷取 spinlock時也可以阻塞,反過來,當關閉中斷或者搶佔時,
不應該去申請spinlock。這也意味著spinlock_t中使用的spin_lock_irqsave()沒有
關閉硬體中斷。

    測試#1: 在普通核心中怎樣支援semaphore臨界區搶佔?

那麼在中斷或者搶佔關閉的條件下需要申請鎖時該如何去做?可以使用raw_spinlock_t
而不是spinlock_t。在raw_spinlock_t中會調用spin_lock()。
PREEMPT_RT中引入了一系列的宏讓spin_lock()表現的像C++中的重載一樣。當在raw_spinlock_t
中調用時,它表現為傳統的spinlock,當在spinlock_t中調用時,它的臨界區又可被搶佔。
例如,多種_irq原語(如spin_lock_irqsave())用在raw_spinlock_t中時,會關閉硬體中斷。
但是,用在spinlock_t中時卻不會關閉硬體中斷。然而,對於raw_spinlock_t(以及相應的rwlock_t
、raw_rwlock_t)的使用不遵循該規則。在調度器、平台相關的代碼和RCU等很少的底層部分才
需要這些raw lock,其他地方用不到。

因為臨界區可以被搶佔,那麼不能指望給定的臨界區僅在一個固定的CPU上執行。由於搶佔的原因,
它可能會遷移到另一個不同的CPU上運行。這樣,當在臨界區中使用per-CPU變數時,就需要
單獨的處理可能發生的搶佔帶來的問題。因為spinlock_t和rwlock_t不會處理這些事情。
可行的方法有:
 1. 通過get_cpu_var()、preempt_disable()或者關閉硬體中斷等顯式地關閉搶佔。
 2. 使用per-CPU鎖保護per-CPU變數。一種方法是使用DEFINE_PER_CPU_LOCKED(),之後會更多
    的介紹。

因為現在spin_lock()可以睡眠,那麼需要增加額外的任務狀態。看下Ingo Molnar提供的
程式碼片段:

    spin_lock(&mylock1);
    current->state = TASK_UNINTERRUPTIBLE;
    spin_lock(&mylock2);                    // [*]
    blah();
    spin_unlock(&mylock2);
    spin_unlock(&mylock1);

由於[*]處的spin_lock可能會睡眠,這樣就會破壞current->state的值。這對blah()來說是不應該
發生的。因此,引入了TASK_RUNNING_MUTEX位來告知調度器在調度前對之前的current->state值進行
保護。雖然這麼做有些怪怪的,但是這麼做確實可以在代碼修改量最小的前提下支援臨界區搶佔。
並且,這麼做使得同樣的代碼可以在PREEMPT_RT, PREEMPT和non-PREEMPT配置下工作。

///可搶佔中斷處理常式

在PREEMPT_RT環境中幾乎所有的中斷處理函數都運行在進程上下文中。儘管任何中斷都可以被標記為
SA_NODELAY使之運行在中斷上下文,目前只有fpu_irq、irq0、irq2和lpptest中斷設定了SA_NODELAY
標記。這些中斷中只有irq0(per-CPU定時器中斷)是常用的,fpu_irq用於浮點副處理器中斷,lpptest
用於中斷延遲評測。注意:軟體時鐘(add_timer()之類的函數)不是運行在中斷上下文,而是運行在
進程上下文中,是完全可以被搶佔的。

不要輕易使用SA_NODELAY,它會使得系統的中斷和調度延遲大大增加。之所以對per-CPU timer
使用SA_NODELAY是因為它和調度和其他核心核心組建聯絡很緊密。另外,必須非常謹慎地處理
標記為SA_NODELAY的中斷處理函數,否則將可能導致oops和死結。後面的章節中會有介紹。

因為per-CPU時鐘中斷(比如scheduler_tick())運行在硬體中斷上下文,任何與進程上下文共用
的鎖必須是raw spinlock(raw_spinlock_t/raw_rwlock_t)。當需要在進程上下中申請spinlock時
必須使用_irq的變種函數。比如,spin_lock_irqsave()。另外,當在進程上下文中訪問與標記為
SA_NODELAY的中斷處理函數共用的per-CPU變數時,通常需要關閉硬體中斷,接下來將詳細介紹。

/// 可搶佔的“關中斷”代碼序列

乍一看可搶佔的“關中斷”代碼本身有點衝突,但是這與PREEMPT_RT的核心思想一點也不衝突。
它的核心思想是基於Linux核心的SMP能力來解決與中斷處理函數之間的競爭。不要忘了,
幾乎所有的中斷處理函數都運行在進程上下文。任何與中斷處理函數互動的代碼都必須時刻準備著
被調度到其他的CPU上運行。

因此,spin_lock_irqsave()等相關的原語不需要關閉搶佔。之所以說這樣是安全的,因為如果
中斷處理函數搶佔了擁有spinlock_t的代碼開始運行,但一旦嘗試擷取spinlock_t就會阻塞。
這樣臨界區仍然是受保護的。

因為沒有可以依賴的鎖,local_irq_save()會禁止搶佔。使用鎖而不是local_irq_save()可以
降低調度延遲,但是會降低SMP的效能,需要謹慎。

必須與標記為SA_NODELAY的中斷互動的代碼應該使用raw_local_irq_save(),而不是local_irq_save()。
因為使用local_irq_save()並不會關閉硬體中斷。類似地當需要和標記為SA_NODELAY的中斷的代碼互動時,
應該使用raw spinlock(raw_spinlock_t、raw_rwlock_t和raw_seqlock_t)。但是,不應該在低層次的
領域(如調度器、體繫結構相關代碼和RCU)之外使用raw spinlock。

//// 核心中的spinlocks和semaphores支援優先順序繼承

設計即時程式的程式員通常很關心優先順序的翻轉問題。在如下的情況下會發生優先順序翻轉:
    * 低優先順序的任務A擷取到一個資源,比如一個鎖(L)。
    * 中優先順序任務B開始執行,搶佔了任務A。
    * 高優先順序任務C嘗試擷取資源L。因為中優先順序的任務B搶佔了任務A,(任務A無法釋放鎖L)那麼
      高優先順序的任務就會阻塞。

優先順序翻轉可能導致一個高優先順序任務被無限期延遲執行。通常有兩種方法來解決該問題:
(1)禁止搶佔
(2)優先順序繼承
由於方法1中沒有搶佔,所以任務B就無法搶佔任務A,這樣就避免了優先順序翻轉的發生。
該方法在PREEMPT核心的spinlock中,而在PREEMPT核心的semaphore中沒有使用。
因為在持有semaphore時阻塞是合法的,這時即便是沒有搶佔也會發生優先順序逆轉,所以
在這種情況下禁止搶佔沒有意義。對於某些即時任務,禁止搶佔會引入顯著的調度延遲,
所以就連spinlock中也不能禁止搶佔。

優先順序繼承用在禁用搶佔不適用的場合。核心思想是:高優先順序任務暫時將其優先順序贈與擁有臨界
資源(鎖)的低優先順序任務。此處優先順序繼承是變化的:比如,又有一個優先順序更高的任務D也嘗試
擷取鎖L,那麼任務C和A的優先順序都會暫時提升為任務D的優先順序。優先順序繼承的期間是非常
短暫的。因為一旦低優先順序任務A釋放了鎖,它馬上就會失去短暫提升的優先順序,然後將鎖交給
任務C。

然而,任務C要想運行或者需要一段時間。因為很有可能另一個更高優先順序的任務E在同一時間嘗試
擷取鎖L。那麼,任務E就會從任務C手中將鎖L“偷去”。這是合法的,因為任務C根本沒有運行,也沒有
正真的擷取到鎖L。另一種情況是,任務C在任務E嘗試擷取鎖L之前已經開始運行,那麼任務E將無法再
“偷去”鎖L。任務E必須等待任務C釋放鎖L,這可能會暫時使得任務C的優先順序提高,從而快速運行加快
對鎖的釋放。

另外,在很多時候任務會長時間持有鎖。如果其他任務需要該鎖,可以通過增加“搶佔點”來使得鎖持有人主動
放棄鎖。JBD(Journal Block Device)層包括大量的這類例子。

PREEMPT_RT將問題簡單化為:在一段時間內僅僅允許一個任務讀持有讀者-寫者鎖/semaphore。允許
該任務遞迴擷取該鎖。儘管喪失一些靈活性,卻使得優先順序繼承變得切實可行。
    
    快速測試#2: 從寫者到多個讀者的情況下,怎樣簡單快速的實現優先順序繼承?

對於semaphore在有些情形下,不需要優先順序繼承,比如:
當semaphore被用作事件機制而不是鎖的時候(在事件發生之前,我們不知道誰會發出該事件,所以
無法提高其優先順序)。在這些情形下,可以使用compat_semaphore和compat_rw_semaphore變種。
多種semaphore原語(up(),down()等)既可以用於compat_semaphore也可以用於semaphore。
類似讀者-寫者semaphore原語(up_read(),down_write()等)既可以用於compat_rw_semaphore也可以
用於rw_semaphore。然而,通常completion機制是解決此類問題的好選擇。

總結一下:優先順序繼承使得高優先順序任務可以及時地擷取鎖和semaphore,即便是鎖或semaphore
已經被低優先順序的任務擷取。PREEMPT_RT的優先順序繼承提供短暫的繼承,這是高優先順序任務突然
要擷取低優先順序任務的鎖所需要的。compat_semaphore和compat_rw_semaphore可以用於不需要semaphore優先順序
繼承的事件類別的使用場合。

/// 延遲操作

因為現在spin_lock()可以睡眠,所以在搶佔/中斷禁止時調用它是不合法的。在某些情況下的解決辦法是,
延遲對spin_lock()的申請直到搶佔又被重新開啟為止:
    * put_task_struct_delayed()將put_task_struct()用隊列管理起來,直到對task_struct 中的
      spinlock_t alloc_lock申請合法為止。
    * 和put_task_struct_delayed()類似, mmdrop_delayed()將mmdrop()用隊列管理起來。
    * 使用TIF_NEED_RESCHED_DELAYED標誌可以進行重新調度,但是調度會延遲到進程準備好返回使用者空間,
      或者下一個preempt_check_resched_delayed()。兩者的關鍵點是避免了無謂的搶佔(將被喚醒的高優先順序任務
      會等待當前任務釋放一個鎖)。如果不指定TIF_NEED_RESCHED_DELAYED標誌,高優先順序的任務將
      立即搶佔低優先順序任務,卻很快又會阻塞在對低優先順序佔有的鎖的申請上。
      
      解決的辦法是:將後面緊跟著spin_unlock()的wake_up()替換為wake_up_process_sync()。
      這樣,如果將要被喚醒的進程會搶佔當前進程,那麼喚醒操作將通過指定TIF_NEED_RESCHED_DELAYED標記被延遲。

對於所有上述的情形,解決方案是:延遲一個動作,直到它可以被更安全和方便地執行為止。

//// 降低延遲的措施

有些PREEMTP_RT的修改的主要原因是降低調度/中斷延遲。
x86 MMX/SSE硬體就是一個列子。該硬體在核心空間搶佔關閉的情況下進行操作。這意味著,
直到MMX/SSE指令運行完畢,搶佔才能開啟。有些MMX/SSE指令沒有問題,但是有些指令的
執行需要很長時間。對此PREEMPT_RT的解決方案是不使用慢的MMX/SSE指令。

另一些修改是:向slab分配器申請per-CPU變數也是對肆意關閉中斷的一種解決方案。

///// PREEMPT_RT基本工具概述

本節對PREEMPT_RT中增加的或者被PREEMT_RT改變很多的核心設施進行簡要介紹。
///鎖
   * spinlock_t
        臨界區可搶佔。_irq 操作(比如,spin_lock_irqsave())不會禁止硬體中斷。優先權繼承被用來
     解決優先順序翻轉。在PREEMPT_RT中spinlock_t是利用rt_mutex來實現的(同樣,rwlock_t, struct semaphore,
     和struct rw_semaphore也是如此)。
   * raw_spinlock_t
     spinlock_t的特殊變種,提供傳統的spin鎖的功能。使用時,臨界區將不可搶佔,並且_irq操作將
     禁止硬體中斷。需要注意的是,通常你應該使用通常的鎖(比如,spin_lock())而不是raw_spinlock_t。
     除了體繫結構相關的代碼或者底層調度和同步設施,不應該使用raw_spinlock_t。
   * rwlock_t
     臨界區可搶佔。_irq操作(比如,write_lock_irqsave())不會關閉硬體中斷。使用優先順序繼承來解決
     優先順序翻轉問題。為了簡化優先順序繼承的實現,一次只能有一個任務可以讀持有一個給定的rwlock_t,
     該任務可以遞迴地讀持有該鎖。
   * RW_LOCK_UNLOCKED(mylock)
     該宏只有一個mylock參數,這是優先順序繼承操作所需要的。不幸的是,這在PREEMPT_RT和非PREEMP_RT
     核心之間是不相容的。因此應該使用DEFINE_RWLOCK(),而不是本宏。
   * raw_rwlock_t
     rwlock_t的特殊變種,提供傳統的行為,臨界區非搶佔_irq操作將真正關閉硬體中斷。類似於
     類似於raw_spinlock_t。除了體繫結構相關的代碼或者底層調度和同步設施,不應該使用raw_rwlock_t.
   * seqlock_t
     臨界區可搶佔。更新端使用了優先順序繼承(讀端不能參與優先順序繼承,因為seqlock_t的讀者不能阻塞寫者)。
   * SEQLOCK_UNLOCKED(name)
     應該使用DECLARE_SEQLOCK()。
   * struct semaphore
     現在轉向支援優先順序繼承。
   * down_trylock()
     可調度,不能在硬體中斷禁止或者搶佔禁止時調用。然而,由於幾乎所有的中斷都運行在進程上下文,並且
     允許搶佔和中斷,所以影響不大。
   * struct compat_semaphore
     struct semaphore的變體,不支援優先權繼承。這對於當你需要一個事件機制而不是一個睡眠鎖時很有用處。
   * struct rw_semaphore
     支援優先權繼承。一次只能有一個任務可以讀持該rw_semaphore,該任務可以遞迴地讀持有該鎖。
   * struct compat_rw_semaphore
     struct rw_semaphore的變體,不支援優先權繼承。這對於當你需要一個事件機制而不是一個睡眠鎖時很有用處。
   測試3:為什麼事件機制不使用優先權繼承?
///per-CPU變數
///中斷處理函數
///其他

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.