系統中的鎖,說簡單點就是為了保護共享資源,進而更好的實作系統并發。本文對核心中的相關常用鎖進行了介紹以及部分使用。
1.
信号量
第一個經驗法則是設計驅動時在任何可能的時候記住避免共享的資源。
全局變量遠遠不是共享資料的唯一方式
信号量Semaphores是一個單個整型值, 結合有一對函數, 典型地稱為 P 和 V 。當信号量用作互斥,阻止多個程序同時在同一個臨界區内運作
-- 它們的值将初始化為 1. 這樣的信号量在任何給定時間隻能由一個單個程序或者線程持有. 以這種模式使用的信号量有時稱為一個mutex互斥鎖。
核心中實作
Linux 核心提供了信号量實作, 核心代碼必須包含 <asm/semaphore.h>. 相關的類型是 struct
semaphore; 實際可以用 幾種方法來聲明和初始化. 一種是直接建立一個, 接着使用 sema_init 來設定它:
sema_init(struct semaphore *sem, int val)
其中val是初始化的值。
如果是互斥鎖,val就設定為1 即可。
擷取信号量可以使用:
down_interruptible,擷取信号量除非中斷打斷。
down_trylock,嘗試擷取信号量但不等待。
down,嘗試擷取信号量,如果沒有擷取則等待直到擷取。
和down對應的方法是up函數。
up(struct semaphore
*sem)
除了互斥鎖外,還有讀寫信号量,這樣讀寫可以有更好的保障。
一個 rwsem 允許一個讀者或者不限數目的讀者來持有信号量。寫者有優先權; 當一個寫者試圖進入臨界區, 就不會允許讀者進入直到所有的寫者完成了它們的工作. 這個實作可能導緻讀者饑餓 -- 讀者被長時間拒絕存取。 是以, rwsem 最好用在很少請求寫的時候, 并且寫者隻占用短時間。
2.
completions機制
如果信号量有很多競争,性能會受損并且加鎖方案需要重新審視.在調用down 的線程将幾乎是一直不得不等待.在一些情況中, 信号可能在調用 up 的程序用完它之前消失.
在2.4.7 核心中增加了
"completion" 接口 ,一個輕量級機制: 允許一個線程告訴另一個線程工作已經完成. 為使用 completion,代碼必須包含 <linux/completion.h>
通過DECLARE_COMPLETION(work)來靜态建立。
也可以動态建立,通過init_completion來初始化。
init_completion - Initialize a dynamically allocated
completion
使用中程序可以調用wait_for_completion來等待,這是一個不可中斷的等待,也不能殺死了。
另一方面,complete和complete_all可以喚醒wait_for_completion的程序。
complete 隻喚醒一個等待的線程, 而
complete_all 允許所有都繼續.
completion 機制的典型使用是在子產品退出時。當子產品準備被清理時, exit函數告知線程退出并且等待結束。核心包含一個特殊的函數complete_and_exit給線程使用.
void complete_and_exit(struct completion *comp, long code)
{
if (comp)
complete(comp);
do_exit(code);
}
代碼執行個體
使用執行個體如下,建立一個字元裝置,關聯一個讀操作和寫操作。
#include <linux/module.h>
#include <linux/init.h>
#include <linux/sched.h> /* current and everything */
#include <linux/kernel.h> /* printk() */
#include <linux/fs.h> /* everything... */
#include <linux/types.h> /* size_t */
#include <linux/completion.h>
MODULE_LICENSE("Dual BSD/GPL");
static int complete_major = 0;
DECLARE_COMPLETION(comp);
ssize_t complete_read (struct file *filp, char __user *buf, size_t count, loff_t *pos)
printk(KERN_DEBUG "process %i (%s) going to sleep\n",
current->pid,
current->comm);
wait_for_completion(&comp);
printk(KERN_DEBUG "awoken %i (%s)\n", current->pid, current->comm);
return 0; /* EOF */
ssize_t complete_write (struct file *filp, const char __user *buf, size_t count,
loff_t *pos)
printk(KERN_DEBUG "process %i (%s) awakening the readers...\n",
complete(&comp);
return count; /* succeed, to avoid retrial
*/
struct file_operations complete_fops = {
.owner = THIS_MODULE,
.read = complete_read,
.write = complete_write,
};
int complete_init(void)
int result;
/*
* Register your major, and accept a dynamic number
result = register_chrdev(complete_major, "complete", &complete_fops);
if (result < 0)
return result;
if (complete_major == 0)
complete_major = result; /* dynamic */
return 0;
void complete_cleanup(void)
unregister_chrdev(complete_major, "complete");
module_init(complete_init);
module_exit(complete_cleanup);
3.
自旋鎖
核心中大部分加鎖通過自旋鎖來完成。自旋鎖用在不能睡眠的代碼中,例如中斷處理。
自旋鎖概念上簡單,是一個互斥裝置,有2個值:"上鎖"和"解鎖".要想擷取一個特殊鎖的代碼,需要測試相關的位. 如果鎖是可用的, 這個"上鎖"位被置位并且代碼繼續進入臨界區.如果這個鎖已經被别人獲得, 代碼進入一個緊湊的循環中反複檢查這個鎖, 直到它變為可用。
"測試并置位"操作必須以原子方式進行, 以便隻有一個線程能夠獲得鎖
自旋鎖是設計用在多處理器系統上,如果一個非搶占的單處理器系統進入一 個鎖上的自旋,
它将永遠自旋; 沒有其他的線程再能夠獲得 CPU 來釋放這個鎖. 在沒有打開搶占的單處理器系統上自旋鎖操作被優化為什麼不作,如果是支援搶占的單處理系統也是正确加鎖的。
核心實作
自旋鎖的核心規則是任何代碼必須在持有自旋鎖時, 是原子性的,不能睡眠(其實而很多核心函數可能睡眠)。
持有自旋鎖時禁止中斷( 隻在本地 CPU )
自旋鎖必須一直是盡可能短時間的持有
自旋鎖原語要求的包含檔案是 <linux/spinlock.h>
實際所的類型是spinlock_t.像其他資料結構一樣, 一個自旋鎖必須初始化,初始化函數spin_lock_init。
擷取自旋鎖的函數是spin_lock。
釋放自旋鎖是spin_unlock函數。
此外還有三對擷取自旋鎖函數如下:
spin_lock_irqsave/ spin_unlock_irqrestore:在獲得鎖之前,禁止本地CPU中斷,中斷狀态儲存在flags.
spin_lock_irq/spin_unlock_irq進入之前中斷時開啟的,不用儲存中斷狀态,用完繼續開啟中斷即可。
spin_lock_bh/spin_unlock_bh禁用軟中斷。
另外還有非阻塞的自旋鎖操作:spin_trylock,
spin_trylock_bh
讀寫自旋鎖
讀寫鎖有一個類型 rwlock_t, 在<linux/spinlokc.h> 中定義 ,類似信号量中的讀寫信号量.
使用rwlock_init來初始化。操作和spin_lock基本類似。
read_lock/read_unlock
read_lock_irqsave/read_unlock_irqsave
read_lock_irq/read_unlock_irq
read_lock_bh/read_unlock_bh
write_lock/write_unlock
write_trylock
write_lock_irqsave/write_unlock_irqsave
write_lock_irq/write_unlock_irq
write_lock_bh/write_unlock_bh
讀寫鎖會引起讀饑餓,要在多讀少寫場景使用。
4.
其他鎖
因為加鎖機制是實作存在缺陷,有些情況不需要加鎖。例如環形緩沖區,在網絡擴充卡中的使用。
還可以使用原子變量atomic_t來替代一個完整的加鎖體制。對一個變量實作加鎖顯得有些過分,原子變量的操作如下。
void atomic_set(atomic_t *v, int i);
atomic_t v = ATOMIC_INIT(0);
int
atomic_read(atomic_t *v);
void atomic_add(int i, atomic_t *v);
void
atomic_sub(int i, atomic_t *v);
void atomic_inc(atomic_t *v);
void atomic_dec(atomic_t *v);
……
atomic_t在整數算術時是不錯的,但是如果要以原子方式操作可能就不行了。
原子位操作非常快, 因為它們使用單個機器指令來進行操作, 而在任何時候低層平台做的 時候不用禁止中斷,因為中斷來不及中斷原子位操作。如:
set_bit(nr, void *addr);
clear_bit(nr, void *addr);
change_bit(nr, void *addr);
不過在新代碼中,還是建議使用自旋鎖,至少别人知道是在做什麼。
seqlock鎖
spin_lock對于臨界區是不做區分的。而讀寫鎖是對臨界區做讀寫區分,寫程序進入時需要等待讀程序退出臨界區。為了保護寫程序的優先權,并使得寫程序可以更快的獲得鎖,引入了順序鎖。
順序鎖的思想是:對某一個共享資料讀取的時候不加鎖,寫的時候加鎖。在讀取者和寫入者之間引入變量sequence,讀取者在讀取之前讀取sequence, 讀取之後再次讀取此值,如果不相同,則說明本次讀取操作過程中資料發生了更新,需要重新讀取。而對于寫程序在寫入資料的時候就需要更新sequence的值。
初始化可以如下:
seqlock_t
lock1 = SEQLOCK_UNLOCKED;//靜态初始化
lock2;
seqlock_init(&lock2);//動态初始化
讀之前調用函數:read_seqbegin,讀完繼續調用read_seqretry。
如果是寫則是:write_seqlock,寫完調用write_sequnlock
考慮到中斷影響,讀寫都有irqsave,irq,bh版本。
RCU
讀取-拷貝-更新(RCU) 是一個進階的互斥方法,當資料結構需要改變, 寫線程做一個拷貝, 改變這個拷貝, 接着使相關的指針對準新的版本.RCU 的代碼應當包含 <linux/rcupdate.h>。使用一個 RCU-保護的資料結構的代碼應當用 rcu_read_lock 和 rcu_read_unlock 調用将它的引用包含起來.