Linux內核同步

Linux內核剖析 之 內核同步

主要內容

    一、內核請求什麼時候以交錯(interleave)的方式執行以及交錯程度如何。php

    二、內核所實現的基本同步機制。css

    三、一般狀況下如何使用內核提供的同步機制。html


內核如何爲不一樣的請求服務

    哪些服務?node

    ====>>>linux

    爲了更好地理解內核是如何執行的,咱們把內核看作必須知足兩種請求的侍者:一種請求來自顧客,另外一種請求來自數量有限的幾個不一樣的老闆。對於不一樣的請求,侍者採用以下的策略:
nginx

    一、老闆提出請求時,若是侍者空閒,則侍者開始爲老闆服務。算法

    二、若是老闆提出請求時侍者正在爲顧客服務,那麼侍者中止爲顧客服務,開始爲老闆提供服務。sql

    三、若是一個老闆提出請求時侍者正在爲另外一個老闆服務,那麼侍者中止爲第一個老闆提供服務,而開始爲第二個老闆服務,服務完畢後再繼續爲第二個老闆服務。編程

    四、一個老闆可能命令侍者中止正在爲顧客提供的服務。侍者在完成對老闆最近請求的服務以後,可能暫時不理會原來的顧客而去爲新選中的顧客服務。windows

    這裏,將其對應到內核中的功能:

    侍者提供的服務 <<<————>>> CPU處於內核態時所執行的代碼和程序。若是CPU在用戶態執行,則侍者被認爲處於空閒狀態。

    老闆的請求 <<<————>>> 中斷。

    顧客的請求 <<<————>>> 用戶態進程發出的系統調用或異常。

    ====>>>

    一、二、3對應於中斷和異常處理程序的嵌套執行,4對應於內核搶佔(kernel preemption)。


內核搶佔與非內核搶佔

    簡單定義,若是進程正執行內核函數時,即它在內核態運行時,容許發生內核切換(被替換的進程是正執行內核函數的進程),那麼這個內核就是搶佔的。

    * 不管在搶佔內核仍是非搶佔內核中,運行在內核態的進程均可以自動放棄CPU,例如,其可能的緣由是,進程因爲等待資源而不得不轉入睡眠狀態。咱們把這種進程切換稱爲計劃性進程切換。可是,搶佔式內核在響應引發進程切換的異步事件(例如喚醒高優先權的中斷處理程序)的方式與非搶佔式內核是有着極大差異的,咱們將這種進程切換稱爲強制性進程切換。

    * 全部的進程切換都由宏(switch_to)來完成。在搶佔式內核和非搶佔式內核中,當進程執行完某些具備內核功能的線程,並且調度程序被調用後,就發生進程切換。不過,在非搶佔內核中,當前進程是不可能被替換的,除非它打算切換到用戶態。

    Linux 2.6版本提出的可搶佔式內核是指內核搶佔,即當進程位於內核空間時,有一個更高優先級的任務出現時,若是當前內核容許搶佔,則能夠將當前任務掛起,執行優先級更高的進程。在2.5版本及以前,Linux內核是不可搶佔的,高優先級的進程不能停止正在內核中運行的低優先級的進程而搶佔CPU運行。進程一旦處於核心態(例如用戶進程執行系統調用),則除非進程自願放棄CPU,不然該進程將一直運行下去,直至完成或退出內核。與此相反,一個可搶佔的Linux內核可讓Linux內核如同用戶空間同樣容許被搶佔。當一個高優先級的進程到達時,無論當前進程處於用戶態仍是核心態,若是當前容許搶佔,可搶佔內核的Linux都會調度高優先級的進程運行。

    如今,總結一下搶佔式內核與非搶佔式內核的特色與區別:

    一、非搶佔式內核

    非搶佔式內核是由任務主動放棄CPU的使用權。非搶佔式調度法也稱爲合做型多任務,各個任務彼此共享一個CPU。異步事件由中斷服務處理。中斷服務能夠使一個高優先級的任務由掛起狀態轉爲就緒狀態。但中斷服務之後的控制權仍是回到原來被中斷了的那個任務,直到該任務主動放棄CPU的使用權時,那個高優先級的任務才能得到CPU的使用權。非搶佔式內核以下圖。

    

    非搶佔式內核的優勢:

    * 中斷響應快(與搶佔式內核相比);

    * 容許使用不可重入函數;

    * 幾乎不須要使用信號量保護共享數據。運行的任務佔有CPU,沒必要擔憂被其餘任務搶佔。

    非搶佔式內核的缺點:

    * 任務相應時間慢。高優先級的任務已經進入就緒態,但還不能運行,要等到當前運行着的任務釋放CPU後才能進行任務執行。

    * 非搶佔式內核的任務級響應時間是不肯定的,最高優先級的任務得到CPU的控制權的時間,徹底取決於已經運行進程什麼時候釋放CPU。

    二、搶佔式內核

    使用搶佔式內核能夠保障系統響應時間。最高優先級的任務一旦就緒,總能獲得CPU的使用權。當一個運行着的任務使一個比它優先級高的任務進入了就緒態,當前任務的CPU使用權就會被剝奪,或者說被掛起了,那個高優先級的任務便會馬上獲得CPU的控制權。若是是中斷服務子程序使一個高優先級的任務進入就緒狀態,中斷完成時,中斷了的任務就會被掛起,優先級高的任務便開始控制CPU。搶佔式內核以下圖:

    

    搶佔式內核的優勢:

    * 使用搶佔式內核,最高優先級的任務可以獲得最快程度的相應,高優先級任務確定可以得到CPU使用權。搶佔式內核使得任務優先級相應時間機制得以最優化。

    * 使內核可搶佔的目的是減小用戶態進程的分派延遲(dispatch latency),即從進程變爲可執行狀態到它實際開始運行之間的時間間隔。內核搶佔對執行及時被調度的任務(如硬件控制器,環境監視器,電影播放器等)的進程確實是由好處的,由於它下降了這種進程被另外一個運行在內核態的進程延遲的風險。

    搶佔式內核的缺點:

    * 不能直接使用不可重入型函數。調用不可重入函數時,要知足互斥條件,能夠使用互斥性信號量來實現。若是調用不可重入型函數時,對於低優先級的任務,其CPU使用權會被高優先級任務剝奪,不可重入型函數中的數據可能會被破壞。

    三、內核態搶佔的設計:

    首先,須要作何種改進才能支持內核可搶佔性呢?

    只有當內核正在執行異常處理程序(尤爲是系統調用),並且內核搶佔沒有被顯式地禁用時,纔可能搶佔內核。此外,由從中斷和異常中返回的知識,本地CPU必須打開本地中斷,不然沒法完成內核搶佔。

    另外,Linux2.6獨具特點的容許用戶在編譯內核時經過設置選項來禁用或啓用內核搶佔,固然,經過內核內部,也能夠顯式地禁用內核搶佔。那麼,應該如何設置來禁止內核搶佔呢?

    由從中斷和異常中返回可知,當被current_thread_info()宏所引用的thread_info描述符的preempt_count字段大於0時,就禁止內核搶佔。

    這樣,咱們能夠經過控制如下三個不一樣狀況來控制內核搶佔禁用:a、內核正在執行中斷服務例程;b、可延遲函數被禁止;c、經過把搶佔計數器設置爲正數而顯式地禁用內核搶佔。

    關於preempt_count字段,有以下操做宏:

說明
preempt_count() 在thread_info描述符中選擇preempt_count字段
preempt_disable() 使搶佔計數器的值加1
preempt_enable_no_resched() 使搶佔計數器的值減1
preempt_enable() 使搶佔計數器的值減1,並在

    對於preempt_enable宏遞減搶佔計數器,而後檢查TIF_NEED_RESCHED標誌是否被設置。在這種狀況下,進程切換請求是掛起的,所以宏調用preempt_schedule()函數,preempt_schedule()函數本質執行下面代碼:

if (!current_thread_info->preempt_count && !irqs_disabled()){
current_thread_info->preempt_count = PREEMPT_ACTIVE;
schedule();
current_thread_info->preempt_count = 0;
}
    該函數檢查是否容許本地中斷,以及當前進程的preempt_count是否爲0,若是兩個條件都爲真,就調用schedule()函數選擇另外一個進程來運行。所以,內核搶佔可能在結束內核控制路徑時發生,也可能在異常處理程序調用preempt_enable()從新容許內核搶佔發生。

    其次,要知足什麼條件時,其餘的內核態任務才能夠搶佔已運行任務的內核態呢?

    * 沒有持有鎖(lock)。鎖用於保護臨界區,不能被搶佔。

    * 內核態任務代碼可重入(code reentrant)。

    那麼,如何判斷當前上下文(context)(中斷處理例程,系統調用,內核線程等)是沒有持有鎖的?

    咱們在前面已經說起過thread_info中的preempt_count能夠經過設置正數來顯式地禁用內核搶佔。這裏,經過控制此變量便可實現持有鎖機制,preempt_count初始爲0,當加鎖時便執行加1操做,當解鎖時便執行減1操做,由此能夠實現控制內核搶佔的目的。

    另外,這裏須要補充一些關於可重入函數的知識。

    所謂可重入是指一個能夠被多個任務調用的過程,任務在調用時沒必要擔憂數據是否會出錯。不可重入函數在實時系統設計中被視爲不安全函數。

    若一個函數是可重入的,則該函數必須知足如下必要條件: 

    * 不能含有靜態(全局)很是量數據。

    不能返回靜態(全局)很是量數據的地址。 

    只能處理由調用者提供的數據。

    做爲可重入函數的輸入參數,只能由調用者提供,並且所提供的輸入數據必須知足下面三點要求。

    * 不能依賴於單實例模式資源的鎖。 

    不能調用不可重入的函數。 

    * 在函數內部,儘可能不能用 malloc 和 free 之類的方法進行內存分配和釋放,若是使用,通常狀況下會形成該函數的不可重入。 

    可重入函數主要用於多任務環境中。一個可重入的函數簡單來講就是能夠被中斷的函數,也就是說,能夠在這個函數執行的任什麼時候刻中斷它,轉入OS調度下去執行另一段代碼,而返回控制時不會出現什麼錯誤。

    不可重入的函數因爲使用了一些系統資源,好比全局變量區,中斷向量表等,因此它若是被中斷的話,可能會出現問題,這類函數是不能運行在多任務環境下的。 

    可重入函數也能夠這樣理解,重入即表示重複進入,首先它意味着這個函數能夠被中斷,其次意味着它除了使用本身棧上的變量之外不依賴於任何環境(包括 static),這樣的函數就是purecode(純代碼)可重入,能夠容許有該函數的多個副本在運行,因爲它們使用的是分離的棧,因此不會互相干擾。


    再則,咱們來討論一下關於內核態須要搶佔的觸發條件:

    內核提供了一個need_resched標誌(這個標誌在任務結構thread_info中,其返回的是TIF_NEED_RESCHED)來代表是否須要從新執行調度。當執行調度程序時,內核搶佔會根據內核搶佔是否禁止來進行內核搶佔操做。

    在觸發內核搶佔及從新調度時,有如下幾個重要的函數:

    set_tsk_need_resched():設置指定進程中的need_resched標誌;

    clear_tsk_need_resched():清除指定進程中的need_resched標誌;

    need_resched():檢查need_resched標誌的值:若是被設置就返回真,不然返回假。

    那麼,什麼時候觸發從新調度呢?

    * 時鐘中斷處理例程檢查當前任務的時間片,當任務的時間片消耗完時,scheduler_tick()函數就會設置need_resched標誌;

    * 信號量、等待隊列等機制喚醒時都是基於等待隊列(waitqueue)的,而等待隊列的喚醒函數爲default_wake_function,其調用try_to_wake_up將被喚醒的任務更改成就緒狀態並設置need_resched標誌;

    * 設置用戶進程的nice值時,可能會使高優先級的任務進入就緒狀態;

    * 改變任務的優先級時,可能會使高優先級的任務進入就緒狀態;

    * 對CPU(SMP)進行負載均衡時,當前任務可能須要移動至另一個CPU上運行。

    另外,搶佔發生的時機:

    * 當一箇中斷處理例程退出,在返回到內核態時,此時隱式調用schedule()函數,當前任務沒有主動放棄CPU使用權,而是被剝奪了CPU使用權。

    * 當內核代碼(程序)從不可搶佔狀態變爲可搶佔狀態時(preemptible),也就是preempt_count從正數變爲0時,此時一樣隱式調用schedule()函數。

    * 一個任務在內核態中,顯式的調用schedule()函數,任務主動放棄CPU使用權。

    * 一個任務在內核態中被阻塞,致使須要調用schedule()函數,任務主動放棄CPU使用權。

    那些時候不容許內核搶佔呢?

    * 內核正在進行中斷處理。在Linux內核中不能搶佔中斷(中斷只能被其餘中斷停止和搶佔,進程不能停止和搶佔中斷,內核搶佔是被進程搶佔和停止),在中斷例程中不容許進行進程調度。進程調度函數schedule()會對此作出判斷,若是是在中斷中調用,會打印錯誤。

    * 內核正在進行中斷上下文的下半部處理時,硬件中斷返回前會執行軟中斷,此時仍然處於中斷上下文中,因此此時沒法進行內核搶佔。

    * 內核的代碼段正持有自旋鎖(spinlock)、讀寫鎖(writelock/readlock)時,內核代碼段處於鎖保護狀態。此時,內核不能被搶佔,不然因爲搶佔將致使其餘CPU長期不能得到鎖而出現死鎖狀態。

    * 內核正在對每CPU私有的數據結構(Per-CPU data structures)進行操做。在SMP(對稱多處理器)中,對於每CPU數據結構並未採用自旋鎖進行保護,由於這些數據結構隱含地被保護了(不一樣的CPU上有不一樣的每CPU數據,其餘CPU上運行的進程不能訪問另外一個CPU的每CPU數據)。在這種狀況下,雖然並未採用鎖機制,一樣不能進行內核搶佔,由於若是容許內核搶佔,一個進程被搶佔後從新調度,有可能調度到其餘的CPU上去,這時定義的每CPU數據變量就會發生錯位。所以,對於每CPU數據訪問時,一樣也沒法進行內核搶佔。


同步原語

    如何避免因爲對共享數據的不安全訪問致使的數據崩潰?

    內核使用的各類同步技術:

技術 說明 適用範圍
每CPU變量 在CPU之間複製數據結構 全部CPU
原子操做 對一個計數器原子地「讀-修改-寫」的指令 全部CPU
內存屏障 避免指令從新排序 本地CPU或全部CPU
自旋鎖 加鎖時忙等 全部CPU
信號量 加鎖時阻塞等待 全部CPU
順序鎖 基於訪問計數器的鎖 全部CPU
本地中斷的禁止 禁止單個CPU上的中斷處理 本地CPU
本地軟中斷的禁止 禁止單個CPU上的可延遲函數處理 本地CPU
讀-複製-更新(RCU) 經過指針而不是鎖來訪問共享數據結構 全部CPU


每CPU變量

    最好的同步技術是把設計不須要同步的臨界資源放在首位,這是一種思惟方法,由於每一種顯式的同步原語都有不容忽視的性能開銷。最簡單也是最重要的同步技術包括把內核變量或數據結構聲明爲每CPU變量(per-cpu variable)。每CPU變量主要是數據結構的數組,系統的每一個CPU對應數組的一個元素。

    多核狀況下,CPU是同時併發運行的,可是它們共同使用其餘的硬件資源,所以咱們須要解決多個CPU之間的同步問題。每CPU變量(per-cpu-variable)是內核中一種重要的同步機制。顧名思義,每CPU變量就是爲每一個CPU構造一個變量的副本,這樣多個CPU相互操做各自的副本,互不干涉。好比咱們標識當前進程的變量current_task就被聲明爲每CPU變量。

    一個CPU不該該訪問與其餘CPU對應的數組元素,另外,它能夠隨意讀或修改它本身的元素而不用擔憂出現競爭條件,由於它是惟一有資格這麼作的CPU。可是,這也意味着每CPU變量基本上只能在特殊狀況下使用,也就是當它肯定在系統的CPU上的數據在邏輯上是獨立的時候。

    每CPU變量的特色:

    一、用於多個CPU之間的同步,若是是單核結構,每CPU變量沒有任何用處。

    二、每CPU變量不能用於多個CPU相互協做的場景(每一個CPU的副本都是獨立的)。

    三、每CPU變量不能解決由中斷或延遲函數致使的同步問題。

    四、訪問每CPU變量的時候,必定要確保關閉內核搶佔,不然一個進程被搶佔後可能會更換CPU運行,這會致使每CPU變量的引用錯誤。

    咱們能夠用數組來實現每CPU變量嗎?好比,咱們要保護變量var,咱們能夠聲明int var[NR_CPUS],CPU num就訪問var[num]不就能夠了嗎?

    顯然,每CPU變量的實現不會這麼簡單。理由:咱們知道爲了加快內存訪問,處理器中設計了硬件高速緩存(也就是CPU的cache),每一個處理器都會有一個硬件高速緩存。若是每CPU變量用數組來實現,那麼任何一個CPU修改了其中的內容,都會致使其餘CPU的高速緩存中對應的塊失效,而頻繁的失效會致使性能急劇的降低。所以,每CPU的數組元素在主存中被排列以使每一個數據結構存放在硬件高速緩存的不一樣行,這樣,對每CPU數組的併發訪問不會致使高速緩存行的竊用和失效(這種操做會帶來昂貴的系統開銷)。

    雖然每CPU變量爲來自不一樣CPU的併發訪問提供保護,但對來自異步函數(中斷處理程序和可延遲函數)的訪問不提供保護,在這種狀況下須要另外的同步技術。

    每CPU變量分爲靜態和動態兩種,靜態的每CPU變量使用DEFINE_PER_CPU聲明,在編譯的時候分配空間;而動態的使用alloc_percpu和free_percpu來分配回收存儲空間。

    每CPU變量的函數和宏:

    每CPU變量的定義在include\linux\percpu.h以及include\asm-generic\percpu.h中。這些文件中定義了單核和多核狀況下的每CPU變量的操做,這是爲了代碼的統一設計的,實際上只有在多核狀況下(定義了CONFIG_SMP)每CPU變量纔有意義。

    常見的操做和含義以下:

函數名 說明
DECLARE_PER_CPU(type, name) 聲明每CPU變量name,類型爲type
DEFINE_PER_CPU(type, name) 靜態分配一個每CPU數組,數組名爲name,類型爲type
alloc_percpu(type) 動態爲type類型的每CPU變量分配空間,並返回它的地址
free_percpu(pointer) 釋放爲動態分配的每CPU變量的空間,pointer是起始地址
per_cpu(name, cpu) 獲取編號cpu的處理器上面的變量name的副本
get_cpu_var(name) 獲取本處理器上面的變量name的副本,該函數禁用內核搶佔,主要由__get_cpu_var來完成具體的訪問
get_cpu_ptr(name)  獲取本處理器上面的變量name的副本的指針,該函數禁用內核搶佔,主要由__get_cpu_var來完成具體的訪問
put_cpu_var(name) & put_cpu_ptr(name) 表示每CPU變量的訪問結束,啓用內核搶佔(不使用name)
__get_cpu_var(name)  獲取本處理器上面的變量name的副本,該函數不由用內核搶佔


原子操做

    若干彙編語言指令具備「讀-修改-寫」類型——也就是說,它們訪問存儲器單元兩次,第一次讀原值,第二次寫新值。

    假定運行在兩個CPU上的兩個內核控制路徑試圖經過執行非原子操做來同時「讀-修改-寫」同一存儲器單元,首先,兩個CPU都試圖讀同一單元,可是存儲器仲裁器(對訪問RAM芯片的操做進行串行化的硬件電路)插手,只容許其中的一個訪問而讓另外一個延遲,然而,當第一個讀操做已經完成後,延遲的CPU從那個存儲器單元正好讀到同一個值(舊值)。而後,兩個CPU都試圖向那個存儲器單元寫一新值,總線存儲器訪問再一次被存儲器仲裁器串行化,最終,兩個寫操做都成功。可是,全局的結果是不正確的,由於兩個CPU寫入同一(新)值。所以,兩個交錯的「讀-修改-寫」操做成了一個單獨的操做。

    避免因爲「讀-修改-寫」指令引發的競爭條件的最容易的辦法,就是確保這樣的操做在芯片上是原子的。任何一個這樣的操做都必須以單個指令執行,中間不能中斷,且避免其餘的CPU訪問同一存儲單元。這些很小的原子操做(atomic operations)能夠創建在其餘更靈活機制的基礎之上以建立臨界區。

    原子操做能夠保證指令以原子的方式執行,執行過程不被打斷。它經過把讀取和修改變量的行爲包含在一個單步中執行,從而防止了競爭的發生,保證操做結果老是一致的。

    例如:

    int i=9;

    線程1:   i++;
    ===>>>   i=9 OR i=8

    線程2:   i–-;
    ===>>>   i=9 OR i=8

    兩個線程併發的執行,致使結果不肯定性。原子操做的做用和信號量機制是同樣,都是爲了防止同時訪問臨界資源,保證結果的一致性。大多數硬件體系結構要麼原本就支持簡單的原子操做,要麼就提供了鎖內在總線的指令,例如x86平臺上,就支持CPU鎖總線操做,彙編指令前綴「LOCK」就能夠將總線鎖做,直到指令結束時鎖打開;而有些硬件體系結構自己就不太支持原子操做,好比SPARC,可是Linux內核經過一些方法,作到了原子操做。

    原子操做在Linux內核裏分爲原子整數操做和原子位操做,下面咱們來看看這兩個操做用法。

    原子整數操做:

    針對整數的原子操做只能對atomic_t類型的數據進行處理,之因此沒有用C語言的int類型,主要有三個緣由:

    一、讓原子函數只接受atomic_t類型的操做數,能夠確保原子操做只與這種特殊類型數據一塊兒使用,防止該類型數據不會傳給其它非原子操做。

    二、使用atomic_t類型確保編譯器不對相應的值進行訪問優化。

    三、在不一樣體系結構上實現原子操做的時候,使用atomic_t能夠屏蔽其間的差別。

    在Linux內核中提供了一系統的原子整數操做函數。

原子整數操做 描述
ATOMIC_INIT(int i) 在聲明一個atmoic_t變量時,將它初始化爲i
int atmoic_read(atmoic_t *v) 原子地讀取整數變量v
void atmoic_set(atmoic_t *v, int i) 原子地設置v值爲i
void atmoic_add(atmoic_t *v, int i) 原子地從v值加i
void atmoic_sub(atmoic_t *v, int i) 原子地從v值減i
void atmoic_inc(atmoic_t *v)  原子地從v值加1
void atmoic_dec(atmoic_t *v) 原子地從v值減1
int atmoic_sub_and_test(int i,atmoic_t *v)  原子地從v值減i,若是結果等於0返回真,不然返回假
int atmoic_add_negative(int i,atmoic_t *v) 原子地從v值減i,若是結果是負數返回真,不然返回假
int atmoic_dec_and_test(atmoic_t *v) 原子地給v減1,若是結果等於0返回真,不然返回假
int atmoic_inc_and_test(atmoic_t *v)  原子地給v加1,若是結果等於0返回真,不然返回假

    原子操做最多見的用途就是實現計數器,使用複雜的鎖機制來保護一個單純的計數是很笨拙的,原子操做比起復雜的同步方法來講,給系統帶來的開銷小,對高速緩存行的影響也小。

    原子位操做:

    除了原子整數操做外,內核還提供了一組針對位這一級數據進行操做的函數,位操做函數是對普通的內在地址進行操做的,它的參數是一個指針和一個位號。因爲是對普通的指針進程操做,因此沒有像atomic_t這樣的類型約束。

原子位操做 描述
void set_bit(int nr, void *addr) 原子地設置addr所指對象的第nr位
void clear_bit(int nr, void *addr) 原子地清空addr所指對象的第nr位
void change_bit(int nr, void *addr)  原子地翻轉addr所指對象的第nr位
int test_and_set_bit(int nr, void *addr)  原子地設置addr所指對象的第nr位,並返回原先的值
int test_and_clear_bit(int nr, void *addr)  原子地清空addr所指對象的第nr位,並返回原先的值
int test_and_change_bit(int nr, void *addr) 原子地翻轉addr所指對象的第nr位,並返回原先的值
int test_bit(int nr, void *addr)  原子地返回addr所指對象的第nr位
void atomic_clear_mask(void *mask, void *addr) 清零mask指定的*addr的全部位
void atomic_set_mask(void *mask, void *addr) 設置mask指定的*addr的全部位


優化和內存屏障

    當使用優化的編譯器是,指令並不會嚴格地按照它們在源代碼中出現的順序執行。例如,編譯器可能從新安排彙編語言指令以使寄存器以最優的方式使用。此外,現代CPU一般並行地執行若干條指令,且可能重現安排內存訪問,這種從新排序可能極大地加速程序的執行。

    然而,當處理同步時,必須避免指令從新排序,若是放在同步原語以後的一條指令在同步原語自己以前執行,事情很快就會變得失控。事實上,全部的同步原語起優化和內存屏障的做用。

    優化屏障(optimization barrier)原語保證編譯程序不會混淆放在原語操做以前的彙編語言指令和放在原語操做以後的彙編語言指令,這些彙編語言指令在C中都由對應的語句。在Linux中,優化屏障就是barrier()宏。

    內存屏障(memory barrier)原語確保,在原語以後的操做開始執行以前,原語以前的操做已經完成。所以,內存屏障相似於防火牆,讓任何彙編語言指令都不能經過。

    在《獨闢蹊徑品內核》一書中,如此定義內存屏障:爲了防止編譯器和硬件的不正確優化,使得對存儲器的訪問順序(其實就是變量)和書寫程序時的訪問順序不一致而提出的一種解決辦法。 內存屏障不是一種錯誤的現象,而是一種對錯誤現象提出的一種解決方法。

    前面概述了內存屏障,如今咱們進行一些詳細說明:

    一、爲何會亂序執行?

    如今的CPU通常採用流水線來執行指令。一個指令的執行被分劃成:取指、譯碼、訪存、執行、寫回等若干個階段。而後,多條指令能夠同時存在於流水線中,同時被執行。

    指令流水線並非串行的,並不會由於一個耗時很長的指令在「執行」階段呆很長時間,而致使後續的指令都卡在「執行」以前的階段上。相反,流水線是並行的,多個指令能夠同時處於同一個階段,只要CPU內部相應的處理部件未被佔滿便可。好比說CPU有一個加法器和一個除法器,那麼一條加法指令和一條除法指令就可能同時處於「執行」階段,而兩條加法指令在「執行」階段就只能串行工做。

    可見,相比於串行+阻塞的方式,流水線像這樣並行的工做,效率是很是高的。

    然而,這樣一來,亂序可能就產生了。好比一條加法指令本來出如今一條除法指令的後面,可是因爲除法的執行時間很長,在它執行完以前,加法可能先執行完了。再好比兩條訪存指令,可能因爲第二條指令命中了cache而致使它先於第一條指令完成。

    通常狀況下,指令亂序並非CPU在執行指令以前刻意去調整順序。CPU老是順序的去內存裏面取指令,而後將其順序的放入指令流水線。可是指令執行時的各類條件,指令與指令之間的相互影響,可能致使順序放入流水線的指令,最終亂序執行完成。這就是所謂的「順序流入,亂序流出」。

    指令流水線除了在資源不足的狀況下會阻塞以外(如前所述的一個加法器應付兩條加法指令的狀況),指令之間的相關性也是致使流水線阻塞的重要緣由。

    CPU的亂序執行並非任意的亂序,而是以保證程序上下文因果關係爲前提的。有了這個前提,CPU執行的正確性纔有保證。好比:

a++;
b=f(a);
c--;

    因爲b=f(a)這條指令依賴於前一條指令a++的執行結果,因此b=f(a)將在「執行」階段以前被阻塞,直到a++的執行結果被生成出來;而c--跟前面沒有依賴,它可能在b=f(a)以前就能執行完。(注意,這裏的f(a)並不表明一個以a爲參數的函數調用,而是表明以a爲操做數的指令。C語言的函數調用是須要若干條指令才能實現的,狀況要更復雜些。)

    像這樣有依賴關係的指令若是捱得很近,後一條指令一定會由於等待前一條執行的結果,而在流水線中阻塞好久,佔用流水線的資源。而編譯器的亂序,做爲編譯優化的一種手段,則試圖經過指令重排將這樣的兩條指令拉開必定的距離,以致於後一條指令進入CPU的時候,前一條指令結果已經獲得了,那麼也就再也不須要阻塞等待了。好比將指令重排爲:

a++;
c--;
b=f(a);

    相比於CPU的亂序,編譯器的亂序纔是真正對指令順序作了調整。可是編譯器的亂序也必須保證程序上下文的因果關係不發生改變。

    二、亂序的後果

    亂序執行,有了「保證上下文因果關係」這一前提,通常狀況下是不會有問題的。所以,在絕大多數狀況下,咱們寫程序都不會去考慮亂序所帶來的影響。

    可是,有些程序邏輯,單純從上下文是看不出它們的因果關係的。好比:

*addr=5;
val=*data;

    從表面上看,addr和data是沒有什麼聯繫的,徹底能夠放心的去亂序執行。可是若是這是在某設備驅動程序中,這兩個變量卻可能對應到設備的地址端口和數據端口。而且,這個設備規定了,當你須要讀寫設備上的某個寄存器時,先將寄存器編號設置到地址端口,而後就能夠經過對數據端口的讀寫而操做到對應的寄存器。那麼,對前面那兩條指令的亂序執行就可能形成錯誤。

    對於這樣的邏輯,咱們姑且將其稱做隱式的因果關係;而指令與指令之間直接的輸入輸出依賴,也姑且稱做顯式的因果關係。CPU或者編譯器的亂序是以保持顯式的因果關係不變爲前提的,可是它們都沒法識別隱式的因果關係。再舉個例子:

Thread 1:
 
obj->data = 123;
obj->ready = 1;

    當設置了data以後,記下標誌,而後在另外一個線程中可能執行:

Thread 2:
 
if (obj->ready)
do_something(obj->data);

    雖然這個代碼看上去有些彆扭,可是彷佛沒錯。不過,考慮到亂序,若是標誌被置位先於data被設置,那麼結果極可能就悲劇了(原本不會執行do_something函數,可是因爲亂序致使執行了該函數)。由於從字面上看,前面的那兩條指令其實並不存在顯式的因果關係,亂序是有可能發生的。

    總的來講,若是程序具備顯式的因果關係的話,亂序必定會尊重這些關係;不然,亂序就可能打破程序原有的邏輯。這時候,就須要使用屏障來抑制亂序,以維持程序所指望的邏輯。

    三、優化和內存屏障的做用

    內存屏障主要有:讀屏障、寫屏障、通用屏障、優化屏障幾種。

    以讀屏障爲例,它用於保證讀操做有序。屏障以前的讀操做必定會先於屏障以後的讀操做完成,寫操做不受影響,同屬於屏障的某一側的讀操做也不受影響。相似的,寫屏障用於限制寫操做。而通用屏障則對讀寫操做都有做用。而優化屏障則用於限制編譯器的指令重排,不區分讀寫。前三種屏障都隱含了優化屏障的功能。好比:

tmp = 2048;
*addr = 5;
mb();
val = *data;

    有了內存屏障就了確保先設置地址端口,再讀數據端口。而至於設置地址端口與tmp的賦值孰先孰後,屏障則不作干預。有了內存屏障,就能夠在隱式因果關係的場景中,保證因果關係邏輯正確。

    四、內存屏障原語

    Linux使用六個內存屏障原語。這些原語也被當作優化屏障,由於咱們必須保證編譯器程序不在屏障先後移動彙編語言指令。

說明
mb() 適用於MP和UP的內存屏障
rmb() 適用於MP和UP的讀內存屏障
wmb() 適用於MP和UP的寫內存屏障
smp_mb() 僅適用於MP的內存屏障
smp_rmb() 僅適用於MP的讀內存屏障
smp_wmb() 僅適用於MP的寫內存屏障

    內存屏障既用於多處理器系統(MP),也用於單處理器系統(UP)。當內存屏障應該防止僅出如今多處理器系統上的競爭條件時,就使用smp_xxx()原語;在單處理器系統上,它們什麼也不作。其餘的內存屏障原語防止出如今單處理器和多處理器系統上的競爭條件。

    內存屏障原語的實現依賴於系統地體系結構。

    在80x86微處理器上,若是CPU支持lfence彙編語言指令,就把rmb()宏展開爲 asm volatile("lfence"),不然就展開爲 asm volatile("lock;addl $0, 0(%%esp)")。asm指令告訴編譯器插入一些彙編語言指令並起優化屏障的做用。lock;addl $0, 0(%%esp)彙編指令把0加到棧頂的內存單元;這條指令自己沒有什麼價值,可是,lock前綴使得這條指令成爲CPU的一個內存屏障。

    而對於wmb()宏,其實現即爲barrier()宏,這是由於Intel處理器不對寫內存訪問進行從新排序,所以,沒有必要在代碼中插入一條串行化彙編指令。不過,此宏禁止編譯器從新組合指令。

    五、多處理器系統狀況

    前面只是考慮了單處理器指令亂序的問題,而在多處理器下,除了每一個處理器要獨自面對上面討論的問題以外,當多個處理器之間存在交互的時候,一樣要面對亂序的問題。

    一個處理器(記爲a)對內存的寫操做並非直接就在內存上生效的,而是要先通過自身的cache。另外一個處理器(記爲b)若是要讀取相應內存上的新值,先得等a的cache同步到內存,而後b的cache再從內存同步這個新值。而若是須要同步的值不止一個的話,就會存在順序問題。舉一個例子:

<CPU-a>: | <CPU-b>:
|
obj->data = 123; | if (obj->ready)
wmb(); | do_something(obj->data);
obj->ready = 1; |

    前面也說過,必需要使用屏障來保證CPU-a不發生亂序,從而使得ready標記置位的時候,data必定是有效的。可是在多處理器狀況下,這還不夠。緣由在於,data和ready標記的新值可能以相反的順序更新到CPU-b上。

    其實這種狀況在大多數體系結構下並不會發生,不過內核文檔memory-barriers.txt舉了alpha機器的例子。alpha機器可能使用分列的cache結構,每一個cache列能夠並行工做,以提高效率。而每一個cache列上面緩存的數據是互斥的(若是不互斥就還得解決cache列之間的一致性),因而就可能引起cache更新不一樣步的問題。

    假設cache被分紅兩列,而CPU-a和CPU-b上的data和ready都分別被緩存在不一樣的cache列上。

    首先是CPU-a更新了cache以後,會發送消息讓其餘CPU的cache來同步新的值,對於data和ready的更新消息是須要按順序發出的。若是cache只有一列,那麼指令執行的順序就決定了操做cache的順序,也就決定了cache更新消息發出的順序。可是如今假設了有兩個cache列,可 能因爲緩存data的cache列比較繁忙而使得data的更新消息晚於ready發出,那麼程序邏輯就無法保證了。不過好在SMP下的內存屏障在解決指令亂序問題以外,也將cache更新消息亂序的問題解決了。只要使用了屏障,就能保證屏障以前的cache更新消息先於屏障以後的消息被髮出。

    而後就是CPU-b的問題。在使用了屏障以後,CPU-a已經保證data的更新消息先發出了,那麼CPU-b也會先收到data的更新消息。不過一樣,CPU-b上緩存data的cache列可能比較繁忙,致使對data的更新晚於對ready的更新。這裏一樣會出問題。

    因此,在這種狀況下,CPU-b也得使用屏障。CPU-a上要使用寫屏障,保證兩個寫操做不亂序,而且相應的兩個cache更新消息不亂序。CPU-b上則須要使用讀屏障,保證對兩個cache單元的同步不亂序。可見,SMP下的內存屏障必定是須要配對使用的。

    因此,上面的例子應該改寫成: 

<CPU-a>: | <CPU-b>:
|
obj->data = 123; | if (obj->ready){
wmb(); | rmb();
obj->ready = 1; | do_something(obj->data);
| }

    CPU-b上使用的讀屏障還有一種弱化版本,它不保證讀操做的有序性,叫作數據依賴屏障。顧名思義,它是在具備數據依賴狀況下使用的屏障,由於有數據依賴(也就是以前所說的顯式的因果關係),因此CPU和編譯器已經可以保證指令的順序。

    再舉個例子:
<CPU-a>: | <CPU-b>:
|
init(newval); | p = data;
<write barrier> | <data dependency barrier>
data = &newval; | val = *p;

    這裏的屏障就能夠保證:若是data指向了newval,那麼newval必定是初始化過的。


自旋鎖

    ​一種普遍應用的同步技術是加鎖(locking)。當內核控制路徑必須訪問共享數據結構或進入臨界區時,就須要爲本身獲取一把「鎖」。因爲鎖機制保護的資源很是相似與限制於房間內的資源,當某人進入房間時,就把門鎖上。若是內核控制路徑但願訪問資源,就試圖獲取鑰匙「打開門」。當且僅當資源空閒時,它才能成功。而後,只要它還想使用這個資源,門就依然鎖着。當內核控制路徑釋放鎖時,門就打開,另外一個內核控制路徑就能夠進入房間使用資源。

    下圖展示了鎖的使用。


    5個內核控制路徑(P1, P2, P3, P4和P0)試圖訪問兩個臨界區(C1, C2)。內核控制路徑P0正在C1中,而P2和P4正等待進入C1。同時,P1正在C2中,而P3正在等待進入C2。注意P0和P1能夠並行運行。臨界區C3的鎖處於打開狀態,由於沒有內核控制路徑須要進入C3。

    自旋鎖(spinlock)是用來在多處理器環境中工做的一種特殊的鎖。若是內核控制路徑(內核態進程)發現自旋鎖「開着」,就獲取鎖並繼續本身的執行。相反,若是內核控制路徑發現鎖由運行在另外一個CPU上的內核控制路徑「鎖着」,就在周圍「旋轉」,反覆執行一條緊湊的循環指令,直到鎖被釋放。

    自旋鎖的循環指令表示「忙等」。即便等待的內核控制路徑無事可作(除了浪費時間),它也在CPU上保持運行。不過,自旋鎖一般很是方便,由於不少內核資源只鎖1毫秒的時間片斷,因此說,釋放CPU和隨後又得到CPU都不會消耗不少時間。

    通常來講,由自旋鎖所保護的每一個臨界區都是禁止內核搶佔的。在單處理器系統上,這種鎖自己不起鎖的做用,自旋鎖原語僅僅是禁止或啓用內核搶佔。請注意,在自旋鎖忙等期間,內核搶佔仍是有效的,所以,等待自旋鎖釋放的進程有可能被更高優先級的進程所替代。

    下面,進行幾個方面對自旋鎖進行相關說明:

    一、爲何使用自旋鎖?

    操做系統鎖機制的基本原理,就是在某個鎖操做過程當中不能與其餘鎖操做交織執行,以避免多個執行路徑對內核中某些重要的數據及數據結構進行同時操做而形成系統混亂。在不一樣的系統環境中,根據系統特色和操做須要,鎖機制能夠用多種方式來實現。在Linux中,其系統內核的鎖機制通常經過3種基本方式來實現,即原語、關中斷和總線鎖。在單CPU系統中,CPU 的讀—修改—寫原語能夠保證是原子的,即執行過程過中不會被中斷,因此CPU經過關中斷的方式,從芯片級保證該操做所存取的數據不能被多個內核控制路徑同時訪問,避免交叉執行。然而,在對稱多處理器 (SMP) 環境中,單CPU涉及讀—修改—寫原語再也不是原子的,由於在某個CPU執行讀—修改—寫指令時有屢次總線操做,其餘CPU競爭總線,可致使對同一存儲單元的讀—寫操做與其餘CPU對這一存儲單元交叉,這時咱們就須要用一個稱爲自旋鎖(spin lock)的原始對象爲CPU 提供鎖定總線的方法。

    二、關於自旋鎖的幾個事實

    自旋鎖其實是忙等鎖,當鎖不可用時,CPU一直循環執行「測試並設置(test-and-set)」,直到該鎖可用而取得該鎖,CPU在等待自旋鎖時不作任何有用的工做,僅僅是等待。這說明只有在佔用鎖的時間極短的狀況下,使用自旋鎖是合理的,由於此時某個CPU可能正在等待這個自旋鎖。當臨界區較爲短小時,如只是爲了保證對數據修改的原子性,經常使用自旋鎖;當臨界區很大,或有共享設備的時候,須要較長時間佔用鎖,使用自旋鎖就不是一個很好的選擇,會下降CPU的效率。

    自旋鎖也存在死鎖(deadlock)問題。引起這個問題最多見的狀況是要求遞歸使用一個自旋鎖,即若是一個已經擁有某個自旋鎖的CPU但願第二次得到這個自旋鎖,則該CPU將死鎖。自旋鎖沒有與其關聯的「使用計數器」或「全部者標識」;鎖或者被佔用或者空閒。若是你在鎖被佔用時獲取它,你將等待到該鎖被釋放。若是碰巧你的CPU已經擁有了該鎖,那麼用於釋放鎖的代碼將得不到運行,由於你使CPU永遠處於「測試並設置」某個內存變量的自旋狀態。另外,若是進程得到自旋鎖以後再阻塞,也有可能致使死鎖的發生。因爲自旋鎖形成的死鎖,會使整個系統掛起,影響很是大。

    自旋鎖必定是由系統內核調用的。不可能在用戶程序中由用戶請求自旋鎖。當一個用戶進程擁有自旋鎖期間,內核是把代碼提高到管態的級別上運行。在內部,內核能獲取自旋鎖,但任何用戶都作不到這一點。

    三、Linux 自旋鎖

    在Linux中,每一個自旋鎖都用spinlock_t結構表示,其中包含兩個字段:

    slock:該字段表示自旋鎖的狀態:值爲1時,表示未加鎖狀態,而任何負數和0都表示加鎖狀態。

    break_lock:表示進程正在忙等自旋鎖。

    自旋鎖宏:

說明
spin_lock_init() 把自旋鎖置爲1(未鎖)
spin_lock() 循環,直到自旋鎖變爲1(未鎖),而後,把自旋鎖置爲0(鎖上)
spin_unlock() 把自旋鎖置爲1(未鎖)
spin_unlock_wait() 等待,直到自旋鎖變爲1(未鎖)
spin_is_lock() 若是自旋鎖被置爲1(未鎖),返回0;不然,返回1
spin_trylock() 把自旋鎖置爲0(鎖上),若是原來鎖的值爲1,則返回1;不然,返回0

    具備內核搶佔的spin_lock宏

    針對支持SMP系統的搶佔式內核,該宏獲取自旋鎖的地址slp做爲其參數,並執行下面的操做:

    a、調用preempt_disable()以禁用內核搶佔;

    b、調用函數_raw_spin_trylock(),它對自旋鎖的slock字段執行原子性的測試和設置操做。該函數首先執行等價於下面彙編語言片斷的一些指令:

movb $0, %a1
xchgb %a1, slp->slock

    彙編語言指令xchg原子性地交換8位寄存器%a1(存0)和slp->slock指示的內存單元中的內容。隨後,若是存放在自旋鎖中的舊值是正數,函數就返回1,不然返回0。

    c、若是自旋鎖中的舊值是正數,宏結束:內核控制路徑已經得到自旋鎖。

    d、不然,內核控制路徑沒法得到自旋鎖,所以,宏必須執行循環一直到在其餘CPU上運行的內核控制路徑釋放自旋鎖。調用preempt_enable()遞減在第一步遞增了的搶佔計數器。若是在執行spin_lock宏以前內核搶佔被啓用,那麼其餘進程此時能夠取代等待自旋鎖的進程。

    e、若是break_lock字段等於0,則把它設置爲1。經過檢測該字段,擁有鎖並在其餘CPU上運行的進程能夠知道是否有其餘進程在等待這個鎖。若是進程持有某個自旋鎖時間太長,它能夠提早釋放鎖以使等待相同自旋鎖的進程可以繼續向前運行。

    f、執行等待循環:

while (spin_is_locked(slp) && slp->break_lock)
cpu_relax();

    宏cpu_relax()簡化爲一條pause彙編語言指令。

    g、跳轉到a步驟,再次試圖獲取自旋鎖。

    非搶佔式內核中的spin_lock宏

    若是在內核編譯時沒有選擇內核搶佔選項,spin_lock宏就與前面描述的spin_lock宏有着很大的區別。在這種狀況下,宏生成一個彙編語言程序片斷,本質上等價於下面緊湊的忙等待:

1 lock; decb slp->slock (遞減slp->slock, 判斷其是否爲正數)
jns 3f  (f表示向前的,它在程序後面出現)
2: pause
cmpb $0, slp->slock (判斷slp->slock是否爲0)
jle 2b (b表示向後的,前跳回標籤2代碼)
jmp lb (前跳回標籤1代碼)
3:

    JNS(jump if not sign),彙編語言中的條件轉移指令 。結果爲正則轉移。
    JLE(或JNG)(jump if less or equal or not greater),彙編語言中的條件轉移指令。小於或等於,或者不大於則轉移。

    彙編語言指令decb遞減自旋鎖的值,該指令是原子的,由於它帶有lock字節前綴。隨後檢測符號標誌,若是它被清零,說明自旋鎖被設置爲1(未鎖),所以,從標記3處繼續正常執行。不然,在標籤2處執行緊湊循環直到自旋鎖出現正值。而後,從標籤1處開始從新執行,由於不檢查其餘的處理器是否搶佔了鎖就繼續執行是不安全的。

    spin_unlock宏

    spin_unlock宏釋放之前得到的自旋鎖,它本質上執行了下面的彙編語言指令:

movb $1, slp->slock (slock賦值爲1,標爲未鎖)
    並在隨後調用preempt_enable()。


讀/寫自旋鎖

    咱們從以下幾個點進行討論:

    一、什麼是讀寫自旋鎖?

    自旋鎖(Spinlock)是一種經常使用的互斥(Mutual Exclusion)同步原語(Synchronization Primitive),試圖進入臨界區(Critical Section)的線程使用忙等待(Busy Waiting)的方式檢測鎖的狀態,若鎖未被持有則嘗試獲取。這種忙等待的作法無謂地消耗了處理器資源,所以只適用於臨界區很是短小的代碼片斷,例如Linux內核的中斷處理函數。

    因爲互斥的特色,使用自旋鎖的代碼毫無線程併發性可言,多處理器系統的性能受到限制。經過觀察線程(內核控制路徑)在臨界區的訪問行爲,咱們發現有些線程只是簡單地讀取信息,並不修改任何東西,那麼容許它們同時進入臨界區不會有任何危險,反而能大大提升系統的併發性。這種將線程區分爲讀者和寫者、多個讀者容許同時訪問共享資源、申請線程在等待期內依然使用忙等待方式的鎖,咱們稱之爲讀寫自旋鎖(Reader-Writer Spinlock)。

    讀寫自旋鎖一樣是在保護SMP體系下的共享數據結構而引入的,它的引入是爲了增長內核的併發能力。只要內核控制路徑沒有對數據結構進行修改,讀/寫自旋鎖就容許多個內核控制路徑同時讀同一數據結構。若是一個內核控制路徑想對這個結構進行寫操做,那麼它必須首先獲取讀/寫鎖的寫鎖,寫鎖受權獨佔訪問這個資源。這樣設計的目的,即容許對數據結構併發讀能夠提升系統性能。

    下圖顯示有兩個受讀寫自旋鎖保護的臨界區。內核控制路徑R0和R1正在同時讀取C1中的數據結構,而W0正在等待獲取寫鎖。內核控制路徑W1正對C2中的數據進行寫操做,而R2和W1分別等待獲取讀鎖和寫鎖。


    每一個讀/寫自旋鎖都是一個rwlock_t結構:

typedef struct {
raw_rwlock_t raw_lock;
#if defined(CONFIG_PREEMPT) && defined(CONFIG_SMP)
unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
unsigned int magic, owner_cpu;
void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
} rwlock_t;
 
typedef struct {
volatile unsigned int lock;
} raw_rwlock_t;

    其lock字段(raw_lock)是一個32位的字段,分爲兩個不一樣的部分:

    a、24位計數器,表示對受保護的數據結構併發地進行讀操做的內核控制路徑的數目。這個計數器的二進制補碼存放在這個字段的0~23位。(爲何不保存盡心寫操做的內核控制路徑呢?緣由在於:最多隻能有一個寫者訪問受保護的數據結構,只存在0與1兩種狀況。lock字段徹底能夠實現,見下文。)

    b、「未鎖」標誌字段,當沒有內核控制路徑在讀或寫時設置該位(爲1),不然清0。這個「未鎖」標誌存放在lock字段的第24位。

    注意,若是自旋鎖爲空(設置了「未鎖」標誌且無讀者),那麼lock字段的值爲0x01000000;若是寫者已經得到自旋鎖(「未鎖」標誌清0且無讀者),那麼lock字段的值爲0x00000000;若是一個、兩個或多個進程由於讀獲取了自旋鎖,那麼,lock字段的值爲Ox00ffffff,Ox00fffffe等(「未鎖」標誌清0表示寫鎖定,不容許寫該數據結構的進程,讀者個數的二進制補碼在0~23位上;若是全爲0,則表示有一個寫進程在操做此數據結構)。

    與spinlock_t結構同樣,rwlock_t結構也包括break_lock字段。

    rwlock_init()宏把讀/寫自旋鎖的lock字段初始化爲0x01000000(「未鎖」),把break_lock初始化爲0,算法相似spin_lock_init。

    二、讀寫自旋鎖的屬性

    上面說起的共享資源能夠是簡單的單一變量或多個變量,也能夠是像文件這樣的複雜數據結構。爲了防止錯誤地使用讀寫自旋鎖而引起的bug,咱們假定每一個共享資源關聯一把惟一的讀寫自旋鎖,線程只容許按照相似大象裝冰箱的方式訪問共享資源:

    申請鎖 ==>> 得到鎖後,讀寫共享資源 ==>> 釋放鎖。

    對於線程(內核控制路徑)的執行,咱們假設:

    a、系統存在一個全局時鐘,咱們討論的時間是離散的,不是連續的、數學意義上的時間。

    b、任意時刻,系統中活躍線程的總數目是有限的。

    c、線程的執行不會由於調度、缺頁異常等緣由無限期地被延遲。理論上,線程的執行能夠被系統無限期地延遲,所以任何互斥算法都有死鎖的危險。咱們但願排除系統的干擾,集中關注算法及具體實現自己。

    d、線程對共享資源的訪問在有限步驟內結束。

    e、當線程釋放鎖時,咱們但願:線程在有限步驟內釋放鎖。

    由於每一個程序步驟花費有限時間,因此若是知足上述 5 個條件,那麼:得到鎖的線程必然在有限時間內將鎖釋放掉。

    咱們說某個讀寫自旋鎖算法是正確的,是指該鎖知足以下三個屬性:

    a、互斥。任意時刻讀者和寫者不能同時訪問共享資源(即得到鎖);任意時刻只能有至多一個寫者訪問共享資源。

    b、讀者併發。在知足「互斥」的前提下,多個讀者能夠同時訪問共享資源。

    c、無死鎖(Freedom from Deadlock)。若是線程A試圖獲取鎖,那麼某個線程必將得到鎖,這個線程多是A本身;若是線程A試圖可是卻永遠沒有得到鎖,那麼某個或某些線程一定無限次地得到鎖。

    讀寫自旋鎖主要用於比較短小的代碼片斷,線程等待期間不該該進入睡眠狀態,由於睡眠 / 喚醒操做至關耗時,大大延長了得到鎖的等待時間,因此咱們要求:

    d. 忙等待。申請鎖的線程必須不斷地查詢是否發生退出等待的事件,不能進入睡眠狀態。這個要求只是描述線程執行鎖申請操做未成功時的行爲,並不涉及鎖自身的正確性。

    「無死鎖」屬性告訴咱們,從全局來看必定會有申請線程得到鎖,但對於某個或某些申請線程而言,它們可能永遠沒法得到鎖,這種現象稱爲飢餓(Starvation)。一種緣由源於計算機體系結構的特色:例如在使用基於單一共享變量的讀寫自旋鎖的多核系統中,若是鎖的持有者A所處的處理器和等待者B所處的處理器相鄰(也許還能共享二級緩存),B更容易獲知鎖被釋放,增大得到鎖的概率,而距離較遠的處理器上的線程則難與之PK,致使飢餓的發生。還有一種緣由源於設計策略,即讀寫自旋鎖刻意偏好某類角色的線程。

    爲了提升併發性,讀寫自旋鎖能夠選擇偏好讀者,即讀者可以優先得到鎖:

    a、讀者優先(Reader Preference)。若是鎖被讀者持有,那麼新來的讀者能夠當即得到鎖,無需忙等待。至於當鎖被「寫者持有」或「未被持有」時,新來的讀者是否能夠「阻塞」到正在等待的寫者以前,依賴於具體實現。

    若是讀者持續不斷地到來,等待的寫者極可能永遠沒法得到鎖,致使飢餓。在現實中,寫者的數目通常較讀者少量多,並且到來的頻率很低,所以讀寫自旋鎖能夠選擇偏好寫者來有效地緩解飢餓現象:

    b、寫者優先(Writer Preference)。寫者必須在後到的讀者 / 寫者以前得到鎖。由於在寫者以前到來的等待線程數目是有限的,因此能夠保證寫者的等待時間有個合理的上界。可是多個讀者之間得到鎖的順序不肯定,且先到的讀者不必定能在後到的寫者以前得到鎖。可見,若是寫者持續到來,讀者仍然可能產生飢餓。

    爲了完全消除飢餓現象,完美的讀寫自旋鎖還需知足下面任一屬性:

    c、無飢餓(Freedom from Starvation)。若是線程A試圖獲取鎖,那麼A一定能在有限時間內得到鎖。固然,這個「有限時間」也許至關漫長。

    d、公平(Fairness)。咱們把「鎖申請」操做的執行分爲兩個階段:準備階段(Doorway Section),能在有限程序步驟結束;等待階段(Waiting Section),也許永遠沒法結束等待階段一旦結束,線程即得到讀寫自旋鎖。若是線程A和B同時申請鎖,可是A的等待階段完成於B以前,那麼公平讀寫自旋鎖保證A在B以前得到鎖。若是A和B的等待階段在時間上有重疊,那麼它們得到鎖的順序是不肯定的。

    「公平」意味着申請鎖的線程一定在有限時間內得到鎖。若否則,假設A申請一個公平讀寫自旋鎖可是永遠不能得到,那麼在A以後完成準備階段的線程顯然也永遠不能得到鎖。而在A以前或「重疊」地完成等待階段的申請線程數目是 有限的,可見必然發生了「死鎖」,矛盾。同時這也說明釋放鎖的時間也是有限的。使用公平讀寫自旋鎖杜絕了飢餓現象的發生,若是假定線程訪問共享資源和釋放鎖的時間有一個合理的上界,那麼鎖申請線程的等待時間只與前面等待的線程數目有關,不依賴其它因素。

    P.S. 咱們也能夠本身去進行相關算法的設計與實現,好比說從博弈論和統計學的方向來思考(如利用機率進行讀寫者優先權分配等)。

    三、以自動機的觀點看讀寫自旋鎖

    前面關於讀寫自旋鎖的定義和描述雖然通俗易懂,可是並不精確,不少細節比較含糊。例如,讀者和寫者這種角色究竟是什麼含義?「先來」,「後到」,「新來」以及「同時到來」如何界定?申請和釋放鎖的過程究竟是怎樣的?

    如今,咱們集中精力思考一下讀寫自旋鎖究竟是什麼東西?讀寫自旋鎖其實就是一個有限狀態自動機(Finite State Machine)。自動機模型是一種強大的武器,能夠幫助咱們精確描述和理解各類算法。在給出嚴格定義以前,咱們先規範一下上節中出現的各類概念:

    a、首先,咱們把讀寫自旋鎖當作一個獨立的串行系統,線程對鎖函數的調用本質上是向其獨立地提交操做(Operation)。操做必須是基本的,語義清晰的。所謂「基本」,是指任一種類操做的執行效果都不能由其它一種或多種操做的執行累積而成。

    b、讀寫自旋鎖的函數調用的全過程如今能夠建模爲:

    線程提交了一個操做,而後等待讀寫自旋鎖在某個時刻選擇並執行該操做。咱們舉個讀者申請鎖的例子來具體說明。前面提到申請鎖分紅兩個階段,其中準備階段咱們認爲線程向讀寫自旋鎖提交了一個「讀者申請」的操做。讀者在等待階段不停地測試鎖的最新狀態,其實就是在等待讀寫自旋鎖的選擇。最終讀者在被許可的狀況下「原子地」更新鎖的狀態,從而得到鎖,說明讀寫自旋鎖在某個合適的時刻選擇並執行了該「讀者申請」的操做。一旦某個操做被選中,它將不受干擾地在有限時間內成功完成而且在執行過程當中讀寫自旋鎖不能選擇其它的操做。讀者可能會有些奇怪,直觀上鎖的釋放操做彷佛是當即執行,難道也須要「等待」麼?爲了保證鎖狀態的一致性(Consistency),某些實現的釋放函數使用了忙等待方式(參見本文的第一個實現),亦或因爲調度、處理器相對速度等緣由,總之鎖的釋放操做一樣有一個不肯定的等待執行的延時,所以能夠和其它操做統一到相同的執行模型中。在操做成功提交至執行完畢這段時間內,線程不能睡眠。

    c、某個線程對鎖的一次使用既能夠用讀者身份申請,也能夠用寫者身份申請,可是不能以兩種身份同時申請。可見「角色」實質上是線程分別提交了「讀者申請」或「寫者申請」的操做,而不能提交相似「讀者寫者同時申請」的操做。

    d、讀者 / 寫者能夠不停地到來 / 離去,這意味着線程可以持續地向讀寫自旋鎖提交各類操做,可是每次只能提交一個。只有當上次提交的操做被執行後,線程才被容許提交新操做。讀寫自旋鎖有能力知道某個操做是哪一個線程提交的。

    e、線程對鎖的使用必須採用前面說起的規範化流程,這是指線程必須提交配對的「申請」/「釋放」操做,即「申請」操做成功執行後,線程應當在有限時間內提交相應的「釋放」操做,且在此以前不許提交其它操做。

    f、關於讀者 / 寫者先來後到的順序問題,咱們轉換成肯定操做的提交順序。咱們認爲操做的提交效果是「瞬間」產生的,即便多個線程在所謂的「同一時刻」提交操做,這些操做彼此之間也有嚴格的前後順序,不存在兩個或多個操做是「同時」提交成功的。在現實中,提交顯然是須要必定時間的,不一樣線程的提交過程可能在時間上重疊,可是咱們認爲總能夠按照一種策略規定它們的提交順序,雖然這可能影響鎖的實際執行過程,但並不影響正確性;對於同一線程提交的各個操做,它們彼此之間顯然有着嚴格的時序關係,固然可以肯定提交順序。在此,咱們完全取消同時性的概念。

    令 A(t) 爲在時間段 (0, t] 內全部提交的操做構成的集合,A(t) 中的任兩個操做 o1和 o2,要麼 o1在 o2以前提交,要麼 o1在 o2以後提交,這種提交順序是一種全序關係(Total Order)。

    讀寫自旋鎖的形式化定義是一個 6 元組(Q,O,T,S,q0,qf),其中:

    Q = {q0,q1,…,qn},是一個有限集合,稱爲狀態集。狀態 qi描述了讀寫自旋鎖在某時刻t0所處於的一種真實情況。

    O = {o0,o1,…,om},是一個有限集合,稱爲操做種類集。

    T:Q x O -> Q 是轉移函數。T 是一個偏函數(Partial Function),即 T 的定義域是 Q x O 的子集。若是 T 在 (q, o) 有定義,即存在 q’ = T(q, o),咱們稱狀態 q 容許操做 o,在狀態 q 能夠執行操做 o,成功完成後讀寫自旋鎖轉換到狀態 q ’;反之,若是 T 在 (q, o) 沒有定義,咱們稱狀態 q 不容許操做 o,說明在狀態 q 不能執行操做 o,例如在鎖被寫者持有時,不能選擇 「讀者申請獲取鎖」的操做。

    S 是選擇函數,從已提交但未執行的操做實例集合中選擇一個讓讀寫自旋鎖執行,後文詳細討論。因爲任意時刻活躍線程的總數目是有限的,這個集合必然是有限集,所以咱們認爲 S 的每一次選擇能在有限步驟內結束。

    q0是初始狀態。

    qf是結束狀態,對於任一種操做 o,T 在 (qf, o) 無定義。也就是說到達該狀態後,讀寫自旋鎖再也不執行任何操做。

    咱們先畫出與定義等價的狀態圖,而後描述 6 元組具體是什麼。

    

    a、狀態圖中的每一個圓圈表明一個狀態。狀態集合Q至少應該有3個狀態:「未被持有」,「讀者持有」和「寫者持有」。由於可能執行「析構」操做,因此還須要增長一個結束狀態「中止」。除此以外不須要新的狀態。

    b、有向邊上的文字表明瞭一種操做。讀寫自旋鎖須要 6 種操做: 「初始化」、「析構」、「讀者申請」、 「讀者釋放」、 「寫者申請」和「寫者釋放」。操做後面括號內的文字,例如「最後持有者」,只是輔助理解,並不表示一種新的操做。

    c、有向邊及其上的操做定義了轉移函數。若是一條有向邊從狀態q指向q’,且標註的操做是 o,那麼代表狀態q容許操做o,且 q’ = T(q, o)。

    d、初始狀態是「未被持有」。

    e、結束狀態是「中止」,雙圓圈表示,該狀態不射出任何有向邊,代表此後鎖中止執行任何操做。

    結合狀態圖,咱們描述讀寫自旋鎖的工做原理:

    a、咱們規定在時刻 0 執行全局惟一一次的「初始化」操做,將鎖置爲初始狀態「未被持有」,圖中即爲那條沒有起點、標註「初始化「操做的有向邊。若是決定中止使用讀寫自旋鎖,則執行全局惟一一次的「析構」操做,將鎖置爲結束狀態「中止」。

    b、讀寫自旋鎖能夠被當作一個從初始狀態「未被持有」開始依次「吃」操做、不斷轉換狀態的串行機器。令 W(t) 爲時間段 (0, t] 內已提交但未執行的操做構成的集合,W(t) 是全部提交的操做集合 A(t) 的子集。在時刻t,若是鎖準備執行新的操做,假設當前處於狀態q,W(t)不是空集且存在狀態q容許的操做,那麼讀寫自旋鎖使用選擇函數S在W(t)集合中選出一個來執行,執行完成後將自身狀態置爲 q’ = T(q, o)。

    c、咱們稱序列 < qI1,oI1,qI2,oI2,…,oIn,qI(n+1)> 是讀寫自旋鎖在 t 時刻的執行序列,若是:

        ①. oIk是操做,1 <= k <= (n + 1) 且 oI1,oI2,…,oIn屬於集合 A(t)。

        ②. qIk是狀態,1 <= k <= (n + 1)。

        ③. 讀寫自旋鎖在 t 時刻的狀態是 qI(n+1)。

        ④. qI1= q0。

        ⑤. T 在 (qIk, oIk) 有定義,且 qI(k+1)= T(qIk, oIk)(1 <= k <= n)。

    c、假如執行序列的最後一個狀態 qI(n+1)不是結束狀態 qf,且在時刻 t0,W(t0) 爲空或者 qI(n+1)不容許 W(t0) 中的任一個操做 o,咱們稱讀寫自旋鎖在時刻t0處於潛在死鎖狀態。這並不代表讀寫自旋鎖真的死鎖了,由於隨後線程能夠提交新的操做,使其繼續工做下去。例如 qI(n+1)是「寫者持有」狀態,而 W(t0) 中全是「讀者申請」的操做。可是咱們知道鎖的持有者一會定在 t0以後的有限時間內提交「寫者釋放」操做,屆時讀寫自旋鎖能夠選擇執行它,將狀態置爲「未被持有」,而現存的「讀者申請」的操做隨後也可被執行了。

    d、若是存在t0 > 0,且對於任意 t >= t0,讀寫自旋鎖在時刻t都處於潛在死鎖狀態,咱們稱讀寫自旋鎖從時刻t0開始「死鎖」。

    如下是狀態圖正確性的證實概要:

    a、互斥。從圖可知,狀態「讀者持有」只能轉換到自身和「未被持有」,不能轉換到「寫者持有」,同時狀態「寫者持有」只能轉換到「未被持有」,不能轉換到「讀者持有」,因此鎖一旦被持有,另外一種角色的線程只有等到「未被持有」的狀態纔有機會得到鎖,所以讀者和寫者不可能同時得到鎖。狀態「寫者持有」不容許「寫者申請」操做,故而任什麼時候刻只有至多一個寫者得到鎖。

    b、讀者併發。狀態「讀者持有」容許「讀者申請」操做,所以能夠有多個讀者同時持有鎖。

    c、無死鎖。證實關於線程執行的 3 個假設。反證法,假設對任意t >= t0,鎖在時刻t都處於潛在死鎖狀態。令q爲t0時刻鎖的狀態,分 3 種狀況討論:

    「未被持有」。若是線程A在 t1 > t0 的時刻提交「讀者申請」或「寫者申請」的操做,那麼鎖在t1時刻並不處於潛在死鎖狀態。

    「讀者持有」。持有者必須在某個 t1 > t0 的時刻提交「讀者釋放」的操做,那麼鎖在t1時刻並不處於潛在死鎖狀態。

    「寫者持有」。持有者必須在某個 t1 > t0 的時刻提交「寫者釋放」的操做,那麼鎖在t1時刻並不處於潛在死鎖狀態。

    從線程 A 申請鎖的角度來看,由狀態圖知對於任意時刻 t0,不論鎖在 t0的狀態如何,總存在 t1> t0,鎖在時刻 t1一定處於「未被持有」的狀態,那麼在時刻 t1容許鎖申請操做,不是 A 就是別的線程得到鎖。若是 A 永遠不能得到鎖,說明鎖一旦處於「未被持有」的狀態,就選擇了別的線程提交的鎖申請操做,那麼某個或某些線程必然無限次地得到鎖。

    上面提到讀寫自旋鎖有一種選擇未執行的操做的能力,即選擇函數 S正是這個函數的差別,致使鎖展示不一樣屬性:

    a、讀者優先。在任意時刻 t,若是鎖處於狀態「讀者持有」,S 以大於 0 的機率選擇一個還沒有執行的「讀者申請」操做。這意味着:首先,即便有先提交但還沒有執行的「寫者申請」操做,「讀者申請」操做能夠被優先執行;其次,沒有刻意規定如何選「讀者申請」操做,所以多個「讀者申請」操做間的執行順序是不肯定的;最後,不排除連續選擇「讀者釋放」操做,使得鎖狀態迅速變爲「未被持有」,只不過這種概率很小。

    b、寫者優先。在任意時刻 t,若是 o1是還沒有執行的「寫者申請」操做,o2是還沒有執行的「讀者申請」或「寫者申請」操做,且 o1在 o2以前提交,那麼 S 保證必定在 o2以前選擇 o1。

    c、無飢餓。若是線程提交了操做 o,那麼 S 一定在有限時間內選擇 o。即存在時刻 t,讀寫自旋鎖在 t 的執行序列 < qI1,oI1,qI2,oI2,…,oIn,qI(n+1)> 知足 o = oIn。狹義上, o 限定爲「讀者申請」或「寫者申請」操做。

    d、公平。若是操做 o1在 o2以前提交,那麼 S 保證必定在在 o2以前選擇執行 o1。狹義上,o1和 o2限定爲「讀者申請」或「寫者申請」操做。

    四、讀寫自旋鎖的實現細節

    上文闡述的自動機模型是個抽象的機器,用於幫助咱們理解讀寫自旋鎖的工做原理,可是忽略了不少實現的關鍵細節:

    a、操做的執行者。若是按照前面的描述,爲讀寫自旋鎖建立專門的操做執行線程,那麼鎖的實際性能將會比較低下,所以咱們要求申請線程本身執行提交的操做。

    b、操做類別的區分。能夠提供多個調用接口來區分不一樣種類的操做,避免使用額外變量存放類別信息。

    c、肯定操做的提交順序,即線程的到來的前後關係。寫者優先和公平讀寫自旋鎖須要這個信息。能夠有 3 種方法:

        ①、假定系統有一個很是精確的實時時鐘,線程到來的時刻用於肯定順序。可是尋找直接後繼者比較困難,由於事先沒法預知線程到來的精確時間。

        ②、參考銀行的作法,即每一個到來的線程領取一張號碼牌,號碼的大小決定前後關係。

        ③、將線程組織成一個先進先出(FIFO)的隊列,具體實現能夠使用單向鏈表,雙向鏈表等。

    d、在狀態 q,肯定操做(線程)是否被容許執行。這有 2 個條件:首先 q 必須容許該操做;其次對於寫者優先和公平讀寫自旋鎖,不存在先提交但還沒有執行的寫者(讀者 / 寫者)申請操做。能夠有 3 種方法:

        ①、不停地主動查詢這 2 個條件。

        ②、被動等待前一個執行線程通知。

        ③、主動/被動相結合。

    e、選擇執行的線程。在狀態 q,若是存在多個被容許執行的線程,那麼它們必須達成一致(Consensus),保證只有一個線程執行成功,不然會破壞鎖狀態的一致性。有 2 種簡單方法:

        ①、互斥執行。原子指令(總線級別的互斥),或使用鎖(高級互斥原語)。

        ②、投機執行。線程無論三七二十一先執行再說,而後檢查是否成功。若是不成功,可能須要執行回滾操做。

    f、由於多個讀者能夠同時持有鎖,那麼讀者釋放鎖時,有可能須要知道本身是否是最後一個持有者(例如通知後面的寫者)。一個簡單的方法是用共享計數器保存當前持有鎖的讀者數目。若是咱們對具體數目並不關心,只是想知道計數器是大於 0 仍是等於 0,那麼用一種稱爲「非零指示器」(Non-Zero Indicator)的數據結構效果更好。還能夠使用雙向鏈表等特殊數據結構。

    五、爲讀獲取和釋放一個鎖

    read_lock宏,做用於讀/寫自旋鎖的地址*lock,與前面所描述的spin_lock宏很是類似。若是編譯內核時選擇了內核搶佔選項,read_lock宏執行與spin_lock()很是類似的操做,只有一點不一樣:該宏執行_raw_read_trylock( )函數以在第2步有效地獲取讀/寫自旋鎖。

void __lockfunc _read_lock(rwlock_t *lock)
{
preempt_disable();
rwlock_acquire_read(&lock->dep_map, 0, 0, _RET_IP_);
LOCK_CONTENDED(lock, _raw_read_trylock, _raw_read_lock);
}

    在沒有定義調試自旋鎖操做時rwlock_acquire_read爲空函數,咱們不去管它。因此_read_lock的實務函數是_raw_read_trylock:

# define _raw_read_trylock(rwlock) __raw_read_trylock(&(rwlock)->raw_lock)
static inline int __raw_read_trylock(raw_rwlock_t *lock)
{
atomic_t *count = (atomic_t *)lock;
atomic_dec(count);
if (atomic_read(count) >= 0)
return 1;
atomic_inc(count);
return 0;
}

    讀/寫鎖計數器lock字段是經過原子操做來訪問的。注意,儘管如此,但整個函數對計數器的操做並非原子性的,利用原子操做主要目的是禁止內核搶佔。例如,在用if語句完成對計數器值的測試以後並返回1以前,計數器的值可能發生變化。不過,函數可以正常工做:實際上,只有在遞減以前計數器的值不爲0或負數的狀況下,函數才返回1,由於計數器等於0x01000000表示沒有任何進程佔用鎖,等於Ox00ffffff表示有一個讀者,等於0x00000000表示有一個寫者(由於只可能有一個寫者)。

    若是編譯內核時沒有選擇內核搶佔選項,read_lock宏產生下面的彙編語言代碼:

movl $rwlp->lock,%eax
lock; subl $1,(%eax)
jns 1f
call _ _read_lock_failed
1:

    這裏,__read_lock_failed()是下列彙編語言函數:

_ _read_lock_failed:
lock; incl (%eax)
1: pause
cmpl $1,(%eax)
js 1b
lock; decl (%eax)
js _ _read_lock_failed
ret

    read_lock宏原子地把自旋鎖的值減1,由此增長讀者的個數。若是遞減操做產生一個非負值,就得到自旋鎖;不然就算做失敗。咱們看到lock字段的值由Ox00ffffff到0x00000000要減多少次纔可能出現負值,因此幾乎很難出現調用__read_lock_failed()函數的狀況。該函數原子地增長lock字段以取消由read_lock宏執行的遞減操做,而後循環,直到lock字段變爲正數(大於或等於0)。接下來,__read_lock_failed()又試圖獲取自旋鎖(正好在cmpl指令以後,另外一個內核控制路徑可能爲寫獲取自旋鎖)。

    釋放讀自旋鎖是至關簡單的,由於read_unlock宏只須要使用匯編語言指令簡單地增長lock字段的計數器:

lock; incl rwlp->lock

    以減小讀者的計數,而後調用preempt_enable()從新啓用內核搶佔。

    六、爲寫獲取或釋放一個鎖

    write_lock宏實現的方式與spin_lock()和read_lock()類似。例如,若是支持內核搶佔,則該函數禁用內核搶佔並經過調用_raw_write_trylock()當即得到鎖。若是該函數返回0,說明鎖已經被佔用,所以,該宏像前面博文描述的那樣從新啓用內核搶佔並開始忙等待循環。

#define write_lock(lock) _write_lock(lock)
void __lockfunc _write_lock(rwlock_t *lock)
{
preempt_disable();
rwlock_acquire(&lock->dep_map, 0, 0, _RET_IP_);
LOCK_CONTENDED(lock, _raw_write_trylock, _raw_write_lock);
}

    _raw_write_trylock()函數描述以下:

int _raw_write_trylock(rwlock_t *lock)
{
atomic_t *count = (atomic_t *)lock->lock;
if (atomic_sub_and_test(0x01000000, count))
return 1;
atomic_add(0x01000000, count);
return 0;
}
 
static __inline__ int atomic_sub_and_test(int i, atomic_t *v)
{
unsigned char c;
 
__asm__ __volatile__(
LOCK "subl %2,%0; sete %1"
:"=m" (v->counter), "=qm" (c)
:"ir" (i), "m" (v->counter) : "memory");
return c;
}

    函數_raw_write_trylock()調用tomic_sub_and_test(0x01000000, count)從讀/寫自旋鎖lock->lock的值中減去0x01000000,從而清除未上鎖標誌(看見沒有?正好是第24位)。若是減操做產生0值(沒有讀者),則獲取鎖並返回1;不然,函數原子地在自旋鎖的值上加0x01000000,以取消減操做。

    釋放寫鎖一樣很是簡單,由於write_unlock宏只需使用匯編語言指令:

lock; addl $0x01000000,rwlp

    把lock字段中的「未鎖」標識置位,而後再調用preempt_enable()。

    參考:

    http://www.ibm.com/developerworks/cn/linux/l-cn-rwspinlock1/#ibm-pcon
    http://www.ibm.com/developerworks/cn/linux/l-cn-rwspinlock2/index.html
    http://www.ibm.com/developerworks/cn/linux/l-cn-rwspinlock3/index.html


順序鎖

    當使用讀寫自旋鎖時,內核控制路徑發出的執行read_lock或write_lock操做的請求具備相同的優先級:讀者必須等待,直到寫操做完成。一樣的,寫者也必須等待,直到讀操做完成。

    Linux2.6中引入了順序鎖(seqlock),它與讀寫自旋鎖很是類似,只是它爲寫者賦予了更高的優先級:事實上,即便在讀者正在讀的時候也容許寫者繼續運行。這種策略的好處是寫者永遠不會等待(除非另外一個寫者正在寫),缺點就是有些時候讀者不得不反覆屢次讀相同的數據直到它得到有效的副本。

    順序鎖是對讀寫鎖的一種優化,對於順序鎖,讀者毫不會被寫者阻塞,也就說,讀者能夠在寫者對被順序鎖保護的共享資源進行寫操做時仍然能夠繼續讀,而沒必要等待寫者完成寫操做,寫者也不須要等待全部讀者完成讀操做纔去進行寫操做。可是,寫者與寫者之間仍然是互斥的,即若是有寫者在進行寫操做,其餘寫者必須自旋在那裏,直到寫者釋放了順序鎖。

    這種鎖有一個限制,它必需要求被保護的共享資源不含有指針,由於寫者可能使得指針失效,但讀者若是正要訪問該指針,將致使致命錯誤。

    若是讀者在讀操做期間,寫者已經發生了寫操做,那麼,讀者必須從新讀取數據,以便確保獲得的數據是完整的。

    這種鎖對於讀寫同時進行的機率比較小的狀況,性能是很是好的,並且它容許讀寫同時進行,於是更大地提升了併發性。

    順序鎖的結構以下:

typedef struct{
unsigned int sequence;
spinlock_t lock;
} seqlock_t;

    其中包含一個類型爲spinlock_t的lock字段和一個整型的sequence字段,第二個字段是一個順序計數器。每一個讀者都必須在讀數據先後兩次讀順序計數器,並檢查兩次讀到的數據是否相同,若是不相同,說明新的寫者已經開始寫並增長了順序計數器,所以暗示讀者剛讀到的數據是無效的。

    注意,並非每一種資源均可以使用順序鎖來保護,通常來講,必須知足下述條件時才能使用順序鎖:

    * 被保護的數據結構不包括被寫者修改和被讀者間接引用的指針(不然,寫者可能在讀者訪問時修改指針而不被發現);

    * 讀者的臨界區代碼沒有反作用(不然,多個讀者的操做會與單獨的讀操做有着不一樣的結果)。

    順序鎖的API以下:

void write_seqlock(seqlock_t *sl);

    寫者在訪問被順序鎖s1保護的共享資源前須要調用該函數來得到順序鎖s1。它實際功能上等同於spin_lock,只是增長了一個對順序鎖順序號的加1操做,以便讀者可以檢查出是否在讀期間有寫者訪問過。

void write_sequnlock(seqlock_t *sl);

    寫者在訪問完被順序鎖s1保護的共享資源後須要調用該函數來釋放順序鎖s1。它實際功能上等同於spin_unlock,只是增長了一個對順序鎖順序號的加1操做,以便讀者可以檢查出是否在讀期間有寫者訪問過。

    寫者使用順序鎖的模式以下:

write_seqlock(&seqlock_a);
//寫操做代碼塊
write_sequnlock(&seqlock_a);

    所以,對寫者而言,它的使用與spinlock相同。

int write_tryseqlock(seqlock_t *sl);

    寫者在訪問被順序鎖s1保護的共享資源前也能夠調用該函數來得到順序鎖s1。它實際功能上等同於spin_trylock,只是若是成功得到鎖後,該函數增長了一個對順序鎖順序號的加1操做,以便讀者可以檢查出是否在讀期間有寫者訪問過。

unsigned read_seqbegin(const seqlock_t *sl);

    讀者在對被順序鎖s1保護的共享資源進行訪問前須要調用該函數。讀者實際沒有任何獲得鎖和釋放鎖的開銷,該函數只是返回順序鎖s1的當前順序號。

int read_seqretry(const seqlock_t *sl, unsigned iv);

    讀者在訪問完被順序鎖s1保護的共享資源後須要調用該函數來檢查,在讀訪問期間是否有寫者訪問了該共享資源,若是是,讀者就須要從新進行讀操做,不然,讀者成功完成了讀操做。
所以,讀者使用順序鎖的模式以下:

do {
seqnum = read_seqbegin(&seqlock_a);
//讀操做代碼塊
...
} while (read_seqretry(&seqlock_a, seqnum));
write_seqlock_irqsave(lock, flags)

    寫者也能夠用該宏來得到順序鎖lock,與write_seqlock不一樣的是,該宏同時還把標誌寄存器的值保存到變量flags中,而且失效了本地中斷。

write_seqlock_irq(lock)

    寫者也能夠用該宏來得到順序鎖lock,與write_seqlock不一樣的是,該宏同時還失效了本地中斷。與write_seqlock_irqsave不一樣的是,該宏不保存標誌寄存器。

write_seqlock_bh(lock)

    寫者也能夠用該宏來得到順序鎖lock,與write_seqlock不一樣的是,該宏同時還失效了本地軟中斷。

write_sequnlock_irqrestore(lock, flags)

    寫者也能夠用該宏來釋放順序鎖lock,與write_sequnlock不一樣的是,該宏同時還把標誌寄存器的值恢復爲變量flags的值。它必須與write_seqlock_irqsave配對使用。

write_sequnlock_irq(lock)

    寫者也能夠用該宏來釋放順序鎖lock,與write_sequnlock不一樣的是,該宏同時還使能本地中斷。它必須與write_seqlock_irq配對使用。

write_sequnlock_bh(lock)

    寫者也能夠用該宏來釋放順序鎖lock,與write_sequnlock不一樣的是,該宏同時還使能本地軟中斷。它必須與write_seqlock_bh配對使用。

read_seqbegin_irqsave(lock, flags)

    讀者在對被順序鎖lock保護的共享資源進行訪問前也能夠使用該宏來得到順序鎖lock的當前順序號,與read_seqbegin不一樣的是,它同時還把標誌寄存器的值保存到變量flags中,而且失效了本地中斷。注意,它必須與read_seqretry_irqrestore配對使用。

read_seqretry_irqrestore(lock, iv, flags)

    讀者在訪問完被順序鎖lock保護的共享資源進行訪問後也能夠使用該宏來檢查,在讀訪問期間是否有寫者訪問了該共享資源,若是是,讀者就須要從新進行讀操做,不然,讀者成功完成了讀操做。它與read_seqretry不一樣的是,該宏同時還把標誌寄存器的值恢復爲變量flags的值。注意,它必須與read_seqbegin_irqsave配對使用。

    所以,讀者使用順序鎖的模式也能夠爲:

do {
seqnum = read_seqbegin_irqsave(&seqlock_a, flags);
//讀操做代碼塊
...
} while (read_seqretry_irqrestore(&seqlock_a, seqnum, flags));

    讀者和寫者所使用的API的幾個版本應該如何使用與自旋鎖的相似。

    若是寫者在操做被順序鎖保護的共享資源時已經保持了互斥鎖保護對共享數據的寫操做,即寫者與寫者之間已是互斥的,但讀者仍然能夠與寫者同時訪問,那麼這種狀況僅須要使用順序計數(seqcount),而沒必要要spinlock。

    順序計數的API以下:

unsigned read_seqcount_begin(const seqcount_t *s);

    讀者在對被順序計數保護的共享資源進行讀訪問前須要使用該函數來得到當前的順序號。

int read_seqcount_retry(const seqcount_t *s, unsigned iv);

    讀者在訪問完被順序計數s保護的共享資源後須要調用該函數來檢查,在讀訪問期間是否有寫者訪問了該共享資源,若是是,讀者就須要從新進行讀操做,不然,讀者成功完成了讀操做。

    所以,讀者使用順序計數的模式以下:

do {
seqnum = read_seqbegin_count(&seqcount_a);
//讀操做代碼塊
...
} while (read_seqretry(&seqcount_a, seqnum));
void write_seqcount_begin(seqcount_t *s);

    寫者在訪問被順序計數保護的共享資源前須要調用該函數來對順序計數的順序號加1,以便讀者可以檢查出是否在讀期間有寫者訪問過。

void write_seqcount_end(seqcount_t *s);

    寫者在訪問完被順序計數保護的共享資源後須要調用該函數來對順序計數的順序號加1,以便讀者可以檢查出是否在讀期間有寫者訪問過。

    寫者使用順序計數的模式爲:

write_seqcount_begin(&seqcount_a);
//寫操做代碼塊
write_seqcount_end(&seqcount_a);

    須要特別提醒,順序計數的使用必須很是謹慎,只有肯定在訪問共享數據時已經保持了互斥鎖才能夠使用。


讀-拷貝-更新(RCU)

    讀-拷貝-更新(RCU)是爲了保護在多數狀況下被多個CPU讀的數據結構而設計的另外一種同步技術。RCU容許多個讀者和寫者併發執行(相對於只容許一個寫者執行的順序鎖有了改進)。並且,RCU是否是用鎖的,就是說,它不使用被全部CPU共享的鎖或計數器,在這一點上與讀寫自旋鎖和順序鎖相比,RCU具備更大的優點。

    RCU是如何不使用共享數據結構而實現多個CPU同步呢?其關鍵思想以下所示:

    * RCU只保護被動態分配並經過指針引用的數據結構;

    * 在被RCU保護的臨界區中,任何內核控制路徑都不能睡眠。

    當內核控制路徑要讀取被RCU保護的數據結構時,執行宏rcu_read_lock(),它等同於preempt_disable()。接下來,讀者間接引用該數據結構所對應的內存單元並開始讀這個數據結構。讀者在完成對數據結構的讀操做以前,是不能睡眠的。用等同於preempt_enable()的宏rcu_read_unlock()標記臨界區的結束。

    因爲讀者幾乎不作任何事情來防止競爭條件的出現,因此寫者不得不作得更多一些。事實上,當寫者要更新數據結構是,它間接引用指針並生成整個數據結構的副本。接下來,寫者修改這個副本。因爲修改指針值的操做是一個原子操做,因此舊副本和新副本對每一個讀者和寫者是可見的,在數據結構中不會出現數據崩潰。儘管如此,還須要內存屏障來保證:只有在數據結構被修改以後,已更新的指針對其餘CPU纔是可見的。若是把自旋鎖與RCU結合起來以禁止寫者的併發執行,就隱含地引入了這樣的內存屏障。

    然而,使用RCU的真正困難在於:寫者修改指針時不能當即釋放數據結構的舊副本。實際上,寫者開始修改時,正在訪問數據結構的讀者可能還在讀舊副本。只有在CPU上的全部讀者都執行完宏rcu_read_unlock()以後,才能夠釋放舊副本。內核要求每一個潛在的讀者在下面的操做以前執行rcu_read_unlock()宏:

    * CPU執行進程切換;

    * CPU開始在用戶態執行;

    * CPU執行空循環。

    對於上述每種狀況,咱們說CPU已通過了靜止狀態(quiescent state)。

    寫者調用call_rcu()來釋放數據結構的舊副本。該函數把回調函數和其參數的地址存放在rcu_head描述符中,而後把描述符插入回調函數的每一個CPU鏈表中。內核沒通過一個時鐘滴答就週期性的檢查本地CPU是否通過了一個靜止狀態。若是全部CPU都通過了靜止狀態,本地tasklet就執行鏈表中的全部回調函數。

    RCU是Linux 2.6中新加的功能,經常使用在網絡層和虛擬文件系統中。


信號量

    信號量,從本質上說,它實現了一個加鎖原語,即讓等待者睡眠,直到等待的資源變爲空閒。

    實際上,Linux提供兩種信號量:

    * 內核信號量,由內核控制路徑使用。

    * System V IPC信號量,由用戶態進程使用。

    內核信號量相似於自旋鎖,由於當鎖關閉着時,它不容許內核控制路徑繼續進行。然而,當內核控制路徑試圖獲取內核信號量所保護的忙資源時,相應的進程被掛起。只有在資源被釋放時,相應的進程纔再次變爲可運行的。所以,只有能夠睡眠的函數才能獲取信號量:中斷處理程序和可延遲函數都不能使用內核信號量

    信號量由結構semaphore描述,它基於自旋鎖改進而成,其包括一個自旋鎖、信號量計數器和一個等待隊列。用戶程序只能調用信號量API函數,而不能直接訪問semaphore結構。其結構定義以下(include/asm-i386/semaphore.h)

struct semaphore {
atomic_t count;
int sleepers;
wait_queue_head_t wait;
};
    注:在3.0版本以後,semaphore的定義和使用發生了很大的變化。例如:semaphore結構體的定義改進以下(include/linux/semaphore.h):
struct semaphore{
spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};

    在這裏,咱們主要關注linux 2.6版本中的信號量機制。

    現對linux 2.6版本中的信號量結構體字段進行詳細說明:

    ## count字段:存放atomic_t類型的一個值,若是該值大於0,那麼資源就是空閒的,也就是說,該資源如今能夠使用。相反,若是count等於0,那麼信號量是忙的,但沒有進程等待這個被保護的資源。最後,若是count爲負數,那麼資源是不可用的,並至少一個進程在等待該資源。

    ## wait字段:存放等待隊列鏈表的地址,當前等待資源的全部睡眠進程都放在這個鏈表中。固然,若是count大於或等於0,等待隊列就爲空。

    注:wait_queue_head_t結構體:

struct __wait_queue_head {
wq_lock_t lock;
struct list_head task_list;
#if WAITQUEUE_DEBUG
long __magic;
long __creator;
#endif
};
 
typedef struct __wait_queue_head wait_queue_head_t;

    ## sleepers字段:存放一個標誌,表示是否有一些進程在信號量上睡眠。

    在具體的操做中,信號量提供了許多的API供程序調用:                      

    能夠用init_MUTEX()和init_MUTEX_LOCKED()函數來初始化互斥訪問所需的信號量:這兩個函數分別把count字段設置成1(互斥資源訪問的資源空閒)和0(對信號量進行初始化的進程當前互斥訪問的資源忙)。

static inline void sema_init (struct semaphore *sem, int val)
{
/*
* *sem = (struct semaphore)__SEMAPHORE_INITIALIZER((*sem),val);
*
* i'd rather use the more flexible initialization above, but sadly
* GCC 2.7.2.3 emits a bogus warning. EGCS doesn't. Oh well.
*/
atomic_set(&sem->count, val);
sem->sleepers = 0;
init_waitqueue_head(&sem->wait);
}
 
static inline void init_MUTEX (struct semaphore *sem)
{
sema_init(sem, 1);
}
 
static inline void init_MUTEX_LOCKED (struct semaphore *sem)
{
sema_init(sem, 0);
}

    宏DECLARE_MUTEX和DECLARE_MUTEX_LOCKED完成同上的一樣的功能,但它們也靜態分配semaphore結構的變量。固然,也能夠把信號量中的count字段初始化爲任意的正整數n,在這種狀況下,最多有n個進程能夠併發地訪問這個資源。

#define __SEMAPHORE_INITIALIZER(name, n) \
{ \
.count = ATOMIC_INIT(n), \
.sleepers = 0, \
.wait = __WAIT_QUEUE_HEAD_INITIALIZER((name).wait) \
}
 
#define __MUTEX_INITIALIZER(name) \
__SEMAPHORE_INITIALIZER(name,1)
 
#define __DECLARE_SEMAPHORE_GENERIC(name,count) \
struct semaphore name = __SEMAPHORE_INITIALIZER(name,count)
 
#define DECLARE_MUTEX(name) __DECLARE_SEMAPHORE_GENERIC(name,1)
 
#define DECLARE_MUTEX_LOCKED(name) __DECLARE_SEMAPHORE_GENERIC(name,0)

    &*& 獲取和釋放信號量

    獲取信號量函數:static inline void down(struct semaphore * sem);

    釋放信號量函數:static inline void up(struct semaphore * sem);

    首先,從如何釋放信號量開始討論,up()函數本質上等價於下面的彙編語言片斷:

movl $sem->count, %ecx
lock; incl (%ecx)
jg lf # 大於0跳轉至1標記處
lea %ecx, %eax # lea(Load effect address): 取有效地址,也就是取偏移地址---- lea 目的 源:即將源中的地址傳給目的.
pushl %edx # 保存現場
pushl %ecx
call __up # 隊列釋放(喚醒隊列中的睡眠進程)
popl %ecx
popl %edx
 
1:
    其中,__up()是下列C函數:
fastcall void __up(struct semaphore *sem)
{
wake_up(&sem->wait);
}

    up()函數增長*sem信號量count字段的值,而後,檢查它的值是否大於0。count的增長及其後所測試的標誌的設置都必須原子地執行;不然,另外一個內核控制路徑有可能同時訪問這個字段的值,這會致使災難性的後果。若是count大於0,說明沒有進程在等待隊列上睡眠,所以,就什麼事都不作。不然,調用__up()函數以喚醒一個睡眠進程。

    相反,當進程但願獲取內核信號量鎖時,就調用down()函數。down()的實現至關棘手,但本質上等價於下面代碼:

down:
movl $sem->count, %ecx
lock; decl (%ecx)
jns 1f #JNS(jump if not sign),彙編語言中的條件轉移指令.結果爲正則轉移.
lea %ecx, %eax
pushl %edx
pushl %ecx
call __down
popl %ecx
popl %edx
1:
    這裏,__down()是下列C函數:
fastcall void __sched __down(struct semaphore * sem)
{
struct task_struct *tsk = current;
DECLARE_WAITQUEUE(wait, tsk);
unsigned long flags;
 
tsk->state = TASK_UNINTERRUPTIBLE;
spin_lock_irqsave(&sem->wait.lock, flags);
add_wait_queue_exclusive_locked(&sem->wait, &wait);
 
sem->sleepers++;
for (;;) {
int sleepers = sem->sleepers;
 
/*
* Add "everybody else" into it. They aren't
* playing, because we own the spinlock in
* the wait_queue_head.
*/
if (!atomic_add_negative(sleepers - 1, &sem->count))  
// atomic_add_negative(i,v):把i加到*v,若是結果爲負,返回1,若是結果爲0或正數,返回0.
// 同時注意該函數會將sleepers-1加到sem->count並保存該值至sem->count,這就會有這樣一個細節:
// 若是有睡眠進程,sleepers=1, sleepers++;=>>2, 這樣sleepers-1+sem->count就至關於恢復了前面的sem->count
// (注意剛開始時由彙編語言致使sem->count--,如今經過+1即保證了count的取值範圍即爲1/0/-1).
{
sem->sleepers = 0;
break;
}
sem->sleepers = 1; /* us - see -1 above */
spin_unlock_irqrestore(&sem->wait.lock, flags);
 
schedule();
 
spin_lock_irqsave(&sem->wait.lock, flags);
tsk->state = TASK_UNINTERRUPTIBLE;
}
 
remove_wait_queue_locked(&sem->wait, &wait);
wake_up_locked(&sem->wait);
spin_unlock_irqrestore(&sem->wait.lock, flags);
tsk->state = TASK_RUNNING;
}

    down()函數減小*sem信號量的count字段的值,而後檢查該值是否爲負。該值的減小和檢查過程都必須是原子的。若是count大於等於0,當前進程得到資源並繼續正常執行。不然,count爲負,當前進程必須掛起。把一些寄存器的內容保存在棧中,而後調用__down()。

    從本質上說,__down()函數把當前進程的狀態從TASK_RUNNING變爲TASK_UNINTERRUPTIBLE,並把進程放在信號量的等待隊列。該函數在訪問信號量結構的字段以前,要得到用來保護信號量等待隊列的sem->wait.lock自旋鎖,並禁止本地中斷。一般當插入和刪除元素時,等待隊列函數根據須要獲取和釋放等待隊列的自旋鎖。函數__down()也用等待隊列自旋鎖來保護信號量數據結構的其餘字段,以使在其餘CPU上運行的進程不能讀或修改這些字段。最後,__down()使用等待隊列函數的"lock"版本,它假設在調用等待隊列函數以前已經得到了自旋鎖。

    __down()函數的主要任務是掛起當前進程,直到信號量被釋放。然而,要實現這種想法並不容易。爲了更容易地理解代碼,要牢記若是沒有進程在信號量等待隊列上睡眠,則信號量sleepers字段一般被置爲0,不然被置爲1。

    考慮如下幾種典型的狀況:

    * MUTEX信號量打開(count=1,sleepers=0)

    down宏僅僅把count字段置爲0,並跳到主程序的下一條指令;所以,__down()函數根本不執行。

    * MUTEX信號量關閉,沒有睡眠進程(count=0,sleepers=0)

    down宏減count並將count字段置爲-1 且sleepers字段置爲0來調用__down()函數。在循環體的每次循環中,該函數檢查count字段是否爲負。

        # 若是count字段爲負,__down()就調用schedule()掛起當前進程。count字段仍然設置爲-1,而sleepers字段置爲1,。隨後,進程在這個循環內核恢復本身的運行並又進行測試。

        # 若是count字段不爲負,則把sleepers置爲0,並從循環退出。__down()試圖喚醒信號量等待隊列中的另外一個進程,並終止保持的信號量。在退出時,count字段和sleepers字段都置爲0,這表示信號量關閉且沒有進程等待信號量。

    * MUTEX信號量關閉,有其餘睡眠進程(count=-1,sleepers=1)

    down宏減count並將count字段置爲-2且sleepers字段置爲1來調用__down()函數。該函數暫時把sleepers置爲2,而後經過把sleepers - 1 加到count來取消down宏執行的減操做。同時,該函數檢查count是否依然爲負。

        # 若是count字段爲負,__down()函數把sleepers從新設置爲1,並調用schedule()函數掛起當前進程。count字段仍是置爲-1,而sleepers字段置爲1.

        # 若是count字段不爲負,__down()函數吧sleepers置爲0,試圖喚醒信號量等待隊列上的另外一個進程,並退出持有的信號量。在退出時,count字段置爲0且sleepers字段置爲0。

    其餘函數:

    down_trylock()函數:適用於異步處理程序。該函數和down()函數除了對資源繁忙狀況的處理有所不一樣以外,其餘都是相同的。在資源繁忙時,該函數會當即返回,而不是讓進程去睡眠。

    down_interruptible函數:該函數普遍使用在設備驅動程序中,由於若是進程接收了一個信號但在信號量上被阻塞,就容許進程放棄「down」操做。

    另外,由於進程一般發現信號量處於打開狀態,所以,就能夠優化信號量函數。尤爲是,若是信號量等待隊列爲空,up()函數就不執行跳轉指令。一樣,若是信號量是打開的,down()函數就不執行跳轉指令。信號量實現的複雜性是因爲極力在執行流的主分支上避免費時的指令而形成的。

讀寫信號量

    ​讀寫信號量相似於前面的「讀寫自旋鎖」,但不一樣的是:在信號量再次變爲打開以前,等待進程掛起而不是自旋。不少內核控制路徑爲讀能夠併發地獲取讀寫信號量,可是,任何寫者內核控制路徑必須有對被保護資源的互斥訪問。所以,只有在沒有內核控制路徑爲讀訪問或寫訪問持有信號量時,才能夠爲寫獲取信號量。讀寫信號量能夠提升內核中的併發度,並改善了整個系統的性能。

    ​內核以嚴格的FIFO順序處理等待讀寫信號量的全部進程。若是讀者或寫者進程發現信號量關閉,這些進程就被插入到信號量等待隊列鏈表的末尾。當信號量被釋放時,就檢查處於等待隊列鏈表第一個位置的進程。第一個進程常被喚醒。若是是一個寫者進程,等待隊列上其餘的進程就繼續睡眠。若是是一個讀者進程,那麼緊跟第一個進程的其餘全部讀者進程也被喚醒並得到鎖。不過,在寫者進程以後排隊的讀者進程繼續睡眠。

    每一個讀寫信號量都是由rw_semaphore結構描述的,它包含下列字段:

/*
* the semaphore definition
*/
struct rw_semaphore {
signed long count;
#define RWSEM_UNLOCKED_VALUE 0x00000000
#define RWSEM_ACTIVE_BIAS 0x00000001
#define RWSEM_ACTIVE_MASK 0x0000ffff
#define RWSEM_WAITING_BIAS (-0x00010000)
#define RWSEM_ACTIVE_READ_BIAS RWSEM_ACTIVE_BIAS
#define RWSEM_ACTIVE_WRITE_BIAS (RWSEM_WAITING_BIAS + RWSEM_ACTIVE_BIAS)
spinlock_t wait_lock;
struct list_head wait_list;
#if RWSEM_DEBUG
int debug;
#endif
};

    ## count字段:存放兩個16位計數器。其中,最高16位計數器以二進制補碼形式存放非等待寫者進程的總數(0或1)和等待的寫內核控制路徑數。最低16位計數器存放非等待的讀者和寫者進程的總數。

    ## wait_list字段:指向等待進程的鏈表。鏈表中的每一個元素都是一個rwsem_waiter結構,該結構包含一個指針和一個標誌,指針指向睡眠進程的描述符,標誌表示進程是爲讀須要信號量仍是爲寫須要信號量。

    ## wait_lock字段:一個自旋鎖,用於保護等待隊列鏈表和rw_semaphore結構自己。

    init_rwsem()函數初始化rw_semaphore結構,即把count字段置爲0,wait_lock自旋鎖置爲未鎖,而把wait_list置爲空鏈表。

    down_read()和down_write()函數分別爲讀或寫獲取信號量。一樣,up_read()和up_write()函數爲讀或寫釋放之前獲取的讀寫信號量。down_read_trylock()和down_write_trylock()函數分別相似於down_read()和down_write()函數,可是,在信號量忙的狀況下,它們不阻塞進程。最後,函數downgrade_write()自動把寫鎖轉換成讀鎖。

    對於前面說起的5個函數,其實現思想同信號量有着相同的設計思想。


禁止本地中斷

    ​確保一組內核語句被當作一個臨界區處理的主要機制之一就是中斷禁止。即便當硬件設備產生了一個IRQ信號時,中斷禁止也讓內核控制路徑繼續執行。所以,這就提供了一種有效的方式,確保中斷處理程序訪問的數據結構也受到保護。然而,禁止本地中斷並不保護運行在另外一個CPU上的中斷處理程序對數據結構的併發訪問,所以,在多處理器系統上,禁止本地中斷一般與自旋鎖結合使用。

    宏local_irq_disable()使用cli彙編語言指令關閉本地CPU上的中斷,宏local_irq_enable()函數使用sti彙編語言指令打開被關閉的中斷。彙編語言指令cli和sti分別清除和設置eflags控制寄存器的IF標誌。若是eflags寄存器的IF標誌被清零,宏irqs_disabled()產生等於1的值;若是IF標誌被設置,該宏也產生爲1的值。

    保存和恢復eflags的內容是分別經過宏local_irq_save()和local_irq_restore()宏來實現的。local_irq_save宏把eflags寄存器的內容拷貝到一個局部變量中,隨後用cli彙編語言指令把IF標誌清零。在臨界區的末尾,宏local_irq_restore恢復eflags原來的內容。所以,只有在這個控制路徑發出cli彙編指令以前,中斷被激活的狀況下,中斷才處於打開狀態。

/* interrupt control.. */
#define local_save_flags(x) do { typecheck(unsigned long,x); __asm__ __volatile__("pushfl ; popl %0":"=g" (x): /* no input */); } while (0)
#define local_irq_restore(x) do { typecheck(unsigned long,x); __asm__ __volatile__("pushl %0 ; popfl": /* no output */ :"g" (x):"memory", "cc"); } while (0)
#define local_irq_disable() __asm__ __volatile__("cli": : :"memory")
#define local_irq_enable() __asm__ __volatile__("sti": : :"memory")
/* For spinlocks etc */
#define local_irq_save(x) __asm__ __volatile__("pushfl ; popl %0 ; cli":"=g" (x): /* no input */ :"memory")


禁止和激活可延遲函數

    可延遲函數可能在不可預知的時間執行(其實是在硬件中斷程序結束時)。所以,必須保護可延遲函數訪問的數據結構使其避免競爭條件。


    咱們前面在」中斷處理」提到,在由內核執行的幾個任務之間有些不是緊急的的;在必要狀況下它們能夠延遲一段時間。一箇中斷處理程序的幾個中斷服務例程之間是串行執行的,而且一般在一箇中斷的處理程序結束前,不該該再次出現此中斷。相反,可延遲函數能夠在開中斷的狀況下執行。把可延遲函數從中斷處理程序中抽出來有助於使內核保持較短的響應時間。這對於那些指望它們的中斷能在幾毫秒內獲得處理的」急迫」應用來講是很是重要的。

    Linux2.6是經過兩種非緊迫、可中斷內核函數來實現這種機制的:可延遲函數和工做隊列。

    軟中斷和tasklet有密切的關係,tasklet是在軟中斷之上實現。事實上,出如今內核代碼中的術語」軟中斷」經常表示可延遲函數的全部種類。另一種被普遍使用的術語是」中斷上下文」:表示內核當前正執行一箇中斷處理程序或一個可延遲函數。

    軟中斷的分配是靜態的,而tasklet的分配和初始化能夠在運行是進行。軟中斷能夠併發地運行在多個CPU上。所以,軟中斷是可重入函數並且必須明確地使用自旋鎖保護其數據結構。tasklet沒必要擔憂這些問題,由於內核對tasklet的執行了更加嚴格的控制。相同類型的tasklet老是被串行地執行,換句話說就是:不能在兩個CPU上同時運行相同類型的tasklet。可是,類型不一樣的tasklet能夠在幾個CPU上併發執行。tasklet的串行化使tasklet函數沒必要是可重入的,所以簡化了設備驅動程序開發者的工做。

    可延遲函數實現見實時測量一節。

    通常而言,在可延遲函數上能夠執行四種操做:

    初始化

    定義一個新的可延遲函數,這個操做一般在內核自身初始化或加載模塊時進行。

    激活

    標記一個可延遲函數爲」掛起」,激活能夠在任什麼時候候進行。

    屏蔽

    有選擇地屏蔽一個可延遲函數,這樣,即便它被激活,內核也不執行它。禁止可延遲函數有時是必要的。

    執行

    執行一個掛起的可延遲函數和同類型的其它全部掛起的可延遲函數,執行是在特定的時間進行的。

    激活和執行老是捆綁在一塊兒,由給定CPU激活的一個可延遲函數必須在同一個CPU上執行。把可延遲函數綁定在激活CPU上從理論上說能夠充分利用CPU的硬件高速緩存。畢竟,能夠想象,激活的內核線程訪問的一些數據結構,可延遲函數也可能會使用。而後,當可延遲函數運行時,由於它的執行能夠延遲一段時間,所以相關高速緩存行極可能就再也不在高速緩存中了。此外,把一個函數綁定在一個CPU上老是有潛在」危險的」操做,由於一個CPU可能忙死而其它CPU又無所事事。


    禁止可延遲函數在一個CPU上執行的一種簡單方式就是禁止在那個CPU上的中斷。由於沒有中斷處理程序被激活,所以,軟中斷操做就不能異步地開始。

    然而,內核有時須要只禁止可延遲函數而不由止中斷。經過操縱當前thread_info描述符preempt_count字段中存放的軟中斷計數器,能夠在本地CPU上激活或禁止可延遲函數。若是軟中斷計數器是正數,do_softirq()函數就不會執行軟中斷,並且,由於tasklet在軟中斷以前被執行,把這個計數器設置爲大於0的值,由此禁止了在給定CPU上的全部可延遲函數和軟中斷的執行。

    宏local_bh_disable()給本地CPU的軟中斷計數器加1,而函數local_bh_enable()從本地CPU的軟中斷計數器中減掉1。內核所以能使用幾個嵌套的local_bh_disable調用,只有宏local_bh_enable與第一個local_bh_disable調用相匹配,可延遲函數纔再次被激活。

    遞減軟中斷計數器後,local_bh_enable()執行兩個重要的操做以有助於保證適時地執行長時間等待的線程:

    一、檢查本地CPU的preempt_count字段中硬中斷計數器和軟中斷計數器,若是這兩個計數器的值都等於0並且有掛起的軟中斷要執行,就調用do_softirq()來激活這些軟中斷。

    二、檢查本地CPU的TIF_NEED_RESCHED標誌是否被設置,若是是,說明進程切換請求是掛起的,所以調用preempt_schedule()函數。


對內核數據結構的同步訪問

    ​在前文,咱們詳細介紹了內核所提供的幾種同步原語以保護共享數據結構避免競爭條件。系統性能可能隨所選擇的同步原語種類的不一樣而有很大變化。一般狀況下,內核開發者採用下述由經驗獲得的法則:把系統中的併發度保持在儘量高的程度。

    系統中的併發度取決於兩個主要因素:

    * 同時運轉的I/O設備數;

    * 進行有效工做的CPU數。

    爲了使I/O吞吐量最大化,應該使中斷禁止保持在很短的時間。當中斷被禁止時,由I/O設備產生的IRQ被PIC暫時忽略,所以,就沒有新的活動在這種設備上開始。

    爲了有效地利用CPU,應該儘量避免使用基於自旋鎖的同步原語。當一個CPU執行緊指令循環等待自旋鎖打開時,是在浪費寶貴的機器週期。同時,因爲自旋鎖對硬件高速緩存的影響而使其對系統的總體性能產生不利影響。

    在下列兩個例子所展現的狀況下,便可以保持較高的併發度,同時也可以達到同步:

    * 共享的數據結構是一個單獨的整數值,能夠把它聲明爲atomic_t類型並使用原子操做對其更新。原子操做比自旋鎖和中斷禁止操做都快,只有在幾個內核控制路徑同時訪問這個數據結構時速度纔會慢下來。

    * 把一個元素插入到共享鏈表的操做毫不是原子的,由於這至少涉及兩個指針賦值。不過,內核有時並不用鎖或禁止中斷就能夠執行這種插入操做。考慮這樣一種狀況,系統調用服務例程把新元素插入到一個簡單鏈表中,而中斷處理程序或可延遲函數異步地查看該鏈表。

    在C語言中,插入是經過下面的指針賦值來實現的:

new->next = list_element->next;
list_element->next = new;

    在彙編語言中,插入簡化爲兩個連續的原子指令。第一條指令創建new元素的next指針,但不修改鏈表。所以,若是中斷處理程序在第一條指令和第二條指令執行的中間查看這個鏈表,看到的就是沒有新元素的鏈表。若是該處理程序在第二條指令執行以後查看鏈表,就會看到有新元素的鏈表。關鍵是,在任一種狀況下,鏈表都是一致的且處於未損壞狀態。然而,只有在中斷處理程序不修改鏈表的狀況下才能確保這種完整性。若是修改了鏈表,那麼在new元素內剛剛設置的next指針就可能變爲無效。

    然而,上面的兩個賦值操做的順序不能被編譯器或CPU控制器改變,因此能夠添加內存屏障來實現寫順序控制:

new->next = list_element->next;
wmb();
list_element->next = new;


在自旋鎖、信號量及中斷禁止之間選擇

    前面咱們介紹了兩個例子,這兩個例子實現了高併發高同步,可是,實際遇到的問題每每複雜許多,這個時候,咱們就必須使用信號量、自旋鎖、中斷禁止和軟中斷禁止來實現併發同步。通常來講,同步原語的選擇取決於訪問數據結構的內核控制路徑的種類。

訪問數據結構的內核控制路徑 單處理器保護 多處理器進一步保護
異常 信號量
中斷 本地中斷禁止 自旋鎖
可延遲函數 無或自旋鎖(見下表)
異常與中斷 本地中斷禁止 自旋鎖
異常與可延遲函數 本地軟中斷禁止 自旋鎖
中斷與可延遲函數 本地中斷禁止 自旋鎖
異常、中斷與可延遲函數 本地中斷禁止 自旋鎖

    一、保護異常所訪問的數據結構

    當一個數據結構僅由異常處理程序訪問時,競爭條件一般是易於理解也易於避免的。最多見的產生同步問題的異常就是系統調用服務例程。在這種狀況下,CPU運行在內核態而爲用戶態程序提供服務。所以,僅由異常訪問的數據結構一般表示一種資源,能夠分配給一個或多個進程。

    競爭條件能夠經過信號量避免,由於信號量原語容許進程睡眠到資源變爲可用。注意,信號量工做方式在單處理器系統和多處理器系統上徹底相同。

    內核搶佔不會引發太大的問題。若是一個擁有信號量的進程是能夠被搶佔的,運行在同一個CPU上的新進程就可能試圖得到這個信號量。在這種狀況下,讓新進程處於睡眠狀態,並且原來擁有信號量的進程最終會釋放信號量。只有在訪問每CPU變量的狀況下,必須顯式地禁用內核搶佔。

    二、保護中斷所訪問的數據結構

    假定一個數據結構僅被中斷處理程序的「上半部」訪問,那麼,每一箇中斷處理程序都相對本身串行地執行,也就是說,中斷服務例程自己不能同時屢次運行,所以,訪問數據結構就無需任何同步原語。

    可是,若是多箇中斷處理程序訪問一個數據結構,狀況就有所不一樣了。一個處理程序能夠中斷另外一個處理程序,不一樣的中斷處理程序能夠在多處理器系統上同時運行。沒有同步,共享的數據結構就很容易被破壞。

    在單處理器系統上,必須經過在中斷處理程序的全部臨界區上禁止中斷來避免競爭條件。只能用這種方式進行同步,由於其餘的同步原語都不能完成這件事。信號量可以阻塞進程,所以,不能用在中斷處理程序上。另外一方面,自旋鎖可能使系統凍結:若是訪問數據結構的處理程序被中斷,它就不能釋放鎖,所以,新的中斷處理程序在自旋鎖的緊循環上保持等待。

    在多處理器系統上,其要求更加苛刻。不能簡單地經過禁止本地中斷來避免競爭條件。事實上,即便在一個CPU上禁止了中斷,中斷處理程序還能夠在其餘CPU上執行。避免競爭條件的最簡單的方法是禁止本地中斷,並獲取保護數據結構的自旋鎖或讀寫自旋鎖。注意,這些附加的自旋鎖不能凍結系統,由於即便中斷處理程序發現鎖關閉,在另外一個CPU上擁有鎖的中斷處理程序最終也會釋放這個鎖。

    Linux使用了幾個宏,把本地中斷禁止與激活同自旋鎖結合起來。

    

    三、保護由可延遲函數訪問的數據結構

    只被可延遲函數訪問的數據結構的保護主要取決於可延遲函數的種類。

    在單處理器系統上,不存在競爭條件,這是由於可延遲函數的執行老是在一個CPU上串行執行,也就是說,一個可延遲函數不會被另外一個可延遲函數中斷。所以,不須要同步原語。

    在多處理器系統上,幾個可延遲函數的併發運行致使了競爭的存在。

    表:在SMP上可延遲函數訪問的數據結構所需的保護

訪問數據結構的可延遲函數 保護
軟中斷 自旋鎖
一個tasklet
多個tasklet 自旋鎖

    由軟中斷訪問的數據結構必須受到保護,一般使用自旋鎖進行保護,由於一個軟中斷能夠在兩個或多個CPU上併發運行。相反,僅由一個tasklet訪問的數據結構不須要保護,由於同種tasklet不能併發運行,可是,若是數據結構被幾種tasklet訪問,那麼,就必須對數據結構進行保護。


    爲何要使用軟中斷?

    軟中斷做爲下半部機制的表明,是隨着SMP(share memory processor)的出現應運而生的,它也是tasklet實現的基礎(tasklet實際上只是在軟中斷的基礎上添加了必定的機制)。它的特性包括:

    a)產生後並非立刻能夠執行,必需要等待內核的調度才能執行。軟中斷不能被本身打斷,只能被硬件中斷打斷(上半部)。

    b)能夠併發運行在多個CPU上(即便同一類型的也能夠)。因此軟中斷必須設計爲可重入的函數(容許多個CPU同時操做),所以也須要使用自旋鎖來保護其數據結構。

    爲何要使用tasklet?(tasklet和軟中斷的區別)

    因爲軟中斷必須使用可重入函數,這就致使設計上的複雜度變高,做爲設備驅動程序的開發者來講,增長了負擔。而若是某種應用並不須要在多個CPU上並行執行,那麼軟中斷實際上是沒有必要的。所以誕生了彌補以上兩個要求的tasklet。它具備如下特性:

    a)一種特定類型的tasklet只能運行在一個CPU上,不能並行,只能串行執行。

    b)多個不一樣類型的tasklet能夠並行在多個CPU上。

    c)軟中斷是靜態分配的,在內核編譯好以後,就不能改變。但tasklet就靈活許多,能夠在運行時改變(好比添加模塊時)。

    tasklet是在兩種軟中斷類型的基礎上實現的,所以若是不須要軟中斷的並行特性,tasklet就是最好的選擇。


    四、保護由異常和中斷訪問的數據結構

    對於由異常處理程序(如系統調用服務例程)和中斷處理程序訪問的數據結構,一般採用以下策略:

    在單處理器系統上,競爭條件的防止是至關簡單的,由於中斷處理程序不是可重入的且不能被異常中斷。只要內核以本地中斷禁止訪問數據結構,內核在訪問數據結構的過程當中就不會被中斷。不過,若是數據結構正好是被一種中斷處理程序訪問,那麼,中斷處理程序不用禁止本地中斷就能夠自由訪問數據結構。

    在多處理器系統上,必須關注異常和中斷在其餘CPU上的併發執行。本地中斷禁止外還必須外加自旋鎖,強制併發的內核控制路徑進行等待,直到訪問數據結構的處理程序完成本身的工做。

    有時,用信號量代替自旋鎖可能更好。由於中斷處理程序不能被掛起,它們必須用緊循環和down_trylock()函數得到信號量;對於中斷處理程序來講,信號量起的做用本質上與自旋鎖同樣。另外一方面,系統調用服務例程能夠在信號量忙是掛起調用進程。

    五、保護由異常和可延遲函數訪問的數據結構

    異常和可延遲函數都訪問的數據結構與異常和中斷訪問的數據結構處理方式相似。事實上,可延遲函數本質上是由中斷的出現激活的,而可延遲函數執行時不可能產生異常。所以,把本地中斷禁止與自旋鎖結合起來就能夠了。

    異常處理程序能夠調用local_bh_disable()宏簡單地禁止可延遲函數,而不由止本地中斷。僅禁止可延遲函數比禁止中斷更可取,由於中斷還能夠繼續在CPU上獲得服務。在每一個CPU上可延遲函數的執行被串行化,不存在競爭條件。

    一樣,在多處理器系統上,要用自旋鎖確保在任什麼時候候只有一個內核控制路徑訪問數據結構。

    六、保護由中斷和可延遲函數訪問的數據結構

    這種狀況相似於中斷和異常處理程序訪問的數據結構。當可延遲函數運行時可能產生中斷,可是,可延遲函數不能阻止中斷處理程序。所以,必須經過在可延遲函數執行期間禁用本地中斷來避免競爭條件。不過,中斷處理程序能夠隨意訪問被可延遲函數訪問的數據結構而不用關中斷,前提是沒有其餘的中斷處理程序訪問這個數據結構。

    在多處理器系統上,仍是須要自旋鎖禁止對多個CPU上數據結構的併發訪問。

    七、保護由異常、中斷和可延遲函數訪問的數據結構

    相似前面的狀況,禁止本地中斷和獲取自旋鎖幾乎老是避免競爭條件所必需的。可是,沒有必要顯式地禁止可延遲函數,由於當中斷處理程序終止執行時,可延遲函數才能被實質激活,所以,禁止本地中斷就能夠了。


避免競爭條件的實例

    人們老是指望內核開發者肯定和解決由內核控制路徑的交錯執行所引發的同步問題。可是,避免競爭條件是一項艱鉅的任務,由於這須要對內核的各個成分如何相互做用有一個清楚的理解。

引用計數器

    引用計數器普遍地用在內核中以免因爲資源的併發分配和釋放而產生的競爭條件。引用計數器(reference counter)只不過是一個atomic_t計數器,與特定的資源,如內存頁、模塊或文件相關。當內核控制路徑開始使用資源時就原子地減小計數器的值,當內核控制路徑使用完資源時就原子地增長計數器的值。當引用計數器變爲0時,說明該資源未被使用,若有必要,就釋放該資源。

大內核鎖

    大內核鎖(Big Kernel Lock)也叫全局內核鎖或BKL。其用一個kernel_sem的信號量來實現,可是,其比簡單的信號量要複雜一些。

    每一個進程描述符都含有lock_depth字段,這個字段容許同一進程幾回獲取大內核鎖。所以,對大內核鎖兩次連續的請求不掛起處理器(相對於普通自旋鎖)。若是進程未得到過鎖,則這個字段的值爲-1;不然,這個字段的值加1,表示已經請求了多少次鎖。lock_depth字段對中斷處理程序、異常處理程序及可延遲函數獲取大內核鎖都是相當重要的。若是沒有這個字段,那麼,在當前進程已經擁有大內核鎖的請況下,任何試圖得到這個鎖的異步函數均可能產生死鎖。

    lock_kernel()和unlock_kernel()內核函數用來得到和釋放大內核鎖。

    lock_kernel()等價於:

depth = current->lock_depth + 1;
if(depth == 0)
down(&kernel_sem);
current->lock_depth = depth;

    unlock_kernel()等價於:   

if(--current->lock_depth < 0)
up(&kernel_sem);

    BKL(大內核鎖)是一個全局自旋鎖,使用它主要是爲了方便實現從Linux最初的SMP過分到細粒度加鎖機制。

    BKL的特性:

    * 持有BKL的任務仍然能夠睡眠 。由於當任務沒法調度時,所加的鎖會自動被拋棄;當任務被調度時,鎖又會被從新得到。固然,並非說,當任務持有BKL時,睡眠是安全的,緊急是能夠這樣作,由於睡眠不會形成任務死鎖。

    * BKL是一種遞歸鎖。一個進程能夠屢次請求一個鎖,並不會像自旋鎖那麼產生死鎖。

    * BKL能夠在進程上下文中。

    * BKL是有害的。

    在內核中不鼓勵使用BKL。一個執行線程能夠遞歸的請求鎖lock_kernel(),可是釋放鎖時也必須調用一樣次數的unlock_kernel()操做,在最後一個解鎖操做完成以後,鎖纔會被釋放。


內存描述符讀寫信號量

    mm_struct類型的每一個內存描述符在mmap_sem字段中都包含了本身的信號量。因爲幾個輕量級進程之間能夠共享一個內存描述符,所以,信號量保護這個描述符以免可能產生的競爭條件。

    例如,讓咱們假設內核必須爲某個進程建立或擴展一個內存區。爲了作到這一點,內核調用do_mmap()函數分配一個新的vm_area_struct數據結構。在分配的過程當中,若是沒有可用的空閒內存,而共享同一內存描述符的另外一個進程可能在運行,那麼當前進程可能被掛起。若是沒有信號量,那麼須要訪問內存描述符的第二個進程的任何操做均可能會致使嚴重的數據崩潰。

    這種信號量是做爲讀寫信號量來實現的,由於一些內核函數,如缺頁異常處理程序只須要描述內存描述符。

slab高速緩存鏈表的信號量

    slab高速緩存描述符鏈表是經過cache_chain_sem信號量保護的,這個信號量容許互斥地訪問和修改鏈表。

    當kmem_cache_create()在鏈表中增長一個新元素,而kmem_cache_shrink()和kmem_cache_reap()順序地掃描整個鏈表時,可能產生競爭條件。然而,在處理中斷時,這些函數從不被調用,在訪問鏈表時它們也從不阻塞。因爲內核是支持搶佔的,所以這種信號量在多處理器系統和單處理器系統中都會起做用。

索引節點的信號量

    Linux把磁盤文件的信息存放在一種叫作索引節點(inode)的內存對象中。相應的數據結構也包括本身的信號量,存放在l_sem字段中。

    在文件系統的處理過程當中會出現不少競爭條件。實際上,磁盤上的每一個文件都是全部用戶共有的資源,由於全部進程可能會存取文件的內容、修改文件名或文件位置、刪除或複製文件等。例如,讓咱們假設一個進程在顯示某個目錄所包含的文件。因爲每一個磁盤操做均可能會阻塞,所以即便在單處理器系統中,當第一個進程正在執行顯示操做的過程當中,其餘進程也可能存取同一目錄並修改它的內容。或者,兩個不一樣的進程可能同時修改同一目錄。全部這些競爭條件均可以經過用索引節點信號量保護目錄文件來避免。

    只要一個程序使用了兩個或多個信號量,就存在死鎖的可能,由於兩個不一樣的控制路徑可能互相死等着釋放信號量。通常來講,Linux在信號量請求上不多會發生死鎖問題,由於每一個內核控制路徑一般一次只須要得到一個信號量。然而,在有些狀況下,內核必須得到兩個或多個信號量鎖。索引節點信號量傾向於這種狀況,例如,在rename()系統調用的服務例程中就會發生上述狀況。在這種狀況下,操做涉及兩個不一樣的索引節點,所以,必須採用兩個信號量。爲了不這樣的死鎖,信號量的請求按預先肯定的地址順序進行。

 

 

 

 

淺析Linux內核同步機制

很早以前就接觸過同步這個概念了,可是一直都很模糊,沒有深刻地學習瞭解過,近期有時間了,就花時間研習了一下《linux內核標準教程》和《深刻linux設備驅動程序內核機制》這兩本書的相關章節。趁剛看完,就把相關的內容總結一下。爲了弄清楚什麼事同步機制,必需要弄明白如下三個問題:

  • 什麼是互斥與同步?
  • 爲何須要同步機制?
  •  Linux內核提供哪些方法用於實現互斥與同步的機制?

一、什麼是互斥與同步?(通俗理解)

  • 互斥與同步機制是計算機系統中,用於控制進程對某些特定資源的訪問的機制。
  • 同步是指用於實現控制多個進程按照必定的規則或順序訪問某些系統資源的機制。
  • 互斥是指用於實現控制某些系統資源在任意時刻只能容許一個進程訪問的機制。互斥是同步機制中的一種特殊狀況。
  • 同步機制是linux操做系統能夠高效穩定運行的重要機制。

二、Linux爲何須要同步機制?

        在操做系統引入了進程概念,進程成爲調度實體後,系統就具有了併發執行多個進程的能力,但也致使了系統中各個進程之間的資源競爭和共享。另外,因爲中斷、異常機制的引入,以及內核態搶佔都致使了這些內核執行路徑(進程)以交錯的方式運行。對於這些交錯路徑執行的內核路徑,如不採起必要的同步措施,將會對一些關鍵數據結構進行交錯訪問和修改,從而致使這些數據結構狀態的不一致,進而致使系統崩潰。所以,爲了確保系統高效穩定有序地運行,linux必需要採用同步機制。

三、Linux內核提供了哪些同步機制?

        在學習linux內核同步機制以前,先要了解如下預備知識:(臨界資源與併發源)
        在linux系統中,咱們把對共享的資源進行訪問的代碼片斷稱爲臨界區。把致使出現多個進程對同一共享資源進行訪問的緣由稱爲併發源。

        Linux系統下併發的主要來源有:

  • 中斷處理:例如,當進程在訪問某個臨界資源的時候發生了中斷,隨後進入中斷處理程序,若是在中斷處理程序中,也訪問了該臨界資源。雖然不是嚴格意義上的併發,可是也會形成了對該資源的競態。
  • 內核態搶佔:例如,當進程在訪問某個臨界資源的時候發生內核態搶佔,隨後進入了高優先級的進程,若是該進程也訪問了同一臨界資源,那麼就會形成進程與進程之間的併發。
  • 多處理器的併發:多處理器系統上的進程與進程之間是嚴格意義上的併發,每一個處理器均可以獨自調度運行一個進程,在同一時刻有多個進程在同時運行 。

如前所述可知:採用同步機制的目的就是避免多個進程併發併發訪問同一臨界資源。 

四、Linux內核同步機制:

(1)禁用中斷 (單處理器不可搶佔系統)

由前面能夠知道,對於單處理器不可搶佔系統來講,系統併發源主要是中斷處理。所以在進行臨界資源訪問時,進行禁用/使能中斷便可以達到消除異步併發源的目的。Linux系統中提供了兩個宏local_irq_enable與 local_irq_disable來使能和禁用中斷。在linux系統中,使用這兩個宏來開關中斷的方式進行保護時,要確保處於二者之間的代碼執行時間不能太長,不然將影響到系統的性能。(不能及時響應外部中斷)

問題:對於不可搶佔單核的系統來講,若是一塊臨界區代碼正訪問一半,出現時間片輪轉,時間片再次轉回來,是否存在臨界資源訪問問題?

(2)自旋鎖

應用背景:自旋鎖的最初設計目的是在多處理器系統中提供對共享數據的保護。

自旋鎖的設計思想:在多處理器之間設置一個全局變量V,表示鎖。並定義當V=1時爲鎖定狀態,V=0時爲解鎖狀態。自旋鎖同步機制是針對多處理器設計的,屬於忙等機制。自旋鎖機制只容許惟一的一個執行路徑持有自旋鎖。若是處理器A上的代碼要進入臨界區,就先讀取V的值。若是V!=0說明是鎖定狀態,代表有其餘處理器的代碼正在對共享數據進行訪問,那麼此時處理器A進入忙等狀態(自旋);若是V=0,代表當前沒有其餘處理器上的代碼進入臨界區,此時處理器A能夠訪問該臨界資源。而後把V設置爲1,再進入臨界區,訪問完畢後離開臨界區時將V設置爲0。

爲何叫自旋,由於忙等,一直在詢問鎖的狀況。

注意:必需要確保處理器A「讀取V,半段V的值與更新V」這一操做是一個原子操做。所謂的原子操做是指,一旦開始執行,就不可中斷直至執行結束。

自旋鎖的分類:

2.一、普通自旋鎖

普通自旋鎖由數據結構spinlock_t來表示,該數據結構在文件src/include/linux/spinlock_types.h中定義。定義以下:

1 typedef struct { 2  raw_spinklock_t raw_lock; 3 #ifdefined(CONFIG_PREEMPT) && defined(CONFIG_SMP) 4 unsigned int break_lock; 5 #endif 6 } spinlock_t;

成員raw_lock:該成員變量是自旋鎖數據類型的核心,它展開後實質上是一個Volatile unsigned類型的變量。具體的鎖定過程與它密切相關,該變量依賴於內核選項CONFIG_SMP。(是否支持多對稱處理器)

成員break_lock:同時依賴於內核選項CONFIG_SMP和CONFIG_PREEMPT(是否支持內核態搶佔),該成員變量用於指示當前自旋鎖是否被多個內核執行路徑同時競爭、訪問。

在單處理器系統下:CONFIG_SMP沒有選中時,變量類型raw_spinlock_t退化爲一個空結構體。相應的接口函數也發生了退化。相應的加鎖函數spin_lock()和解鎖函數spin_unlock()退化爲只完成禁止內核態搶佔、使能內核態搶佔。

在多處理器系統下:選中CONFIG_SMP時,核心變量raw_lock的數據類型raw_lock_t在文件中src/include/asm-i386/spinlock_types.h中定義以下:

typedef struct {  volatileunsigned int slock;} raw_spinklock_t;

       從定義中能夠看出該數據結構定義了一個內核變量,用於計數工做。當結構中成員變量slock的數值爲1時,表示自旋鎖處於非鎖定狀態,能夠使用。不然,表示處於鎖定狀態,不能夠使用。

普通自旋鎖的接口函數:

複製代碼
1 spin_lock_init(lock) //聲明自旋鎖是,初始化爲鎖定狀態 2 spin_lock(lock)//鎖定自旋鎖,成功則返回,不然循環等待自旋鎖變爲空閒 3 spin_unlock(lock) //釋放自旋鎖,從新設置爲未鎖定狀態 4 spin_is_locked(lock) //判斷當前鎖是否處於鎖定狀態。如果,返回1. 5 spin_trylock(lock) //嘗試鎖定自旋鎖lock,不成功則返回0,不然返回1 6 spin_unlock_wait(lock) //循環等待,直到自旋鎖lock變爲可用狀態。 7 spin_can_lock(lock) //判斷該自旋鎖是否處於空閒狀態。  
複製代碼

普通自旋鎖總結:自旋鎖設計用於多處理器系統。當系統是單處理器系統時,自旋鎖的加鎖、解鎖過程分爲別退化爲禁止內核態搶佔、使能內核態搶佔。在多處理器系統中,當鎖定一個自旋鎖時,須要首先禁止內核態搶佔,而後嘗試鎖定自旋鎖,在鎖定失敗時執行一個死循環等待自旋鎖被釋放;當解鎖一個自旋鎖時,首先釋放當前自旋鎖,而後使能內核態搶佔。

2.二、自旋鎖的變種

        在前面討論spin_lock很好的解決了多處理器之間的併發問題。可是若是考慮以下一個應用場景:處理器上的當前進程A要對某一全局性鏈表g_list進行操做,因此在操做前調用了spin_lock獲取鎖,而後再進入臨界區。若是在臨界區代碼當中,進程A所在的處理器上發生了一個外部硬件中斷,那麼這個時候系統必須暫停當前進程A的執行轉入到中斷處理程序當中。假如中斷處理程序當中也要操做g_list,因爲它是共享資源,在操做前必需要獲取到鎖才能進行訪問。所以當中斷處理程序試圖調用spin_lock獲取鎖時,因爲該鎖已經被進程A持有,中斷處理程序將會進入忙等狀態(自旋)。從而就會出現大問題了:中斷程序因爲沒法得到鎖,處於忙等(自旋)狀態沒法返回;因爲中斷處理程序沒法返回,進程A也處於沒有執行完的狀態,不會釋放鎖。所以這樣致使了系統的死鎖。即spin_lock對存在中斷源的狀況是存在缺陷的,所以引入了它的變種。

spin_lock_irq(lock) 

spin_unlock_irq(lock)

相比於前面的普通自旋鎖,它在上鎖前增長了禁用中斷的功能,在解鎖後,使能了中斷。

2.三、讀寫自旋鎖rwlock

應用背景:前面說的普通自旋鎖spin_lock類的函數在進入臨界區時,對臨界區中的操做行爲不細分。只要是訪問共享資源,就執行加鎖操做。可是有時候,好比某些臨界區的代碼只是去讀這些共享的數據,並不會改寫,若是採用spin_lock()函數,就意味着,任意時刻只能有一個進程能夠讀取這些共享數據。若是系統中有大量對這些共享資源的讀操做,很明顯spin_lock將會下降系統的性能。所以提出了讀寫自旋鎖rwlock的概念。對照普通自旋鎖,讀寫自旋鎖容許多個讀者進程同時進入臨界區,交錯訪問同一個臨界資源,提升了系統的併發能力,提高了系統的吞吐量。

讀寫自旋鎖有數據結構rwlock_t來表示。定義在…/spinlock_types.h中

讀寫自旋鎖的接口函數:

1 DEFINE_RWLOCK(lock) //聲明讀寫自旋鎖lock,並初始化爲未鎖定狀態 2 write_lock(lock) //以寫方式鎖定,若成功則返回,不然循環等待 3 write_unlock(lock) //解除寫方式的鎖定,重設爲未鎖定狀態 4 read_lock(lock) //以讀方式鎖定,若成功則返回,不然循環等待 5 read_unlock(lock) //解除讀方式的鎖定,重設爲未鎖定狀態 

讀寫自旋鎖的工做原理:

對於讀寫自旋鎖rwlock,它容許任意數量的讀取者同時進入臨界區,但寫入者必須進行互斥訪問。一個進程要進行讀,必需要先檢查是否有進程正在寫入,若是有,則自旋(忙等),不然得到鎖。一個進程要進程寫,必需要先檢查是否有進程正在讀取或者寫入,若是有,則自旋(忙等)不然得到鎖。即讀寫自旋鎖的應用規則以下:

(1)若是當前有進程正在寫,那麼其餘進程就不能讀也不能寫。

(2)若是當前有進程正在讀,那麼其餘程序能夠讀,可是不能寫。

2.四、順序自旋鎖seqlock

應用背景:順序自旋鎖主要用於解決自旋鎖同步機制中,在擁有大量讀者進程時,寫進程因爲長時間沒法持有鎖而被餓死的狀況,其主要思想是:爲寫進程提升更高的優先級,在寫鎖定請求出現時,當即知足寫鎖定的請求,不管此時是否有讀進程正在訪問臨界資源。可是新的寫鎖定請求不會,也不能搶佔已有寫進程的寫鎖定。

順序鎖的設計思想:對某一共享數據讀取時不加鎖,寫的時候加鎖。爲了保證讀取的過程當中不會由於寫入者的出現致使該共享數據的更新,須要在讀取者和寫入者之間引入一個整形變量,稱爲順序值sequence。讀取者在開始讀取前讀取該sequence,在讀取後再從新讀取該值,若是與以前讀取到的值不一致,則說明本次讀取操做過程當中發生了數據更新,讀取操做無效。所以要求寫入者在開始寫入的時候更新。

順序自旋鎖由數據結構seqlock_t表示,定義在src/include/linux/seqlcok.h

順序自旋鎖訪問接口函數:

1 seqlock_init(seqlock) //初始化爲未鎖定狀態 2 read_seqbgin()、read_seqretry() //保證數據的一致性 3 write_seqlock(lock) //嘗試以寫鎖定方式鎖定順序鎖 4 write_sequnlock(lock) //解除對順序鎖的寫方式鎖定,重設爲未鎖定狀態。

順序自旋鎖的工做原理:寫進程不會被讀進程阻塞,也就是,寫進程對被順序自旋鎖保護的臨界資源進行訪問時,當即鎖定並完成更新工做,而沒必要等待讀進程完成讀訪問。可是寫進程與寫進程之間還是互斥的,若是有寫進程在進行寫操做,其餘寫進程必須循環等待,直到前一個寫進程釋放了自旋鎖。順序自旋鎖要求被保護的共享資源不包含有指針,由於寫進程可能使得指針失效,若是讀進程正要訪問該指針,將會出錯。同時,若是讀者在讀操做期間,寫進程已經發生了寫操做,那麼讀者必須從新讀取數據,以便確保獲得的數據是完整的。

(3)信號量機制(semaphore)

應用背景:前面介紹的自旋鎖同步機制是一種「忙等」機制,在臨界資源被鎖定的時間很短的狀況下頗有效。可是在臨界資源被持有時間很長或者不肯定的狀況下,忙等機制則會浪費不少寶貴的處理器時間。針對這種狀況,linux內核中提供了信號量機制,此類型的同步機制在進程沒法獲取到臨界資源的狀況下,當即釋放處理器的使用權,並睡眠在所訪問的臨界資源上對應的等待隊列上;在臨界資源被釋放時,再喚醒阻塞在該臨界資源上的進程。另外,信號量機制不會禁用內核態搶佔,因此持有信號量的進程同樣能夠被搶佔,這意味着信號量機制不會給系統的響應能力,實時能力帶來負面的影響。

信號量設計思想:除了初始化以外,信號量只能經過兩個原子操做P()和V()訪問,也稱爲down()和up()。down()原子操做經過對信號量的計數器減1,來請求得到一個信號量。若是操做後結果是0或者大於0,得到信號量鎖,任務就能夠進入臨界區。若是操做後結果是負數,任務會放入等待隊列,處理器執行其餘任務;對臨界資源訪問完畢後,能夠調用原子操做up()來釋放信號量,該操做會增長信號量的計數器。若是該信號量上的等待隊列不爲空,則喚醒阻塞在該信號量上的進程。

信號量的分類:

3.一、普通訊號量

普通訊號量由數據結構struct semaphore來表示,定義在src/inlcude/ asm-i386/semaphore.h中.

信號量(semaphore)定義以下:

1 <include/linux/semaphore.h>
2 struct semaphore{ 3 spinlock_t lock; //自旋鎖,用於實現對count的原子操做 4 unsigned int count; //表示經過該信號量容許進入臨界區的執行路徑的個數 5 struct list_head wait_list; //用於管理睡眠在該信號量上的進程 6 };

普通訊號量的接口函數:

1 sema_init(sem,val)  //初始化信號量計數器的值爲val 2 int_MUTEX(sem) //初始化信號量爲一個互斥信號量 3 down(sem) //鎖定信號量,若不成功,則睡眠在等待隊列上 4 up(sem) //釋放信號量,並喚醒等待隊列上的進程

DOWN操做:linux內核中,對信號量的DOWN操做有以下幾種:

1 void down(struct semaphore *sem); //不可中斷 2 int down_interruptible(struct semaphore *sem);//可中斷 3 int down_killable(struct semaphore *sem);//睡眠的進程能夠由於受到致命信號而被喚醒,中斷獲取信號量的操做。 4 int down_trylock(struct semaphore *sem);//試圖獲取信號量,若沒法得到則直接返回1而不睡眠。返回0則 表示獲取到了信號量 5 int down_timeout(struct semaphore *sem,long jiffies);//表示睡眠時間是有限制的,若是在jiffies指明的時間到期時仍然沒法得到信號量,則將返回錯誤碼。

在以上四種函數中,驅動程序使用的最頻繁的就是down_interruptible函數

UP操做:LINUX內核只提供了一個up函數

void up(struct semaphore *sem)

加鎖處理過程:加鎖過程由函數down()完成,該函數負責測試信號量的狀態,在信號量可用的狀況下,獲取該信號量的使用權,不然將當前進程插入到當前信號量對應的等待隊列中。函數調用關係以下:down()->__down_failed()->__down.函數說明以下:

down()功能介紹:該函數用於對信號量sem進行加鎖,在加鎖成功即得到信號的使用權是,直接退出,不然,調用函數__down_failed()睡眠到信號量sem的等待隊列上。__down()功能介紹:該函數在加鎖失敗時被調用,負責將進程插入到信號量 sem的等待隊列中,而後調用調度器,釋放處理器的使用權。

解鎖處理過程:普通訊號量的解鎖過程由函數up()完成,該函數負責將信號計數器count的值增長1,表示信號量被釋放,在有進程阻塞在該信號量的狀況下,喚醒等待隊列中的睡眠進程。 

3.2讀寫信號量(rwsem)

應用背景:爲了提升內核併發執行能力,內核提供了讀入者信號量和寫入者信號量。它們的概念和實現機制相似於讀寫自旋鎖。

工做原理:該信號量機制使得全部的讀進程能夠同時訪問信號量保護的臨界資源。當進程嘗試鎖定讀寫信號量不成功時,則這些進程被插入到一個先進先出的隊列中;當一個進程訪問完臨界資源,釋放對應的讀寫信號量是,該進程負責將該隊列中的進程按必定的規則喚醒。

喚醒規則:喚醒排在該先進先出隊列中隊首的進程,在被喚醒進程爲寫進程的狀況下,再也不喚醒其餘進程;在喚醒進程爲讀進程的狀況下,喚醒其餘的讀進程,直到遇到一個寫進程(該寫進程不被喚醒)

讀寫信號量的定義以下:

1 <include/linux/rwsem-spinlock.h>
2 sturct rw_semaphore{ 3 __s32 activity; //用於表示讀者或寫者的數量 4  spinlock_t wait_lock; 5 struct list_head wait_list; 6 }; 

讀寫信號量相應的接口函數

讀者up、down操做函數:

1 void up_read(Sturct rw_semaphore *sem); 2 void __sched down_read(Sturct rw_semaphore *sem); 3 Int down_read_trylock(Sturct rw_semaphore *sem);

寫入者up、down操做函數:

1 void up_write(Sturct rw_semaphore *sem); 2 void __sched down_write(Sturct rw_semaphore *sem); 3 int down_write_trylock(Sturct rw_semaphore *sem);
3.三、互斥信號量

在linux系統中,信號量的一個常見的用途是實現互斥機制,這種狀況下,信號量的count值爲1,也就是任意時刻只容許一個進程進入臨界區。爲此,linux內核源碼提供了一個宏DECLARE_MUTEX,專門用於這種用途的信號量定義和初始化

1 <include/linux/semaphore.h>
2 #define DECLARE_MUTEX(name) \ 3 structsemaphore name=__SEMAPHORE_INITIALIZER(name,1) 

(4)互斥鎖mutex

Linux內核針對count=1的信號量從新定義了一個新的數據結構struct mutex,通常都稱爲互斥鎖。內核根據使用場景的不一樣,把用於信號量的down和up操做在struct mutex上作了優化與擴展,專門用於這種新的數據類型。

(5)RCU

RCU概念:RCU全稱是Read-Copy-Update(讀/寫-複製-更新),是linux內核中提供的一種免鎖的同步機制。RCU與前面討論過的讀寫自旋鎖rwlock,讀寫信號量rwsem,順序鎖同樣,它也適用於讀取者、寫入者共存的系統。可是不一樣的是,RCU中的讀取和寫入操做無須考慮二者之間的互斥問題。可是寫入者之間的互斥仍是要考慮的。

RCU原理:簡單地說,是將讀取者和寫入者要訪問的共享數據放在一個指針p中,讀取者經過p來訪問其中的數據,而讀取者則經過修改p來更新數據。要實現免鎖,讀寫雙方必需要遵照必定的規則。

讀取者的操做(RCU臨界區)

對於讀取者來講,若是要訪問共享數據。首先要調用rcu_read_lock和rcu_read_unlock函數構建讀者側的臨界區(read-side critical section),而後再臨界區中得到指向共享數據區的指針,實際的讀取操做就是對該指針的引用。

讀取者要遵照的規則是:(1)對指針的引用必需要在臨界區中完成,離開臨界區以後不該該出現任何形式的對該指針的引用。(2)在臨界區內的代碼不該該致使任何形式的進程切換(通常要關掉內核搶佔,中斷能夠不關)。

寫入者的操做

對於寫入者來講,要寫入數據,首先要從新分配一個新的內存空間作做爲共享數據區。而後將老數據區內的數據複製到新數據區,並根據須要修改新數據區,最後用新數據區指針替換掉老數據區的指針。寫入者在替換掉共享區的指針後,老指針指向的共享數據區所在的空間還不能立刻釋放(緣由後面再說明)。寫入者須要和內核共同協做,在肯定全部對老指針的引用都結束後才能夠釋放老指針指向的內存空間。爲此,寫入者要作的操做是調用call_rcu函數向內核註冊一個回調函數,內核在肯定全部對老指針的引用都結束時會調用該回調函數,回調函數的功能主要是釋放老指針指向的內存空間。Call_rcu函數的原型以下:

Void call_rcu(struct rcu_head *head,void (*func)(struct rcu_head *rcu));

內核肯定沒有讀取者對老指針的引用是基於如下條件的:系統中全部處理器上都至少發生了一次進程切換。由於全部可能對共享數據區指針的不一致引用必定是發生在讀取者的RCU臨界區,並且臨界區必定不能發生進程切換。因此若是在CPU上發生了一次進程切換切換,那麼全部對老指針的引用都會結束,以後讀取者再進入RCU臨界區看到的都將是新指針。

老指針不能立刻釋放的緣由:這是由於系統中愛可能存在對老指針的引用,者主要發生在如下兩種狀況:(1)一是在單處理器範圍看,假設讀取者在進入RCU臨界區後,剛得到共享區的指針以後發生了一箇中斷,若是寫入者剛好是中斷處理函數中的行爲,那麼當中斷返回後,被中斷進程RCU臨界區中繼續執行時,將會繼續引用老指針。(2)另外一個多是在多處理器系統,當處理器A上的一個讀取者進入RCU臨界區並得到共享數據區中的指針後,在其還沒來得及引用該指針時,處理器B上的一個寫入者更新了指向共享數據區的指針,這樣處理器A上的讀取者也餓將引用到老指針。

RCU特色:由前面的討論能夠知道,RCU實質上是對讀取者與寫入者自旋鎖rwlock的一種優化。RCU的可讓多個讀取者和寫入者同時工做。可是RCU的寫入者操做開銷就比較大。在驅動程序中通常比較少用。

爲了在代碼中使用RCU,全部RCU相關的操做都應該使用內核提供的RCU API函數,以確保RCU機制的正確使用,這些API主要集中在指針和鏈表的操做。

下面是一個RCU的典型用法範例:

複製代碼
 1 假設struct shared_data是一個在讀取者和寫入者之間共享的受保護數據  2 Struct shared_data{  3   Int a;  4   Int b;  5   Struct rcu_head rcu;  6 };  7  8 //讀取者側的代碼  9 10 Static void demo_reader(struct shared_data *ptr) 11 { 12 Struct shared_data *p=NULL; 13  Rcu_read_lock(); 14 P=rcu_dereference(ptr); 15  If(p) 16  Do_something_withp(p); 17  Rcu_read_unlock(); 18 } 19 20 //寫入者側的代碼 21 Static void demo_del_oldptr(struct rcu_head *rh) //回調函數 22 { 23 Struct shared_data *p=container_of(rh,struct shared_data,rcu); 24  Kfree(p); 25 } 26 27 Static void demo_writer(struct shared_data *ptr) 28 { 29 Struct shared_data *new_ptr=kmalloc(…); 30 31 New_ptr->a=10; 32 New_ptr->b=20; 33 Rcu_assign_pointer(ptr,new_ptr);//用新指針更新老指針 34 Call_rcu(ptr->rcu,demo_del_oldptr); 向內核註冊回調函數,用於刪除老指針指向的內存空間 35 } 
複製代碼

(6)完成接口completion

Linux內核還提供了一個被稱爲「完成接口completion」的同步機制,該機制被用來在多個執行路徑間做同步使用,也即協調多個執行路徑的執行順序。在此就不展開了。----見分享的另一篇關於completion的文章

 

 

linux內核同步機制中的概念介紹和方法

      Linux設備驅動中必須解決的一個問題是多個進程對共享資源的併發訪問,併發訪問會致使競態,linux提供了多種解決競態問題的方式,這些方式適合不一樣的應用場景。

 

Linux內核是多進程、多線程的操做系統,它提供了至關完整的內核同步方法。內核同步方法列表以下:

=========================

內核中採用的同步技術:    

中斷屏蔽

原子操做  (分爲整數原子操做和位 原子操做)

信號量  (semaphore)

RCU  (read-copy-update)

SMP系統中的同步機制 :
自旋鎖 (spin lock)

讀寫自旋鎖

順序鎖         (seqlock 只包含在2.6內核中)

讀寫信號量  (rw_semaphore)

大內核鎖BKL(Big Kernel Lock)

Seq鎖()。

==========================

1、概念介紹

 

併發與競態:

          併發(concurrency)指的是多個執行單元同時、並行被執行,而併發的執行單元對共享資源(硬件資源和軟件上的全局變量、靜態變量等)的訪問則很容易致使競態(race conditions)。

linux中,主要的競態發生在以下幾種狀況:

一、對稱多處理器(SMP)多個CPU                特色是多個CPU使用共同的系統總線,所以可訪問共同的外設和存儲器。

二、單CPU內進程與搶佔它的進程

三、中斷(硬中斷、軟中斷、Tasklet、底半部)與進程之間

只要併發的多個執行單元存在對共享資源的訪問,競態就有可能發生。

若是中斷處理程序訪問進程正在訪問的資源,則競態也會會發生。

多箇中斷之間自己也可能引發併發而致使競態(中斷被更高優先級的中斷打斷)。 

解決競態問題的途徑是保證對共享資源的互斥訪問,所謂互斥訪問就是指一個執行單元在訪問共享資源的時候,其餘的執行單元都被禁止訪問。 

訪問共享資源的代碼區域被稱爲臨界區,臨界區須要以某種互斥機制加以保護,如:中斷屏蔽,原子操做,自旋鎖,和信號量都是linux設備驅動中可採用的互斥途徑。 

臨界區和競爭條件:

所謂臨界區(critical regions)就是訪問和操做共享數據的代碼段,爲了不在臨界區中併發訪問,編程者必須保證這些代碼原子地執行——也就是說,代碼在執行結束前不可被打斷,就如同整個臨界區是一個不可分割的指令同樣,若是兩個執行線程有可能處於同一個臨界區中,那麼就是程序包含一個bug,若是這種狀況發生了,咱們就稱之爲競爭條件(race conditions),避免併發和防止競爭條件被稱爲同步 

死鎖:

死鎖的產生須要必定條件:要有一個或多個執行線程和一個或多個資源,每一個線程都在等待其中的一個資源,但全部的資源都已經被佔用了,全部線程都在相互等待,但它們永遠不會釋放已經佔有的資源,因而任何線程都沒法繼續,這便意味着死鎖的發生。

=========================================================================================================================

詳細的同步方法以下:

1、中斷屏蔽

CPU範圍內避免競態的一種簡單方法是在進入臨界區以前屏蔽系統的中斷因爲linux內核的進程調度等操做都依賴中斷來實現,內核搶佔進程之間的併發也就得以免了。

中斷屏蔽的使用方法:

[html] view plain copy
  1. local_irq_disable()//屏蔽中斷  
  2. //臨界區  
  3. local_irq_enable()//開中斷  
  4. 特色:因爲<span style="font-family:'Times New Roman';">linux</span>系統的異步<span style="font-family:'Times New Roman';">IO</span>,進程調度等不少重要操做都依賴於中斷,在屏蔽中斷期間全部的中斷都沒法獲得處理,所以<strong>長時間的屏蔽是很危險</strong>的,有可能形成數據丟失甚至系統崩潰,這就要求在屏蔽中斷以後,當前的內核執行路徑應當儘快地執行完臨界區的代碼。  

中斷屏蔽只能禁止本CPU內的中斷,所以,並不能解決多CPU引起的競態,因此單獨使用中斷屏蔽並非一個值得推薦的避免競態的方法,它通常和自旋鎖配合使用。

 

 2、原子操做

定義:原子操做指的是在執行過程當中不會被別的代碼路徑所中斷的操做。(原子本來指的是不可分割的微粒,因此原子操做也就是不可以被分割的指令)

(它保證指令原子的方式執行而不能被打斷)

原子操做是不可分割的,在執行完畢不會被任何其它任務或事件中斷。在單處理器系統(UniProcessor)中,可以在單條指令中完成的操做均可以認爲是"原子操做",由於中斷只能發生於指令之間。這也是某些CPU指令系統中引入了test_and_settest_and_clear等指令用於臨界資源互斥的緣由。可是,在對稱多處理器(Symmetric Multi-Processor)結構中就不一樣了,因爲系統中有多個處理器在獨立地運行,即便能在單條指令中完成的操做也有可能受到干擾。咱們以decl (遞減指令)爲例,這是一個典型的"讀-改-寫"過程,涉及兩次內存訪問。

通俗理解:

      原子操做,顧名思義,就是說像原子同樣不可再細分。一個操做是原子操做,意思就是說這個操做是以原子的方式被執行,要一口氣執行完,執行過程不可以被OS的其餘行爲打斷,是一個總體的過程,在其執行過程當中,OS的其它行爲是插不進來的。

分類:linux內核提供了一系列函數來實現內核中的原子操做,分爲整型原子操做位原子操做共同點是:在任何狀況下操做都是原子的,內核代碼能夠安全的調用它們而不被打斷。

 

原子整數操做:

針對整數的原子操做只能對atomic_t類型的數據進行處理,在這裏之因此引入了一個特殊的數據類型,而沒有直接使用C語言的int型,主要是出於兩個緣由:

第1、讓原子函數只接受atomic_t類型的操做數,能夠確保原子操做只與這種特殊類型數據一塊兒使用,同時,這也確保了該類型的數據不會被傳遞給其它任何非原子函數;

第2、使用atomic_t類型確保編譯器不對相應的值進行訪問優化——這點使得原子操做最終接收到正確的內存地址,而不是一個別名,最後就是在不一樣體系結構上實現原子操做的時候,使用atomic_t能夠屏蔽其間的差別。

原子整數操做最多見的用途就是實現計數器。

另外一點須要說明原子操做只能保證操做是原子的,要麼完成,要麼不完成,不會有操做一半的可能,但原子操做並不能保證操做的順序性,即它不能保證兩個操做是按某個順序完成的。若是要保證原子操做的順序性,請使用內存屏障指令。

atomic_t和ATOMIC_INIT(i)定義以下:

[html] view plain copy
  1. typedef struct { volatile int counter; } atomic_t;  
  2. #define ATOMIC_INIT(i)  { (i) }  

=========================整數相關函數: ===========================================          

[html] view plain copy
  1. ATOMIC_INIT(int i)                                         //聲明一個atomic_t變量而且初始化爲i  
  2.          int atomic_read(atomic_t *v)                      //原子地讀取整數變量v  
  3.          void atomic_set(atomic_t *v, int i)               //原子地設置v爲i  
  4.          void atomic_add(int i, atomic_t *v)         //v+i  
  5.          void atomic_sub(int i, atomic_t *v)         //v-i  
  6.          void atomic_inc(atomic_t *v)                //v+1  
  7.          void atomic_dec(atomic_t *v)                //v-1  
  8.          int atomic_sub_and_test(int i, atomic_t *v)       //v-i, 結果等於0,返回真,不然返回假   
  9.            int atomic_add_negative(int i, atomic_t *v)       //原子地給v+i,結果是負數,返回真,不然返回假  
  10.            int atomic_dec_and_test(atomic_t *v)              //v-1, 結果是0,返回真,不然返回假  
  11.            int atomic_inc_and_test(atomic_t *v)              //v+1,若是是0,返回真,不然返回假  

在你編寫代碼的時候,能使用原子操做的時候,就儘可能不要使用複雜的加鎖機制,對多數體系結構來說,原子操做與更復雜的同步方法相比較,給系統帶來的開銷小,對高速緩存行的影響也小,可是,對於那些有高性能要求的代碼,對多種同步方法進行測試比較,不失爲一種明智的做法。

 

原子位操做:

針對位這一級數據進行操做的函數,是對普通的內存地址進行操做的。它的參數是一個指針和一個位號。 

=================================位操做相關函數:====================================

[html] view plain copy
  1. void set_bit(int nr, void *addr)                  //原子地設置addr所指對象的第nr位  
  2. void clear_bit(int nr, void *addr)                  //清空addr第nr位  
  3. void change_bit(int nr, void *addr)                 //反轉addr第nr位  
  4. int test_and _set_bit(int nr, void *addr)           //設置addr第nr位,並返回原先的位的值  
  5. int test_and_clear_bit(int nr, void *addr)          //清除addr第nr位,並返回原先的值  
  6. int test_and_change_bit(int nr, void *addr)         //反轉addr第nr,並返回原先的值  
  7. int test_bit(int nr, void *addr)                            //原子地返回addr的第nr位  

爲方便其間,內核還提供了一組與上述操做對應的非原子位函數,非原子位函數與原子位函數的操做徹底相同,可是,非原子位函數不保證原子性,且其名字前綴多兩個下劃線。例如,與test_bit()對應的非原子形式是_test_bit(),若是你不須要原子性操做(好比,若是你已經用鎖保護了本身的數據),那麼這些非原子的位函數相比原子的位函數可能會執行得更快些。

 使用示例:

建立和初始化原子變量                   
atomic_t my_counter ATOMIC_INIT(0);    //聲明一個atomic_t類型的變量my_counter 而且初始化爲0
或者 
atomic_set( &my_counter, 0 ); 

[html] view plain copy
  1. <span style="font-size:12px;color:#000000;">簡單的算術原子函數                      
  2. val = atomic_read( &my_counter );      
  3. atomic_add( 1, &my_counter );      
  4. atomic_inc( &my_counter );      
  5. atomic_sub( 1, &my_counter );      
  6. atomic_dec( &my_counter );    
  7. </span>  

3、自旋鎖(spin lock)

自旋鎖的引入:

若是每一個臨界區都能像增長變量這樣簡單就行了,惋惜現實不是這樣,而是臨界區能夠跨越多個函數,例如:先得從一個數據結果中移出數據,對其進行格式轉換和解析,最後再把它加入到另外一個數據結構中,整個執行過程必須是原子的,在數據被更新完畢以前,不能有其餘代碼讀取這些數據,顯然,簡單的原子操做是無能爲力的(在單處理器系統(UniProcessor)中,可以在單條指令中完成的操做均可以認爲是"原子操做",由於中斷只能發生於指令之間),這就須要使用更爲複雜的同步方法——鎖來提供保護。 

自旋鎖的介紹:

Linux內核中最多見的鎖是自旋鎖(spin lock),自旋鎖最多隻能被一個可執行線程持有,若是一個執行線程試圖得到一個被爭用(已經被持有)的自旋鎖,那麼該線程就會一直進行忙循環—旋轉—等待鎖從新可用,要是鎖未被爭用,請求鎖的執行線程便能馬上獲得它,繼續執行,在任意時間,自旋鎖均可以防止多於一個的執行線程同時進入理解區,注意同一個鎖能夠用在多個位置—例如,對於給定數據的全部訪問均可以獲得保護和同步。

一個被爭用的自旋鎖使得請求它的線程在等待鎖從新可用時自旋(特別浪費處理器時間),因此自旋鎖不該該被長時間持有,事實上,這點正是使用自旋鎖的初衷,在短時間間內進行輕量級加鎖,還能夠採起另外的方式來處理對鎖的爭用:讓請求線程睡眠,直到鎖從新可用時再喚醒它,這樣處理器就沒必要循環等待,能夠去執行其餘代碼,這也會帶來必定的開銷——這裏有兩次明顯的上下文切換,被阻塞的線程要換出和換入。所以,持有自旋鎖的時間最好小於完成兩次上下文切換的耗時,固然咱們大多數人不會無聊到去測量上下文切換的耗時,因此咱們讓持有自旋鎖的時間應儘量的短就能夠了,信號量能夠提供上述第二種機制,它使得在發生爭用時,等待的線程能投入睡眠,而不是旋轉。

自旋鎖能夠使用在中斷處理程序中(此處不能使用信號量,由於它們會致使睡眠),在中斷處理程序中使用自旋鎖時,必定要在獲取鎖以前,首先禁止本地中斷(在當前處理器上的中斷請求),不然,中斷處理程序就會打斷正持有鎖的內核代碼,有可能會試圖去爭用這個已經持有的自旋鎖,這樣以來,中斷處理程序就會自旋,等待該鎖從新可用,可是鎖的持有者在這個中斷處理程序執行完畢前不可能運行,這正是咱們在前一章節中提到的雙重請求死鎖,注意,須要關閉的只是當前處理器上的中斷,若是中斷髮生在不一樣的處理器上,即便中斷處理程序在同一鎖上自旋,也不會妨礙鎖的持有者(在不一樣處理器上)最終釋放鎖。 

自旋鎖的簡單理解:

理解自旋鎖最簡單的方法是把它做爲一個變量看待,該變量把一個臨界區或者標記爲「我當前正在運行,請稍等一會」或者標記爲「我當前不在運行,能夠被使用」。若是A執行單元首先進入例程,它將持有自旋鎖,當B執行單元試圖進入同一個例程時,將獲知自旋鎖已被持有,需等到A執行單元釋放後才能進入 

自旋鎖的API函數: 

其實介紹的幾種信號量和互斥機制,其底層源碼都是使用自旋鎖,能夠理解爲自旋鎖的再包裝。因此從這裏就能夠理解爲何自旋鎖一般能夠提供比信號量更高的性能。
自旋鎖是一個互斥設備,他只能會兩個值:「鎖定」和「解鎖」。它一般實現爲某個整數之中的單個位。
「測試並設置」的操做必須以原子方式完成。
任什麼時候候,只要內核代碼擁有自旋鎖,在相關CPU上的搶佔就會被禁止。

適用於自旋鎖的核心規則:
1)任何擁有自旋鎖的代碼都必須使原子的,除服務中斷外(某些狀況下也不能放棄CPU,如中斷服務也要得到自旋鎖。爲了不這種鎖陷阱,須要在擁有自旋鎖時禁止中斷),不能放棄CPU(如休眠,休眠可發生在許多沒法預期的地方)。不然CPU將有可能永遠自旋下去(死機)。
2)擁有自旋鎖的時間越短越好。


須要強調的是,自旋鎖設計用於多處理器的同步機制,對於單處理器(對於單處理器而且不可搶佔的內核來講,自旋鎖什麼也不做),內核在編譯時不會引入自旋鎖機制,對於可搶佔的內核,它僅僅被用於設置內核的搶佔機制是否開啓的一個開關,也就是說加鎖和解鎖實際變成了禁止或開啓內核搶佔功能。若是內核不支持搶佔,那麼自旋鎖根本就不會編譯到內核中。
內核中使用spinlock_t類型來表示自旋鎖,它定義在<linux/spinlock_types.h>

[html] view plain copy
  1. typedef struct {  
  2.     raw_spinlock_t raw_lock;  
  3. #if defined(CONFIG_PREEMPT) && defined(CONFIG_SMP)  
  4.     unsigned int break_lock;  
  5. #endif  
  6. } spinlock_t;  
  7.    

對於不支持SMP的內核來講,struct raw_spinlock_t什麼也沒有,是一個空結構。對於支持多處理器的內核來講,struct raw_spinlock_t定義爲

[html] view plain copy
  1. typedef struct {  
  2.     unsigned int slock;  
  3. } raw_spinlock_t;<span style="color:#2a2a2a;"><span style="font-family:'Times New Roman';"</span></span>  

slock表示了自旋鎖的狀態,「1」表示自旋鎖處於解鎖狀態(UNLOCK),「0」表示自旋鎖處於上鎖狀態(LOCKED)。
break_lock表示當前是否由進程在等待自旋鎖,顯然,它只有在支持搶佔的SMP內核上才起做用。
    自旋鎖的實現是一個複雜的過程,說它複雜不是由於須要多少代碼或邏輯來實現它,其實它的實現代碼不多。自旋鎖的實現跟體系結構關係密切,核心代碼基本也是由彙編語言寫成,與體協結構相關的核心代碼都放在相關的<asm/>目錄下,好比<asm/spinlock.h>。對於咱們驅動程序開發人員來講,咱們沒有必要了解這麼spinlock的內部細節,若是你對它感興趣,請參考閱讀Linux內核源代碼。對於咱們驅動的spinlock接口,咱們只需包括<linux/spinlock.h>頭文件。在咱們詳細的介紹spinlockAPI以前,咱們先來看看自旋鎖的一個基本使用格式:

 使用示例:

[html] view plain copy
  1. #include <linux/spinlock.h>  
  2. spinlock_t mr_lock = SPIN_LOCK_UNLOCKED;  
  3. spin_lock(&mr_lock);  
  4. /*  
  5. 臨界區  
  6. */  
  7. spin_unlock(&mr_lock);  

從使用上來講,spinlockAPI還很簡單的,通常咱們會用的的API以下表,其實它們都是定義在<linux/spinlock.h>中的宏接口,真正的實如今<asm/spinlock.h>

  1. #include <linux/spinlock.h>
    SPIN_LOCK_UNLOCKED
    DEFINE_SPINLOCK
    spin_lock_init( spinlock_t *)
    spin_lock(spinlock_t *)
    spin_unlock(spinlock_t *)
    spin_lock_irq(spinlock_t *)
    spin_unlock_irq(spinlock_t *)
    spin_lock_irqsace(spinlock_t *unsigned long flags)
    spin_unlock_irqsace(spinlock_t *, unsigned long flags)
    spin_trylock(spinlock_t *)
    spin_is_locked(spinlock_t *) 

初始化

spinlock有兩種初始化形式,一種是靜態初始化,一種是動態初始化。對於靜態的spinlock對象,咱們用 SPIN_LOCK_UNLOCKED來初始化,它是一個宏。固然,咱們也能夠把聲明spinlock和初始化它放在一塊兒作,這就是 DEFINE_SPINLOCK宏的工做,所以,下面的兩行代碼是等價的。

[html] view plain copy
  1. DEFINE_SPINLOCK (lock);  
  2. spinlock_t lock = SPIN_LOCK_UNLOCKED;  

spin_lock_init函數通常用來初始化動態建立的spinlock_t對象,它的參數是一個指向spinlock_t對象的指針。固然,它也能夠初始化一個靜態的沒有初始化的spinlock_t對象。

[html] view plain copy
  1. spinlock_t *lock  
  2. ......  
  3. spin_lock_init(lock);  

獲取鎖

內核提供了多個函數用於獲取一個自旋鎖。

[html] view plain copy
  1. spin_try_lock()        試圖得到某個特定的自旋鎖,若是該鎖已經被爭用,該方法會馬上返回一個非0值,而不會自旋等待鎖被釋放,若是成果得到了這個鎖,那麼就返回0.  
  2. spin_is_locked()          //方法和spin_try_lock()是同樣的功效,該方法只作判斷,並不生效.<span style="color:#2a2a2a;">若是是則返回非<span style="font-family:'Times New Roman';">0</span>,不然,返回<span style="font-family:'Times New Roman';">0</span>。</span>  
  3. spin_lock();              //獲取指定的自旋鎖  
  4. spin_lock_irq();          //禁止本地中斷獲取指定的鎖  
  5. spin_lock_irqsave();      //保存本地中斷的狀態,禁止本地中斷,並獲取指定的鎖  


自旋鎖是能夠使用在中斷處理程序中的,這時需要使用具備關閉本地中斷功能的函數,咱們推薦使用 spin_lock_irqsave,由於它會保存加鎖前的中斷標誌,這樣就會正確恢復解鎖時的中斷標誌。若是spin_lock_irq在加鎖時中斷是關閉的,那麼在解鎖時就會錯誤的開啓中斷。

釋放鎖

同獲取鎖相對應,內核提供了三個相對的函數來釋放自旋鎖。

[html] view plain copy
  1. <span style="color:#000000;">spin_unlock:釋放指定的自旋鎖。  
  2. spin_unlock_irq:釋放自旋鎖並激活本地中斷。  
  3. spin_unlock_irqsave:釋放自旋鎖,並恢復保存的本地中斷狀態。</span>  


4、讀寫自旋鎖

若是臨界區保護的數據是可讀可寫的,那麼只要沒有寫操做,對於讀是能夠支持併發操做的。對於這種只要求寫操做是互斥的需求,若是仍是使用自旋鎖顯然是沒法知足這個要求(對於讀操做實在是太浪費了)。爲此內核提供了另外一種鎖-讀寫自旋鎖,讀自旋鎖也叫共享自旋鎖,寫自旋鎖也叫排他自旋鎖。

讀寫自旋鎖是一種比自旋鎖粒度更小的鎖機制,它保留了「自旋」的概念,可是在寫操做方面,只能最多有一個寫進程,在讀操做方面,同時能夠有多個讀執行單元,固然,讀和寫也不能同時進行。
    讀寫自旋鎖的使用也普通自旋鎖的使用很相似,首先要初始化讀寫自旋鎖對象:

[html] view plain copy
  1. //靜態初始化  
  2. rwlock_t rwlock = RW_LOCK_UNLOCKED;  
  3. //動態初始化  
  4. rwlock_t *rwlock;  
  5. ...  
  6. rw_lock_init(rwlock);  


 在讀操做代碼裏對共享數據獲取讀自旋鎖:

[html] view plain copy
  1. read_lock(&rwlock);  
  2. ...  
  3. read_unlock(&rwlock);  


 在寫操做代碼裏爲共享數據獲取寫自旋鎖:

[html] view plain copy
  1. write_lock(&rwlock);  
  2. ...  
  3. write_unlock(&rwlock);  


 須要注意的是,若是有大量的寫操做,會使寫操做自旋在寫自旋鎖上而處於寫飢餓狀態(等待讀自旋鎖的所有釋放),由於讀自旋鎖會自由的獲取讀自旋鎖。

讀寫自旋鎖的函數相似於普通自旋鎖,這裏就不一一介紹了,咱們把它列在下面的表中。

[html] view plain copy
  1. RW_LOCK_UNLOCKED  
  2. rw_lock_init(rwlock_t *)  
  3. read_lock(rwlock_t *)  
  4. read_unlock(rwlock_t *)  
  5. read_lock_irq(rwlock_t *)  
  6. read_unlock_irq(rwlock_t *)  
  7. read_lock_irqsave(rwlock_t *, unsigned long)  
  8. read_unlock_irqsave(rwlock_t *, unsigned long)  
  9. write_lock(rwlock_t *)  
  10. write_unlock(rwlock_t *)  
  11. write_lock_irq(rwlock_t *)  
  12. write_unlock_irq(rwlock_t *)  
  13. write_lock_irqsave(rwlock_t *, unsigned long)  
  14. write_unlock_irqsave(rwlock_t *, unsigned long)  
  15. rw_is_locked(rwlock_t *)  

5、順序瑣

順序瑣(seqlock)是對讀寫鎖的一種優化,若使用順序瑣,讀執行單元毫不會被寫執行單元阻塞,也就是說,讀執行單元能夠在寫執行單元對被順序瑣保護的共享資源進行寫操做時仍然能夠繼續讀,而沒必要等待寫執行單元完成寫操做,寫執行單元也不須要等待全部讀執行單元完成讀操做纔去進行寫操做。可是,寫執行單元與寫執行單元之間仍然是互斥的,即若是有寫執行單元在進行寫操做,其它寫執行單元必須自旋在哪裏,直到寫執行單元釋放了順序瑣。

若是讀執行單元在讀操做期間,寫執行單元已經發生了寫操做,那麼,讀執行單元必須從新讀取數據,以便確保獲得的數據是完整的,這種鎖在讀寫同時進行的機率比較小時,性能是很是好的,並且它容許讀寫同時進行,於是更大的提升了併發性,

注意,順序瑣由一個限制,就是它必須被保護的共享資源不含有指針,由於寫執行單元可能使得指針失效,但讀執行單元若是正要訪問該指針,將致使Oops

6、信號量

Linux中的信號量是一種睡眠鎖,若是有一個任務試圖得到一個已經被佔用的信號量時,信號量會將其推動一個等待隊列,而後讓其睡眠,這時處理器能重獲自由,從而去執行其它代碼,當持有信號量的進程將信號量釋放後,處於等待隊列中的哪一個任務被喚醒,並得到該信號量。

信號量,或旗標,就是咱們在操做系統裏學習的經典的P/V原語操做。
P:若是信號量值大於0,則遞減信號量的值,程序繼續執行,不然,睡眠等待信號量大於0
V:遞增信號量的值,若是遞增的信號量的值大於0,則喚醒等待的進程。


    信號量的值肯定了同時能夠有多少個進程能夠同時進入臨界區,若是信號量的初始值始1,這信號量就是互斥信號量(MUTEX)。對於大於1的非0值信號量,也可稱爲計數信號量(counting semaphore)。對於通常的驅動程序使用的信號量都是互斥信號量。

相似於自旋鎖,信號量的實現也與體系結構密切相關,具體的實現定義在<asm/semaphore.h>頭文件中,對於x86_32系統來講,它的定義以下:

[html] view plain copy
  1. struct semaphore {  
  2.    <span style="color:#ff9900;">atomic_t</span> count;  
  3.    int sleepers;  
  4.    wait_queue_head_t wait;  
  5. };  


 信號量的初始值countatomic_t類型的,這是一個原子操做類型,它也是一個內核同步技術,可見信號量是基於原子操做的。咱們會在後面原子操做部分對原子操做作詳細介紹。

信號量的使用相似於自旋鎖,包括建立、獲取和釋放。咱們仍是來先展現信號量的基本使用形式:

[html] view plain copy
  1. static DECLARE_MUTEX(my_sem);  
  2. ......  
  3. if (down_interruptible(&my_sem))  
  4. {  
  5.     return -ERESTARTSYS;  
  6. }  
  7. ......  
  8. up(&my_sem);  


 Linux內核中的信號量函數接口以下:

static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);
seam_init(struct semaphore *, int);
init_MUTEX(struct semaphore *);
init_MUTEX_LOCKED(struct semaphore *)
down_interruptible(struct semaphore *);
down(struct semaphore *)
down_trylock(struct semaphore *)
up(struct semaphore *)

初始化信號量

信號量的初始化包括靜態初始化和動態初始化。靜態初始化用於靜態的聲明並初始化信號量。

static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);

 

對於動態聲明或建立的信號量,能夠使用以下函數進行初始化:

seam_init(sem, count);
init_MUTEX(sem);
init_MUTEX_LOCKED(struct semaphore *)

 

顯然,帶有MUTEX的函數始初始化互斥信號量。LOCKED則初始化信號量爲鎖狀態。

使用信號量

[html] view plain copy
  1. down_interruptible(struct semaphore *);          
  2.                                    <span style="color:#3366ff;">使進程進入可中斷的睡眠狀態。關於進程狀態的詳細細節,咱們在內核的進程管理裏在作詳細介紹。  
  3. </span>down(struct semaphore *)           嘗試獲取指定的信號量,若是信號量已經被使用了,則進程進入不可中斷的睡眠狀態。  
  4. down_trylock(struct semaphore *)   嘗試獲取信號量,若是獲取成功則返回0,失敗則會當即返回非0。  
  5. up(struct semaphore *)             釋放信號量,若是信號量上的睡眠隊列不爲空,則喚醒其中一個等待進程。  



7、讀寫信號量

相似於自旋鎖,信號量也有讀寫信號量。讀寫信號量API定義在<linux/rwsem.h>頭文件中,它的定義其實也是體系結構相關的,所以具體實現定義在<asm/rwsem.h>頭文件中,如下是x86的例子:

struct rw_semaphore {
    signed long       count;
    spinlock_t       wait_lock;
    struct list_head    wait_list;
};

 

首先要說明的是全部的讀寫信號量都是互斥信號量。讀鎖是共享鎖,就是同時容許多個讀進程持有該信號量,但寫鎖是獨佔鎖,同時只能有一個寫鎖持有該互斥信號量。顯然,寫鎖是排他的,包括排斥讀鎖。因爲寫鎖是共享鎖,它容許多個讀進程持有該鎖,只要沒有進程持有寫鎖,它就始終會成功持有該鎖,所以這會形成寫進程寫飢餓狀態。

在使用讀寫信號量前先要初始化,就像你所想到的,它在使用上幾乎與讀寫自旋鎖一致。先來看看讀寫信號量的建立和初始化:

//靜態初始化
static DECLARE_RWSEM(rwsem_name);

// 動態初始化
static struct rw_semaphore rw_sem
init_rwsem(&rw_sem);

 

讀進程獲取信號量保護臨界區數據:

down_read(&rw_sem);
...
up_read(&rw_sem);

 

寫進程獲取信號量保護臨界區數據:

down_write(&rw_sem);
...
up_write(&rw_sem);

 

更多的讀寫信號量API請參考下表:

#include <linux/rwsem.h>

DECLARE_RWSET(name);
init_rwsem(struct  rw_semaphore *);
void down_read(struct rw_semaphore *sem);
void down_write(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
void downgrade_write(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);

 

同自旋鎖同樣,down_read_trylockdown_write_trylock會嘗試着獲取信號量,若是獲取成功則返回1,不然返回0。奇怪爲何返回值與信號量的對應函數相反,使用是必定要當心這點。

 

9、自旋鎖和信號量區別

在驅動程序中,當多個線程同時訪問相同的資源時(驅動程序中的全局變量是一種典型的共享資源),可能會引起"競態",所以咱們必須對共享資源進行併發控制。Linux內核中解決併發控制的最經常使用方法是自旋鎖與信號量(絕大多數時候做爲互斥鎖使用)。

  自旋鎖與信號量"相似而不類",相似說的是它們功能上的類似性,"不類"指代它們在本質和實現機理上徹底不同,不屬於一類。

  自旋鎖不會引發調用者睡眠,若是自旋鎖已經被別的執行單元保持,調用者就一直循環查看是否該自旋鎖的保持者已經釋放了鎖,"自旋"就是"在原地打轉"。而信號量則引發調用者睡眠,它把進程從運行隊列上拖出去,除非得到鎖。這就是它們的"不類"

  可是,不管是信號量,仍是自旋鎖,在任什麼時候刻,最多隻能有一個保持者,即在任什麼時候刻最多隻能有一個執行單元得到鎖。這就是它們的"相似"

  鑑於自旋鎖與信號量的上述特色,通常而言,自旋鎖適合於保持時間很是短的狀況,它能夠在任何上下文使用;信號量適合於保持時間較長的狀況,會只能在進程上下文使用。若是被保護的共享資源只在進程上下文訪問,則能夠以信號量來保護該共享資源,若是對共享資源的訪問時間很是短,自旋鎖也是好的選擇。可是,若是被保護的共享資源須要在中斷上下文訪問(包括底半部即中斷處理句柄和頂半部即軟中斷),就必須使用自旋鎖。

區別總結以下:

1、因爲爭用信號量的進程在等待鎖從新變爲可用時會睡眠,因此信號量適用於鎖會被長時間持有的狀況。

2、相反,鎖被短期持有時,使用信號量就不太適宜了,由於睡眠引發的耗時可能比鎖被佔用的所有時間還要長。

3、因爲執行線程在鎖被爭用時會睡眠,因此只能在進程上下文中才能獲取信號量鎖,由於在中斷上下文中(使用自旋鎖)是不能進行調度的。

4、你能夠在持有信號量時去睡眠(固然你也可能並不須要睡眠),由於當其它進程試圖得到同一信號量時不會所以而死鎖,(由於該進程也只是去睡眠而已,而你最終會繼續執行的)。

5、在你佔用信號量的同時不能佔用自旋鎖,由於在你等待信號量時可能會睡眠,而在持有自旋鎖時是不容許睡眠的。

6、信號量鎖保護的臨界區可包含可能引發阻塞的代碼,而自旋鎖則絕對要避免用來保護包含這樣代碼的臨界區,由於阻塞意味着要進行進程的切換,若是進程被切換出去後,另外一進程企圖獲取本自旋鎖,死鎖就會發生。

7、信號量不一樣於自旋鎖,它不會禁止內核搶佔(自旋鎖被持有時,內核不能被搶佔),因此持有信號量的代碼能夠被搶佔,這意味着信號量不會對調度的等待時間帶來負面影響。

除了以上介紹的同步機制方法之外,還有BKL(大內核鎖),Seq鎖等。

BKL是一個全局自旋鎖,使用它主要是爲了方便實現從Linux最初的SMP過分到細粒度加鎖機制。

Seq鎖用於讀寫共享數據,實現這樣鎖只要依靠一個序列計數器。

 

 

 

linux內核開發總結----內核同步與異步

雜項:

gcc編譯器內置宏變量:
__FILE__ :當前文件名
__FUNCTION__ :當前函數名;
__LINE__ :的文件中的行數
__DATA__ :編譯時的日期
__TIME__ :編譯時間

gcc -E 預處理 
-S 彙編
-c 編譯

as  彙編器

ld  連接器

1.屬性聲名:指定一個屬性只需在其聲明後添加__attribute__((ATTRIBUTE));

例如:void do_exit(int n)__attribute((noreturn));
屬性:
noreturn:表示函數從不返回任何值。
format:表示函數使用printf,scanf或strftime風格的參數,根據格式串檢查參數類型。
unused:表示該函數或變量可能不會被用到。
aligned(n):指定變量,結構體或聯合體的對齊方式,以字節爲單位。
packed:做用於變量或結構體成員使用最小可能的對齊。

2.內核模塊參數:

MODULE_LICENSE("Dual BSD/GPL"); //聲明模塊採用BSD/GPL雙license
參數類型 參數名;(定義一個參數)
module_parma(參數名,參數類型,參數讀/寫權限(/sys/module/xxx/parameters));
insmod 模塊名 參數名=參數值
參數類型:byte,short,ushort,int,uint,long,ulong,bool,charp(字符指針)
讀寫權限:S_IRWXU,S_IRUSR

EXPORT_SYMBOL(符號名);
EXPORT_SYMBOL_GPL(符號名);
/proc/kallsyms 內核符號表

可選:
MODULE_AUTHOR();做者
MODULE_DESCRIPTION();描述
MODULE_VERSION();版本
MODULE_DEVICE_TABLE();設備表
MODULE_ALIAS();別名


3.內核空間與用戶空間傳遞數據

用戶空間--》內核空間
unsigned long copy_from_user(void*to,const void*from,unsigned long n);
to:內核目標地址
from:用戶空間源地址
n:要拷貝的字節數
返回:成功返回0,失敗返回沒有拷貝成功的字節數。
int get_user(data,ptr);
data:能夠是字節,半字,字,雙字類型的內核變量。
ptr:用戶空間內存指針。
返回:成功返回0,失敗返回非0.
內核空間--》用戶空間
unsigned long copt_to_user(void*to,const void*from,unsigned long n);
to:用戶空間目標地址
from:內核空間源地址
n:要拷貝的字節數。
返回:成功返回0,失敗返回沒有拷貝成功的字節數。
int put_user(data,ptr);
data:能夠是字節,半字,字,雙字類型的內核變量。
ptr:用戶空間內存指針。
返回:成功返回0,失敗返回非0
用戶空間內存可訪問性驗證:
int access_ok(int type,const void*addr,unsigned long size);
type:取值爲VERIFY_READ 或VERIFY_WRITE
addr:待驗證的用戶內存地址
size:待驗證的用戶內存長度
返回值:返回非0表明用戶內存可訪問 ,返回0表明失敗

調度:SCHED_NORMAL,SCHED_FIFO,SCHED_RR,SCHED_BATCH,SCHED_IDLE
主動調度:
current->state=TASK_INTERRUPTIBLE;
schedule();
schedule_timeout();
 被動調度:中斷,時間片
 

 4.procfs:

 struct proc_dir_entry{
mode_t mode;文件權限保護位
struct module *owner;當前擁有者
read_proc_t *read_proc;讀函數
write_proc_t *write_proc;寫函數
 }
 typedef int(read_proc_t)(char*page,char**start,off_t off,int count,int *eof,void*data)
page:要返回給用戶的信息存放頁面,最多一個PAGE_SIZE.
start:通常不使用 data:通常不使有
off:讀數據偏移
count:用戶要讀取的數據長度
eof:讀到文件結尾時,須要把*eof設爲1

typedef int(write_proc_t)(struct file*file,const char __user*buffer,unsigned long count,void*data)
file:該procfs文件對應的內核struct file結構。
buffer:用戶要寫入的數據在用戶空間的指針
count:用戶要寫入的數據大小
data:通常不使用
建立目錄:
 struct proc_dir_entry*proc_mkdir(char*name,struct proc_dir_entry*parent);
建立文件:
struct proc_dir_entry*create_proc_entry(char*name,mode_t mode,struct proc_dir_entry*parent);
刪除文件:
void remove_proc_entry(char*name,struct proc_dir_entry*parent);

5,內核線程:

內核線程: 內核支持,多線程內核。
輕量級進程(LWP): 由內核支持的用戶線程(各類系統調用)。是基於內核線程的抽象。
用戶線程: 基於用戶空間的線程庫。用戶線程的創建,同步,銷燬,調度徹底在用戶空間完成。

建立內核線程:

linux/sched.h

pid_t  kernel_thread(int(*fn)(void*),void*arg,unsigned long flags);//fork實現原理

建立線程:

linux/kthread.h

struct task_struct*kthread_creat(int(*threadfn)(void*data),void*data,char namefmt[],...);

wake_up_process(struct task_struct*p);//喚醒指定的線程

建立並當即喚醒線程:

#definekthead_run(threadfn,data,namefmt,...) \
{ struct task_struct *__k=kthread_create(threadfn,data,namefmt,##__VA_ARGS__); \
if(!IS_ERR(__k))\
wake_up_process(__k); \
k;  \
}

綁定到指定的CPU核:

voidkthread_bind(struct task_struct*k,unsigned int cpu);
int kthread_stop(struct task_struct*k);結束指定的線程


線程函數內API:

int kthread_should_stop(void);//線程函數內判斷是否收到結束信號
set_current_state(TASK_INTERRUPTIBLE);//設置進程當前狀態
schedule();調度
schedule_timeout();//定時調度

一,Linux 內核同步方法

競態條件 兩個或更多線程同時操做資源時將會致使不一致的結果。
臨界段 用於協調對共享資源的訪問的代碼段。
互斥鎖 確保對共享資源進行排他訪問的軟件特性。
死鎖 由兩個或更多進程和資源鎖致使的一種特殊情形,將會下降進程的工做效率。

中斷屏蔽:
屏蔽本CPU中斷: local_irq_disable();local_irq_save();
critical section //臨界區
開本CPU中斷: local_irq_enable();local_irq_restore();
屏蔽本CPU中斷下半部:local_bh_disable();
使能本CPU中斷下半部:local_bh_enable();

1.原子操做:

typedef struct {
int counter;
} atomic_t;
定義一個原子變量: atomic_t my_counter;           
定義並初始化原子變量: atomic_t my_counter=ATOMIC_INIT(0);
設置原子變量: atomic_set( &my_counter, n );
讀原子變量的值:val = atomic_read( &my_counter );
原子變量+n: atomic_add( 1, &my_counter ); val=atomic_add_return(value,&atomic) 
原子變量+1: atomic_inc( &my_counter );val=atomic_inc_return(atomic_t*v);
原子變量-n:atomic_sub( 1, &my_counter );val=atomic_sub_return(value,&atomic)
原子變量-1:atomic_dec( &my_counter );var=atomic_dec_return(atomic_t*v);

改變原子變量的值而且測試是否爲0
atomic_sub_and_test( 1, &my_counter )
atomic_dec_and_test( &my_counter )
atomic_inc_and_test( &my_counter )
atomic_add_negative( 1, &my_counter )

位原子操做:
設置位:set_bit(nr,void*addr);//設置addr地址的第nr位爲1.
清除位:clear_bit(nr,void*addr);//設置addr地址的第nr位爲0.
改變位:change_bit(nr,void*addr);//對addr地址的第nr位進行反置。
測試位:test_bit(nr,void*addr);//返回addr地址的第nr位。
測試並操做位:
int test_and_set_bit(nr,void*addr);
int test_and_clear_bit(nr,void*addr);
int test_and_change_bit(nr,void*addr);
位掩碼
unsigned long my_bitmask;
atomic_clear_mask( 0, &my_bitmask );
atomic_set_mask( (1<<24), &my_bitmask );

2.自旋鎖(用於SMP,單CPU時禁止內核搶佔)


定義自旋鎖:spinlock_t my_spinlock = SPIN_LOCK_UNLOCKED;

DEFINE_SPINLOCK( my_spinlock );

初始化自旋鎖:spin_lock_init( &my_spinlock );
加鎖:spin_lock( &my_spinlock );spin_trylock(lock);
臨界區...
解鎖:spin_unlock( &my_spinlock );

(爲了不在臨界區中還受到中斷和底半部bh的影響)
加鎖並關本地CPU中斷:spin_lock_irqsave( &my_spinlock, flags );
//critical section
解鎖並恢復本地CPU中斷:spin_unlock_irqrestore( &my_spinlock, flags );
加鎖並關本地CPU軟中斷:spin_lock_bh( &my_spinlock );
// critical section
加鎖並恢復本地CPU軟中斷:spin_unlock_bh( &my_spinlock );

3.讀寫(自旋)鎖-->RCU(讀拷貝更新):能夠看做讀寫鎖的高性能版。

讀寫鎖變量:rwlock_t my_rwlock=RW_LOCK_LOCKED;//靜態初始化
讀寫鎖初始化:rwlock_init( &my_rwlock );
寫鎖加鎖:write_lock( &my_rwlock );

write_lock_irq(lock);
write_lock_irqsave(lock,flags);
write_lock_bh(lock);
write_trylock(lock);
//critical section -- can read and write

寫鎖解鎖:write_unlock( &my_rwlock );

write_unlock_irq(lock);
write_unlock_irqrestore(lock,flags);
write_unlock_bh(lock);
write_unlock_irq(lock);
write_unlock_irqrestore(lock,flags);
write_unlock_bh(lock);


讀鎖加鎖: read_lock( &my_rwlock );
read_lock_irq(lock);
read_lock_irqsave(lock,unsigned long flags);
read_lock_bh(lock);
//critical section -- can read only
讀鎖解鎖:read_unlock( &my_rwlock );

read_unlock_irq(lock);
read_unlock_irqrestore(lock,flags);
read_unlock_bh(lock);


4.順序鎖:seqlock是對讀寫鎖的一種優化。要求被保護的共享資源不含有指針。

定義: seqlock_t sl;
初始化: seqlock_init(&sl);

讀順序鎖: unsigned rsl=read_seqbegin(seqlock_t &sl);
讀順序鎖檢查: int read_seqretry(seqlock_t*sl,unsigned start);//對共享資源訪問完後,檢查在讀訪問期間是否有寫操做發生。
使用模型:
do{

sequnum=read_seqbegin(&sl);
......
critical section;臨界區
.....

}while(read_seqretry(&sl,seqnum));

寫順序鎖鎖定:write_seqlock(seqlock_t*sl);

write_tryseqlock(seqlock_t*sl);
write_seqlock_irq(seqlock_t*sl);
write_seqlock_irqsave(seqlock_t*lock,unsigned long flags);
write_seqlock_bh(seqlock_t*lock);
......critical section;//臨界區

寫順序鎖解鎖:

write_sequnlock(seqlock_t*sl);
write_sequnlock_irq(seqlock_t*sl);
write_sequnlock_irqrestore(seqlock_t*lock,unsigned long flags);
write_sequnlock_bh(seqlock_t*lock);


5.互斥體(鎖)(信號量爲1的特例)

聲明一個互斥體: struct mutex my_mutex;
初始化互斥體: mutex_init(&my_mutex);
 定義一個互斥鎖:DEFINE_MUTEX(my_mutex);
 非阻塞加鎖: void fastcall mutex_trylock(&my_mutex);
 阻塞加鎖: void fastcall mutex_lock(&my_mutex);
 可中斷阻塞加鎖:void fastcall mutex_lock_interruptible(&my_mutex);
...critical section;臨界區
 解鎖:void fastcall mutex_unlock( &my_mutex );
 檢查鎖狀態:mutex_is_locked( &my_mutex );

6.信號量(初始化爲0時,用於同步)

struct semaphore {

raw_spinlock_tlock;
unsigned int count;
struct list_headwait_list;

};
定義信號量:struct semaphore sem;
初始化信號量:sema_init(struct semaphore*sem,int val);//初始化信號量sem,並設置count=val;
init_MUTEX(struct semaphore*sem);//初始化信號量sem,並設置count=1;
init_MUTEX_LOCKED(struct semaphore*sem);
//初始化信號量,並將count=0。也就是建立時就處於鎖定狀態。
獲取信號量:
void down(struct semaphore*sem);//獲取信號量,可能睡眠,不能用於中斷上下文
int down_interruptible(struct semaphore*sem);//獲取信號量,信號量不可用時,
把進程設置爲TASK_INTERRUPTIBLE的睡眠狀態,
返回值0表明獲取信號量正常返回,非0表明被信號打斷返回。
int down_killable(struct semaphore*sem);//獲取信號量,當信號量不可用時,把進程設置爲TASK_KILLABLE的睡眠狀態。
int down_trylock(struct semaphore*sem);//不會引發睡眠,可在中斷上下文中使用。
......critical section臨界區
釋放信號量:
void up(struct semaphore*sem);//釋放信號量sem,實質上把sem的值加1,
若是sem的值爲非正數,代表有任務在等待,所以須要喚醒等待者。

7.完成變量completion;

struct completion {

unsigned int done;
wait_queue_head_t wait;

};
定義完成變量:

struct completion my_completion;

初始化完成變量:

init_completion(&my_completion);
DECLARE_COMPLETION(my_completion);
DECLARE_COMPLETION_ONSTACK(work);

等待完成變量:

void wait_for_completion(struct completion *c);

喚醒完成變量:

void complete(struct completion*c);
void complete_all(struct completion*c);


8,大內核鎖BLK(愈來愈少用)

 lock_kernel();

 unlock_kernel();

 

二,內核異步方法(API) 

1.linux中斷

 中斷請求號IRQ->中斷向量-> 224:irq_desc_t irq_desc[NR_IRQ](前32個爲異常和NMI,32~48爲INTR,48~255爲軟中斷)
中斷線->中斷處理程序---------->中斷服務程序(ISR)
中斷上半部(Tob Half) 中斷下半部(Bottom Half)
軟中斷(softirq,tasklet,work queue)

中斷申請:
int request_irq(unsigned int irq,irq_handler_t handler,unsigned long irqflags,const char*name,void*dev_id); 
irq:想要申請的中斷號
handler:想要註冊的中斷處理函數(返回:IRQ_NONE:未作處理,IRQ_HANDLED:正常處理後返回該值)
static irqreturn_t (*irq_handler_t)(int irq,void*dev_id);
irqflags:中斷標誌

IRQF_SHARED:表示多個設備共享中斷
IRQF_SAMPLE_RANDOM:用於隨機數種子的隨機採樣
IRQF_TRIGGER_RISING:上升沿觸發中斷
IRQF_TRIGGER_FALLING:降低沿觸發中斷
IRQF_TRIGGER_HIGH:高電平觸發中斷
IRQF_TRIGGER_LOW:低電平觸發中斷

name:中斷設備的名稱
dev_id:傳遞給中斷處理函數的指針,一般用於共享中斷時傳遞設備結構體指針
成功返回0,失敗返回負值。
-EINVAL:表示申請的中斷號無效或中斷處理函數指針爲空
-EBUSY:表示中斷已經被佔用而且不能共享


中斷釋放:

void free_irq(unsigned int irq,void *dev_id);

 使能和屏蔽中斷:

 void disable_irq(unsigned int irq);屏蔽指定的中斷 void disable_irq_nosync(unsigned int irq);屏蔽指定的中斷,當即返回,不等待可能正在執行的中斷處理程序。 void enable_irq(unsigned int irq);使能指定的中斷

本CPU所有中斷:

local_irq_disable();local_irq_save();......local_irq_enable();local_irq_restore(); 

軟中斷和tasklet:

local_bh_disable();local_bh_enable();

2.中斷下半部:(軟中斷,tasklet仍然運行於中斷上下文,工做隊列是進程上下文) 2.1.軟中斷;用軟件方式模擬硬件中斷,來實現異步執行。 聲明軟中斷:<linux/interrupt.h>中定義一個枚舉類型來靜態聲明軟中斷。HI_SOFTIRQ 0TIMER_SOFTIRQ 1NET_TX_SOFTIRQ2NET_RX_SOFTIRQ3......RCU_SOFTIRQ //創建一個新的軟中斷在此以前添加一項NR_SOFTIRQS 註冊軟中斷: open_softirq(int nr,void(*action)(struct softirq_action*)); 觸發軟中斷: raise_softirq(int nr);raise_softirq_irqoff(int nr); 喚醒軟中斷處理線程ksoftirqd:void wakeup_softirqd(void); 2.2.tasklet:基於軟中斷 tasklet定義並初始化:DECLARE_TASKLET(taskletname,task_func,data);DECLARE_TASKLET_DISABLED(name,func,data);tasklet_init(taskletname,task_func,data); tasklet處理函數:void tasklet_func(unsigned long data); tasklet調用:void tasklet_schedule(struct tasklet_struct*taskletname);void tasklet_hi_schedule(struct tasklet_struct*d); tasklet禁止:void tasklet_disable_nosync( struct tasklet_struct * );void tasklet_disable( struct tasklet_struct * ); tasklet使能:void tasklet_enable( struct tasklet_struct * );void tasklet_hi_enable( struct tasklet_struct * ); tasklet關閉:void tasklet_kill( struct tasklet_struct * );void tasklet_kill_immediate( struct tasklet_struct *, unsigned int cpu ); 2.3.工做隊列:把推後的工做交由內核線程去執行。容許從新調度或睡眠。 #include <linux/workqueu.h> #include <linux/kthread.h> #include <linux/sched.h> 工做數據類型定義(2.6.20後分紅兩部分):struct work_struct { atomic_long_t data;struct list_head entry;工做數據鏈成員work_func_t func;工做處理函數,由用戶實現 };struct delayed_work { struct work_struct work;工做結構體struct timer_list timer;推後執行的定時器/* target workqueue and CPU ->timer uses to queue ->work */struct workqueue_struct *wq;int cpu; }; 2.3.1,工做 初始化工做:(使用默認的工做者線程) 聲明並初始化工做: DECLARE_WORK(name,func,data); 初始化工做隊列: INIT_WORK(struct work_struct*work,work_func_t func); 初始化廷遲工做隊列: INIT_DELAYED_WORK(struct delayed_work*work,wrok_func_t func); 調度工做:int schedule_work(struct work_struct*work);//把工做提交給缺省的工做者線程處理;內部是queue_work(system_wq, work);int schedule_delayed_work(struct delayed_work*work,unsigned long delay);把工做提交給缺省的工做者線程處理,並指定延遲時間。 刷新工做隊列:(喚配工做線程)void flush_scheduled_work(void); //刷新缺省的工做隊列,此函數一直等待,直到隊列中的全部工做完成。bool flush_delayed_work(struc delayed_work*dwork);bool flush_work(struct work_struct*work); 取消延遲工做:int cancel_delayed_work(struct delayed_work*work); //取消缺省工做隊列中處於等待狀態的延遲工做。bool cancel_delayed_work_sync(struct delayed_work*work); 取消工做:int cancel_work_sync(struct work_struct*work); //取消缺省工做隊列中處於等待狀態的工做,若是工做已經開始執行,該函數會阻塞直到工做處理函數完成。 2.3.2,工做者線程:默認狀況下,每一個CPU均有一個events/n的工做者線程,當調用schedule_work()時,這個工做者線會被喚醒去執行工做鏈表上的全部工做。 工做隊列數據類型: struct workqueue_struct{ ......const char*name;struct list_head list;...... } 建立工做隊列:struct workqueue_struct*create_workqueue(const char*name); stuct workqueue_struct *create_singlethread_workqueue(const char* name); (3.x內核API) struct workqueue_struct *alloc_workqueue(fmt, flags, max_active,args...); //3.9內核中工做隊列使用線程池,所有由kworker/n 執行 //建立新的工做隊列和相應的工做者線程, 調度工做:int queue_work(struct workqueue_struct*wq,struct work_struct*work);//調度工做,相似於schedule_work()函數,將指定的工做work加入到工做隊列wq. 調度延遲工做:int queue_delayed_work(struct workqueue_struct*wq,struct delayed_work*dwork,unsigned long delay);//調度工做,相似於schedule_work()函數,將延遲工做work提交給工做隊列wq,並指定延遲時間. 刷新工做隊列:void flush_workqueue(struct workqueue_struct*wq);//刷新工做隊列wq,此函數一直等待,直到隊列中的全部工做完成。 銷燬工做隊列:void destroy_workqueue(struct workqueue_struct*wq);//銷燬工做隊列wq  3,內核定時器  struct timer_list { struct list_head entry;鏈表接入件unsigned long expires;到期時間struct tvec_base *base;void (*function)(unsigned long);超時處理函數unsigned long data;超時處理函數參數int slack; } struct timer_list mytimer; DEFINE_TIMER(name,function,expires,data); TIMER_INITIALIZER(function,expires,data); 初始化計時器: void init_timer(struct timer_list *timer); 安裝計時器: setup_timer(timer, fn, data) 添加定時器: add_timer(timer);//修改expieres+n*jiffies,再次添加。 修改計時器: mod_timer(struct timer_list*timer,unsigned long expires); 刪除計時器: del_timer(struct timer_list*timer); 檢測計時器是否在等待(還沒發出):int timer_pending(struct timer_list*timer); 4,高精度計時器hrtimer-->基於ktime_t void hrtimer_init(struct hrtimer *timer, clockid_t clock_id,enum hrtimer_mode mode); int hrtimer_start(struct hrtimer *timer, ktime_t tim, const enum hrtimer_mode mode); int hrtimer_cancel(struct hrtimer *timer); int hrtimer_try_to_cancel(struct hrtimer *timer) 5,等待隊列wait queue 定義等待隊列頭:wait_queue_head_t wqh; 初始化等待隊列頭:init_waitqueue_head(wait_queue_head_t*wqh); 定義並初始化等待隊列頭: DECLARE_WAIT_QUEUE_HEAD(name); 定義等待隊列: wait_queue_t name; 初始化等待隊列: init_wait(name); 定義並初始化等待隊列:DECLARE_WAITQUEUE(name, tsk); 添加等待隊列:add_wait_queue(wait_queue_head_t*q,wait_queue_t*wait);//將等待隊列wait加入到等待隊列頭q執行的等待隊列鏈表中去。或者從中刪除。 移除等待隊列:remove_wait_queue(wait_queue_head_t*q,wait_queue_t*wait); 等待事情: wait_event(qhead,condition);//當條件爲真時,當即返回, 不然進入TASK_UNINTERRUPTIBLE的睡眠狀態,並掛在queue指定的等待隊列上。 wait_event_interruptible(qhead,condition); add_wait_queue(qhead,condition);//當條件爲真時,當即返回, 不然進入TASK_INTERRUPTIBLE的睡眠狀態,並掛在queue指定的等待隊列上。 wait_event_killable(qhead,condition);//當條件爲真時,當即返回, 不然進入TASK_KILLABLE的睡眠狀態,並掛在queue指定的等待隊列上。 wait_event_timeout(qhead,condition,timeout);//當條件爲真時,當即返回, 不然進入TASK_UNINTERRUPTIBLE的睡眠狀態,並掛在queue指定的等待隊列上,當阻塞時間timeout超時後,當即返回。wait_event_interruptible_timeout(qhead,condition,timeout);//當條件爲真時,當即返回, 不然進入TASK_INTERRUPTIBLE的睡眠狀態,並掛在queue指定的等待隊列上,當阻塞時間timeout超時後,當即返回。 喚醒隊列: wake_up(wait_queue_head_t*queue); wake_up_interruptible(wait_queue_head_t*queue); //喚醒由queue指向的等待隊列頭鏈表中全部等待隊列對應的進程。 在等待隊列中睡眠: sleep_on(wait_queue_head_t*q);//讓進程進入不可中斷的睡眠,並將它放入等待隊列。 interruptible_sleep_on(wait_queue_head_t*q);//讓進程進入可中斷的睡眠,並將它放入等待隊列。 等待隊列頭使用: init_waitqueue_head(wait_queue_head_t*wqh);wait_event_interruptible(qhead,condition);wake_up_interruptible(wait_queue_head_t*queue); 等待隊列使用: init_waitqueue_head(wait_queue_head_t*qhead);DECLARE_WAITQUEUE(name, current);add_wait_queue(&qhead,&name);set_current_state(TASK_INTERRUPTIBLE);schedule();wake_up_interruptible(*qhead);remove_wait_queue(&qhead,&name);set_current_state(TASK_RUNNING); 內核等待隊列通常使用方法:1.定義和初始化等待隊列,將進程狀態改變,並將等待隊列放入等待隊列數據鏈中2.改變進程狀態a>,set_current_state(state_value);b>,set_task_state(task,state_value);c>,current->state=TASK_INTERRUPTIBLE;3.放棄CPU,調度其它進程執行:schedule();//schedule_timeout();4.進程被其它地方喚醒,將等待隊列移出等待隊列頭指向的數據鏈。 6,內核鏈表 struct mydata{ ... struct list_head list } struct list_head*pos,*q; struct mydata*obj; 建立並初始化鏈表頭:LIST_HEAD(myhead); 初始化鏈表頭: struct list_head myhead = LIST_HEAD_INIT(myhead);obj=kmalloc(sizeof(struct mydata),GFP_KERNEL);obj->...對象成員操做list_add(&obj->list,&myhead) 插入鏈表前面:list_add(struct list_head*new,struct list_head*head); 插入鏈表後面:list_add_tail(struct list_head*new,struct list_head*head); for循環獲取鏈表中的鏈入件: list_for_each(pos,&myhead){ 從鏈入件獲得對象:list_entry(pos,struct mydata,list); //list_entry(ptr,type,member)=container_of(ptr, type, member) } for循環獲取鏈表中的對象:list_for_each_entry(obj,&myhead,list){};//list_for_each_entry(pos,head,member){} (刪除時)for循環取鏈表中的鏈入件:list_for_each_safe(pos,q,&myhead){struct mydata*tmp;tmp=list_entry(pos,struct mydata,list); 刪除一個連接件: list_del(pos);kfree(tmp);} 把一個鏈表插入一個鏈表前:list_splice(struct list_head*list,struct list_head*head);list_splice_tail(list,head); 判斷鏈表是否爲空:list_empty(struct list_head*head); 7,輪詢:(字符輪詢 驅動實現) pull_wait(); 8,異步通知隊列:(支持異步通知機制設備驅動實現) struct fasync_struct { spinlock_t fa_lock;int magic;int fa_fd;struct fasync_struct*fa_next; /* singly linked list */struct file *fa_file;struct rcu_headfa_rcu; }; 常見用法: 設備驅動中聲明一個該類型變量:struct xxx_cdev{struct cdev chrdev;...struct fasync_struct *async_queue;} 設備驅動中實現fasync():static int xxx_fasync(){struct xxx_cdev*dev=filep->private_data;return fasync_helper(fd,filp,mode,&dev->async_queue);} fasync_helper()函數;初始化異步事件通知隊列,包括分配內存和設置屬性;釋放初始化時分配的內存。 kill_fasync(struct fasync_struct**fp,int sig,int band)函數; 在設備資源能夠得到時,應該調用此函數釋放SIGIO信號,(band)可讀時爲POLL_IN,可寫時爲POLL_OUT;

 

 

 

 

 

linux環境內存分配原理

Linux的虛擬內存管理有幾個關鍵概念

Linux 虛擬地址空間如何分佈?malloc和free是如何分配和釋放內存?如何查看堆內內存的碎片狀況?既然堆內內存brk和sbrk不能直接釋放,爲何不所有使用 mmap 來分配,munmap直接釋放呢 ?

 

Linux 的虛擬內存管理有幾個關鍵概念:
一、每一個進程都有獨立的虛擬地址空間,進程訪問的虛擬地址並非真正的物理地址;
二、虛擬地址可經過每一個進程上的頁表(在每一個進程的內核虛擬地址空間)與物理地址進行映射,得到真正物理地址;
三、若是虛擬地址對應物理地址不在物理內存中,則產生缺頁中斷,真正分配物理地址,同時更新進程的頁表;若是此時物理內存已耗盡,則根據內存替換算法淘汰部分頁面至物理磁盤中。
  

1、Linux 虛擬地址空間如何分佈?
Linux 使用虛擬地址空間,大大增長了進程的尋址空間,由低地址到高地址分別爲
一、只讀段:該部分空間只能讀,不可寫;(包括:代碼段、rodata 段(C常量字符串和#define定義的常量) )
二、數據段:保存全局變量、靜態變量的空間;
三、堆 :就是平時所說的動態內存, malloc/new 大部分都來源於此。其中堆頂的位置可經過函數 brk 和 sbrk 進行動態調整。
四、文件映射區域 :動態庫、共享內存等映射物理空間的內存,通常是 mmap 函數所分配的虛擬地址空間。
五、棧:用於維護函數調用的上下文空間,通常爲 8M ,可經過 ulimit –s 查看。
六、內核虛擬空間:用戶代碼不可見的內存區域,由內核管理(頁表就存放在內核虛擬空間)。
下圖是 32 位系統典型的虛擬地址空間分佈(來自《深刻理解計算機系統》)。

32 位系統有4G 的地址空間::

      其中 0x08048000~0xbfffffff 是用戶空間,0xc0000000~0xffffffff 是內核空間,包括內核代碼和數據、與進程相關的數據結構(如頁表、內核棧)等。另外,%esp 執行棧頂,往低地址方向變化;brk/sbrk 函數控制堆頂_edata往高地址方向變化


64位系統結果怎樣呢? 64 位系統是否擁有 2^64 的地址空間嗎?
事實上, 64 位系統的虛擬地址空間劃分發生了改變:
一、地址空間大小不是2^32,也不是2^64,而通常是2^48。

由於並不須要 2^64 這麼大的尋址空間,過大空間只會致使資源的浪費。64位Linux通常使用48位來表示虛擬地址空間,40位表示物理地址,
這可經過#cat  /proc/cpuinfo 來查看:

二、其中,0x0000000000000000~0x00007fffffffffff 表示用戶空間, 0xFFFF800000000000~ 0xFFFFFFFFFFFFFFFF 表示內核空間,共提供 256TB(2^48) 的尋址空間。
這兩個區間的特色是,第 47 位與 48~63 位相同,若這些位爲 0 表示用戶空間,不然表示內核空間。
三、用戶空間由低地址到高地址仍然是只讀段、數據段、堆、文件映射區域和棧

 

2、malloc和free是如何分配和釋放內存?

如何查看進程發生缺頁中斷的次數

         用# ps -o majflt,minflt -C program 命令查看


          majflt表明major fault,中文名叫大錯誤,minflt表明minor fault,中文名叫小錯誤

          這兩個數值表示一個進程自啓動以來所發生的缺頁中斷的次數。

能夠用命令ps -o majflt minflt -C program來查看進程的majflt, minflt的值,這兩個值都是累加值,從進程啓動開始累加。在對高性能要求的程序作壓力測試的時候,咱們能夠多關注一下這兩個值。 
若是一個進程使用了mmap將很大的數據文件映射到進程的虛擬地址空間,咱們須要重點關注majflt的值,由於相比minflt,majflt對於性能的損害是致命的,隨機讀一次磁盤的耗時數量級在幾個毫秒,而minflt只有在大量的時候纔會對性能產生影響。

發成缺頁中斷後,執行了那些操做?

當一個進程發生缺頁中斷的時候,進程會陷入內核態,執行如下操做
一、檢查要訪問的虛擬地址是否合法
二、查找/分配一個物理頁
三、填充物理頁內容(讀取磁盤,或者直接置0,或者啥也不幹)
四、
創建映射關係(虛擬地址到物理地址)
從新執行發生缺頁中斷的那條指令
若是第3步,須要讀取磁盤,那麼此次缺頁中斷就是majflt,不然就是minflt。

內存分配的原理

從操做系統角度來看,進程分配內存有兩種方式,分別由兩個系統調用完成:brk和mmap(不考慮共享內存)。

一、brk是將數據段(.data)的最高地址指針_edata往高地址推;

二、mmap是在進程的虛擬地址空間中(堆和棧中間,稱爲文件映射區域的地方)找一塊空閒的虛擬內存

     這兩種方式分配的都是虛擬內存,沒有分配物理內存在第一次訪問已分配的虛擬地址空間的時候,發生缺頁中斷,操做系統負責分配物理內存,而後創建虛擬內存和物理內存之間的映射關係。


在標準C庫中,提供了malloc/free函數分配釋放內存,這兩個函數底層是由brk,mmap,munmap這些系統調用實現的。


下面以一個例子來講明內存分配的原理:

狀況1、malloc小於128k的內存,使用brk分配內存,將_edata往高地址推(只分配虛擬空間,不對應物理內存(所以沒有初始化),第一次讀/寫數據時,引發內核缺頁中斷,內核才分配對應的物理內存,而後虛擬地址空間創建映射關係),以下圖:

一、進程啓動的時候,其(虛擬)內存空間的初始佈局如圖1所示。
      其中,mmap內存映射文件是在堆和棧的中間(例如libc-2.2.93.so,其它數據文件等),爲了簡單起見,省略了內存映射文件。
      _edata指針(glibc裏面定義)指向數據段的最高地址。
二、
進程調用A=malloc(30K)之後,內存空間如圖2:
      malloc函數會調用brk系統調用,將_edata指針往高地址推30K,就完成虛擬內存分配。
      你可能會問:只要把_edata+30K就完成內存分配了?
      事實是這樣的,_edata+30K只是完成虛擬地址的分配,A這塊內存如今仍是沒有物理頁與之對應的,等到進程第一次讀寫A這塊內存的時候,發生缺頁中斷,這個時候,內核才分配A這塊內存對應的物理頁。也就是說,若是用malloc分配了A這塊內容,而後歷來不訪問它,那麼,A對應的物理頁是不會被分配的。
三、
進程調用B=malloc(40K)之後,內存空間如圖3。

狀況2、malloc大於128k的內存,使用mmap分配內存,在堆和棧之間找一塊空閒內存分配(對應獨立內存,並且初始化爲0),以下圖:

四、進程調用C=malloc(200K)之後,內存空間如圖4:
      默認狀況下,malloc函數分配內存,若是請求內存大於128K(可由M_MMAP_THRESHOLD選項調節),那就不是去推_edata指針了,而是利用mmap系統調用,從堆和棧的中間分配一塊虛擬內存。
      這樣子作主要是由於::
      brk分配的內存須要等到高地址內存釋放之後才能釋放(例如,在B釋放以前,A是不可能釋放的,這就是內存碎片產生的緣由,何時緊縮看下面),而mmap分配的內存能夠單獨釋放。
      固然,還有其它的好處,也有壞處,再具體下去,有興趣的同窗能夠去看glibc裏面malloc的代碼了。
五、進程調用D=malloc(100K)之後,內存空間如圖5;
六、進程調用free(C)之後,C對應的虛擬內存和物理內存一塊兒釋放。

七、進程調用free(B)之後,如圖7所示:
        B對應的虛擬內存和物理內存都沒有釋放,由於只有一個_edata指針,若是往回推,那麼D這塊內存怎麼辦呢
固然,B這塊內存,是能夠重用的,若是這個時候再來一個40K的請求,那麼malloc極可能就把B這塊內存返回回去了
八、進程調用free(D)之後,如圖8所示:
        B和D鏈接起來,變成一塊140K的空閒內存。
九、默認狀況下:
       當最高地址空間的空閒內存超過128K(可由M_TRIM_THRESHOLD選項調節)時,執行內存緊縮操做(trim)。在上一個步驟free的時候,發現最高地址空閒內存超過128K,因而內存緊縮,變成圖9所示。

真相大白
說完內存分配的原理,那麼被測模塊在內核態cpu消耗高的緣由就很清楚了:每次請求來都malloc一塊2M的內存,默認狀況下,malloc調用mmap分配內存,請求結束的時候,調用munmap釋放內存。假設每一個請求須要6個物理頁,那麼每一個請求就會產生6個缺頁中斷,在2000的壓力下,每秒就產生了10000屢次缺頁中斷,這些缺頁中斷不須要讀取磁盤解決,因此叫作minflt;缺頁中斷在內核態執行,所以進程的內核態cpu消耗很大。缺頁中斷分散在整個請求的處理過程當中,因此表現爲分配語句耗時(10us)相對於整條請求的處理時間(1000us)比重很小。

解決辦法
將動態內存改成靜態分配,或者啓動的時候,用malloc爲每一個線程分配,而後保存在threaddata裏面。可是,因爲這個模塊的特殊性,靜態分配,或者啓動時候分配都不可行。另外,Linux下默認棧的大小限制是10M,若是在棧上分配幾M的內存,有風險。 
禁止malloc調用mmap分配內存,禁止內存緊縮。
在進程啓動時候,加入如下兩行代碼:
mallopt(M_MMAP_MAX, 0); // 禁止malloc調用mmap分配內存
mallopt(M_TRIM_THRESHOLD, -1); // 禁止內存緊縮
效果:加入這兩行代碼之後,用ps命令觀察,壓力穩定之後,majlt和minflt都爲0。進程的系統態cpu從20降到10。

 

3、如何查看堆內內存的碎片狀況 ?

glibc 提供瞭如下結構和接口來查看堆內內存和 mmap 的使用狀況。
struct mallinfo {
  int arena;            /* non-mmapped space allocated from system */
  int ordblks;         /* number of free chunks */
  int smblks;          /* number of fastbin blocks */
  int hblks;             /* number of mmapped regions */
  int hblkhd;           /* space in mmapped regions */
  int usmblks;        /* maximum total allocated space */
  int fsmblks;         /* space available in freed fastbin blocks */
  int uordblks;        /* total allocated space */
  int fordblks;         /* total free space */
  int keepcost;       /* top-most, releasable (via malloc_trim) space */
};

/*返回heap(main_arena)的內存使用狀況,以 mallinfo 結構返回 */
struct mallinfo mallinfo();

/* 將heap和mmap的使用狀況輸出到stderr*/
void malloc_stats();

可經過如下例子來驗證mallinfo和malloc_stats輸出結果。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <malloc.h>

size_t  heap_malloc_total, heap_free_total,mmap_total, mmap_count;

void print_info()
{
    struct mallinfo mi = mallinfo();
 
    printf("count by itself:\n");
    printf("\theap_malloc_total=%lu heap_free_total=%lu heap_in_use=%lu\n\tmmap_total=%lu mmap_count=%lu\n",
              heap_malloc_total*1024, heap_free_total*1024, heap_malloc_total*1024-heap_free_total*1024,
              mmap_total*1024, mmap_count);
 
 printf("count by mallinfo:\n");
 printf("\theap_malloc_total=%lu heap_free_total=%lu heap_in_use=%lu\n\tmmap_total=%lu mmap_count=%lu\n",
             mi.arena, mi.fordblks, mi.uordblks,
             mi.hblkhd, mi.hblks);
 
 printf("from malloc_stats:\n");
 malloc_stats();
}

#define ARRAY_SIZE 200
int main(int argc, char** argv)
{
    char** ptr_arr[ARRAY_SIZE];
    int i; 
    for( i = 0; i < ARRAY_SIZE; i++)
    {
            ptr_arr[i] = malloc(i * 1024); 
            if ( i < 128)                                      //glibc默認128k以上使用mmap
            {
                    heap_malloc_total += i;
            }
            else
            {
                    mmap_total += i;
                   mmap_count++;
            }
    } 
    print_info(); 


    for( i = 0; i < ARRAY_SIZE; i++)
    {
           if ( i % 2 == 0)
                continue;
           free(ptr_arr[i]);

           if ( i < 128)
           {
                   heap_free_total += i;
           }
           else
           {
                  mmap_total -= i;
                  mmap_count--;
           }
    } 
    
    printf("\nafter free\n");
    print_info(); 


    return 1;
}

該例子第一個循環爲指針數組每一個成員分配索引位置 (KB) 大小的內存塊,並經過 128 爲分界分別對 heap 和 mmap 內存分配狀況進行計數;
第二個循環是 free 索引下標爲奇數的項,同時更新計數狀況。經過程序的計數與mallinfo/malloc_stats 接口獲得結果進行對比,並經過 print_info打印到終端。

 
下面是一個執行結果:
count by itself:
        heap_malloc_total=8323072 heap_free_total=0 heap_in_use=8323072
        mmap_total=12054528 mmap_count=72
  
count by mallinfo:
        heap_malloc_total=8327168 heap_free_total=2032 heap_in_use=8325136
        mmap_total=12238848 mmap_count=72

from malloc_stats:
Arena 0:
system bytes     =    8327168
in use bytes     =    8325136
Total (incl. mmap):
system bytes     =   20566016
in use bytes     =   20563984
max mmap regions =         72
max mmap bytes   =   12238848

after free
count by itself:
        heap_malloc_total=8323072 heap_free_total=4194304 heap_in_use=4128768
        mmap_total=6008832 mmap_count=36

count by mallinfo:
        heap_malloc_total=8327168 heap_free_total=4197360 heap_in_use=4129808
        mmap_total=6119424 mmap_count=36

from malloc_stats:
Arena 0:
system bytes     =    8327168
in use bytes     =    4129808
Total (incl. mmap):
system bytes     =   14446592
in use bytes     =   10249232
max mmap regions =         72
max mmap bytes   =   12238848

由上可知,程序統計和mallinfo 獲得的信息基本吻合,其中 heap_free_total 表示堆內已釋放的內存碎片總和。 
 
       若是想知道堆內究竟有多少碎片,可經過 mallinfo 結構中的 fsmblks 、smblks 、ordblks 值獲得,這些值表示不一樣大小區間的碎片總個數,這些區間分別是 0~80 字節,80~512 字節,512~128k。若是 fsmblks 、 smblks 的值過大,那碎片問題可能比較嚴重了。
    不過, mallinfo 結構有一個很致命的問題,就是其成員定義所有都是 int ,在 64 位環境中,其結構中的 uordblks/fordblks/arena/usmblks 很容易就會致使溢出,應該是歷史遺留問題,使用時要注意!

 

4、既然堆內內存brk和sbrk不能直接釋放,爲何不所有使用 mmap 來分配,munmap直接釋放呢? 
        既然堆內碎片不能直接釋放,致使疑似「內存泄露」問題,爲何 malloc 不所有使用 mmap 來實現呢(mmap分配的內存能夠會經過 munmap 進行 free ,實現真正釋放)?而是僅僅對於大於 128k 的大塊內存才使用 mmap ? 

        其實,進程向 OS 申請和釋放地址空間的接口 sbrk/mmap/munmap 都是系統調用,頻繁調用系統調用都比較消耗系統資源的。而且, mmap 申請的內存被 munmap 後,從新申請會產生更多的缺頁中斷。例如使用 mmap 分配 1M 空間,第一次調用產生了大量缺頁中斷 (1M/4K 次 ) ,當munmap 後再次分配 1M 空間,會再次產生大量缺頁中斷。缺頁中斷是內核行爲,會致使內核態CPU消耗較大。另外,若是使用 mmap 分配小內存,會致使地址空間的分片更多,內核的管理負擔更大。
        同時堆是一個連續空間,而且堆內碎片因爲沒有歸還 OS ,若是可重用碎片,再次訪問該內存極可能不需產生任何系統調用和缺頁中斷,這將大大下降 CPU 的消耗。 所以, glibc 的 malloc 實現中,充分考慮了 sbrk 和 mmap 行爲上的差別及優缺點,默認分配大塊內存 (128k) 才使用 mmap 得到地址空間,也可經過 mallopt(M_MMAP_THRESHOLD, <SIZE>) 來修改這個臨界值。

 

5、如何查看進程的缺頁中斷信息?
可經過如下命令查看缺頁中斷信息
ps -o majflt,minflt -C <program_name>
ps -o majflt,minflt -p <pid>
其中:: majflt 表明 major fault ,指大錯誤;

           minflt 表明 minor fault ,指小錯誤。

這兩個數值表示一個進程自啓動以來所發生的缺頁中斷的次數。
其中 majflt 與 minflt 的不一樣是::

        majflt 表示須要讀寫磁盤,多是內存對應頁面在磁盤中須要load 到物理內存中,也多是此時物理內存不足,須要淘汰部分物理頁面至磁盤中。

參看:: http://blog.163.com/xychenbaihu@yeah/blog/static/132229655201210975312473/

 

6、除了 glibc 的 malloc/free ,還有其餘第三方實現嗎?

        其實,不少人開始詬病 glibc 內存管理的實現,特別是高併發性能低下和內存碎片化問題都比較嚴重,所以,陸續出現一些第三方工具來替換 glibc 的實現,最著名的當屬 google 的tcmalloc和facebook 的jemalloc 。
        網上有不少資源,能夠本身查(只用使用第三方庫,代碼不用修改,就能夠使用第三方庫中的malloc)。

 

參考資料:
《深刻理解計算機系統》第 10 章
http://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt

https://www.ibm.com/developerworks/cn/linux/l-lvm64/

http://www.kerneltravel.net/journal/v/mem.htm

http://blog.csdn.net/baiduforum/article/details/6126337

http://www.nosqlnotes.net/archives/105

 

 

 

 

 

Linux的內存管理(free 詳解)

Linux的內存管理,實際上跟windows的內存管理有很相像的地方,都是用虛擬內存這個的概念,說到這裏不得不罵MS,爲何在不少時候還有很大的物理內存的時候,卻仍是用到了pagefile. 因此才常常要跟一幫人吵着說Pagefile的大小,以及如何分配這個問題,在Linux你們就不用再吵什麼swap大小的問題,我我的認爲,swap設個512M已經足夠了,若是你問說512M的SWAP不夠用怎麼辦?只能說大哥你仍是加內存吧,要不就檢查你的應用,是否是真的出現了memory leak.
夜也深了,就再也不說廢話了。

在Linux下查看內存咱們通常用command free
[root@nonamelinux ~]# free
         total       used       free     shared    buffers     cached
Mem:    386024     377116    8908      0      21280     155468
-/+ buffers/cache:     200368    185656
Swap:    393552        0      393552
下面是對這些數值的解釋:
第二行(mem):
total:總計物理內存的大小。
used:已使用多大。
free:可用有多少。
Shared:多個進程共享的內存總額。
Buffers/cached:磁盤緩存的大小。
第三行(-/+ buffers/cached):
used:已使用多大。
free:可用有多少。
第四行就很少解釋了。
區別:
第二行(mem)的used/free與第三行(-/+ buffers/cache) used/free的區別。
這兩個的區別在於使用的角度來看,第一行是從OS的角度來看,由於對於OS,buffers/cached 都是屬於被使用,因此他的可用內存是8908KB,已用內存是377116KB,其中包括,內核(OS)使用+Application(X,oracle,etc)使用的+buffers+cached. 
第三行所指的是從應用程序角度來看,對於應用程序來講,buffers/cached 是等於可用的,由於buffer/cached是爲了提升文件讀取的性能,當應用程序需在用到內存的時候,buffer/cached會很快地被回收。
因此從應用程序的角度來講,可用內存=系統free memory+buffers+cached.
如上例:
185656=8908+21280+155468
接下來解釋何時內存會被交換,以及按什麼方交換。
當可用內存少於額定值的時候,就會開會進行交換.
如何看額定值(RHEL4.0):
#cat /proc/meminfo
交換將經過三個途徑來減小系統中使用的物理頁面的個數: 
1.減小緩衝與頁面cache的大小,
2.將系統V類型的內存頁面交換出去, 
3.換出或者丟棄頁面。(Application 佔用的內存頁,也就是物理內存不足)。
事實上,少許地使用swap是否是影響到系統性能的。  

 

 

 

 

 

Linux設備驅動以內存管理

 

對於包含 MMU 的處理器而言, Linux 系統提供了複雜的存儲管理系統,使得進程所能訪問的內存達到 4GB。進程的 4GB 內存空間被分爲兩個部分—用戶空間與內核空間。用戶空間地址通常分佈爲 0~3GB(即 PAGE_OFFSET),這樣,剩下的 3~4GB 爲內核空間。
內核空間申請內存涉及的函數主要包括 kmalloc()、__get_free_pages()和 vmalloc()等。
經過內存映射,用戶進程能夠在用戶空間直接訪問設備。


內核地址空間

每一個進程的用戶空間都是徹底獨立、互不相干的,用戶進程各自有不一樣的頁表。而內核空間是由內核負責映射,它並不會跟着進程改變,是固定的。內核空間地址有本身對應的頁表,內核的虛擬空間獨立於其餘程序。用戶進程只有經過系統調用(表明用戶進程在內核態執行)等方式才能夠訪問到內核空間。

Linux 中 1GB 的內核地址空間又被劃分爲物理內存映射區、虛擬內存分配區、高端頁面映射區、專用頁面映射區和系統保留映射區這幾個區域,如圖所示。
內核地址空間

  • 保留區

    Linux 保留內核空間最頂部 FIXADDR_TOP~4GB 的區域做爲保留區。
  • 專用頁面映射區

    緊接着最頂端的保留區如下的一段區域爲專用頁面映射區(FIXADDR_START~FIXADDR_TOP),它的總尺寸和每一頁的用途由 fixed_address 枚舉結構在編譯時預約義,用__fix_to_virt(index)可獲取專用區內預約義頁面的邏輯地址。
  • 高端內存映射區

    當系統物理內存大於 896MB 時,超過物理內存映射區的那部份內存稱爲高端內存(而未超過物理內存映射區的內存一般被稱爲常規內存),內核在存取高端內存時必須將它們映射到高端頁面映射區。
  • 虛存內存分配區

    用於 vmalloc()函數,它的前部與物理內存映射區有一個隔離帶,後部與高端映射區也有一個隔離帶。
  • 物理內存映射區

    通常狀況下,物理內存映射區最大長度爲 896MB,系統的物理內存被順序映射在內核空間的這個區域中。

虛擬地址與物理地址關係

對於內核物理內存映射區的虛擬內存,使用 virt_to_phys()能夠實現內核虛擬地址轉化爲物理地址, virt_to_phys()的實現是體系結構相關的,對於 ARM 而言, virt_to_phys()的定義如代碼:

static inline unsigned long virt_to_phys(void *x) { return __virt_to_phys((unsigned long)(x)); } /* PAGE_OFFSET 一般爲 3GB,而 PHYS_OFFSET 則定於爲系統 DRAM 內存的基地址 */ #define __virt_to_phys(x) ((x) - PAGE_OFFSET + PHYS_OFFSET)

內存分配

在 Linux 內核空間申請內存涉及的函數主要包括 kmalloc()、__get_free_pages()和 vmalloc()等。kmalloc()和__get_free_pages()( 及其相似函數) 申請的內存位於物理內存映射區域,並且在物理上也是連續的,它們與真實的物理地址只有一個固定的偏移,所以存在較簡單的轉換關係。而vmalloc()在虛擬內存空間給出一塊連續的內存區,實質上,這片連續的虛擬內存在物理內存中並不必定連續,而 vmalloc()申請的虛擬內存和物理內存之間也沒有簡單的換算關係。

kmalloc()

void *kmalloc(size_t size, int flags);

給 kmalloc()的第一個參數是要分配的塊的大小,第二個參數爲分配標誌,用於控制 kmalloc()的行爲。

flags

  • 最經常使用的分配標誌是 GFP_KERNEL,其含義是在內核空間的進程中申請內存。 kmalloc()的底層依賴__get_free_pages()實現,分配標誌的前綴 GFP 正好是這個底層函數的縮寫。使用 GFP_KERNEL 標誌申請內存時,若暫時不能知足,則進程會睡眠等待頁,即會引發阻塞,所以不能在中斷上下文或持有自旋鎖的時候使用 GFP_KERNEL 申請內存。
  • 在中斷處理函數、 tasklet 和內核定時器等非進程上下文中不能阻塞,此時驅動應當使用GFP_ATOMIC 標誌來申請內存。當使用 GFP_ATOMIC 標誌申請內存時,若不存在空閒頁,則不等待,直接返回。
  • 其餘的相對不經常使用的申請標誌還包括 GFP_USER(用來爲用戶空間頁分配內存,可能阻塞)、GFP_HIGHUSER(相似 GFP_USER,可是從高端內存分配)、 GFP_NOIO(不容許任何 I/O 初始化)、 GFP_NOFS(不容許進行任何文件系統調用)、 __GFP_DMA(要求分配在可以 DMA 的內存區)、 __GFP_HIGHMEM(指示分配的內存能夠位於高端內存)、 __GFP_COLD(請求一個較長時間不訪問的頁)、 __GFP_NOWARN(當一個分配沒法知足時,阻止內核發出警告)、 __GFP_HIGH(高優先級請求,容許得到被內核保留給緊急情況使用的最後的內存頁)、 __GFP_REPEAT(分配失敗則盡力重複嘗試)、 __GFP_NOFAIL(標誌只許申請成功,不推薦)和__GFP_NORETRY(若申請不到,則當即放棄)。

使用 kmalloc()申請的內存應使用 kfree()釋放,這個函數的用法和用戶空間的 free()相似。

__get_free_pages ()

__get_free_pages()系列函數/宏是 Linux 內核本質上最底層的用於獲取空閒內存的方法,由於底層的夥伴算法以 page 的 2 的 n 次冪爲單位管理空閒內存,因此最底層的內存申請老是以頁爲單位的。
__get_free_pages()系列函數/宏包括 get_zeroed_page()、 __get_free_page()和__get_free_pages()。

/* 該函數返回一個指向新頁的指針而且將該頁清零 */ get_zeroed_page(unsigned int flags); /* 該宏返回一個指向新頁的指針可是該頁不清零 */ __get_free_page(unsigned int flags); /* 該函數可分配多個頁並返回分配內存的首地址,分配的頁數爲 2^order,分配的頁也不清零 */ __get_free_pages(unsigned int flags, unsigned int order); /* 釋放 */ void free_page(unsigned long addr); void free_pages(unsigned long addr, unsigned long order);

__get_free_pages 等函數在使用時,其申請標誌的值與 kmalloc()徹底同樣,各標誌的含義也與kmalloc()徹底一致,最經常使用的是 GFP_KERNEL 和 GFP_ATOMIC。

vmalloc()

vmalloc()通常用在爲只存在於軟件中(沒有對應的硬件意義)的較大的順序緩衝區分配內存,vmalloc()遠大於__get_free_pages()的開銷,爲了完成 vmalloc(),新的頁表須要被創建。所以,只是調用 vmalloc()來分配少許的內存(如 1 頁)是不妥的。
vmalloc()申請的內存應使用 vfree()釋放, vmalloc()和 vfree()的函數原型以下:

void *vmalloc(unsigned long size); void vfree(void * addr);

vmalloc()不能用在原子上下文中,由於它的內部實現使用了標誌爲 GFP_KERNEL 的 kmalloc()。

slab

一方面,徹底使用頁爲單元申請和釋放內存容易致使浪費(若是要申請少許字節也須要 1 頁);另外一方面,在操做系統的運做過程當中,常常會涉及大量對象的重複生成、使用和釋放內存問題。在Linux 系統中所用到的對象,比較典型的例子是 inode、 task_struct 等。若是咱們可以用合適的方法使得在對象先後兩次被使用時分配在同一塊內存或同一類內存空間且保留了基本的數據結構,就能夠大大提升效率。 內核的確實現了這種類型的內存池,一般稱爲後備高速緩存(lookaside cache)。內核對高速緩存的管理稱爲slab分配器。實際上 kmalloc()便是使用 slab 機制實現的。
注意, slab 不是要代替__get_free_pages(),其在最底層仍然依賴於__get_free_pages(), slab在底層每次申請 1 頁或多頁,以後再分隔這些頁爲更小的單元進行管理,從而節省了內存,也提升了 slab 緩衝對象的訪問效率。

#include <linux/slab.h> /* 建立一個新的高速緩存對象,其中可容納任意數目大小相同的內存區域 */ struct kmem_cache *kmem_cache_create(const char *name, /* 通常爲將要高速緩存的結構類型的名字 */ size_t size, /* 每一個內存區域的大小 */ size_t offset, /* 第一個對象的偏移量,通常爲0 */ unsigned long flags, /* 一個位掩碼: SLAB_NO_REAP 即便內存緊縮也不自動收縮這塊緩存,不建議使用 SLAB_HWCACHE_ALIGN 每一個數據對象被對齊到一個緩存行 SLAB_CACHE_DMA 要求數據對象在DMA內存區分配 */ /* 可選參數,用於初始化新分配的對象,多用於一組對象的內存分配時使用 */ void (*constructor)(void*, struct kmem_cache *, unsigned long), void (*destructor)(void*, struct kmem_cache *, unsigned long) ); /* 在 kmem_cache_create()建立的 slab 後備緩衝中分配一塊並返回首地址指針 */ void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags); /* 釋放 slab 緩存 */ void kmem_cache_free(struct kmem_cache *cachep, void *objp); /* 收回 slab 緩存,若是失敗則說明內存泄漏 */ int kmem_cache_destroy(struct kmem_cache *cachep);

Tip: 高速緩存的使用統計狀況能夠從/proc/slabinfo得到。

內存池(mempool)

內核中有些地方的內存分配是不容許失敗的,內核開發者創建了一種稱爲內存池的抽象。內存池其實就是某種形式的高速後備緩存,它試圖始終保持空閒的內存以便在緊急狀態下使用。mempool很容易浪費大量內存,應儘可能避免使用。

#include <linux/mempool.h> /* 建立 */ mempool_t *mempool_create(int min_nr, /* 須要預分配對象的數目 */ mempool_alloc_t *alloc_fn, /* 分配函數,通常直接使用內核提供的mempool_alloc_slab */ mempool_free_t *free_fn, /* 釋放函數,通常直接使用內核提供的mempool_free_slab */ void *pool_data); /* 傳給alloc_fn/free_fn的參數,通常爲kmem_cache_create建立的cache */ /* 分配釋放 */ void *mempool_alloc(mempool_t *pool, int gfp_mask); void mempool_free(void *element, mempool_t *pool); /* 回收 */ void mempool_destroy(mempool_t *pool);

內存映射

通常狀況下,用戶空間是不可能也不該該直接訪問設備的,可是,設備驅動程序中可實現mmap()函數,這個函數可以使得用戶空間直能接訪問設備的物理地址。
這種能力對於顯示適配器一類的設備很是有意義,若是用戶空間可直接經過內存映射訪問顯存的話,屏幕幀的各點的像素將再也不須要一個從用戶空間到內核空間的複製的過程。
從 file_operations 文件操做結構體能夠看出,驅動中 mmap()函數的原型以下:

int(*mmap)(struct file *, struct vm_area_struct*);

驅動程序中 mmap()的實現機制是創建頁表,並填充 VMA 結構體中 vm_operations_struct 指針。VMA 即 vm_area_struct,用於描述一個虛擬內存區域:

struct vm_area_struct { unsigned long vm_start; /* 開始虛擬地址 */ unsigned long vm_end; /* 結束虛擬地址 */ unsigned long vm_flags; /* VM_IO 設置一個內存映射I/O區域; VM_RESERVED 告訴內存管理系統不要將VMA交換出去 */ struct vm_operations_struct *vm_ops; /* 操做 VMA 的函數集指針 */ unsigned long vm_pgoff; /* 偏移(頁幀號) */ void *vm_private_data; ... } struct vm_operations_struct { void(*open)(struct vm_area_struct *area); /*打開 VMA 的函數*/ void(*close)(struct vm_area_struct *area); /*關閉 VMA 的函數*/ struct page *(*nopage)(struct vm_area_struct *area, unsigned long address, int *type); /*訪問的頁不在內存時調用*/ /* 當用戶訪問頁前,該函數容許內核將這些頁預先裝入內存。驅動程序通常沒必要實現 */ int(*populate)(struct vm_area_struct *area, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock); ...

創建頁表的方法有兩種:使用remap_pfn_range函數一次所有創建或者經過nopage VMA方法每次創建一個頁表。

  • remap_pfn_range
    remap_pfn_range負責爲一段物理地址創建新的頁表,原型以下:

    int remap_pfn_range(struct vm_area_struct *vma, /* 虛擬內存區域,必定範圍的頁將被映射到該區域 */ unsigned long addr, /* 從新映射時的起始用戶虛擬地址。該函數爲處於addr和addr+size之間的虛擬地址創建頁表 */ unsigned long pfn, /* 與物理內存對應的頁幀號,實際上就是物理地址右移 PAGE_SHIFT 位 */ unsigned long size, /* 被從新映射的區域大小,以字節爲單位 */ pgprot_t prot); /* 新頁所要求的保護屬性 */

    demo:

    static int xxx_mmap(struct file *filp, struct vm_area_struct *vma) { if (remap_pfn_range(vma, vma->vm_start, vm->vm_pgoff, vma->vm_end - vma->vm_start, vma->vm_page_prot)) /* 創建頁表 */ return - EAGAIN; vma->vm_ops = &xxx_remap_vm_ops; xxx_vma_open(vma); return 0; } /* VMA 打開函數 */ void xxx_vma_open(struct vm_area_struct *vma) { ... printk(KERN_NOTICE "xxx VMA open, virt %lx, phys %lx\n", vma->vm_start, vma->vm_pgoff << PAGE_SHIFT); } /* VMA 關閉函數 */ void xxx_vma_close(struct vm_area_struct *vma) { ... printk(KERN_NOTICE "xxx VMA close.\n"); } static struct vm_operations_struct xxx_remap_vm_ops = { /* VMA 操做結構體 */ .open = xxx_vma_open, .close = xxx_vma_close, ... };
  • nopage
    除了 remap_pfn_range()之外,在驅動程序中實現 VMA 的 nopage()函數一般能夠爲設備提供更加靈活的內存映射途徑。當訪問的頁不在內存,即發生缺頁異常時, nopage()會被內核自動調用。

    static int xxx_mmap(struct file *filp, struct vm_area_struct *vma) { unsigned long offset = vma->vm_pgoff << PAGE_SHIFT; if (offset >= _ _pa(high_memory) || (filp->f_flags &O_SYNC)) vma->vm_flags |= VM_IO; vma->vm_flags |= VM_RESERVED; /* 預留 */ vma->vm_ops = &xxx_nopage_vm_ops; xxx_vma_open(vma); return 0; } struct page *xxx_vma_nopage(struct vm_area_struct *vma, unsigned long address, int *type) { struct page *pageptr; unsigned long offset = vma->vm_pgoff << PAGE_SHIFT; unsigned long physaddr = address - vma->vm_start + offset; /* 物理地址 */ unsigned long pageframe = physaddr >> PAGE_SHIFT; /* 頁幀號 */ if (!pfn_valid(pageframe)) /* 頁幀號有效? */ return NOPAGE_SIGBUS; pageptr = pfn_to_page(pageframe); /* 頁幀號->頁描述符 */ get_page(pageptr); /* 得到頁,增長頁的使用計數 */ if (type) *type = VM_FAULT_MINOR; return pageptr; /*返回頁描述符 */ }

    上述函數對常規內存進行映射, 返回一個頁描述符,可用於擴大或縮小映射的內存區域。

因而可知, nopage()與 remap_pfn_range()的一個較大區別在於 remap_pfn_range()通常用於設備內存映射,而 nopage()還可用於 RAM 映射,其調用發生在缺頁異常時。

 

 

 

 

 

Linux設備驅動之中斷處理

中斷(interrupt)是指CPU在執行程序的過程當中,出現了某些突發事件急待處理,CPU必須暫停執行當前的程序,轉去處理突發事件,處理完畢後CPU又返回原程序被中斷的位置並繼續執行。
中斷服務程序的執行並不存在於進程上下文,所以,要求中斷服務程序的時間儘量地短。所以,Linux在中斷處理中引入了頂半部和底半部分離的機制。頂半部處理緊急的硬件操做,底半部處理不緊急的耗時操做。
tasklet和工做隊列都是調度中斷底半部的良好機制,tasklet基於軟中斷實現,原子操做,速度快,經常使用,但不可阻塞或睡眠。


中斷分類

  • 根據中斷的來源,可分爲內部中斷和外部中斷

    內部中斷的中斷源來自CPU內部(軟件中斷指令、溢出、除法錯誤等,例如,操做系統從用戶態切換到內核態需藉助CPU內部的軟件中斷),外部中斷的中斷源來自CPU外部,由外設提出請求。
  • 根據中斷是否能夠屏蔽分爲可屏蔽中斷與不屏蔽中斷(NMI)

    可屏蔽中斷能夠經過屏蔽字(MASK)被屏蔽,屏蔽後,該中斷再也不獲得響應,而不屏蔽中斷不能被屏蔽。
  • 根據中斷入口跳轉方法的不一樣,分爲向量中斷和非向量中斷

    採用向量中斷的CPU一般爲不一樣的中斷分配不一樣的中斷號,當檢測到某中斷號的中斷到來後,就自動跳轉到與該中斷號對應的地址執行。不一樣中斷號的中斷有不一樣的入口地址。非向量中斷的多箇中斷共享一個入口地址,進入該入口地址後再經過軟件判斷中斷標誌來識別具體是哪一個中斷。也就是說,向量中斷由硬件提供中斷服務程序入口地址,非向量中斷由軟件提供中斷服務程序入口地址。

Linux中斷處理(Interrupt Handling)架構

設備的中斷會打斷內核中進程的正常調度和運行,系統對更高吞吐率的追求勢必要求中斷服務程序儘量的短小精悍。爲了在中斷執行時間儘量短和中斷處理需完成大量工做之間找到一個平衡點, Linux 將中斷處理程序分解爲兩個半部:頂半部(top half)和底半部(bottom half)。

  • top half

    頂半部完成儘量少的比較緊急的功能,它每每只是簡單地讀取寄存器中的中斷狀態並清除中斷標誌後就進行「登記中斷」的工做。「登記中斷」意味着將底半部處理程序掛到該設備的底半部執行隊列中去。這樣,頂半部執行的速度就會很快,能夠服務更多的中斷請求。軟件上通常採用handler中斷響應程序實現。
  • bottom half

    底半部由頂半部調度而來進行延後處理,幾乎作了中斷處理程序全部的事情,並且能夠被新的中斷打斷,這也是底半部和頂半部的最大不一樣,由於頂半部每每被設計成不可中斷。底半部則相對來講並非很是緊急的,並且相對比較耗時,不在硬件中斷服務程序中執行。軟件上通常採用tasklet或工做隊列機制。

Tip:儘管頂半部、底半部的結合可以改善系統的響應能力,可是,僵化地認爲 Linux 設備驅動中的中斷處理必定要分兩個半部則是不對的。若是中斷要處理的工做自己不多,則徹底能夠直接在頂半部所有完成。


Linux中斷編程

申請和釋放(Installing an Interrupt Handler)

#include <linux/interrupt.h> int /* 返回 0 -- OK, -EINVAL -- irq/handler invalid, -EBUSY -- 中斷被佔用不能共享 */ request_irq( unsigned int irq, /* 要申請的硬件中斷號 */ irq_handler_t handler, /* 向系統登記的中斷處理函數(頂半部)*/ unsigned long flags, /* 中斷處理的屬性,能夠指定中斷的觸發方式以及處理方式等 */ const char *devname, /* used in /proc/interrupts */ void *dev_id /* 傳遞給handler的參數 */ ); void free_irq(unsigned int irq,void *dev_id);

Tip: 若是中斷肯定不被共享可將其安裝在初始化中,不然應安裝在打開函數中。interrupt.h有irq_handler_t的定義及flags的詳細註釋

使能和屏蔽 (Enabling and Disabling Interrupts)

  • Disabling a single interrupt

    #include <asm/irq.h> /* disable並等待指定的中斷被處理完,若是調用線程佔有interrupt handler須要的資源如spinlock那麼就會死鎖 */ void disable_irq(int irq); /* disable並當即返回,有可能產生競態 */ void disable_irq_nosync(int irq); void enable_irq(int irq);
  • Disabling all interrupts

    #include <asm/system.h> void local_irq_save(unsigned long flags); /* save flags then disable local all */ void local_irq_disable(void); /* disable local all directly */ void local_irq_restore(unsigned long flags); void local_irq_enable(void);

底半部機制

  • tasklet

    void my_tasklet_func(unsigned long); /*定義一個處理函數*/ /* 定義一個tasklet結構my_tasklet,並與my_tasklet_func(data)處理函數相關聯 */ DECLARE_TASKLET(my_tasklet, my_tasklet_func, data); /* 在須要調度tasklet的時候引用一個tasklet_schedule()函數就能使系統在適當的時候進行調度運行 通常在top half即中斷響應函數中調用 */ tasklet_schedule(&my_tasklet);
  • 工做隊列(workqueues)
    與tasklet相似:

    void my_wq_func(unsigned long); /*定義一個處理函數*/ struct work_struct my_wq; /*定義一個工做隊列*/ /* 初始化工做隊列並將其與處理函數綁定 */ INIT_WORK(&my_wq, (void (*)(void *)) my_wq_func, NULL); schedule_work(&my_wq);/*調度工做隊列執行*/

Tip: tasklet在中斷上下文中執行,不可阻塞或睡眠;而workqueues由內核線程去執行,屬進程上下文,可阻塞和睡眠。

 

 

 

 

 

Linux設備驅動之字符設備

字符設備是3大類設備(字符設備、塊設備和網絡設備)中較簡單的一類設備,其驅動程序中完成的主要工做是初始化、添加和刪除cdev結構體,申請和釋放設備號,以及填充 file_operations結構體中的操做函數,實現file_operations結構體中的read()、write()和ioctl()等函數是驅動設計的主體工做。


參考例程

源碼

/* * 虛擬字符設備globalmem實例: * 在globalmem字符設備驅動中會分配一片大小爲 GLOBALMEM_SIZE(4KB) * 的內存空間,並在驅動中提供針對該片內存的讀寫、控制和定位函數,以供用戶空間的進程能經過 * Linux系統調用訪問這片內存。 */ #include <linux/module.h> #include <linux/types.h> #include <linux/fs.h> #include <linux/errno.h> #include <linux/mm.h> #include <linux/sched.h> #include <linux/init.h> #include <linux/cdev.h> #include <asm/io.h> #include <asm/system.h> #include <asm/uaccess.h> #define DEV_NAME "globalmem" /* /dev中顯示的設備名 */ #define DEV_MAJOR 0 /* 指定主設備號,爲0則動態獲取 */ /* ioctl用的控制字 */ #define GLOBALMEM_MAGIC 'M' #define MEM_CLEAR _IO(GLOBALMEM_MAGIC, 0) /*--------------------------------------------------------------------- local vars */ /*globalmem設備結構體*/ typedef struct { struct cdev cdev; /* 字符設備cdev結構體*/ #define MEM_SIZE 0x1000 /*全局內存最大4K字節*/ unsigned char mem[MEM_SIZE]; /*全局內存*/ struct semaphore sem; /*併發控制用的信號量*/ } globalmem_dev_t; static int globalmem_major = DEV_MAJOR; static globalmem_dev_t *globalmem_devp; /*設備結構體指針*/ /*--------------------------------------------------------------------- file operations */ /*文件打開函數*/ static int globalmem_open(struct inode *inodep, struct file *filep) { /* 獲取dev指針 */ globalmem_dev_t *dev = container_of(inodep->i_cdev, globalmem_dev_t, cdev); filep->private_data = dev; return 0; } /*文件釋放函數*/ static int globalmem_release(struct inode *inodep, struct file *filep) { return 0; } /*讀函數*/ static ssize_t globalmem_read(struct file *filep, char __user *buf, size_t len, loff_t *ppos) { globalmem_dev_t *dev = filep->private_data; unsigned long p = *ppos; int ret = 0; /*分析和獲取有效的長度*/ if (p > MEM_SIZE) { printk(KERN_EMERG "%s: overflow!\n", __func__); return - ENOMEM; } if (len > MEM_SIZE - p) { len = MEM_SIZE - p; } if (down_interruptible(&dev->sem)) /* 得到信號量*/ return - ERESTARTSYS; /*內核空間->用戶空間*/ if (copy_to_user(buf, (void*)(dev->mem + p), len)) { ret = - EFAULT; }else{ *ppos += len; printk(KERN_INFO "%s: read %d bytes from %d\n", DEV_NAME, (int)len, (int)p); ret = len; } up(&dev->sem); /* 釋放信號量*/ return ret; } /*寫函數*/ static ssize_t globalmem_write(struct file *filep, const char __user *buf, size_t len, loff_t *ppos) { globalmem_dev_t *dev = filep->private_data; int ret = 0; unsigned long p = *ppos; if (p > MEM_SIZE) { printk(KERN_EMERG "%s: overflow!\n", __func__); return - ENOMEM; } if (len > MEM_SIZE - p) { len = MEM_SIZE - p; } if (down_interruptible(&dev->sem)) /* 得到信號量*/ return - ERESTARTSYS; /*用戶空間->內核空間*/ if (copy_from_user(dev->mem + p, buf, len)) { ret = - EFAULT; }else{ *ppos += len; printk(KERN_INFO "%s: written %d bytes from %d\n", DEV_NAME, (int)len, (int)p); ret = len; } up(&dev->sem); /* 釋放信號量*/ return ret; } /* seek文件定位函數 */ static loff_t globalmem_llseek(struct file *filep, loff_t offset, int start) { globalmem_dev_t *dev = filep->private_data; int ret = 0; if (down_interruptible(&dev->sem)) /* 得到信號量*/ return - ERESTARTSYS; switch (start) { case SEEK_SET: if (offset < 0 || offset > MEM_SIZE) { printk(KERN_EMERG "%s: overflow!\n", __func__); return - ENOMEM; } ret = filep->f_pos = offset; break; case SEEK_CUR: if ((filep->f_pos + offset) < 0 || (filep->f_pos + offset) > MEM_SIZE) { printk(KERN_EMERG "%s: overflow!\n", __func__); return - ENOMEM; } ret = filep->f_pos += offset; break; default: return - EINVAL; break; } up(&dev->sem); /* 釋放信號量*/ printk(KERN_INFO "%s: set cur to %d.\n", DEV_NAME, ret); return ret; } /* ioctl設備控制函數 */ static long globalmem_ioctl(struct file *filep, unsigned int cmd, unsigned long arg) { globalmem_dev_t *dev = filep->private_data; switch (cmd) { case MEM_CLEAR: if (down_interruptible(&dev->sem)) /* 得到信號量*/ return - ERESTARTSYS; memset(dev->mem, 0, MEM_SIZE); up(&dev->sem); /* 釋放信號量*/ printk(KERN_INFO "%s: clear.\n", DEV_NAME); break; default: return - EINVAL; } return 0; } /*文件操做結構體*/ static const struct file_operations globalmem_fops = { .owner = THIS_MODULE, .open = globalmem_open, .release = globalmem_release, .read = globalmem_read, .write = globalmem_write, .llseek = globalmem_llseek, .compat_ioctl = globalmem_ioctl }; /*---------------------------------------------------------------------*/ /*初始化並註冊cdev*/ static int globalmem_setup(globalmem_dev_t *dev, int minor) { int ret = 0; dev_t devno = MKDEV(globalmem_major, minor); cdev_init(&dev->cdev, &globalmem_fops); dev->cdev.owner = THIS_MODULE; ret = cdev_add(&dev->cdev, devno, 1); if (ret) { printk(KERN_NOTICE "%s: Error %d dev %d.\n", DEV_NAME, ret, minor); } return ret; } /*設備驅動模塊加載函數*/ static int __init globalmem_init(void) { int ret = 0; dev_t devno; /* 申請設備號*/ if(globalmem_major){ devno = MKDEV(globalmem_major, 0); ret = register_chrdev_region(devno, 2, DEV_NAME); }else{ /* 動態申請設備號 */ ret = alloc_chrdev_region(&devno, 0, 2, DEV_NAME); globalmem_major = MAJOR(devno); } if (ret < 0) { return ret; } /* 動態申請設備結構體的內存,建立兩個設備 */ globalmem_devp = kmalloc(2*sizeof(globalmem_dev_t), GFP_KERNEL); if (!globalmem_devp) { unregister_chrdev_region(devno, 2); return - ENOMEM; } ret |= globalmem_setup(&globalmem_devp[0], 0); /* globalmem0 */ ret |= globalmem_setup(&globalmem_devp[1], 1); /* globalmem1 */ if(ret) return ret; init_MUTEX(&globalmem_devp[0].sem); /*初始化信號量*/ init_MUTEX(&globalmem_devp[1].sem); printk(KERN_INFO "globalmem: up %d,%d.\n", MAJOR(devno), MINOR(devno)); return 0; } /*模塊卸載函數*/ static void __exit globalmem_exit(void) { cdev_del(&globalmem_devp[0].cdev); cdev_del(&globalmem_devp[1].cdev); kfree(globalmem_devp); unregister_chrdev_region(MKDEV(globalmem_major, 0), 2); printk(KERN_INFO "globalmem: down.\n"); } /* 定義參數 */ module_param(globalmem_major, int, S_IRUGO); module_init(globalmem_init); module_exit(globalmem_exit); /* 模塊描述及聲明 */ MODULE_AUTHOR("Archie Xie <archixie@cnblogs.com>"); MODULE_LICENSE("Dual BSD/GPL"); MODULE_DESCRIPTION("A char device module just for demo."); MODULE_ALIAS("cdev gmem"); MODULE_VERSION("1.0");

用戶空間驗證

  1. 切換到root用戶
  2. 插入模塊

    insmod globalmem.ko
  3. 建立設備節點(後續例程會展現自動建立節點的方法)

    cat /proc/devices 找到主設備號major mknod /dev/globalmem0 c major 0 和 /dev/globalmem1 c major 1
  4. 讀寫測試

    echo "hello world" > /dev/globalmem cat /dev/globalmem

 

 

 

Linux設備驅動之platform

根據Linux設備模型可知,一個現實的Linux設備和驅動一般都須要掛接在一種總線上,對於自己依附於PCI、USB等的設備而言,這天然不是問題,可是在嵌入式系統裏面,SoC系統中集成的獨立的外設控制器、掛接在 SoC 內存空間的外設等卻不依附於此類總線。基於這一背景,Linux設計了一種虛擬的總線,稱爲platform總線,相應的設備稱爲platform_device,而驅動稱爲platform_driver。


設計目的

  • 兼容設備模型

    使得設備被掛接在一個總線上,所以,符合 Linux 2.6 的設備模型。其結果是,配套的sysfs結點、設備電源管理都成爲可能。
  • BSP和驅動隔離

    在BSP中定義platform設備和設備使用的資源、設備的具體配置信息。而在驅動中,只須要經過通用API去獲取資源和數據,作到了板相關代碼和驅動代碼的分離,使得驅動具備更好的可擴展性和跨平臺性。

軟件架構

內核中Platform設備有關的實現位於include/linux/platform_device.h和drivers/base/platform.c兩個文件中,它的軟件架構以下:
platform architecture

由圖片可知,Platform設備在內核中的實現主要包括三個部分:

  • Platform Bus,基於底層bus模塊,抽象出一個虛擬的Platform bus,用於掛載Platform設備;
  • Platform Device,基於底層device模塊,抽象出Platform Device,用於表示Platform設備;
  • Platform Driver,基於底層device_driver模塊,抽象出Platform Driver,用於驅動Platform設備。

platform_device

注意,所謂的platform_device並非與字符設備、塊設備和網絡設備並列的概念,而是Linux系統提供的一種附加手段,例如,在S3C2440處理器中,把內部集成的I2C、RTC、SPI、LCD、看門狗等控制器都概括爲platform_device,而它們自己就是字符設備。

/* defined in <linux/platform_device.h> */ struct platform_device { const char * name; / * 設備名 */ u32 id; /* 用於標識該設備的ID */ struct device dev; /* 真正的設備(Platform設備只是一個特殊的設備,所以其核心邏輯仍是由底層的模塊實現)*/ u32 num_resources; / * 設備所使用各種資源數量 */ struct resource * resource; / * 資源 */ }; /* defined in <linux/ioport.h> */ struct resource { resource_size_t start; /* 資源起始 */ resource_size_t end; /* 結束 */ const char *name; unsigned long flags; /* 類型 */ struct resource *parent, *sibling, *child; }; /* 設備驅動獲取BSP定義的resource */ struct resource *platform_get_resource(struct platform_device *, unsigned int flags, unsigned int num); #include <linux/platform_device.h> int platform_device_register(struct platform_device *); void platform_device_unregister(struct platform_device *);

Tip: 和板級緊密相關的資源描述放在dev.paltform_data中。

paltform_driver

platform_driver這個結構體中包含probe()、remove()、shutdown()、suspend()、resume()函數,一般也須要由驅動實現:

struct platform_driver { int (*probe)(struct platform_device *); int (*remove)(struct platform_device *); void (*shutdown)(struct platform_device *); int (*suspend)(struct platform_device *, pm_message_t state); int (*suspend_late)(struct platform_device *, pm_message_t state); int (*resume_early)(struct platform_device *); int (*resume)(struct platform_device *); struct device_driver driver; }; #include <linux/platform_device.h> int platform_driver_register(struct platform_driver *); void platform_driver_unregister(struct platform_driver *);

platform_bus

系統中爲platform總線定義了一個bus_type的實例platform_bus_type:

struct bus_type platform_bus_type = { .name = "platform", .dev_attrs = platform_dev_attrs, .match = platform_match, .uevent = platform_uevent, .pm = PLATFORM_PM_OPS_PTR, }; EXPORT_SYMBOL_GPL(platform_bus_type);

這裏要重點關注其 match()成員函數,正是此成員函數肯定了 platform_device 和 platform_driver之間如何匹配:

static int platform_match(struct device *dev, struct device_driver *drv) { struct platform_device *pdev; pdev = container_of(dev, struct platform_device, dev); return (strncmp(pdev->name, drv->name, BUS_ID_SIZE) == 0); }

 

 

 

 

Linux設備驅動之devicetree

Devicetree(設備樹)是用來描述系統硬件信息的樹模型,其旨在unify內核。經過bootloader將devicetree的信息傳給kernel,而後kernel根據這些設備描述初始化相應的板級驅動,達到一個內核多個平臺共享的目的。


Overview

Devicetree主要爲描述不可插拔(非動態)設備的板級硬件信息而設計的。它由分層的描述設備信息的節點(node)組成樹結構。每一個node包含的內容經過property/value對來表示。除root節點外,每一個節點都有parent。如圖所示:
simple example

Node Names

除root節點名用'/'表示外,其他節點都由node-name@unit-address來命名,且同一層級必須是惟一的。

  • node-name

    表示節點名,由1-31個字符組成。如非必須,推薦使用如下通用的node-name: 
    cpu、memory、memory-controller、gpio、serial、watchdog、flash、compact-flash、rtc、interrupt-controller、dma-controller、ethernet、ethernet-phy、timer、mdio、spi、i2c、usb、can、keyboard、ide、disk、display、sound、atm、cache-controller、crypto、fdc、isa、mouse、nvram、parallel、pc-card、pci、pcie、sata、scsi、vme。
  • unit-address

    表示這個節點所在的bus類型。它必須和節點中reg屬性的第一個地址一致。若是這個節點沒有reg屬性,則不需「@unit-address」。

Path Names

表示一個節點的完整路徑(full path)。型如:

/node-name-1/node-name-2/node-name-N

Properties

每一個節點包含的主要內容就是這個所描述的設備的屬性信息,由name和value組成:

  • Property Names

    1-31個字符,可包含字母、數字、及‘,’,‘.’,‘_’,‘+’,‘?’,‘#’。
  • Property Values

Value Description
empty 屬性值爲空,用來表示true-false信息
u32/u64 32/64位大端字節序的無符號整形,表示時需加<>
string,stringlist null-terminated字符串或其組成的列表

Standard Properties

  • compatible

    Value type: <stringlist> Description: 表示兼容的設備類型,內核據此選擇合適的驅動程序。由多個字符串組成,從左到由列出這個設備兼容的驅動(from most specific to most general)。 推薦的格式爲:「製造商名,具體型號」。 Example: compatible = "fsl,mpc8641-uart", "ns16550"; 內核先搜索支持「fsl,mpc8641-uart」的驅動,如未找到,則搜索支持更通用的「ns16550」設備類型的驅動。
  • model

    Value type: <stringlist> Description: 代表設備型號。 推薦的格式爲:「製造商名,具體型號」。 Example: model = "fsl,MPC8349EMITX";
  • phandle

    Value type: <u32> Description: 用一個樹內惟一的數字標識所在的這個節點,其餘節點能夠直接經過這個數字標識來引用這個節點。 Example: pic@10000000 { phandle = <1>; interrupt-controller; }; interrupt-parent = <1>;
  • status

    Value type: <string> Description: 表示設備的可用狀態: "okay" -> 設備可用 "disabled" -> 目前不可用,但之後可能會可用 "fail" -> 不可用。出現嚴重問題,得修一下 "fail-sss" -> 不可用。出現嚴重問題,得修一下。sss指明錯誤類型。
  • #address-cells and #size-cells

    Value type: <u32> Description: 在擁有子節點的節點中使用,來描述它的字節點的地址分配問題。即分別表示子節點中使用多少個u32大小的cell來編碼reg屬性中的address域和size域。 這兩個屬性不會繼承,必須明確指出。如未指出,默認#address-cells=2,#size-cells=1。 Example: soc { #address-cells = <1>; #size-cells = <1>; serial { reg = <0x4600 0x100>; }; };
  • reg

    Value type: <prop-encoded-array> encoded as an arbitraty number of (address, length) pairs. Description: 描述該設備在parent bus定義的地址空間中的地址資源分配。 Example: reg = <0x3000 0x20 0xFE00 0x100>; a 32-byte block at offset 0x3000 and a 256-byte block at offset 0xFE00。
  • virtual-reg

    Value type: <stringlist> Description: 表示映射到reg第一個物理地址對應的effective address。使bootloader可以提供給內核它所創建的virtual-to-physical mappings。
  • ranges

    Value type: <empty> or <prop-encoded-array> encoded as an arbitrary number of (child-bus-address,parent-bus-address, length) triplets. Description: 提供了子地址空間與父地址空間的映射關係,若是值爲空則父子地址相等,無需轉換。 Example: soc { compatible = "simple-bus"; #address-cells = <1>; #size-cells = <1>; ranges = <0x0 0xe0000000 0x00100000>; serial { compatible = "ns16550"; reg = <0x4600 0x100>; }; }; 將子節點serial的0x0地址映射到父節點soc的0xe0000000,映射長度爲0x100000。此時reg的實際物理地址就爲0xe0004600。
  • dma-ranges

    Value type: <empty> or <prop-encoded-array> encoded as an arbitrary number of (child-bus-address,parent-bus-address, length) triplets. Description: 提供了dma地址的映射方法。

Interrupts

描述中斷的屬性有4個:

  • interrupt-controller

    一個空的屬性用來指示這個節點設備是接收中斷信號的控制器。
  • #interrupt-cells

    這是上面所說中斷控制器中的一個屬性,用來描述須要用多少個cell來描述這個中斷控制器的interrupt specifier(相似#address-cells和#size-cells)。
  • interrupt-parent

    常出如今根節點的一個屬性,它的屬性值是指向interrupt-controller的一個phandle。可從parent繼承。
  • interrupts

    包含interrupt specifiers列表,每個specifier表示一箇中斷輸出信號。

Example

/ { interrupt-parent = <&intc>;  intc: interrupt-controller@10140000 { compatible = "arm,pl190"; reg = <0x10140000 0x1000 >; interrupt-controller; #interrupt-cells = <2>; }; serial@101f0000 { interrupts = < 1 0 >; }; };

Base Device Node Types

全部的設備樹都必須有一個root節點,且root節點下必須包含一個cpus節點和至少一個memory節點。

  • root node

    root節點須包含 #address-cells、#size-cells、model、compatible等屬性。
  • /cpus node

    cpu子節點的父節點容器。須包含 #address-cells、#size-cells屬性。
  • /cpus/cpu* node

    是描述系統cpu的節點。
  • /memory node

    描述系統物理內存的layout。須包含reg節點。
    Example: 假如一個64位系統有以下兩塊物理內存: - RAM: starting address 0x0, length 0x80000000 (2GB) - RAM: starting address 0x100000000, length 0x100000000 (4GB) 則咱們能夠有下面兩種描述方法(#address-cells = <2> and #size-cells =<2>): Example #1 memory@0 { reg = <0x000000000 0x00000000 0x00000000 0x80000000 0x000000001 0x00000000 0x00000001 0x00000000>; }; Example #2 memory@0 { reg = <0x000000000 0x00000000 0x00000000 0x80000000>; }; memory@100000000 { reg = <0x000000001 0x00000000 0x00000001 0x00000000>; };
  • /chosen node

    根節點下的一個子節點,不是描述設備而是描述運行時參數。經常使用來給內核傳遞bootargs:
    chosen { bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200"; };
  • /aliases node

    1-31個字母、數字或下劃線組成的設備節點full path的別名。它的值是節點的全路徑,所以最終會被編碼成字符串。 aliases { serial0 = "/simple-bus@fe000000/serial@llc500"; }

Device Bindings

更多具體設備具體類別的描述信息:內核源代碼/Documentation/devicetree/bindings。


DTS是描述devicetree的源文本文件,它經過內核中的DTC(Devicetree Compiler)編譯後生成相應平臺可燒寫的二進制DTB。

Devicetree Blob (DTB) Structure

DTB又稱Flattened Devicetree(FDT),在內存中的結構以下圖所示:
dtb overview

大端字節序結構體:

struct fdt_header { uint32_t magic; /* contain the value 0xd00dfeed (big-endian) */ uint32_t totalsize; /* the total size of the devicetree data structure */ uint32_t off_dt_struct; /* offset in bytes of the structure block */ uint32_t off_dt_strings; /* offset in bytes of the strings block */ uint32_t off_mem_rsvmap; /* offset in bytes of the memory reservation block */ uint32_t version; /* the version of the devicetree data structure */ uint32_t last_comp_version; /* the lowest version used is backwards compatible */ uint32_t boot_cpuid_phys; /* the physical ID of the system’s boot CPU */ uint32_t size_dt_strings; /* the length in bytes of the strings block */ uint32_t size_dt_struct; /* the length in bytes of the structure block */ };

Memory Reservation Block

  • Purpose

    爲系統保留一些特殊用途的memory。這些保留內存不會進入內存管理系統。
  • Format

    struct fdt_reserve_entry { uint64_t address; uint64_t size; };

Structure Block

Devicetree結構體存放的位置。由一行行「token+內容」片斷線性組成。

  • token
    每一行內容都由一個32位的整形token起始。token指明瞭其後內容的屬性及格式。共有如下5種token:
token Description
FDT_BEGIN_NODE (0x00000001) 節點起始,其後內容爲節點名
FDT_END_NODE (0x00000002) 節點結束
FDT_PROP (0x00000003) 描述屬性
FDT_NOP (0x00000004) nothing,devicetree解析器忽略它
FDT_END (0x00000009) block結束
  • tree structure
    • (optionally) any number of FDT_NOP tokens
    • FDT_BEGIN_NODE
      • The node’s name as a null-terminated string
      • [zeroed padding bytes to align to a 4-byte boundary]
    • For each property of the node:
      • (optionally) any number of FDT_NOP tokens
      • FDT_PROP token
        • property information
        • [zeroed padding bytes to align to a 4-byte boundary]
    • Representations of all child nodes in this format
    • (optionally) any number of FDT_NOP tokens
    • FDT_END_NODE token

Devicetree Source (DTS) Format

Node and property definitions

[label:] node-name[@unit-address] {
        [properties definitions]
            [child nodes]
    };

File layout

Version 1 DTS files have the overall layout:

/dts-v1/; /* dts 版本1 */ [memory reservations] /* DTB中內存保留表的入口 */ / { [property definitions] [child nodes] };

 

 

Linux設備驅動之定時與延時

Linux經過系統硬件定時器以規律的間隔(由HZ度量)產生定時器中斷,每次中斷使得一個內核計數器的值jiffies累加,所以這個jiffies就記錄了系統啓動開始的時間流逝,而後內核據此實現軟件定時器和延時。

Demo for jiffies and HZ

#include <linux/jiffies.h> unsigned long j, stamp_1, stamp_half, stamp_n; j = jiffies; /* read the current value */ stamp_1 = j + HZ; /* 1 second in the future */ stamp_half = j + HZ/2; /* half a second */ stamp_n = j + n * HZ / 1000; /* n milliseconds */

內核定時器

硬件時鐘中斷處理程序會喚起 TIMER_SOFTIRQ 軟中斷,運行當前處理器上到期的全部內核定時器。

定時器定義/初始化

在Linux內核中,timer_list結構體的一個實例對應一個定時器:

/* 當expires指定的定時器到期時間期滿後,將執行function(data) */ struct timer_list { unsigned long expires; /*定時器到期時間*/ void (*function)(unsigned long); /* 定時器處理函數 */ unsigned long data; /* function的參數 */ ... }; /* 定義 */ struct timer_list my_timer; /* 初始化函數 */ void init_timer(struct timer_list * timer); /* 初始化宏 */ TIMER_INITIALIZER(_function, _expires, _data) /* 定義並初始化宏 */ DEFINE_TIMER(_name, _function, _expires, _data)

定時器添加/移除

/* 註冊內核定時器,將定時器加入到內核動態定時器鏈表中 */ void add_timer(struct timer_list * timer); /* del_timer_sync()是 del_timer()的同步版,在刪除一個定時器時需等待其被處理完, 所以該函數的調用不能發生在中斷上下文 */ void del_timer(struct timer_list * timer); void del_timer_sync(struct timer_list * timer);

定時時間修改

int mod_timer(struct timer_list *timer, unsigned long expires);

延時

短延時

void ndelay(unsigned long nsecs); void udelay(unsigned long usecs); void mdelay(unsigned long msecs);

內核在啓動時,會運行一個延遲測試程序(delay loop calibration),計算出lpj(loops per jiffy),根據lpj就實現了這幾個函數,屬忙等待。

長延時

  • 一個很直觀的方法是比較當前的 jiffies 和目標 jiffies:

    int time_after(unsigned long a, unsigned long b); /* a after b, true */ int time_before(unsigned long a, unsigned long b); /* a before b */ int time_after_eq(unsigned long a, unsigned long b); /* a after or equal b */ int time_before_eq(unsigned long a, unsigned long b);/* a before or equal b */
  • 睡着延時

    void msleep(unsigned int millisecs); unsigned long msleep_interruptible(unsigned int millisecs); void ssleep(unsigned int seconds);

    Tip: msleep()、 ssleep()不能被打斷。

 

 

Linux設備驅動之設備模型

Linux設備模型是對系統設備組織架構進行抽象的一個數據結構,旨在爲設備驅動進行分層、分類、組織。下降設備多樣性帶來的Linux驅動開發的複雜度,以及設備熱拔插處理、電源管理等。


Overview

設計目的

  • 電源管理和系統關機(Power management and system shutdown)

    設備之間大多狀況下有依賴、耦合,所以要實現電源管理就必須對系統的設備結構有清楚的理解,應知道先關哪一個而後才能再關哪一個。設計設備模型就是爲了使系統能夠按照正確順序進行硬件的遍歷。
  • 與用戶空間的交互(Communications with user space)

    實現了sysfs虛擬文件系統。它能夠將設備模型中定義的設備屬性信息等導出到用戶空間,使得在用戶空間能夠實現對設備屬性的訪問及參數的更改。詳見Documentation/filesystems/sysfs.txt。
  • 可熱插拔設備(Hotpluggable devices)

    設備模型管理內核所使用的處理用戶空間熱插拔的機制,支持設備的動態添加與移除。
  • 設備類別(Device classes)

    系統的許多部分對設備如何鏈接沒有興趣, 可是它們須要知道什麼類型的設備可用。設備模型也實現了一個給設備分類的機制, 它在一個更高的功能性級別描述了這些設備。
  • 對象生命期(Object lifecycles)

    設備模型的實現一套機制來處理對象生命期。

設備模型框圖

Linux 設備模型是一個複雜的數據結構。如圖所示爲和USB鼠標相關聯的設備模型的一小部分:
Overview diagram

這個框圖展現了設備模型最重要的四個部分的組織關係(在頂層容器中詳解):

  • Devices

    描述了設備如何鏈接到系統。
  • Drivers

    系統中可用的驅動。
  • Buses

    跟蹤什麼鏈接到每一個總線,負責匹配設備與驅動。
  • classes

    設備底層細節的抽象,描述了設備所提供的功能。

底層實現

kobject

做用與目的

Kobject是將整個設備模型鏈接在一塊兒的基礎。主要用來實現如下功能:

  • 對象的引用計數(Reference counting of objects)

    一般, 當一個內核對象被建立, 沒有方法知道它會存在多長時間。 一種跟蹤這種對象生命週期的方法是經過引用計數。 當沒有內核代碼持有對給定對象的引用, 那個對象已經完成了它的有用壽命而且能夠被刪除。
  • sysfs 表示(Sysfs representation)

    在sysfs中顯示的每個項目都是經過一個與內核交互的kobject實現的。
  • 數據結構粘和(Data structure glue)

    設備模型總體來看是一個極端複雜的由多級組成的數據結構, kobject實現各級之間的鏈接粘和。
  • 熱插拔事件處理(Hotplug event handling)

    kobject處理熱插拔事件並通知用戶空間。

數據結構

/* include in <linux/kobject.h> */ struct kobject { const char *name; /* 該kobject的名稱,同時也是sysfs中的目錄名稱 */ struct list_head entry; /* kobjetct雙向鏈表 */ struct kobject *parent; /* 指向kset中的kobject,至關於指向父目錄 */ struct kset *kset; /*指向所屬的kset*/ struct kobj_type *ktype; /*負責對kobject結構跟蹤*/ ... }; /* 定義kobject的類型及釋放回調 */ struct kobj_type { void (*release)(struct kobject *); /* kobject釋放函數指針 */ struct sysfs_ops *sysfs_ops; /* 默認屬性操做方法 */ struct attribute **default_attrs; /* 默認屬性 */ }; /* kobject上層容器 */ struct kset { struct list_head list; /* 用於鏈接kset中全部kobject的鏈表頭 */ spinlock_t list_lock; /* 掃描kobject組成的鏈表時使用的鎖 */ struct kobject kobj; /* 嵌入的kobject */ const struct kset_uevent_ops *uevent_ops; /* kset的uevent操做 */ }; /* 包含kset的更高級抽象 */ struct subsystem { struct kset kset; /* 定義一個kset */ struct rw_semaphore rwsem; /* 用於串行訪問kset內部鏈表的讀寫信號量 */ };

kobject和kset關係:
kobject and kset

如圖所示,kset將它的children(kobjects)組成一個標準的內核鏈表。因此說kset是一個包含嵌入在同種類型結構中的kobject的集合。它自身也內嵌一個kobject,因此也是一個特殊的kobject。設計kset的主要目的是容納,能夠說是kobject的頂層容器。kset老是會在sysfs中以目錄的形式呈現。須要注意的是圖中所示的kobject實際上是嵌入在其餘類型中(不多單獨使用),也多是其餘kset中。

kset和subsystem關係:
一個子系統subsystem, 其實只是一個附加了個讀寫信號量的kset的包裝,反過來就是說每一個 kset 必須屬於一個子系統。根據subsystem之間的成員關係創建kset在整個層級中的位置。
子系統經常使用宏直接靜態定義:

/* 定義一個struct subsystem name_subsys 並初始化kset的type及hotplug_ops */ decl_subsys(name, struct kobj_type *type,struct kset_hotplug_ops *hotplug_ops);

操做函數

  • 初始化
/* 初始化kobject內部結構 */ void kobject_init(struct kobject *kobj); /* 設置name */ int kobject_set_name(struct kobject *kobj, const char *format, ...); /* 先將kobj->kset指向要添加的kset中,而後調用會將kobject加入到指定的kset中 */ int kobject_add(struct kobject *kobj); /* kobject_register = kobject_init + kobject_add */ extern int kobject_register(struct kobject *kobj); /* 對應的Kobject刪除函數 */ void kobject_del(struct kobject *kobj); void kobject_unregister(struct kobject *kobj); /* 與kobject相似的kset操做函數 */ void kset_init(struct kset *kset); kobject_set_name(&my_set->kobj, "The name"); int kset_add(struct kset *kset); int kset_register(struct kset *kset); void kset_unregister(struct kset *kset);

Tip: 初始化前應先使用memset將kobj清零;初始化完成後引用計數爲1

  • 引用計數管理
/* 引用計數加1並返回指向kobject的指針 */ struct kobject *kobject_get(struct kobject *kobj); /* 當一個引用被釋放, 調用kobject_put遞減引用計數,當引用爲0時free這個object */ void kobject_put(struct kobject *kobj); /* 與kobject相似的kset操做函數 */ struct kset *kset_get(struct kset *kset); void kset_put(struct kset *kset);
  • 釋放
當引用計數爲0時,會調用ktype中的release,所以能夠這樣定義release回調函數: void my_object_release(struct kobject *kobj) { struct my_object *mine = container_of(kobj, struct my_object, kobj); /* Perform any additional cleanup on this object, then... */ kfree(mine); } /* 查找ktype */ struct kobj_type *get_ktype(struct kobject *kobj);
  • subsystem相關
decl_subsys(name, type, hotplug_ops);
void subsystem_init(struct subsystem *subsys); int subsystem_register(struct subsystem *subsys); void subsystem_unregister(struct subsystem *subsys); struct subsystem *subsys_get(struct subsystem *subsys); void subsys_put(struct subsystem *subsys);

Low-Level Sysfs Operations

kobject和sysfs關係

kobject是實現sysfs虛擬文件系統背後的機制。sysfs中的每個目錄都對應內核中的一個kobject。將kobject的屬性(atrributes)導出就會在sysfs對應的目錄下產生由內核自動生成的包含這些屬性信息的文件。只需簡單的調用前面所提到的kobject_add就會在sysfs中生成一個對應kobject的入口,但值得注意的是:

  • 這個入口總會以目錄呈現, 也就是說生成一個入口就是建立一個目錄。一般這個目錄會包含一個或多個屬性文件(見下文)。
  • 分配給kobject的名字(用kobject_set_name)就是給 sysfs 目錄使用的名字,所以在sysfs層級中相同部分的kobject命名必須惟一,不能包含下劃線,避免使用空格。
  • 這個入口所處的目錄表示kobject的parent指針,若是parent爲NULL,則指向的是它的kset,所以能夠說sysfs的層級其實對應的就是kset的層級。但當kset也爲NULL時,這個入口就會建立在sysfs的top level,不過實際中不多出現這種狀況。

屬性(atrributes)

屬性即爲上面所提到的一旦導出就會由內核自動生成的包含kobject內核信息的文件。結構以下:

struct attribute { char *name; /* 屬性名,也是sysfs對應entry下的文件名 */ struct module *owner; /* 指向負責實現這個屬性的模塊 */ mode_t mode; /* 權限位,在<linux/stat.h>中定義 */ }; 

屬性的導出顯示及導入存儲函數:

/* kobj: 須要處理的kobject attr: 須要處理的屬性 buffer: 存儲編碼後的屬性信息,大小爲PAGE_SIZE return: 實際編碼的屬性信息長度 */ struct sysfs_ops { ssize_t (*show)(struct kobject *kobj, struct attribute *attr,char *buffer); /* 導出到用戶空間 */ ssize_t (*store)(struct kobject *kobj, struct attribute *attr,const char *buffer, size_t size); /* 存儲進內核空間 */ };

須要注意的是:

  • 每一個屬性都是用name=value表示,name即便屬性的文件名,value即文件內容,若是value超過PAGE_SIZE,則應分爲多個屬性來處理;
  • 上述函數能夠處理不一樣的屬性。能夠在內部實現時同過屬性名進行區分來實現;
  • 因爲store是從用戶空間到內核,因此實現時首先要檢查參數的合法行,以避免內核崩潰及其餘問題。
缺省屬性(Default Attributes)

在kobject建立時都會賦予一些缺省的默認屬性,即上面所提到的kobj_type中的default_attrs數組,這個數組的最後一個成員須設置成NULL,以表示數組大小。全部使用這個kobj_type的kobject都是經過kobj_type中的sfsfs_ops回調函數入口實現對缺省屬性的定義。

非缺省屬性(Nondefault Attributes)

通常來講,定義時就能夠經過default_attrs完成全部的屬性,但這裏也提供了後續動態添加和刪除屬性的方法:

int sysfs_create_file(struct kobject *kobj, struct attribute *attr); int sysfs_remove_file(struct kobject *kobj, struct attribute *attr);
二進制屬性(Binary Attributes)

上述屬性包含的可讀的文本值,二進制屬性不多使用,大多用在從用戶空間傳遞一些不改動的文件如firmware給設備的狀況下。

struct bin_attribute { struct attribute attr; /* 定義name,owner,mode */ size_t size; /* 屬性最大長度,如沒有最大長度則設爲0 */ ssize_t (*read)(struct kobject *kobj, char *buffer,loff_t pos, size_t size); ssize_t (*write)(struct kobject *kobj, char *buffer,loff_t pos, size_t size); };

read/write一次加載屢次調用,每次最多PAGE_SIZE大小。注意write沒法指示最後一個寫操做,得經過其餘方式判斷操做的結束。
二進制屬性不能定義爲缺省值,所以需明確的建立與刪除:

int sysfs_create_bin_file(struct kobject *kobj,struct bin_attribute *attr); int sysfs_remove_bin_file(struct kobject *kobj,struct bin_attribute *attr);

方法:

int sysfs_create_link(struct kobject *kobj, struct kobject *target,char *name); void sysfs_remove_link(struct kobject *kobj, char *name);

熱插拔事件生成(Hotplug Event Generation)

熱插拔事件即當系統配置發生改變是內核向用戶空間的通知。而後用戶空間會調用/sbin/hotplug經過建立節點、加載驅動等動做進行響應。這個熱插拔事件的產生是在kobject_add和kobject_del時。咱們能夠經過上面kset中定義的uevent_ops對熱插拔事件產生進行配置:

struct kset_uevent_ops { /* 實現事件的過濾,其返回值爲0時不產生事件 */ int (* const filter)(struct kset *kset, struct kobject *kobj); /* 生成傳遞給/sbin/hotplug的name參數 */ const char *(* const name)(struct kset *kset, struct kobject *kobj); /* 其餘傳遞給/sbin/hotplug的參數經過這種設置環境變量的方式傳遞 */ int (* const uevent)(struct kset *kset, struct kobject *kobj, struct kobj_uevent_env *env); };

頂層容器

Buses, Devices, Drivers and Classes

Buses

總線Buses是處理器和設備的通道。在設備模型中,全部設備都是經過總線鏈接在一塊兒的,哪怕是一個內部虛擬的platform總線。

/* defined in <linux/device.h> */ struct bus_type { const char *name; /* 總線類型名 */ struct bus_attribute *bus_attrs; /* 總線的屬性 */ struct device_attribute *dev_attrs; /* 設備屬性,爲每一個加入總線的設備創建屬性鏈表 */ struct driver_attribute *drv_attrs; /* 驅動屬性,爲每一個加入總線的驅動創建屬性鏈表 */ /* 驅動與設備匹配函數:當一個新設備或者驅動被添加到這個總線時, 這個方法會被調用一次或屢次,若指定的驅動程序可以處理指定的設備,則返回非零值。 必須在總線層使用這個函數, 由於那裏存在正確的邏輯,核心內核不知道如何爲每一個總線類型匹配設備和驅動程序 */ int (*match)(struct device *dev, struct device_driver *drv); /*在爲用戶空間產生熱插拔事件以前,這個方法容許總線添加環境變量(參數和 kset 的uevent方法相同)*/ int (*uevent)(struct device *dev, struct kobj_uevent_env *env); ... struct subsys_private *p; /* 一個很重要的域,包含了device鏈表和drivers鏈表 */ } /* 定義bus_attrs的快捷方式 */ BUS_ATTR(name, mode, show, store); /* bus屬性文件的建立移除 */ int bus_create_file(struct bus_type *bus, struct bus_attribute *attr); void bus_remove_file(struct bus_type *bus, struct bus_attribute *attr); /* 總線註冊 */ int bus_register(struct bus_type *bus); void bus_unregister(struct bus_type *bus); /* 遍歷總線上的設備與驅動 */ int bus_for_each_dev(struct bus_type *bus, struct device *start, void *data, int(*fn)(struct device *, void *)); int bus_for_each_drv(struct bus_type *bus, struct device_driver *start, void *data, int(*fn)(struct device_driver *, void *));

Devices

Linux中,每個底層設備都是structure device的一個實例:

struct device { struct device *parent; /* 父設備,總線設備指定爲NULL */ struct device_private *p; /* 包含設備鏈表,driver_data(驅動程序要使用數據)等信息 */ struct kobject kobj; const char *init_name; /* 初始默認的設備名 */ struct bus_type *bus; /* type of bus device is on */ struct device_driver *driver; /* which driver has allocated this device */ ... void (*release)(struct device *dev); }; int device_register(struct device *dev); void device_unregister(struct device *dev); DEVICE_ATTR(name, mode, show, store); int device_create_file(struct device *device,struct device_attribute *entry); void device_remove_file(struct device *dev,struct device_attribute *attr);

Drivers

設備模型跟蹤全部系統已知的驅動。

struct device_driver { const char *name; /* 驅動名稱,在sysfs中以文件夾名出現 */ struct bus_type *bus; /* 驅動關聯的總線類型 */ int (*probe) (struct device *dev); /* 查詢設備的存在 */ int (*remove) (struct device *dev); /* 設備移除回調 */ void (*shutdown) (struct device *dev); ... } int driver_register(struct device_driver *drv); void driver_unregister(struct device_driver *drv); DRIVER_ATTR(name, mode, show, store); int driver_create_file(struct device_driver *drv,struct driver_attribute *attr); void driver_remove_file(struct device_driver *drv,struct driver_attribute *attr);

Classes

類是設備的一個高級視圖,實現了底層細節。經過對設備進行分類,同類代碼可共享,減小了內核代碼的冗餘。

struct class { const char *name; /* class的名稱,會在「/sys/class/」目錄下體現 */ struct class_attribute *class_attrs; struct device_attribute *dev_attrs; /* 該class下每一個設備的attribute */ struct kobject *dev_kobj; /* 當該class下有設備發生變化時,會調用class的uevent回調函數 */ int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env); char *(*devnode)(struct device *dev, mode_t *mode); void (*class_release)(struct class *class); void (*dev_release)(struct device *dev); int (*suspend)(struct device *dev, pm_message_t state); int (*resume)(struct device *dev); struct class_private *p; }; int class_register(struct class *cls); void class_unregister(struct class *cls); CLASS_ATTR(name, mode, show, store); int class_create_file(struct class *cls,const struct class_attribute *attr); void class_remove_file(struct class *cls,const struct class_attribute *attr);

Putting It All Together

all Together

相關文章
相關標籤/搜索