共計 9440 個字符,預計需要花費 24 分鐘才能閱讀完成。
自動寫代碼機器人,免費開通
這篇文章給大家分享的是有關 Linux 中自旋鎖 Spinlock 怎么把 Ubuntu 弄死鎖的內容。丸趣 TV 小編覺得挺實用的,因此分享給大家做個參考,一起跟隨丸趣 TV 小編過來看看吧。
背景
由于在多處理器環境中某些資源的有限性,有時需要互斥訪問(mutual exclusion),這時候就需要引入鎖的概念,只有獲取了鎖的任務才能夠對資源進行訪問,由于多線程的核心是 CPU 的時間分片,所以同一時刻只能有一個任務獲取到鎖。
內核當發生訪問資源沖突的時候,通常有兩種處理方式:
一個是原地等待
一個是掛起當前進程,調度其他進程執行(睡眠)
自旋鎖
Spinlock 是內核中提供的一種比較常見的鎖機制,自旋鎖是“原地等待”的方式解決資源沖突的。即,一個線程獲取了一個自旋鎖后,另外一個線程期望獲取該自旋鎖,獲取不到,只能夠原地“打轉”(忙等待)。
由于自旋鎖的這個忙等待的特性,注定了它使用場景上的限制 mdash; mdash; 自旋鎖不應該被長時間的持有(消耗 CPU 資源)。
自旋鎖的優點
自旋鎖不會使線程狀態發生切換,一直處于用戶態,即線程一直都是 active 的; 不會使線程進入阻塞狀態,減少了不必要的上下文切換,執行速度快。
非自旋鎖在獲取不到鎖的時候會進入阻塞狀態,從而進入內核態,當獲取到鎖的時候需要從內核態恢復,需要線程上下文切換。(線程被阻塞后便進入內核 (Linux) 調度狀態,這個會導致系統在用戶態與內核態之間來回切換,嚴重影響鎖的性能)。
自旋鎖的使用
在 linux kernel 的實現中,經常會遇到這樣的場景:共享數據被中斷上下文和進程上下文訪問,該如何保護呢?
如果只有進程上下文的訪問,那么可以考慮使用 semaphore 或者 mutex 的鎖機制,但是現在中斷上下文也摻和進來,那些可以導致睡眠的 lock 就不能使用了,這時候,可以考慮使用 spin lock。
在中斷上下文,是不允許睡眠的,所以,這里需要的是一個不會導致睡眠的鎖 mdash; mdash;spinlock。
換言之,中斷上下文要用鎖,首選 spinlock。
使用自旋鎖,有兩種方式定義一個鎖:
動態的:
spinlock_t lock; spin_lock_init (lock);
靜態的:
DEFINE_SPINLOCK(lock);
使用步驟
spinlock 的使用很簡單:
我們要訪問臨界資源需要首先申請自旋鎖;
獲取不到鎖就自旋,如果能獲得鎖就進入臨界區;
當自旋鎖釋放后,自旋在這個鎖的任務即可獲得鎖并進入臨界區,退出臨界區的任務必須釋放自旋鎖。
使用實例
static spinlock_t lock; static int flage = 1; spin_lock_init(lock); static int hello_open (struct inode *inode, struct file *filep) { spin_lock( lock); if(flage !=1) { spin_unlock( lock); return -EBUSY; } flage =0; spin_unlock(lock); return 0; } static int hello_release (struct inode *inode, struct file *filep) { flage = 1; return 0; }
補充
中斷上下文不能睡眠的原因是:
1. 中斷處理的時候, 不應該發生進程切換,因為在中斷 context 中,唯一能打斷當前中斷 handler 的只有更高優先級的中斷,它不會被進程打斷,如果在 中斷 context 中休眠,則沒有辦法喚醒它,因為所有的 wake_up_xxx 都是針對某個進程而言的,而在中斷 context 中,沒有進程的概念,沒 有一個 task_struct(這點對于 softirq 和 tasklet 一樣),因此真的休眠了,比如調用了會導致 block 的例程,內核幾乎肯定會死。
2.schedule()在切換進程時,保存當前的進程上下文(CPU 寄存器的值、進程的狀態以及堆棧中的內容),以便以后恢復此進程運行。中斷發生后,內核會先保存當前被中斷的進程上下文(在調用中斷處理程序后恢復);
但在中斷處理程序里,CPU 寄存器的值肯定已經變化了吧(最重要的程序計數器 PC、堆棧 SP 等),如果此時因為睡眠或阻塞操作調用了 schedule(),則保存的進程上下文就不是當前的進程 context 了. 所以不可以在中斷處理程序中調用 schedule()。
3. 內核中 schedule()函數本身在進來的時候判斷是否處于中斷上下文:
if(unlikely(in_interrupt())) BUG();
因此,強行調用 schedule()的結果就是內核 BUG。
4. 中斷 handler 會使用被中斷的進程內核堆棧,但不會對它有任何影響,因為 handler 使用完后會完全清除它使用的那部分堆棧,恢復被中斷前的原貌。
5. 處于中斷 context 時候,內核是不可搶占的。因此,如果休眠,則內核一定掛起。
自旋鎖的死鎖
自旋鎖不可遞歸,自己等待自己已經獲取的鎖,會導致死鎖。
自旋鎖可以在中斷上下文中使用,但是試想一個場景:一個線程獲取了一個鎖,但是被中斷處理程序打斷,中斷處理程序也獲取了這個鎖(但是之前已經被鎖住了,無法獲取到,只能自旋),中斷無法退出,導致線程中后面釋放鎖的代碼無法被執行,導致死鎖。(如果確認中斷中不會訪問和線程中同一個鎖,其實無所謂)。
一、考慮下面的場景(內核搶占場景):
(1)進程 A 在某個系統調用過程中訪問了共享資源 R
(2)進程 B 在某個系統調用過程中也訪問了共享資源 R
會不會造成沖突呢?
假設在 A 訪問共享資源 R 的過程中發生了中斷,中斷喚醒了沉睡中的,優先級更高的 B,在中斷返回現場的時候,發生進程切換,B 啟動執行,并通過系統調用訪問了 R,如果沒有鎖保護,則會出現兩個 thread 進入臨界區,導致程序執行不正確。OK,我們加上 spin lock 看看如何:A 在進入臨界區之前獲取了 spin lock,同樣的,在 A 訪問共享資源 R 的過程中發生了中斷,中斷喚醒了沉睡中的,優先級更高的 B,B 在訪問臨界區之前仍然會試圖獲取 spin lock,這時候由于 A 進程持有 spin lock 而導致 B 進程進入了永久的 spin hellip; hellip; 怎么破?linux 的 kernel 很簡單,在 A 進程獲取 spin lock 的時候,禁止本 CPU 上的搶占(上面的永久 spin 的場合僅僅在本 CPU 的進程搶占本 CPU 的當前進程這樣的場景中發生)。如果 A 和 B 運行在不同的 CPU 上,那么情況會簡單一些:A 進程雖然持有 spin lock 而導致 B 進程進入 spin 狀態,不過由于運行在不同的 CPU 上,A 進程會持續執行并會很快釋放 spin lock,解除 B 進程的 spin 狀態。
二、再考慮下面的場景(中斷上下文場景):
運行在 CPU0 上的進程 A 在某個系統調用過程中訪問了共享資源 R
運行在 CPU1 上的進程 B 在某個系統調用過程中也訪問了共享資源 R
外設 P 的中斷 handler 中也會訪問共享資源 R
在這樣的場景下,使用 spin lock 可以保護訪問共享資源 R 的臨界區嗎?
我們假設 CPU0 上的進程 A 持有 spin lock 進入臨界區,這時候,外設 P 發生了中斷事件,并且調度到了 CPU1 上執行,看起來沒有什么問題,執行在 CPU1 上的 handler 會稍微等待一會 CPU0 上的進程 A,等它立刻臨界區就會釋放 spin lock 的,但是,如果外設 P 的中斷事件被調度到了 CPU0 上執行會怎么樣?CPU0 上的進程 A 在持有 spin lock 的狀態下被中斷上下文搶占,而搶占它的 CPU0 上的 handler 在進入臨界區之前仍然會試圖獲取 spin lock,悲劇發生了,CPU0 上的 P 外設的中斷 handler 永遠的進入 spin 狀態,這時候,CPU1 上的進程 B 也不可避免在試圖持有 spin lock 的時候失敗而導致進入 spin 狀態。為了解決這樣的問題,linux kernel 采用了這樣的辦法:如果涉及到中斷上下文的訪問,spin lock 需要和禁止本 CPU 上的中斷聯合使用。
三、再考慮下面的場景(底半部場景)
linux kernel 中提供了豐富的 bottom half 的機制,雖然同屬中斷上下文,不過還是稍有不同。我們可以把上面的場景簡單修改一下:外設 P 不是中斷 handler 中訪問共享資源 R,而是在的 bottom half 中訪問。使用 spin lock+ 禁止本地中斷當然是可以達到保護共享資源的效果,但是使用牛刀來殺雞似乎有點小題大做,這時候 disable bottom half 就可以了。
四、中斷上下文之間的競爭
同一種中斷 handler 之間在 uni core 和 multi core 上都不會并行執行,這是 linux kernel 的特性。如果不同中斷 handler 需要使用 spin lock 保護共享資源,對于新的內核(不區分 fast handler 和 slow handler),所有 handler 都是關閉中斷的,因此使用 spin lock 不需要關閉中斷的配合。bottom half 又分成 softirq 和 tasklet,同一種 softirq 會在不同的 CPU 上并發執行,因此如果某個驅動中的 softirq 的 handler 中會訪問某個全局變量,對該全局變量是需要使用 spin lock 保護的,不用配合 disable CPU 中斷或者 bottom half。tasklet 更簡單,因為同一種 tasklet 不會多個 CPU 上并發。
自旋鎖的實現原理
數據結構
首先定義一個 spinlock_t 的數據類型,其本質上是一個整數值(對該數值的操作需要保證原子性),該數值表示 spin lock 是否可用。初始化的時候被設定為 1。當 thread 想要持有鎖的時候調用 spin_lock 函數,該函數將 spin lock 那個整數值減去 1,然后進行判斷,如果等于 0,表示可以獲取 spin lock,如果是負數,則說明其他 thread 的持有該鎖,本 thread 需要 spin。
內核中的 spinlock_t 的數據類型定義如下:
typedef struct spinlock { struct raw_spinlock rlock; } spinlock_t; typedef struct raw_spinlock { arch_spinlock_t raw_lock; } raw_spinlock_t;
通用 (適用于各種 arch) 的 spin lock 使用 spinlock_t 這樣的 type name,各種 arch 定義自己的 struct raw_spinlock。聽起來不錯的主意和命名方式,直到 linux realtime tree(PREEMPT_RT)提出對 spinlock 的挑戰。
spin lock 的命名規范定義如下:
鴻蒙官方戰略合作共建——HarmonyOS 技術社區
spinlock,在 rt linux(配置了 PREEMPT_RT)的時候可能會被搶占 (實際底層可能是使用支持 PI(優先級翻轉) 的 mutext)。
raw_spinlock,即便是配置了 PREEMPT_RT 也要頑強的 spin
arch_spinlock,spin lock 是和 architecture 相關的,
ARM 結構體系 arch_spin_lock 接口實現
加鎖
同樣的,這里也只是選擇一個典型的 API 來分析,其他的大家可以自行學習。我們選擇的是 arch_spin_lock,其 ARM32 的代碼如下:
static inline void arch_spin_lock(arch_spinlock_t *lock) { unsigned long tmp; u32 newval; arch_spinlock_t lockval; prefetchw( lock- slock);---------(0) __asm__ __volatile__( 1: ldrex %0, [%3]\n ---------(1) add %1, %0, %4\n ----------(2) strex %2, %1,[%3]\n ---------(3) teq %2, #0\n -------------(4) bne 1b : = r (lockval), = r (newval), = r (tmp) : r (lock- slock), I (1 TICKET_SHIFT) : cc while (lockval.tickets.next != lockval.tickets.owner) {----(5) wfe();------------(6) lockval.tickets.owner = ACCESS_ONCE(lock- tickets.owner);----(7) } smp_mb();-----------(8) }
(0)和 preloading cache 相關的操作,主要是為了性能考慮 (1)lockval = lock- slock (如果 lock- slock 沒有被其他處理器獨占,則標記當前執行處理器對 lock- slock 地址的獨占訪問;否則不影響) (2)newval = lockval + (1 TICKET_SHIFT) (3)strex tmp, newval, [lock- slock] (如果當前執行處理器沒有獨占 lock- slock 地址的訪問,不進行存儲,返回 1 給 temp;如果當前處理器已經獨占 lock- slock 內存訪問,則對內存進行寫,返回 0 給 temp,清除獨占標記) lock- tickets.next = lock- tickets.next + 1 (4)檢查是否寫入成功 lockval.tickets.next (5)初始化時 lock- tickets.owner、lock- tickets.next 都為 0,假設第一次執行 arch_spin_lock,lockval = *lock,lock- tickets.next++,lockval.tickets.next 等于 lockval.tickets.owner,獲取到自旋鎖;自旋鎖未釋放,第二次執行的時候,lock- tickets.owner = 0, lock- tickets.next = 1,拷貝到 lockval 后,lockval.tickets.next != lockval.tickets.owner,會執行 wfe 等待被自旋鎖釋放被喚醒,自旋鎖釋放時會執行 lock- tickets.owner++,lockval.tickets.owner 重新賦值 (6)暫時中斷掛起執行。如果當前 spin lock 的狀態是 locked,那么調用 wfe 進入等待狀態。更具體的細節請參考 ARM WFI 和 WFE 指令中的描述。 (7)其他的 CPU 喚醒了本 cpu 的執行,說明 owner 發生了變化,該新的 own 賦給 lockval,然后繼續判斷 spin lock 的狀態,也就是回到 step 5。 (8)memory barrier 的操作,具體可以參考 memory barrier 中的描述。
釋放鎖
static inline void arch_spin_unlock(arch_spinlock_t *lock) { smp_mb(); lock- tickets.owner++; ---------------------- (0) dsb_sev(); ---------------------------------- (1) }
(0)lock- tickets.owner 增加 1,下一個被喚醒的處理器會檢查該值是否與自己的 lockval.tickets.next 相等,lock- tickets.owner 代表可以獲取的自旋鎖的處理器,lock- tickets.next 你一個可以獲取的自旋鎖的 owner; 處理器獲取自旋鎖時,會先讀取 lock- tickets.next 用于與 lock- tickets.owner 比較并且對 lock- tickets.next 加 1,下一個處理器獲取到的 lock- tickets.next 就與當前處理器不一致了,兩個處理器都與 lock- tickets.owner 比較,肯定只有一個處理器會相等,自旋鎖釋放時時對 lock- tickets.owner 加 1 計算,因此,先申請自旋鎖多處理器 lock- tickets.next 值更新,自然先獲取到自旋鎖
(1)執行 sev 指令,喚醒 wfe 等待的處理器
自旋鎖導致死鎖實例
死鎖的 2 種情況
1)擁有自旋鎖的進程 A 在內核態阻塞了,內核調度 B 進程,碰巧 B 進程也要獲得自旋鎖,此時 B 只能自旋轉。而此時搶占已經關閉,不會調度 A 進程了,B 永遠自旋,產生死鎖。
2)進程 A 擁有自旋鎖,中斷到來,CPU 執行中斷函數,中斷處理函數,中斷處理函數需要獲得自旋鎖,訪問共享資源,此時無法獲得鎖,只能自旋,產生死鎖。
如何避免死鎖
鴻蒙官方戰略合作共建——HarmonyOS 技術社區
如果中斷處理函數中也要獲得自旋鎖,那么驅動程序需要在擁有自旋鎖時禁止中斷;
自旋鎖必須在可能的最短時間內擁有;
避免某個獲得鎖的函數調用其他同樣試圖獲取這個鎖的函數,否則代碼就會死鎖; 不論是信號量還是自旋鎖,都不允許鎖擁有者第二次獲得這個鎖,如果試圖這么做,系統將掛起;
鎖的順序規則 按同樣的順序獲得鎖; 如果必須獲得一個局部鎖和一個屬于內核更中心位置的鎖,則應該首先獲取自己的局部鎖 ; 如果我們擁有信號量和自旋鎖的組合,則必須首先獲得信號量; 在擁有自旋鎖時調用 down(可導致休眠)是個嚴重的錯誤的。
死鎖舉例
因為自旋鎖持有時間非常短,沒有直觀的現象,下面舉一個會導致死鎖的實例。
運行條件
虛擬機:vmware
OS:Ubuntu 14
配置:將虛擬機的處理個數設置為 1,否則不會死鎖
原理
針對單 CPU,擁有自旋鎖的任務不應該調度會引起休眠的函數,否則會導致死鎖。
步驟:
鴻蒙官方戰略合作共建——HarmonyOS 技術社區
進程 A 在 open()字符設備后,對應的內核函數會申請自旋鎖,此時自旋鎖空閑,申請到自旋鎖,進程 A 隨即進入執行 sleep()函數進入休眠;
在進程 A 處于 sleep 期間,自旋鎖一直屬于進程 A 所有;
運行進程 B,進程 B 執行 open 函數,對應的內核函數也會申請自旋鎖,此時自旋鎖歸進程 A 所有,所以進程 B 進入自旋狀態;
因為此時搶占已經關閉,系統死鎖。
驅動代碼如下:
#include linux/init.h #include linux/module.h #include linux/kdev_t.h #include linux/fs.h #include linux/cdev.h #include linux/device.h #include linux/spinlock.h static int major = 250; static int minor = 0; static dev_t devno; static struct cdev cdev; static struct class *cls; static struct device *test_device; static spinlock_t lock; static int flage = 1; #define DEAD 1 static int hello_open (struct inode *inode, struct file *filep) { spin_lock( lock); if(flage !=1) { spin_unlock( lock); return -EBUSY; } flage =0; #if DEAD #elif spin_unlock(lock); #endif return 0; } static int hello_release (struct inode *inode, struct file *filep) { flage = 1; #if DEAD spin_unlock( lock); #endif return 0; } static struct file_operations hello_ops = { .open = hello_open, .release = hello_release, }; static int hello_init(void) { int result; int error; printk( hello_init \n result = register_chrdev( major, hello , hello_ops); if(result 0) { printk( register_chrdev fail \n return result; } devno = MKDEV(major,minor); cls = class_create(THIS_MODULE, helloclass if(IS_ERR(cls)) { unregister_chrdev(major, hello return result; } test_device = device_create(cls,NULL,devno,NULL, test if(IS_ERR(test_device )) { class_destroy(cls); unregister_chrdev(major, hello return result; } spin_lock_init(lock); return 0; } static void hello_exit(void) { printk( hello_exit \n device_destroy(cls,devno); class_destroy(cls); unregister_chrdev(major, hello return; } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL
測試程序如下:
#include stdio.h #include sys/types.h #include sys/stat.h #include fcntl.h main() { int fd; fd = open( /dev/test ,O_RDWR); if(fd 0) { perror( open fail \n return; } sleep(20); close(fd); printf(open ok \n }
測試步驟:
編譯加載內核
make insmod hello.ko
運行進程 A
gcc test.c -o a ./a
打開一個新的終端,運行進程 B
gcc test.c -o b ./b
感謝各位的閱讀!關于“Linux 中自旋鎖 Spinlock 怎么把 Ubuntu 弄死鎖”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!
向 AI 問一下細節
丸趣 TV 網 – 提供最優質的資源集合!