小結:
核心同步方法:
順序和屏障
第10章 核心同步方法
臨界區:訪問和操作共享資料的**。競爭條件:兩個執行執行緒有可能在同乙個臨界區中同時執行。
同步:避免併發和防止競爭條件。
鎖的形式和鎖的粒度各不相同,各個鎖機制之前的主要區別在於:當鎖被其他執行緒持有時,其他的行為表現。
造成併發執行的原因
使用者空間:因為會被搶占或重新排程。
訊號處理:非同步發生
中斷:任何時刻非同步發生
軟中斷和tasklet:任何時刻喚醒或排程
核心搶占
睡眠及與使用者空間的同步:核心執行程序可能睡眠,會喚醒排程程式排程乙個新的使用者程序
對稱多處理
辨識出真正需要共享的資料和相應的臨界區才是真正的挑戰。
如:一段核心**操作某資源時產生系統中斷,而該中斷的處理函式還要訪問這個資源。
中斷安全**:在中斷處理程式中能避免併發訪問的安全**
smp安全**:在smp中。。。。。。。。。。。。。。。
搶占安全**:在核心搶占時。。。。。。。。。。。。。
要保護什麼
要在一開始設計時就要仔細考慮。
需要保護:大多數核心資料結構需要加鎖。記住:給資料而不是給**加鎖。
編寫核心**時,要問自己:
(1). 這個資料是不是全域性的?除了當前執行緒外,其他執行緒能不能訪問它?
(2). 這個資料會不會在程序上下文和中斷上下文中共享?是不是要在兩個不同的中斷處理程式中共享?
(3). 程序在訪問資料時可不可能被搶占?被排程的新程式會不會訪問同一資料?
(4). 當前程序是不是會睡眠在某些資源上?如果是,它會讓共享資料處於何種狀態?
(5). 怎樣防止資料失控?
(6). 如果這個函式又在另乙個處理器上被排程了將會發生什麼?
(7). 如何確保**遠離併發威脅呢?
條件死鎖避免:
當鎖爭用嚴重時,加鎖太粗會降低可擴充套件性;而鎖爭用不明顯時,加鎖過細會加大系統開銷,造成浪費。
原子操作:不可分割的指令。
核心提供了2組原子操作介面 - 一組對整數進行操作;另一組針對單獨的位進行操作。
大多數系統結構會支援原子操作的簡單算術指令,或者通過鎖記憶體匯流排的方式實現。
最常見使用者:實現計數器
特性:開銷小
原子整數操作
atomic_t:
atomic_t u = atomic_init(0);
atomic_set()
atomic_add()
atomic_inc()
atomic_read()
atomic_dec_and_test()
原子性:確保指令執行期間不被打斷(通過原子操作等)
順序性:確保多條指令出現,本該的順序性依然要保持(通過屏障barrier)
64位原子操作
atomic64_t
原子位操作
set_bit()
clear_bit()
test_and_set_bit()
對應的非原子位函式,多了兩個下劃線
__test_bit()
核心提供從指定位址開始搜尋第乙個被設定(或未設定)的位。
int find_first_bit(unsigned long *addr, unsigned int size)
int find_first_zero_bit(unsigned long *addr, unsigned int size)
等待鎖時,一直迴圈-旋轉-等待。
場景:適合短時間內輕量級鎖
自旋鎖方法
自旋鎖的實現和體系結構相關,**往往通過彙編實現。
警告:自旋鎖是不可遞迴的!
自旋鎖和下半部
spin_lock_bh(): 獲取指定鎖,並禁止所有下半部的執行
(1). 下半部和程序上下文共享資料時,必須對程序上下文中共享資料保護,需要加鎖同時禁止下半部。【下半部可搶占程序】
(2). 中斷處理程式和下半部共享資料時,下半部必須獲取恰當的鎖的同時禁止中斷。【中斷可搶占下半部】
(3). 不同tasklet共享資料時,需要加普通自旋鎖,這裡不需要禁止下半部。【tasklet不會相互搶占】
(4). 軟中斷共享時,需要加普通鎖,也不需要禁止下半部【軟中斷之間不會搶占】
乙個或多個任務可以併發持有讀者鎖,但寫者鎖只能有乙個。
特點:照顧讀。
define_rwlock(mr_rwlock);
read_lock(&mr_rwlock);
…read_unlock(&mr_rwlock);
write_lock(&mr_rwlock);
write_unlock(&mr_rwlock);
訊號量特徵:
(1). 訊號量適用於鎖會被長時間持有的情況,睡眠、維護等待佇列及喚醒的開銷很大。
(2). 只能在程序上下文中獲取訊號量鎖
(3). 占用訊號量的同時不能占用自旋鎖。
(4). 往往在需要和使用者空間同步時,你的**需要睡眠,此時使用訊號量是唯一選擇。
(5). 訊號量不同於自旋鎖,它不會禁止核心搶占,所有持訊號量的**可以被搶占。
計數訊號量和二值訊號量
核心使用訊號量時基本用到的都是互斥訊號量。
建立和初始化訊號量
訊號量的實現和體系結構相關。
靜態:static declare_mutex(name)
動態:sema_init(sem, count); 或 init_mutex(sem);
使用訊號量
down_interruptible():睡眠時可喚醒。task_interruptible
down() :睡眠時不可喚醒。task_uninterruptible
down_trylock():試圖獲得指定訊號量,如果被徵用,不等待,直接返回非0值
up()
rw_semaphore,區分讀寫的訊號量。所有讀寫訊號量都是互斥訊號量,所有讀寫鎖的睡眠都不會被訊號打斷。
靜態初始化:static declare_rwsem(name);
動態初始化:init_rwsem(sem);
down()
down_read_trylock()
down_write_trylock()
downgradge_write():動態地將寫鎖轉換成讀鎖。
mutex,類似計數為1的訊號量,但操作介面更簡單,實現更高效,使用限制更強。
靜態:define_mutex(name);
動態:mutex_init(&mutex);
鎖定:mutex_lock(&mutex);
解鎖:mutex_unlock(&mutex);
mutex相比訊號量的場景更嚴格:
核心配置:config_debug_mutexes
需求建議加鎖方式
低開銷加鎖
優先使用自旋鎖
短期鎖定
優先使用自旋鎖
長期加鎖
優先使用互斥體
中斷上下文加鎖
使用自旋鎖
持有鎖需睡眠
使用互斥體
如果在核心中乙個任務需要發出訊號通知另乙個任務發生了某個特定事件,利用完成變數。如子程序執行或退出時,vfork()系統呼叫使用完成變數喚醒父程序
bkl是乙個全域性自旋鎖,特性:
(1). 持有bkl的任務仍然可以睡眠。因為當任務無法被排程時,鎖會被自動丟棄;當任務被排程時,鎖會被重新獲得。睡眠不會造成任務死鎖。
(2). bkl是一種遞迴鎖。
(3). bkl只能在程序上下文中。
(4). bkl在持有時會禁止核心搶占。
用於讀寫共享資料。實現這種鎖主要依靠乙個序列計數器,當有疑義的資料被寫入時會得到乙個鎖,並且序列值會增加。在讀取資料之前和之後,序列號都被讀取,如果序列號相同,說明沒有被寫操作打斷過。此外,如果讀資料是偶數,也表明寫沒發生過。
核心搶占可以使用自旋鎖作為非搶占區域的標記。當然,每個處理器上的資料不要鎖保護。
禁止核心搶占:preempt_disable()和preempt_enable()
每個處理器上的資料訪問問題:get_cpu()和put_cpu():在返回當預處理器號前首先關閉核心搶占。
屏障:保證順序要求,指示編譯器不要對給定點周圍的指令進行重新排序。
Linux核心設計與實現 從核心出發
使用補丁 原始碼樹根目錄中的很多檔案值得提及。配置項會被存放在核心 樹根目錄下的.config檔案中,可以查詢和修改核心選項,修改後或者在用已有的配置檔案配置新的 樹的時候,應該驗證和更新配置 make oldconfig 編譯核心之前都應該這麼做 配置選項config ikconfig proc把...
Linux核心設計與實現 從核心出發
使用補丁 原始碼樹根目錄中的很多檔案值得提及。配置項會被存放在核心 樹根目錄下的.config檔案中,可以查詢和修改核心選項,修改後或者在用已有的配置檔案配置新的 樹的時候,應該驗證和更新配置 make oldconfig 編譯核心之前都應該這麼做 配置選項config ikconfig proc把...
Linux核心設計與實現 10 核心同步介紹
臨界區 就是訪問和操作共享資料的 段。如果兩個執行執行緒有可能處於同乙個臨界區中同時執行,如果這個情況發生了,就叫做競爭條件。避免併發和防止競爭條件稱為同步。我們必須在某些操作期間對資料加鎖,確保每個事務相對其他操作是原子性的,這樣的事務必須完整地發生,要麼乾脆不發生,但是決不能打斷。對於單個變數的...