前置知識
臨界區:通常指某個代碼片段,在該代碼片段會訪問共享資源,比如共享數據、共享硬件資源(打印機、IO)。串行使用共享資源,才能保證正確的輸出結果,因此一個進程要等待另一個進程使用完后才能使用。
進程上下文和中斷上下文:進程上下文包括:CPU所有寄存器中的值、進程的狀態以及堆棧上的內容,當內核需要切換到另一個進程時,它需要保存當前進程的所有狀態,即保存當前進程的進程上下文,以便再次執行該進程時,能夠恢復切換時的狀態。中斷對於進程來說是異步的,中斷可以打斷進程進入中斷上下文,中斷進入和退出時會保存恢復進程上下文。
同步:協調步調,進程按預定的先后次序運行。如A進程執行到一定程度時要依賴B進程的某個結果於是停下來;B繼續執行將結果給A,A才再繼續操作。
互斥:若系統中的訪問共享資源,則這些進程和中斷處於競爭狀態。
並發的來源:
- 系統是可搶占的,任何時候都有可能被搶走CPU
- SMP系統,代碼在多個處理器上運行
- 設備中斷導致的異步事件
獲取鎖沖突時2種處理方式:
- 一個是原地等待
- 一個是掛起當前進程,調度其他進程執行(睡眠)
編程設計時盡量減少資源共享
阻塞和非阻塞:
程序在等待調用結果(消息,返回值)時的狀態,阻塞是指會掛起當前任務,非阻塞是指會定期查詢狀態。
信號量 semaphore
semaphore的基本結構
使用semaphore時,都是通過接口進行操作,不要訪問成員。
lock:信號量基於自旋鎖實現對資源的互斥管理。lock用來管理count和wait_list。
count:資源的個數。大於0,代表有剩余可用資源;等於0,代表資源忙,進程會進入睡眠並且等待其他進程釋放資源。
wait_list:等待此資源的所有進程的隊列。
/* 文件:include/linux/semaphore.h *//* Please don't access any members of this structure directly */
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
常用操作
semaphore初始化
例如,定義全局變量g_sem,調用sema_init初始化為0,代表此時信號量不能獲取。
struct semaphore g_sem;
void sema_init(struct semaphore *sem, int val);
DEFINE_SEMAPHORE(name);
獲取信號量down
如果成員count大於0,則count減1后返回成功;
如果成員count等於0,則CPU調度到其他任務,等再次調度到本任務時檢查是否應該退出for循環。
執行路徑:down->_ _down->__down_common->schedule_timeout->schedule
/* 文件:kernel/locking/semaphore.c */
void down(struct semaphore *sem)
{
unsigned long flags;
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0))
sem->count--; /* count大於0代表有剩余資源,count減1后返回 */
else
__down(sem); /* count等於0代表資源忙,__down會調度其他任務 */
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
static inline int __sched __down_common(struct semaphore *sem, long state, long timeout)
{
struct semaphore_waiter waiter;
list_add_tail(&waiter.list, &sem->wait_list);
waiter.task = current;
waiter.up = false;
for (;;) {
if (signal_pending_state(state, current))
goto interrupted;
if (unlikely(timeout <= 0))
goto timed_out;
__set_current_state(state);
raw_spin_unlock_irq(&sem->lock);
timeout = schedule_timeout(timeout); /* 調度其他進程 */
raw_spin_lock_irq(&sem->lock);
if (waiter.up) /* 其他進程會設置當前進程狀態為ture,退出for循環 */
return 0;
}
timed_out:
list_del(&waiter.list);
return -ETIME;
interrupted:
list_del(&waiter.list);
return -EINTR;
}
down的不同版本
void down(struct semaphore *sem);
int __must_check down_interruptible(struct semaphore *sem);
int __must_check down_killable(struct semaphore *sem);
int __must_check down_trylock(struct semaphore *sem);
int __must_check down_timeout(struct semaphore *sem, long jiffies);
down會一直等待,不能被用戶中斷。
down_interruptible完成相同的工作,但是操作是可中斷的,它允許等待某個信號量上的用戶空間進程可被用戶中斷。如果操作被中斷,該函數會返回非零值,而調用者不會擁有改信號量,因此必須檢查返回。
down_trylock在調用時會立即返回是否獲取到信號量的結果。
down_timeout是超時等待。
釋放信號量up
信號量可以在任務或者中斷上下文釋放。
down和up可以不成對使用,在A進程調用up,在B進程調用down。
如果當前任務隊列為空,則count++,直接返回;否則,喚醒隊首任務。
執行路徑:up->__up->wake_up_process
/* 文件:kernel/locking/semaphore.c */
void up(struct semaphore *sem)
{
unsigned long flags;
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(list_empty(&sem->wait_list)))
sem->count++; /* 如果當前任務隊列為空,則count++,直接返回 */
else
__up(sem); /* 如果當前任務隊列不為空,則喚醒隊首任務 */
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
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 = true; /* 設置為true后,等待鎖的進程會退出for循環 */
wake_up_process(waiter->task); /* 調度指定任務 */
}
/* 文件:kernel/sched/core.c */
/**
* wake_up_process - Wake up a specific process
* @p: The process to be woken up.
* Attempt to wake up the nominated process and move it to the set of runnable processes.
* Return: 1 if the process was woken up, 0 if it was already running.
* It may be assumed that this function implies a write memory barrier before
* changing the task state if and only if any tasks are woken up.
*/
int wake_up_process(struct task_struct *p)
{
return try_to_wake_up(p, TASK_NORMAL, 0, 1);
}
讀寫鎖 rwsem
許多任務只會讀取臨界區數據而不修改內容,那么允許多個並發的讀取者可以大大的提高性能。一個rwsem允許一個寫入者或者無限多個讀取者擁有該信號量。寫入者具有更高的優先級,當某個給定寫入者試圖進入臨界區時,在所有寫入者完成工作之前,不允許讀取者獲取。如果有大量的寫入者競爭該信號量,則這種實現會導致讀取者餓死,即可能會長時間拒絕讀取者的訪問,因此rwsem適用於很少寫訪問且較多的讀訪問場景。reader/writer semaphore,讀取者/寫入者信號量。
rw_semaphore基本結構
/* if count is 0 then there are no active readers or writers
if count is +ve then that is the number of active readers
if count is -1 then there is one active writer
if wait_list is not empty, then there are processes waiting for the semaphore */
struct rw_semaphore {
__s32 count;
raw_spinlock_t wait_lock;
struct list_head wait_list;
};
常用操作
初始化接口
init_rwsem(struct rw_semaphore *sem);
讀接口
down_read是不可中斷的休眠。down_read_trylock會立即返回,注意其用法與其他內核函數不同,其他函數在成功時返回零。由down_read獲取的rwsem對象必須通過up_write釋放。
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
up_read(struct rw_semaphore *sem);
寫接口
down_write、down_write_trylock、up_write與讀取者的對應函數行為相同。當某個快速修改完成時,我們可以使用downgrade_write來允許其他讀取者的訪問。
void down_write(struct rw_semaphore *sem);
void down_write_trylock(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);
void downgrade_write(struct rw_semaphore *sem);
同步 completion
completion信號量是一個輕量級的機制,它允許一個線程告訴另一個線程某個工作已經做完了。與semaphore區別是,
- completion可以喚醒一個任務,也可以喚醒所有任務。semaphore沒有此功能
- semaphore可用於排他訪問(可處理優先級反轉),而completion僅用於同步操作不能用來處理排他訪問。
completion結構體
done表示完成操作的次數。wait表示等待任務的隊列
struct completion {
unsigned int done;
wait_queue_head_t wait;
};
常用操作
初始化
靜態初始化:DECLARE_COMPLETION(struct completion *c)
動態初始化:init_completion(struct completion *c)
等待完成
void wait_for_completion(struct completion *);
unsigned long wait_for_completion_timeout(struct completion *x, unsigned long timeout);
完成
complete只會喚醒一個等待的任務,complete_all會喚醒所有等待任務。如果使用了complete_all,需要重新初始化該變量reinit_completion。
complete_all會把done變為最大值,而初始化時done為0。
void complete(struct completion *);
void complete_all(struct completion *);
互斥鎖mutex
mutex基本結構
owner:atomic_long_t為一個原子型變量。
wait_lock:自旋鎖,用於wait_list鏈表的保護操作。
wait_list:等待互斥鎖的進程隊列。
/* 文件:include/linux/mutex.h *//*
* Simple, straightforward mutexes with strict semantics:
* 使用用法:
* - only one task can hold the mutex at a time
* - only the owner can unlock the mutex
* - multiple unlocks are not permitted
* - recursive locking is not permitted
* - a mutex object must be initialized via the API
* - a mutex object must not be initialized via memset or copying
* - task may not exit with mutex held
* - memory areas where held locks reside must not be freed
* - held mutexes must not be reinitialized
* - mutexes may not be used in hardware or software interrupt
* contexts such as tasklets and timers
*/
struct mutex {
atomic_long_t owner;
spinlock_t wait_lock;
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
struct optimistic_spin_queue osq; /* Spinner MCS lock */
#endif
struct list_head wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
void *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
常用操作
mutex初始化
定義全局變量g_mutex,調用mutex_init初始化。不允許對mutex進行重復初始化。
struct mutex g_mutex;
mutex_init(&g_mutex);
獲取mutex
執行路徑:mutex_lock->__mutex_lock_slowpath->__mutex_lock->__mutex_lock_common
釋放mutex
執行路徑:mutex_unlock->__mutex_unlock_slowpath->wake_up_q
自旋鎖spinlock
什么是自旋鎖
和信號量不同的是,如果自旋鎖被其他人獲得,則進入忙等待並重復檢查這個鎖,直到該鎖可用為止,這就是自旋鎖的自旋部分。自旋鎖最初是為了在多核處理器系統中使用而設計的,可參考以下文的使用場景分析。
自旋鎖的核心規則:
- 任何擁有自旋鎖的代碼都必須是原子的,即不能休眠。當我們編寫需要在自旋鎖下執行的代碼時,必須注意每一個所調用的函數。比如copy_from_user、kmalloc、copy_to_user。
- 自旋鎖必須在可能的最短時間內擁有。長的鎖擁有時間將阻止系統調度,增加了內核的延遲。
spinlock使用場景
如果共享數據只在進程上下文訪問,那么可以考慮使用semaphore、mutex、spinlock機制。但是如果在進程上下文和中斷上下文都要訪問共享數據,由於中斷上下文中不能使用睡眠鎖,這個時候就考慮使用spinlock。
進程上下文場景
獲取spinlock鎖時,需要禁止本地搶占。假設存在如下場景:
1)單核CPU,進程A、B都訪問共享資源R。
進程A訪問共享資源R的過程中發生了中斷,中斷喚醒了優先級更高的進程B,如果B訪問共享資源R,那么由於A已獲得鎖,B獲取獲取不到鎖,CPU進入永久的spin。因此在獲取spinlock時要禁止本地搶占(preempt_disable)。
中斷上下文場景
在中斷上下文和進程上下文都訪問spinlock時,需要禁止本地搶占和禁止本地中斷。假設存在如下場景:
1)進程A運行在CPU0訪問共享資源R。
2)進程B運行在CPU1訪問共享資源R。
3)中斷服務函數也會訪問共享資源R。
如果在CPU0上進程A持有spinlock進入臨界區,這時中斷發生調度到CPU1,看起來沒什么問題,CPU0繼續執行,CPU1等待CPU0釋放鎖。但是如果中斷調度到CPU0,那么CPU0就會在中斷永久spin,CPU1上的進程B也會由於獲取不到鎖而進入spin態。所有核都spin。為解決這個問題,禁止本地中斷。
spinlock的基本結構
spinlock簡單調用raw_spinlock,raw_spinlock包含arch_spinlock_t系統結構相關的定義。早期spinlock只有一個整型變量用於標識鎖是否已獲取,在現代多核的cpu中,因為每個cpu都有cache,不需要去訪問主存獲取lock,所以持有lock的cpu釋放鎖后使其他cpu的cache都失效,當前cpu下一次獲取鎖更容易,導致出現了不公平。
現在arch_spinlock_t結構體使用了ticket機制防止飢餓現象。可以用吃飯排隊的情況去模擬cpu獲取鎖。next代表CPU調用spin_lock函數時獲取的號碼牌,每個CPU獲取的號碼牌要保證唯一,持有之后就等着叫號,因此next變量要進行原子操作;owner代表當前可以就餐的號碼,每次釋放鎖要對owner+1,然后通知其他CPU去檢查其持有的號碼牌是否與owner相等,如果不相等就要繼續等待。剛開始的時候next和owner的值都是0,當CPU0獲取鎖時持有號碼0,將next++得1,CPU0判斷持有號碼0與owner相等進入臨界區。由於CPU0吃飯較慢,這時CPU1、CPU2也過來了,CPU1調用spin_lock持有號碼1,然后next++得2,CPU2調用spin_lock持有號碼2,然后next++得3。CPU1和CPU2持有的號碼分別是1和2,而當前owner是0,所以CPU1和CPU2都要等待通知。當CPU0吃完飯出來時將owner++得1,然后通知CPU1和CPU2去檢查它們持有的號碼。由於CPU1持有的號碼是1與owner相等,所以CPU1進入臨界區,而CPU2需要繼續等到。這種ticket機制是排隊機制,保證了公平性。
typedef struct spinlock {
union {
struct raw_spinlock rlock; /* raw_spinlock出現后,統一使用raw_spinlock */
#ifdef CONFIG_DEBUG_LOCK_ALLOC /* 調試使用 */
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
struct {
u8 __padding[LOCK_PADSIZE];
struct lockdep_map dep_map;
};
#endif
};
} spinlock_t;
/* 體系結構定義文件:arch/arm/include/asm/spinlock_types.h */
typedef struct raw_spinlock {
arch_spinlock_t raw_lock; /* 根據cpu體系架構定義*/
#ifdef CONFIG_GENERIC_LOCKBREAK
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
} raw_spinlock_t;
typedef struct {
union {
u32 slock;
struct __raw_tickets {
u16 owner;
u16 next;
} tickets;
};
} arch_spinlock_t; /* arm spinlock定義 */
spinlock常用操作
spinlock初始化
spin_lock_init初始化目的是把變量清零。
/* 文件:include/linux/spinlock.h */
#define spin_lock_init(_lock) \
do { \
spinlock_check(_lock); \
raw_spin_lock_init(&(_lock)->rlock); \
} while (0)
# define raw_spin_lock_init(lock) \
do { *(lock) = __RAW_SPIN_LOCK_UNLOCKED(lock); } while (0)
/* 文件:include/linux/spinlock_types.h */
#define __RAW_SPIN_LOCK_UNLOCKED(lockname) \
(raw_spinlock_t) __RAW_SPIN_LOCK_INITIALIZER(lockname)
#define __RAW_SPIN_LOCK_INITIALIZER(lockname) \
{ \
.raw_lock = __ARCH_SPIN_LOCK_UNLOCKED, \ /* 初始化為0*/
SPIN_DEBUG_INIT(lockname) \
SPIN_DEP_MAP_INIT(lockname) }
/* 文件:arch/arm/include/asm/spinlock_types.h */
#define __ARCH_SPIN_LOCK_UNLOCKED { { 0 } }
spinlock獲取鎖
spin_lock最終調用與體系架構相關的arch_spin_lock函數,arch_spin_lock使用了arm原子操作指令ldrex和strex。
執行路徑:執行路徑:spin_lock->raw_spin_lock->_raw_spin_lock->__raw_spin_lock->do_raw_spin_lock->arch_spin_lock
/* 文件:include/linux/spinlock.h */
static __always_inline void spin_lock(spinlock_t *lock)
{
raw_spin_lock(&lock->rlock);
}
define raw_spin_lock(lock) _raw_spin_lock(lock)
/* 文件:include/linux/spinlock_api_smp.h */
#define _raw_spin_lock(lock) __raw_spin_lock(lock)
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
preempt_disable();
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}
#define LOCK_CONTENDED(_lock, try, lock) \
lock(_lock)
/* 文件:kernel/locking/spinlock_debug.c */
void do_raw_spin_lock(raw_spinlock_t *lock)
{
debug_spin_lock_before(lock);
arch_spin_lock(&lock->raw_lock);
debug_spin_lock_after(lock);
}
/* 文件:arch/arm/include/asm/spinlock.h */
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
unsigned long tmp;
u32 newval;
arch_spinlock_t lockval;
prefetchw(&lock->slock);
__asm__ __volatile__(
"1: ldrex %0, [%3]\n" /* 取lock地址內容賦值給本地變量lockval。lockval=lock->slock */
" add %1, %0, %4\n" /* 將lockval->nexet加1后保存newval */
" strex %2, %1, [%3]\n" /* 把newval存在lock中,將是否成功結果存入在tmp中 */
" teq %2, #0\n" /* 判斷上條指令是否成功,如果不成功執行”bne 1b”跳到標號1執行 */
" bne 1b"
: "=&r" (lockval), "=&r" (newval), "=&r" (tmp) /* 3個輸出變量分別對應%0,%1,%2 */
: "r" (&lock->slock), "I" (1 << TICKET_SHIFT) /* 2個輸入變量分別對應%3,%4 */
: "cc");
while (lockval.tickets.next != lockval.tickets.owner) { /* 判斷本地持有的號碼next是否與最新的owner相等 */
wfe(); /* 如果不相等進入wfe模式,而不是while(1)死循環;等待其他CPU發送sev*/
lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);
}
smp_mb();
}
spinlock釋放鎖
釋放鎖相對簡單,給鎖的owner成員加1。
/* 文件:include/linux/spinlock.h */
static __always_inline void spin_unlock(spinlock_t *lock)
{
raw_spin_unlock(&lock->rlock);
}
#define raw_spin_unlock(lock) _raw_spin_unlock(lock)
/* 文件:include/linux/spinlock_api_smp.h */
#define _raw_spin_unlock(lock) __raw_spin_unlock(lock)
static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
spin_release(&lock->dep_map, 1, _RET_IP_);
do_raw_spin_unlock(lock);
preempt_enable();
}
/* 文件:kernel/locking/spinlock_debug.c */
void do_raw_spin_unlock(raw_spinlock_t *lock)
{
debug_spin_unlock(lock);
arch_spin_unlock(&lock->raw_lock);
}
/* 文件:arch/arm/include/asm/spinlock.h */
static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
smp_mb();
lock->tickets.owner++; /* 將owner+1,然后發送sev命令*/
dsb_sev();
}
spinlock接口總結
接口 | 接口作用 |
---|---|
spin_lock_init | 動態初始化spin lock |
spin_lock | 獲取指定的鎖。如果是多核系統會本地搶占,如果是單核系統只需要禁止本地中斷即可。 |
spin_lock_irq | 獲取鎖前,禁止本地中斷 |
spin_lock_irqsave | 獲取鎖前,保存當前irq開關狀態,然后禁止本地中斷 |
spin_unlock | 釋放鎖,恢復本地搶占 |
spin_unlock_irq | 釋放鎖后,打開本地中斷 |
spin_unlock_irqstore | 釋放鎖后,恢復保存的irq開關狀態 |
讀取者/寫入者自旋鎖
內核提供自旋鎖的讀取者/寫入者形式,這種自旋鎖和前面提到的讀取者/寫入者信號量非常類似。這種鎖允許任意數量的讀取者同時進入臨界區,但寫入者必須互斥訪問。
讀取者/寫入者鎖的類型為rwlock_t,定義在<linux/spinlock.h>。相關讀寫函數有
void read_lock
void read_lock_irqsave
void read_lock_irq
void read_unlock
void read_unlock_irqrestore
void read_unlock_irq
void write_lock
void write_lock_irqsave
void write_lock_irq
void write_unlock
void write_unlock_irqrestore
void write_unlock_irq
RCU鎖
讀多寫少的一種鎖機制。RCU(Read-Copy Update),顧名思義就是讀-拷貝修改,它是基於其原理命名的。對於被RCU保護的共享數據結構,讀者不需要獲得任何鎖就可以訪問它,但寫者在訪問它時首先拷貝一個副本,然后對副本進行修改,最后使用一個回調(callback)機制在適當的時機把指向原來數據的指針重新指向新的被修改的數據。這個時機就是所有引用該數據的CPU都退出對共享數據的操作。
它很少應用在驅動程序中,可考慮應用在網絡路由表。
seqlock
seqlock提供對共享資源的快速、免鎖訪問。當想要保護的資源很小、很簡單、會頻繁訪問但是很少修改且很修改快速時,就可以使用seqlock。從本質上來說,seqlock會允許讀取者對資源的自由訪問,但需要讀取者檢查是否和寫入者發生沖突,如果沖突時,就需要讀取者重新對資源的訪問。
seqlock_init(seqlock_t *lock);
read_seqlock(seqlock_t *lock);
write_seqlock(seqlock_t *lock);
原子變量
有時,共享的資源可能只是一個簡單的整數值,完整的鎖機制對一個簡單的整數來講顯得有些浪費。針對這種情況,內核提供了一種原子的整數類型,稱為atomic_t,定義在<asm/atomic.h>。一個atomic_t變量在所有內核支持的架構上保存一個int值。針對這種整型的操作是非常快的,可能會編譯成單個指令。
int atomic_read(atomic_t *v) // 返回當前值
void atomic_add(int i, atomic_t *v) // 將i增加到v中
void atomic_sub(int i, atomic_t *v)
void atomic_inc(atomic_t *v) // 增加或減一
void atomic_dec(atomic_t *v)
位操作
atomic_t類型對執行整數算術比較有用,但是對於操作單個的位時,這種類型就派不上用場。
void set_bit(nr, void *addr);
void clear_bit(nr, void *addr);
鎖陷阱
恰當的鎖定模式需要清晰和明確的規則。
- 在編寫代碼時肯定會遇到幾個函數均需要訪問某個相同的鎖。如果某個獲得鎖的函數要調用其他同樣試圖獲取這個鎖的函數,那么就會死鎖。不論是信號量還是自旋鎖,都不允許擁有者第二次獲得這個鎖。
- 鎖的順序規則。使用大量鎖的系統中,代碼通常需要一次擁有多個鎖,這時需要謹慎處理。加入有lock1和lock2。想象某個任務鎖定了lock1,其他任務獲得了lock2。這時每個任務都試圖獲取另外那個鎖。於是兩個任務都將死鎖。因此需要規定獲取鎖的順序。信號量與自旋鎖的組合時,優先獲取信號量,再獲取鎖。局部鎖以及靠近內核的鎖,優先獲取局部鎖。
細粒度鎖和粗粒度鎖的對比
第一個支持多處理器系統的Linux內核是2.0,其中有且只有一個鎖。這個大的內核鎖會讓內核進入一個大的臨界區,只有一個CPU可以在任意給定時間執行內核代碼。這種機制不具有良好的伸縮性,發揮不了多核系統的優勢。
在內核2.2版本開始支持子系統級別的鎖,現代內核可包含數千個鎖。細粒度鎖本身有其成本,面臨數千個鎖,需要知道有哪些鎖以及鎖定的順序,增加了操作鎖的復雜性,對內核的可維護性產生很大的副作用。
參考文章
arm ldrex和strex指令:https://blog.csdn.net/Roland_Sun/article/details/47670099
嵌入式匯編符號含義:https://www.dazhuanlan.com/2019/10/04/5d962dde119c5/
為什么禁止搶占:https://blog.csdn.net/kasalyn/article/details/11473885
spinlock_t raw_spinlock區別:https://www.cnblogs.com/hadis-yuki/p/5540046.html
同步和互斥:https://blog.csdn.net/daaikuaichuan/article/details/82950711