|
一、讀寫鎖
使用自旋鎖時,無論什么時候都只有一個可執行單元進入臨界區,無論該執行單元是讀操作還是寫操作,大多數情況下我們讀操作會多于寫操作,
多個執行單元進入臨界區進行讀操作是不會有問題的,如果仍采用自旋鎖就會有效率低下的問題,而采用讀寫鎖的話就會大大的提升效率。
讀寫鎖,從自旋鎖中衍生而出,分讀自旋鎖和寫自旋鎖。
1、多個執行單元可以同時獲取到讀鎖并訪問臨界區的資源(或代碼),但只能獲取一個寫自旋鎖。
2、如果某些執行單元已經獲取到讀鎖仍未釋放該鎖,這時B執行單元去獲取寫鎖,B執行單元就會阻塞,直到所有讀鎖被釋放。
3、如果有個A執行單元獲取到寫鎖仍未釋放該鎖,這時B執行單元去獲取寫鎖,B執行單元就會阻塞,直到A寫鎖被釋放。
4、如果有個A執行單元獲取到寫鎖仍未釋放該鎖,這時B執行單元去獲取讀鎖,B執行單元就會阻塞,直到A寫鎖釋放。
無論是讀鎖還是寫鎖,都會對臨界區加鎖:
讀鎖:讀鎖不會互斥讀鎖,但會互斥寫鎖,也就是說可以重復獲取讀鎖(讀鎖上鎖期間不應修改共享資源)
寫鎖:寫鎖會互斥寫鎖和讀鎖,也就是說在上了寫鎖期間,其他執行單元無法獲取到寫鎖和讀鎖
(讀鎖上鎖期間不允許修改共享資源,寫鎖上鎖期間只允許一個執行單元修改共享資源。)
使用示例:
// 定義并初始化讀寫自旋鎖
static DEFINE_RWLOCK(rwlock);
static ssize_t demo_read(struct file *file, char __user *buf, size_t count,
loff_t *ppos)
{
// ......
read_lock(&rwlock);// 加了讀鎖,為了互斥寫鎖
// ..
// 讀臨界區這里可以并發進入臨界區不會阻塞,提高效率
// ..
read_unlock(&rwlock);
// ......
}
static ssize_t demo_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
// .......
write_lock(&rwlock); //加了寫鎖,為了互斥讀鎖和寫鎖
// ..
// 修改臨界區 這里只允許有一個執行單元在所有讀鎖釋放后進入
// ..
write_unlock(&rwlock);
// .......
}
優點:在沒有寫操作時可以并發讀操作,不會被阻塞。
缺點:當執行寫操作時,所有讀操作都會被阻塞。
讀寫鎖僅適合在讀多寫少并且臨界區很短的情況下。
一些接口:
DEFINE_RWLOCK(lock)
void rwlock_init(rwlock_t *lock)
int read_trylock(rwlock_t *lock)
void read_lock(rwlock_t *lock)
void read_unlock(rwlock_t *lock)
void read_lock_irq(rwlock_t *lock)
void read_unlock_irq(rwlock_t *lock)
void read_lock_bh(rwlock_t *lock)
void read_unlock_bh(rwlock_t *lock)
int write_trylock(rwlock_t *lock)
void write_lock(rwlock_t *lock)
void write_unlock(rwlock_t *lock)
void write_lock_irq(rwlock_t *lock)
void write_unlock_irq(rwlock_t *lock)
void write_lock_bh(rwlock_t *lock)
void write_unlock_bh(rwlock_t *lock)
二 順序鎖
順序鎖則是自旋鎖的另外一個升級版,和讀寫鎖有些相似。順序鎖主要是圍繞順序鎖和順序號來設計的。
typedef struct{
unsigned sequence;// 順序鎖的順序計數器-順序號
spinlock_t lock;// 自旋鎖
}seqlock_t;
順序鎖:順序鎖未被釋放時,獲取順序鎖的執行單元會阻塞(自旋)
在需要修改共享資源(臨界區)的時候獲取順序鎖,成功獲取順序號+1
當完成共享資源的操作后釋放順序鎖,順序號+1
順序號:順序鎖未被釋放時,獲取順序號的執行單元會阻塞(自旋)
在讀取共享資源時需要先取得順序號,只有在順序鎖釋放的情況下才會得到順序號,該順序號一定是偶數。
完成共享資源的讀取后再次取得順序號并對比之前獲取的順序號,若不一致則需要重新讀取共享資源。
成功獲取順序鎖時順序號一定是奇數,釋放順序鎖時一定是偶數。之所以有這種規律跟順序鎖實現有關。
// 實現源碼,獲取順序號的過程中并沒有上鎖操作
static _always_inline unsigned read_seqbegin(const seqlock_t *sl)
{
unsigned ret;
repeat:
ret = sl->sequence; // 讀取順序號
smp_rmb(); // 讀內存屏障
// 如果順序號是奇數則循環檢測(自旋)否則返回順序號
if(unlikely(ret & 1))
{
cpu_relax();
goto repeat;
}
return ret;
}
使用示例:
// 定義并初始化讀寫自旋鎖
static DEFINE_SEQLOCK(seqlock);
static ssize_t demo_read(struct file *file, char __user *buf, size_t count,
loff_t *ppos)
{
// ......
// -------------------- 關鍵代碼 ----------------------------
unsigned seq;
do
{// 獲取順序號,如果順序鎖未被釋放 將會被阻塞(自旋)
seq = read_seqbegin(&seqlock); // 成功返回的順序號一定是偶數
// ...
// 臨界區
// ...
//如果當前順序號和seq一致,則退出循環,否則重新讀取共享資源
}while(read_seqretry(&seqlock, seq));
// -------------------- 關鍵代碼 ----------------------------
// ......
}
static ssize_t demo_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
// .......
// 獲取順序鎖 seqlock.sequence 會被+1 這時 seqlock.sequence 會是奇數
write_seqlock(&seqlock);
// ..
// 修改臨界區 這里只允許有一個執行單元在所有讀鎖釋放后進入
// ..
// 釋放順序鎖 seqlock.sequence 會被+1 這時 seqlock.sequence 會是偶數
write_sequnlock(&seqlock);
// .......
}
順序鎖的讀臨界區操作并沒有鎖定臨界區,只是簡單讀取臨界區中的共享資源,所以在讀取共享數據時
是可以修改共享數據的。
優點:讀取到的數據一定是最新的數據,在沒有寫操作時讀操作不會阻塞并且可以并發讀。
缺點:如果在讀共享數據的過程中發生了寫操作,就會使得系統不斷的地循環等待(自旋)和重新執行讀臨界區數據。
順序鎖僅適合在讀多寫少、臨界區很短并要求數據實時更新情況下。
一些接口:
DEFINE_SEQLOCK(lock)
void seqlock_init(seqlock_t *lock)
// 以下接口在成功執行后順序號會+1
int write_tryseqlock(seqlock_t *lock)
void write_seqlock(seqlock_t *lock)
void write_seqlock_irqsave(seqlock_t *lock)
void write_seqlock_irq(lock)
void write_seqlock_bh(lock)
void wrtie_sequnlock(seqlock_t *lock)
void write_sequnlock_irqrestore(lock, flags)
void write_sequnlock_irq(lock)
void write_sequnlock_bh(lock)
// 以下接口不會修改順序號
unsigned read_seqbegin(const seqlock_t *lock)
void read_seqbegin_irqsave(lock, flags)
int read_seqretry(const seqlock_t *lock, unsigned iv)
void read_seqretry_irqrestore(lock, iv, flags)
三、信號量
信號量和鎖機制最大的區別就是實現阻塞的方式不一樣。
自旋鎖、讀寫鎖、順序鎖都是通過不斷循環檢測實現阻塞,當臨界區很短時效率很高,
當臨界區很長的時候性能就會急劇下降。而信號量則是通過休眠方式實現阻塞,適合
臨界區比較長的情況,休眠不會占用CPU資源,所以不會影響系統性能更不會出現死機現象。
信號量比鎖機制要靈活很多,它可以指定可以有幾個執行單元進入臨界區。
1、信號量數據結構
struct semaphore sem;
struct semaphore {
raw_spinlock_tlock;// 鎖
// 資源數 決定可以有幾個執行單元進入臨界區
// >0,資源空閑. ==0,資源忙
// <0 資源不可用,并至少有一個進程等待資源
unsigned intcount;
struct list_headwait_list;// 鏈表
};
2、初始化
sema_init(&sem, 1);// 初始化信號量并指定 sem.count 的初始值(即資源數)
#define __SEMAPHORE_INITIALIZER(name, n)\
{\
.lock= __RAW_SPIN_LOCK_UNLOCKED((name).lock),\
.count= n,\ // <--- 這里設置了可用資源數目
.wait_list= LIST_HEAD_INIT((name).wait_list),\
}
#define DEFINE_SEMAPHORE(name)\
struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)
static inline void sema_init(struct semaphore *sem, int val)
{
static struct lock_class_key __key;
*sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}
3、獲取信號量
中斷無法喚醒:
down(&sem); // 獲取信號量,若計數器的值小于或等于0則進入休眠,若大于0則遞減。
void down(struct semaphore *sem)
{
unsigned long flags;
// 上鎖
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0))
sem->count--;// 計數器大于 0 表示資源可用則將資源數自減
else
__down(sem);// 計數器小于或等于0 表示資源忙則進入休眠狀態
// 解鎖
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
EXPORT_SYMBOL(down);
static noinline void __sched __down(struct semaphore *sem)
{
__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
中斷可喚醒:返回非0值表示是由中斷喚醒,返回0值則表示成功獲取到信號量
int down_interruptible(struct semaphore *sem)
{
unsigned long flags;
int result = 0;
// 上鎖
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0))
sem->count--;
else
result = __down_interruptible(sem);// 這里不一樣
// 解鎖
raw_spin_unlock_irqrestore(&sem->lock, flags);
return result;
}
EXPORT_SYMBOL(down_interruptible);
static noinline int __sched __down_interruptible(struct semaphore *sem)
{// 只是傳遞的參數不一樣 TASK_INTERRUPTIBLE
// MAX_SCHEDULE_TIMEOUT 表示永不超時
return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
static inline int __sched __down_common(struct semaphore *sem, long state, long timeout)
{
struct task_struct *task = current;
struct semaphore_waiter waiter;
// 將當前進程加入到等待鏈表
list_add_tail(&waiter.list, &sem->wait_list);
waiter.task = task;
waiter.up = 0;// 將當前進程標記為休眠
for (;;) {
// 這里是為 down_interruptible() 準備
if (signal_pending_state(state, task))
goto interrupted;
// 這里是為 down_timeout() 準備
// 若超時被喚醒則跳出
if (timeout <= 0)
goto timed_out;
// 設置進程狀態
__set_task_state(task, state);
// 解鎖
raw_spin_unlock_irq(&sem->lock);
// 讓出CPU,此時執行到這里停止往下執行
timeout = schedule_timeout(timeout);
// 進程被喚醒時 再上鎖
raw_spin_lock_irq(&sem->lock);
// 判斷是否由 __up() 喚醒,如果是則表示資源可用返回0。
if (waiter.up)
return 0;
}
// 超時喚醒 將該進程從等待鏈表中刪除。返回非0值
timed_out:
list_del(&waiter.list);
return -ETIME;
// 中斷喚醒 將該進程從等待鏈表中刪除。返回非0值
interrupted:
list_del(&waiter.list);
return -EINTR;
}
4、釋放信號量
up(&sem); // 釋放信號量 若有進程在等待則喚醒,直到沒有進程再等待才將計數器自增
static noinline void __sched __up(struct semaphore *sem)
{
// 獲取等待鏈表中第一個進程
struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list, struct semaphore_waiter, list);
list_del(&waiter->list); // 從等待鏈表中移除該進程
waiter->up = 1; // 喚醒標志
wake_up_process(waiter->task); // 喚醒該進程
}
void up(struct semaphore *sem)
{
unsigned long flags;
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(list_empty(&sem->wait_list)))
sem->count++; // 如果沒有等待鏈表中沒有進程在等待 才自增 表示資源空閑
else
__up(sem);// 如果有進程等待則喚醒
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
EXPORT_SYMBOL(up);
使用示例:
struct semaphore sem;
static ssize_t demo_read(struct file *file, char __user *buf, size_t count,
loff_t *ppos)
{
// ......
// 獲取信號量
down(&sem);
// ...
// 臨界區
// ...
// 釋放信號量
up(&sem);
// ......
}
static ssize_t demo_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
// ......
// 獲取信號量
down(&sem);
// ...
// 臨界區
// ...
// 釋放信號量
up(&sem);
// ......
}
static int __init demo_init(void)
{
// 將信號量值初始化為 1 表示只允許一個執行單元進入臨界區
sema_init(&sem, 1);
}
接口:略
四、讀寫信號量
讀寫信號量與信號量之間的關系和讀寫自旋鎖與自旋鎖一樣。
讀寫信號量可以有多個執行單元獲得讀信號量從而并發讀操作,但只能有一個執行單元獲取到寫信號量。
使用示例:
struct rw_semaphore rw_sem;
static ssize_t demo_read(struct file *file, char __user *buf, size_t count,
loff_t *ppos)
{
// ......
// 獲取讀信號量 如果有寫信號量未釋放則進入休眠
down_read(&rw_sem);
// ...
// 臨界區
// ...
// 釋放讀信號量
up_read(&rw_sem);
// ......
}
static ssize_t demo_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
// ......
// 獲取寫信號量 如果有寫信號量或所有讀信號量未全部釋放則進入休眠
down_write(&rw_sem);
// ...
// 臨界區
// ...
// 釋放寫信號量
up_write(&rw_sem);
// ......
}
static int __init demo_init(void)
{
// 將信號量值初始化為 1 表示只允許一個執行單元進入臨界區
init_rwsem(&rw_sem, 1);
}
接口:略
鎖機制適合在臨界區執行時間短的情況下。
信號量適合在臨界區執行時間較長的情況下。
鎖機制是采用不斷循環檢測,所以實時性、速度快。
信號量是采用休眠喚醒的方式,休眠喚醒需要時間,實時性和速度較慢。
|
|