linuxspin
『壹』 linux里的搶占-preempt
1. 什麼是搶占?
搶占就是進城切換, 以thread_info->preempt_count標識。
thread_info->preempt_count一物多用:
bit0-7代表的是搶占的次數,最大搶占深度為256次,
bit8-15代表的是軟中斷的次數,最大也是256次,
bit16-19表示中斷的次數,注釋的大概意思是避免中斷嵌套,但是也不能防止某些驅動中斷嵌套使用中斷,所以嵌套16層也是最大次數了。
bit20~23代表的NMI中斷
2.搶占的函數:
spin_lock()/spin_unlock()
disable_preempt()/enable_preempt()--禁止或使能內核搶占,調用下面的inc_preempt_count()/dec_preempt_count(),加了memory barrier。
inc_preempt_count()/dec_preempt_count()
get_cpu()/put_cpu()
3.調度點
a) 進程被阻塞時
b) 調整參數時,比如通過sched_setscheler() ,nice()等函數調整進程的調度策略,靜態優先順序時
c) 睡眠進程被喚醒時,比如wake_up喚醒等待隊列中的進程時,如果該進程具有更高優先順序則會設置當前
進程TIF_NEED_RESCHED,如果允許內核態搶占,則會調度一次
d)中斷處理完時,如果中斷處理過程中設置了TIF_NEED_SCHED標志,中斷返回時,不論是要返回內核態還是用戶態,都會發生一次搶占.當然,在這也會檢查有沒有軟中斷需要處理。
e)執行了preempt_enable()函數。
『貳』 linux內核同步問題
Linux內核設計與實現 十、內核同步方法
手把手教Linux驅動5-自旋鎖、信號量、互斥體概述
== 基礎概念: ==
並發 :多個執行單元同時進行或多個執行單元微觀串列執行,宏觀並行執行
競態 :並發的執行單元對共享資源(硬體資源和軟體上的全局變數)的訪問而導致的竟態狀態。
臨界資源 :多個進程訪問的資源
臨界區 :多個進程訪問的代碼段
== 並發場合: ==
1、單CPU之間進程間的並發 :時間片輪轉,調度進程。 A進程訪問列印機,時間片用完,OS調度B進程訪問列印機。
2、單cpu上進程和中斷之間並發 :CPU必須停止當前進程的執行中斷;
3、多cpu之間
4、單CPU上中斷之間的並發
== 使用偏向: ==
==信號量用於進程之間的同步,進程在信號量保護的臨界區代碼裡面是可以睡眠的(需要進行進程調度),這是與自旋鎖最大的區別。==
信號量又稱為信號燈,它是用來協調不同進程間的數據對象的,而最主要的應用是共享內存方式的進程間通信。本質上,信號量是一個計數器,它用來記錄對某個資源(如共享內存)的存取狀況。它負責協調各個進程,以保證他們能夠正確、合理的使用公共資源。它和spin lock最大的不同之處就是:無法獲取信號量的進程可以睡眠,因此會導致系統調度。
1、==用於進程與進程之間的同步==
2、==允許多個進程進入臨界區代碼執行,臨界區代碼允許睡眠;==
3、信號量本質是==基於調度器的==,在UP和SMP下沒有區別;進程獲取不到信號量將陷入休眠,並讓出CPU;
4、不支持進程和中斷之間的同步
5、==進程調度也是會消耗系統資源的,如果一個int型共享變數就需要使用信號量,將極大的浪費系統資源==
6、信號量可以用於多個線程,用於資源的計數(有多種狀態)
==信號量加鎖以及解鎖過程:==
sema_init(&sp->dead_sem, 0); / 初始化 /
down(&sema);
臨界區代碼
up(&sema);
==信號量定義:==
==信號量初始化:==
==dowm函數實現:==
==up函數實現:==
信號量一般可以用來標記可用資源的個數。
舉2個生活中的例子:
==dowm函數實現原理解析:==
(1)down
判斷sem->count是否 > 0,大於0則說明系統資源夠用,分配一個給該進程,否則進入__down(sem);
(2)__down
調用__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);其中TASK_UNINTERRUPTIBLE=2代表進入睡眠,且不可以打斷;MAX_SCHEDULE_TIMEOUT休眠最長LONG_MAX時間;
(3)list_add_tail(&waiter.list, &sem->wait_list);
把當前進程加入到sem->wait_list中;
(3)先解鎖後加鎖;
進入__down_common前已經加鎖了,先把解鎖,調用schele_timeout(timeout),當waiter.up=1後跳出for循環;退出函數之前再加鎖;
Linux內核ARM構架中原子變數的底層實現研究
rk3288 原子操作和原子位操作
原子變數適用於只共享一個int型變數;
1、原子操作是指不被打斷的操作,即它是最小的執行單位。
2、最簡單的原子操作就是一條條的匯編指令(不包括一些偽指令,偽指令會被匯編器解釋成多條匯編指令)
==常見函數:==
==以atomic_inc為例介紹實現過程==
在Linux內核文件archarmincludeasmatomic.h中。 執行atomic_read、atomic_set這些操作都只需要一條匯編指令,所以它們本身就是不可打斷的。 需要特別研究的是atomic_inc、atomic_dec這類讀出、修改、寫回的函數。
所以atomic_add的原型是下面這個宏:
atomic_add等效於:
result(%0) tmp(%1) (v->counter)(%2) (&v->counter)(%3) i(%4)
注意:根據內聯匯編的語法,result、tmp、&v->counter對應的數據都放在了寄存器中操作。如果出現上下文切換,切換機制會做寄存器上下文保護。
(1)ldrex %0, [%3]
意思是將&v->counter指向的數據放入result中,並且(分別在Local monitor和Global monitor中)設置獨占標志。
(2)add %0, %0, %4
result = result + i
(3)strex %1, %0, [%3]
意思是將result保存到&v->counter指向的內存中, 此時 Exclusive monitors會發揮作用,將保存是否成功的標志放入tmp中。
(4) teq %1, #0
測試strex是否成功(tmp == 0 ??)
(5)bne 1b
如果發現strex失敗,從(1)再次執行。
Spinlock 是內核中提供的一種比較常見的鎖機制,==自旋鎖是「原地等待」的方式解決資源沖突的==,即,一個線程獲取了一個自旋鎖後,另外一個線程期望獲取該自旋鎖,獲取不到,只能夠原地「打轉」(忙等待)。由於自旋鎖的這個忙等待的特性,註定了它使用場景上的限制 —— 自旋鎖不應該被長時間的持有(消耗 CPU 資源),一般應用在==中斷上下文==。
1、spinlock是一種死等機制
2、信號量可以允許多個執行單元進入,spinlock不行,一次只能允許一個執行單元獲取鎖,並且進入臨界區,其他執行單元都是在門口不斷的死等
3、由於不休眠,因此spinlock可以應用在中斷上下文中;
4、由於spinlock死等的特性,因此臨界區執行代碼盡可能的短;
==spinlock加鎖以及解鎖過程:==
spin_lock(&devices_lock);
臨界區代碼
spin_unlock(&devices_lock);
==spinlock初始化==
==進程和進程之間同步==
==本地軟中斷之間同步==
==本地硬中斷之間同步==
==本地硬中斷之間同步並且保存本地中斷狀態==
==嘗試獲取鎖==
== arch_spinlock_t結構體定義如下: ==
== arch_spin_lock的實現如下: ==
lockval(%0) newval(%1) tmp(%2) &lock->slock(%3) 1 << TICKET_SHIFT(%4)
(1)ldrex %0, [%3]
把lock->slock的值賦值給lockval;並且(分別在Local monitor和Global monitor中)設置獨占標志。
(2)add %1, %0, %4
newval =lockval +(1<<16); 相當於next+1;
(3)strex %2, %1, [%3]
newval =lockval +(1<<16); 相當於next+1;
意思是將newval保存到 &lock->slock指向的內存中, 此時 Exclusive monitors會發揮作用,將保存是否成功的標志放入tmp中。
(4) teq %2, #0
測試strex是否成功
(5)bne 1b
如果發現strex失敗,從(1)再次執行。
通過上面的分析,可知關鍵在於strex的操作是否成功的判斷上。而這個就歸功於ARM的Exclusive monitors和ldrex/strex指令的機制。
(6)while (lockval.tickets.next != lockval.tickets.owner)
如何lockval.tickets的next和owner是否相等。相同則跳出while循環,否則在循環內等待判斷;
* (7)wfe()和smp_mb() 最終調用#define barrier() asm volatile ("": : :"memory") *
阻止編譯器重排,保證編譯程序時在優化屏障之前的指令不會在優化屏障之後執行。
== arch_spin_unlock的實現如下: ==
退出鎖時:tickets.owner++
== 出現死鎖的情況: ==
1、擁有自旋鎖的進程A在內核態阻塞了,內核調度B進程,碰巧B進程也要獲得自旋鎖,此時B只能自旋轉。 而此時搶占已經關閉,(單核)不會調度A進程了,B永遠自旋,產生死鎖。
2、進程A擁有自旋鎖,中斷到來,CPU執行中斷函數,中斷處理函數,中斷處理函數需要獲得自旋鎖,訪問共享資源,此時無法獲得鎖,只能自旋,產生死鎖。
== 如何避免死鎖: ==
1、如果中斷處理函數中也要獲得自旋鎖,那麼驅動程序需要在擁有自旋鎖時禁止中斷;
2、自旋鎖必須在可能的最短時間內擁有
3、避免某個獲得鎖的函數調用其他同樣試圖獲取這個鎖的函數,否則代碼就會死鎖;不論是信號量還是自旋鎖,都不允許鎖擁有者第二次獲得這個鎖,如果試圖這么做,系統將掛起;
4、鎖的順序規則(a) 按同樣的順序獲得鎖;b) 如果必須獲得一個局部鎖和一個屬於內核更中心位置的鎖,則應該首先獲取自己的局部鎖 ;c) 如果我們擁有信號量和自旋鎖的組合,則必須首先獲得信號量;在擁有自旋鎖時調用down(可導致休眠)是個嚴重的錯誤的;)
== rw(read/write)spinlock: ==
加鎖邏輯:
1、假設臨界區內沒有任何的thread,這個時候任何的讀線程和寫線程都可以鍵入
2、假設臨界區內有一個讀線程,這時候信賴的read線程可以任意進入,但是寫線程不能進入;
3、假設臨界區有一個寫線程,這時候任何的讀、寫線程都不可以進入;
4、假設臨界區內有一個或者多個讀線程,寫線程不可以進入臨界區,但是寫線程也無法阻止後續的讀線程繼續進去,要等到臨界區所有的讀線程都結束了,才可以進入,可見:==rw(read/write)spinlock更加有利於讀線程;==
== seqlock(順序鎖): ==
加鎖邏輯:
1、假設臨界區內沒有任何的thread,這個時候任何的讀線程和寫線程都可以鍵入
2、假設臨界區內沒有寫線程的情況下,read線程可以任意進入;
3、假設臨界區有一個寫線程,這時候任何的讀、寫線程都不可以進入;
4、假設臨界區內只有read線程的情況下,寫線程可以理解執行,不會等待,可見:==seqlock(順序鎖)更加有利於寫線程;==
讀寫速度 : CPU > 一級緩存 > 二級緩存 > 內存 ,因此某一個CPU0的lock修改了,其他的CPU的lock就會失效;那麼其他CPU就會依次去L1 L2和主存中讀取lock值,一旦其他CPU去讀取了主存,就存在系統性能降低的風險;
mutex用於互斥操作。
互斥體只能用於一個線程,資源只有兩種狀態(佔用或者空閑)
1、mutex的語義相對於信號量要簡單輕便一些,在鎖爭用激烈的測試場景下,mutex比信號量執行速度更快,可擴展
性更好,
2、另外mutex數據結構的定義比信號量小;、
3、同一時刻只有一個線程可以持有mutex
4、不允許遞歸地加鎖和解鎖
5、當進程持有mutex時,進程不可以退出。
• mutex必須使用官方API來初始化。
• mutex可以睡眠,所以不允許在中斷處理程序或者中斷下半部中使用,例如tasklet、定時器等
==常見操作:==
struct mutex mutex_1;
mutex_init(&mutex_1);
mutex_lock(&mutex_1)
臨界區代碼;
mutex_unlock(&mutex_1)
==常見函數:==
=
『叄』 Linux下各種鎖的理解和使用及總結解決epoll驚群問題(面試常考)-
鎖出現的原因
臨界資源是什麼: 多線程執行流所共享的資源
鎖的作用是什麼, 可以做原子操作, 在多線程中針對臨界資源的互斥訪問... 保證一個時刻只有一個線程可以持有鎖對於臨界資源做修改操作...
任何一個線程如果需要修改,向臨界資源做寫入操作都必須持有鎖,沒有持有鎖就不能對於臨界資源做寫入操作.
鎖 : 保證同一時刻只能有一個線程對於臨界資源做寫入操作 (鎖地功能)
再一個直觀地代碼引出問題,再從指令集的角度去看問題
上述一個及其奇怪的結果,這個結果每一次運行都可能是不一樣的,Why ? 按照我們本來的想法是每一個線程 + 20000000 結果肯定應該是60000000呀,可以就是達不到這個值
為何? (深入匯編指令來看) 一定將過程放置到匯編指令上去看就可以理解這個過程了.
a++; 或者 a += 1; 這些操作的匯編操作是幾個步驟?
其實是三個步驟:
正常情況下,數據少,操作的線程少,問題倒是不大,想一想要是這樣的情況下,操作次數大,對齊操作的線程多,有些線程從中間切入進來了,在運算之後還沒寫回內存就另外一個線程切入進來同時對於之前的數據進行++ 再寫回內存, 啥效果,多次++ 操作之後結果確實一次加加操作後的結果。 這樣的操作 (術語叫做函數的重入) 我覺得其實就是重入到了匯編指令中間了,還沒將上一次運算的結果寫回內存就重新對這個內存讀取再運算寫入,結果肯定和正常的邏輯後的結果不一樣呀
來一幅圖片解釋一下
咋辦? 其實問題很清楚,我們只需要處理的是多條匯編指令不能讓它中間被插入其他的線程運算. (要想自己在執行匯編指令的時候別人不插入進來) 將多條匯編指令綁定成為一條指令不就OK了嘛。
也就是原子操作!!!
不會原子操作?操作系統給咱提供了線程的 綁定方式工具呀:mutex 互斥鎖(互斥量), 自旋鎖(spinlock), 讀寫鎖(readers-writer lock) 他們也稱作悲觀鎖. 作用都是一個樣,將多個匯編指令鎖成為一條原子操作 (此處的匯編指令也相當於如下的臨界資源)
悲觀鎖:鎖如其名,每次都悲觀地認為其他線程也會來修改數據,進行寫入操作,所以會在取數據前先加鎖保護,當其他線程想要訪問數據時,被阻塞掛起
樂觀鎖:每次取數據的時候,總是樂觀地認為數據不會被其他線程修改,因此不上鎖。但是在更新數據前, 會判斷其他數據在更新前有沒有對數據進行修改。
互斥鎖
最為常見使用地鎖就是互斥鎖, 也稱互斥量. mutex
特徵,當其他線程持有互斥鎖對臨界資源做寫入操作地時候,當前線程只能掛起等待,讓出CPU,存在線程間切換工作
解釋一下存在線程間切換工作 : 當線程試圖去獲取鎖對臨界資源做寫入操作時候,如果鎖被別的線程正在持有,該線程會保存上下文直接掛起,讓出CPU,等到鎖被釋放出來再進行線程間切換,從新持有CPU執行寫入操作
互斥鎖需要進行線程間切換,相比自旋鎖而言性能會差上許多,因為自旋鎖不會讓出CPU, 也就不需要進行線程間切換的步驟,具體原理下一點詳述
加互斥量(互斥鎖)確實可以達到要求,但是會發現運行時間非常的長,因為線程間不斷地切換也需要時間, 線程間切換的代價比較大.
相關視頻推薦
你繞不開的組件—鎖,4個方面手撕鎖的多種實現
「驚群」原理、鎖的設計方案及繞不開的「死鎖」問題
學習地址:C/C++Linux伺服器開發/後台架構師【零聲教育】-學習視頻教程-騰訊課堂
需要C/C++ Linux伺服器架構師學習資料加qun812855908獲取(資料包括 C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg 等),免費分享
自旋鎖
spinlock.自旋鎖.
對比互斥量(互斥鎖)而言,獲取自旋鎖不需要進行線程間切換,如果自旋鎖正在被別的線程佔用,該線程也不會放棄CPU進行掛起休眠,而是恰如其名的在哪裡不斷地循環地查看自旋鎖保持者(持有者)是否將自旋鎖資源釋放出來... (自旋地原來就是如此)
口語解釋自旋:持有自旋鎖的線程不釋放自旋鎖,那也沒有關系呀,我就在這里不斷地一遍又一遍地查詢自旋鎖是否釋放出來,一旦釋放出來我立馬就可以直接使用 (因為我並沒有掛起等待,不需要像互斥鎖還需要進行線程間切換,重新獲取CPU,保存恢復上下文等等操作)
哪正是因為上述這些特點,線程嘗試獲取自旋鎖,獲取不到不會採取休眠掛起地方式,而是原地自旋(一遍又一遍查詢自旋鎖是否可以獲取)效率是遠高於互斥鎖了. 那我們是不是所有情況都使用自旋鎖就行了呢,互斥鎖就可以放棄使用了嗎????
解釋自旋鎖地弊端:如果每一個線程都僅僅只是需要短時間獲取這個鎖,那我自旋占據CPU等待是沒啥問題地。要是線程需要長時間地使用占據(鎖)。。。 會造成過多地無端占據CPU資源,俗稱站著茅坑不拉屎... 但是要是僅僅是短時間地自旋,平衡CPU利用率 + 程序運行效率 (自旋鎖確實是在有些時候更加合適)
自旋鎖需要場景:內核可搶占或者SMP(多處理器)情況下才真正需求 (避免死鎖陷入死循環,瘋狂地自旋,比如遞歸獲取自旋鎖. 你獲取了還要獲取,但是又沒法釋放)
自旋鎖的使用函數其實和互斥鎖幾乎是一摸一樣地,僅僅只是需要將所有的mutex換成spin即可
僅僅只是在init存在些許不同
何為驚群,池塘一堆, 我瞄準一條插過去,但是好似所有的都像是覺著自己正在被插一樣的四處逃竄。 這個就是驚群的生活一點的理解
驚群現象其實一點也不少,比如說 accept pthread_cond_broadcast 還有多個線程共享epoll監視一個listenfd 然後此刻 listenfd 說來 SYN了,放在了SYN隊列中,然後完成了三次握手放在了 accept隊列中了, 現在問題是這個connect我應該交付給哪一個線程處理呢.
多個epoll監視准備工作的線程 就是這群 (),然後connet就是魚叉,這一叉下去肯定是所有的 epoll線程都會被驚醒 (多線程共享listenfd引發的epoll驚群)
同樣如果將上述的多個線程換成多個進程共享監視 同一個 listenfd 就是(多進程的epoll驚群現象)
咱再畫一個草圖再來理解一下這個驚群:
如果是多進程道理是一樣滴,僅僅只是將所有的線程換成進程就OK了
終是來到了今天的正題了: epoll驚群問題地解決上面了...
首先 先說說accept的驚群問題,沒想到吧accept 平時大家寫它的多線程地時候,多個線程同時accept同一個listensock地時候也是會存在驚群問題地,但是accept地驚群問題已經被Linux內核處理了: 當有新的連接進入到accept隊列的時候,內核喚醒且僅喚醒一個進程來處理
但是對於epoll的驚群問題,內核卻沒有直接進行處理。哪既然內核沒有直接幫我們處理,我們應該如何針對這種現象做出一定的措施呢?
驚群效應帶來的弊端: 驚群現象會造成epoll的偽喚醒,本來epoll是阻塞掛起等待著地,這個時候因為掛起等待是不會佔用CPU地。。。 但是一旦喚醒就會佔用CPU去處理發生地IO事件, 但是其實是一個偽喚醒,這個就是對於線程或者進程的無效調度。然而進程或者線程地調取是需要花費代價地,需要上下文切換。需要進行進程(線程)間的不斷切換... 本來多核CPU是用來支持高並發地,但是現在卻被用來無效地喚醒,對於多核CPU簡直就是一種浪費 (浪費系統資源) 還會影響系統的性能.
解決方式(一般是兩種)
Nginx的解決方式:
加鎖:驚群問題發生的前提是多個進程(線程)監聽同一個套接字(listensock)上的事件,所以我們只讓一個進程(線程)去處理監聽套接字就可以了。
畫兩張圖來理解一下:
上述還沒有進行一個每一個進程都對應一個listensock 而是多線程共享一個listensock 運行結果如下
所有的線程同時被喚醒了,但是實際上會處理連接的僅僅只是一個線程,
咱僅僅只是將主線程做如上這樣一個簡單的修改,每一個線程對應一個listensock;每一個線程一個獨有的監視窗口,將問題拋給內核去處理,讓內核去負載均衡 : 結果如下
僅僅喚醒一個線程來進行處理連接,解決了驚群問題
本文通過介紹兩種鎖入手,以及為什麼需要鎖,鎖本質就是為了保護,持有鎖你就有權力有能力操作寫入一定的臨界保護資源,沒有鎖你就不行需要等待,本質其實是將多條匯編指令綁定成原子操作
然後介紹了驚群現象,通過一個巧妙地例子,扔一顆石子,只是瞄準一條魚扔過去了,但是整池魚都被驚醒了,
對應我們地實際問題就是, 多個線程或者進程共同監視同一個listensock。。。。然後IO連接事件到來地時候本來僅僅只是需要一個線程醒過來處理即可,但是卻會使得所有地線程(進程)全部醒過來,造成不必要地進程線程間切換,多核CPU被浪費喔,系統資源被浪費
處理方式 一。 Nginx 源碼加互斥鎖處理。。 二。設置SO_REUSEPORT, 使得多個進程線程可以同時連接同一個port , 為每一個進程線程搞一個listensock... 將問題拋給內核去處理,讓他去負載均衡地僅僅將IO連接事件分配給一個進程或線程
『肆』 linux 時鍾中斷 哪個定時器
一. Linux的硬體時間
PC機中的時間有三種硬體時鍾實現,這三種都是基於晶振產生的方波信號輸入。這三種時鍾為:(1)實時時鍾RTC ( Real Time Clock) (2)可編程間隔器PIT(Programmable Interval Timer )(3)時間戳計數器TSC(Time Stamp Clock)
1. 實時時鍾 RTC
用於長時間存放系統時間的設備,即時關機後也可依靠主板CMOS電池繼續保持系統的計時,原理圖如下:
Note: Linux與RTC的關系是,當Linux啟動時從RTC讀取時間和日期的基準值,然後在Kernel運行期間便拋開RTC,以軟體的形式維護系統的時間日期,並在適當時機由Kernel將時間寫回RTC Register.
1.1 RTC Register
(1). 時鍾與日歷Register
共10個,地址:0x00-0x09,分別用於保存時間日歷的具體信息,詳情如下:
00 Current Second for RTC
01 Alarm Second
02 Current Minute
03 Alarm Minute
04 Current Hour
05 Alarm Hour
06 Current Day of Week(1=Sunday)
07 Current Date of Month
08 Current Month
09 Current Year
(2).狀態和控制Register
共四個,地址:0x0a-0x0d,控制RTC晶元的工作方式,並表示當前狀態。
l 狀態RegisterA , 0x0A 格式如下:
bit[7]——UIP標志(Update in Progress),為1表示RTC正在更新日歷寄存器組中的值,此時日歷寄存器組是不可訪問的(此時訪問它們將得到一個無意義的漸變值)。
bit[6:4]——這三位是用來定義RTC的操作頻率。各種可能的值如下:
DV2 DV1 DV0
0 0 0 4.194304 MHZ
0 0 1 1.048576 MHZ
0 1 0 32.769 KHZ
1 1 0/1 任何
PC機通常設置成「010」。
bit[3:0]——速率選擇位(Rate Selection bits),用於周期性或方波信號輸出。
RS3 RS2 RS1 RS0 周期性中斷 方波 周期性中斷 方波
0 0 0 0 None None None None
0 0 0 1 30.517μs 32.768 KHZ 3.90625ms 256 HZ
0 0 1 0 61.035μs 16.384 KHZ
0 0 1 1 122.070μs 8.192KHZ
0 1 0 0 244.141μs 4.096KHZ
0 1 0 1 488.281μs 2.048KHZ
0 1 1 0 976.562μs 1.024KHZ
0 1 1 1 1.953125ms 512HZ
1 0 0 0 3.90625ms 256HZ
1 0 0 1 7.8125ms 128HZ
1 0 1 0 15.625ms 64HZ
1 0 1 1 31.25ms 32HZ
1 1 0 0 62.5ms 16HZ
1 1 0 1 125ms 8HZ
1 1 1 0 250ms 4HZ
1 1 1 1 500ms 2HZ
PC機BIOS對其默認的設置值是「0110」
l 狀態Register B , 0x0B 格式如下:
bit[7]——SET標志。為1表示RTC的所有更新過程都將終止,用戶程序隨後馬上對日歷寄存器組中的值進行初始化設置。為0表示將允許更新過程繼續。
bit[6]——PIE標志,周期性中斷enable標志。
bit[5]——AIE標志,告警中斷enable標志。
bit[4]——UIE標志,更新結束中斷enable標志。
bit[3]——SQWE標志,方波信號enable標志。
bit[2]——DM標志,用來控制日歷寄存器組的數據模式,0=BCD,1=BINARY。BIOS總是將它設置為0。
bit[1]——24/12標志,用來控制hour寄存器,0表示12小時制,1表示24小時制。PC機BIOS總是將它設置為1。
bit[0]——DSE標志。BIOS總是將它設置為0。
l 狀態Register C,0x0C 格式如下:
bit[7]——IRQF標志,中斷請求標志,當該位為1時,說明寄存器B中斷請求 發生。
bit[6]——PF標志,周期性中斷標志,為1表示發生周期性中斷請求。
bit[5]——AF標志,告警中斷標志,為1表示發生告警中斷請求。
bit[4]——UF標志,更新結束中斷標志,為1表示發生更新結束中斷請求。
l 狀態Register D,0x0D 格式如下:
bit[7]——VRT標志(Valid RAM and Time),為1表示OK,為0表示RTC 已經掉電。
bit[6:0]——總是為0,未定義。
2.可編程間隔定時器 PIT
每個PC機中都有一個PIT,以通過IRQ0產生周期性的時鍾中斷信號,作為系統定時器 system timer。當前使用最普遍的是Intel 8254 PIT晶元,它的I/O埠地址是0x40~0x43。
Intel 8254 PIT有3個計時通道,每個通道都有其不同的用途:
(1) 通道0用來負責更新系統時鍾。每當一個時鍾滴答過去時,它就會通過IRQ0向 系統 產生一次時鍾中斷。
(2) 通道1通常用於控制DMAC對RAM的刷新。
(3) 通道2被連接到PC機的揚聲器,以產生方波信號。
每 個通道都有一個向下減小的計數器,8254 PIT的輸入時鍾信號的頻率是1.193181MHZ,也即一秒鍾輸入1193181個clock-cycle。每輸入一個clock-cycle其時間 通道的計數器就向下減1,一直減到0值。因此對於通道0而言,當他的計數器減到0時,PIT就向系統產生一次時鍾中斷,表示一個時鍾滴答已經過去了。計數 器為16bit,因此所能表示的最大值是65536,一秒內發生的滴答數是:1193181/65536=18.206482.
PIT的I/O埠:
0x40 通道0 計數器 Read/Write
0X41 通道1計數器 Read/Write
0X42 通道2計數器 Read/Write
0X43 控制字 Write Only
Note: 因PIT I/O埠是8位,而PIT相應計數器是16位,因此必須對PIT計數器進行兩次讀寫。
8254 PIT的控制寄存器(0X43)的格式如下:
bit[7:6] — 通道選擇位:00 ,通道0;01,通道1;10,通道2;11,read-back command,僅8254。
bit[5:4] – Read/Write/Latch鎖定位,00,鎖定當前計數器以便讀取計數值;01,只讀高位元組;10,只讀低位元組;11,先高後低。
bit[3:1] – 設定各通道的工作模式。
000 mode0 當通道處於count out 時產生中斷信號,可用於系統定時
001 mode1 Hardware retriggerable one-shot
010 mode2 Rate Generator。產生實時時鍾中斷,通道0通常工作在這個模式下
011 mode3 方波信號發生器
100 mode4 Software triggered strobe
101 mode5 Hardware triggered strobe
3. 時間戳計數器 TSC
從Pentium開始,所有的Intel 80x86 CPU就都包含一個64位的時間戳記數器(TSC)的寄存器。該寄存器實際上是一個不斷增加的計數器,它在CPU的每個時鍾信號到來時加1(也即每一個clock-cycle輸入CPU時,該計數器的值就加1)。
匯編指令rdtsc可以用於讀取TSC的值。利用CPU的TSC,操作系統通常可以得到更為精準的時間度量。假如clock-cycle的頻率是400MHZ,那麼TSC就將每2.5納秒增加一次。
二. Linux時鍾中斷處理程序
1. 幾個概念
(1)時鍾周期(clock cycle)的頻率:8253/8254 PIT的本質就是對由晶體振盪器產生的時鍾周期進行計數,晶體振盪器在1秒時間內產生的時鍾脈沖個數就是時鍾周期的頻率。Linux用宏 CLOCK_TICK_RATE來表示8254 PIT的輸入時鍾脈沖的頻率(在PC機中這個值通常是1193180HZ),該宏定義在include/asm-i386/timex.h頭文件中
#define CLOCK_TICK_RATE 1193180 kernel=2.4 &2.6
(2)時鍾滴答(clock tick):當PIT通道0的計數器減到0值時,它就在IRQ0上產生一次時鍾中斷,也即一次時鍾滴答。PIT通道0的計數器的初始值決定了要過多少時鍾周期才產生一次時鍾中斷,因此也就決定了一次時鍾滴答的時間間隔長度。
(3)時鍾滴答的頻率(HZ):1秒時間內PIT所產生的時鍾滴答次數。 這個值也由PIT通道0的計數器初值決定的.Linux內核用宏HZ來表示時鍾滴答的頻率,而且在不同的平台上HZ有不同的定義值。對於ALPHA和 IA62平台HZ的值是1024,對於SPARC、MIPS、ARM和i386等平台HZ的值都是100。該宏在i386平台上的定義如下 (include/asm-i386/param.h):
#define HZ 100 kernel=2.4
#define HZ CONFIG_HZ kernel=2.6
(4)宏LATCH:定義要寫到PIT通道0的計數器中的值,它表示PIT將隔多少個時鍾周期產生一次時鍾中斷。公式計算:
LATCH=(1秒之內的時鍾周期個數)÷(1秒之內的時鍾中斷次數)=(CLOCK_TICK_RATE)÷(HZ)
定義在<include/linux/timex.h>
#define LATCH ((CLOCK_TICK_RATE + HZ/2) / HZ)
(5)全局變數jiffies:用於記錄系統自啟動以來產生的滴答總數。啟動時,kernel將該變數初始為0,每次時鍾中斷處理程序timer_interrupt()將該變數加1。因為一秒鍾內增加的時鍾中斷次數等於Hz,所以jiffies一秒內增加的值也是Hz。由此可得系統運行時間是jiffies/Hz 秒。
jiffies定義於<linux/jiffies.h>中:
extern unsigned long volatile jiffies;
Note:在kernel 2.4,jiffies是32位無符號數;kernel 2.6,jiffies是64位無符號數。
(6)全局變數xtime: 結構類型變數,用於表示當前時間距UNIX基準時間1970-01-01 00:00:00的相對秒數值。當系統啟動時,Kernel通過讀取RTC Register中的數據來初始化系統時間(wall_time),該時間存放在xtime中。
void __init time_init (void) {
... ...
xtime.tv_sec = get_cmos_time ();
xtime.tv_usec = 0;
... ... }
Note:實時時鍾RTC的最主要作用便是在系統啟動時用來初始化xtime變數。
2.Linux的時鍾中斷處理程序
Linux下時鍾中斷處理由time_interrupt() 函數實現,主要完成以下任務:
l 獲得xtime_lock鎖,以便對訪問的jiffies_64 (kernel2.6)和 xtime進行保護
l 需要時應答或重新設置系統時鍾。
l 周期性的使用系統時間(wall_time)更新實時時鍾RTC
l 調用體系結構無關的時鍾常式:do_timer()。
do_timer()主要完成以下任務:
l 更新jiffies;
l 更新系統時間(wall_time),該時間存放在xtime變數中
l 執行已經到期的動態定時器
l 計算平均負載值
void do_timer(unsigned long ticks)
{
jiffies_64 += ticks;
update_process_times(user_mode(regs));
update_times (ticks);
}
static inline void update_times(unsigned long ticks)
{
update_wall_time ();
calc_load (ticks);
}
time_interrupt ():
static void timer_interrupt(int irq, void *dev_id, struct pt_regs *regs) {
int count;
write_lock (&xtime_lock); //獲得xtime_lock鎖
if(use_cyclone)
mark_timeoffset_cyclone();
else if (use_tsc) {
rdtscl(last_tsc_low); //讀TSC register到last_tsc_low
spin_lock (&i8253_lock); //對自旋鎖i8253_lock加鎖,對8254PIT訪問
outb_p (0x00, 0x43);
count = inb_p(0x40);
count |= inb(0x40) << 8;
if (count > LATCH) {
printk (KERN_WARNING "i8253 count too high! resetting../n");
outb_p (0x34, 0x43);
outb_p (LATCH & 0xff, 0x40);
outb(LATCH >> 8, 0x40);
count = LATCH - 1;
}
spin_unlock (&i8253_lock);
if (count = = LATCH) {
count- -;
}
count = ((LATCH-1) - count) * TICK_SIZE;
delay_at_last_interrupt = (count + LATCH/2) / LATCH;
} //end use_tsc
do_timer_interrupt (irq, NULL, regs);
write_unlock(&xtime_lock);
}//end time_interrupt
do_timer_interrupt():
static inline void do_timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
……
do_timer(regs);
if((time_status & STA_UNSYNC)= =0&&xtime.tv_sec> last_rtc_update + 660 && xtime.tv_usec >= 500000 - ((unsigned) tick) / 2 && xtime.tv_usec <= 500000 + ((unsigned) tick) / 2) {
if (set_rtc_mmss(xtime.tv_sec) == 0)
last_rtc_update = xtime.tv_sec;
else
last_rtc_update = xtime.tv_sec - 600;
……
}
do_timer_interrupt()主要完成:調用do_timer()和判斷是否需要更新CMOS時鍾。更新CMOS時鍾的條件如下:三個須同時成立
1.系統全局時間狀態變數time_status中沒有設置STA_UNSYNC標志,即Linux沒有設置外部同步時鍾(如NTP)
2.自從上次CMOS時鍾更新已經過去11分鍾。全局變數last_rtc_update保存上次更新CMOS時鍾的時間.
3.由於RTC存在Update Cycle,因此應在一秒鍾間隔的中間500ms左右調用set_rtc_mmss()函數,將當前時間xtime.tv_sec寫回RTC中。
Note. Linux kernel 中定義了一個類似jiffies的變數wall_jiffies,用於記錄kernel上一次更新xtime時,jiffies的值。
Summary: Linux kernel在啟動時,通過讀取RTC里的時間日期初始化xtime,此後由kernel通過初始PIT來提供軟時鍾。
時鍾中斷處理過程可歸納為:系統時鍾system timer在IRQ0上產生中斷;kernel調用time_interrupt();time_interrupt()判斷系統是否使用TSC,若使用 則讀取TSC register;然後讀取PIT 通道0的計數值;調用do_time_interrupt(),實現系統時間更新.
『伍』 linux下的幾種時鍾和定時器機制
1. RTC(Real Time Clock)
所有PC都有RTC. 它和CPU和其他晶元獨立。它在電腦關機之後還可以正常運行。RTC可以在IRQ8上產生周期性中斷. 頻率在2Hz--8192HZ.
Linux只是把RTC用來獲取時間和日期. 當然它允許進程通過對/dev/rtc設備來對它進行編程。Kernel通過0x70和0x71 I/O埠來訪問RTC。
2. TSC(Time Stamp Counter)
80x86上的微處理器都有CLK輸入針腳. 從奔騰系列開始. 微處理器支持一個計數器. 每當一個時鍾信號來的時候. 計數器加1. 可以通過匯編指令rdtsc來得到計數器的值。通過calibrate_tsc可以獲得CPU的頻率. 它是通過計算大約5毫秒里tsc寄存器裡面的增加值來確認的。或者可以通過cat /proc/cpuinfo來獲取cpu頻率。tsc可以提供比PIT更精確的時間度量。
3. PIT(Programmable internval timer)
除了RTC和TSC. IBM兼容機提供了PIT。PIT類似微波爐的鬧鍾機制. 當時間到的時候. 提供鈴聲. PIT不是產生鈴聲. 而是產生一種特殊中斷. 叫定時器中斷或者時鍾中斷。它用來告訴內核一個間隔過去了。這個時間間隔也叫做一個滴答數。可以通過編譯內核是選擇內核頻率來確定。如內核頻率設為1000HZ,則時間間隔或滴答為1/1000=1微秒。滴答月短. 定時精度更高. 但是用戶模式的時間更短. 也就是說用戶模式下程序執行會越慢。滴答的長度以納秒形式存在tick_nsec變數裡面。PIT通過8254的0x40--0x43埠來訪問。它產生中斷號為IRQ 0.
下面是關於pIT裡面的一些宏定義:
HZ:每秒中斷數。
CLOCK_TICK_RATE:值是1,193,182. 它是8254晶元內部振盪器頻率。
LATCH:代表CLOCK_TICK_RATE和HZ的比率. 被用來編程PIT。
setup_pit_timer()如下:
spin_lock_irqsave(&i8253_lock, flags);
outb_p(0x34,0x43);
udelay(10);
outb_p(LATCH & 0xff, 0x40);
udelay(10);
outb (LATCH >> 8, 0x40);
spin_unlock_irqrestore(&i8253_lock, flags);
4. CPU Local Timer
最近的80x86架構的微處理器上的local apic提供了cpu local timer.他和pit區別在於它提供了one-shot和periodic中斷。它可以使中斷發送到特定cpu。one-shot中斷常用在實時系統裡面。
『陸』 關於Linux自旋鎖
既然是對一個變數進行保護,當然是一個自旋鎖了,還沒見過一個變數能當兩個用的。
我覺得你對這段代碼的理解有問題,用 spin_lock 和 spin_unlock 的目的是保證程序在對 xxx_lock 進行操作的時候,不會有其它進程改變這個值,是為了保證數據的准確性。
你可以設想一下,如果沒有自旋鎖,代碼運行起來會有什麼問題。假設 A,B兩個進程同時訪問 open , 沒有使用自旋鎖,此時 xxx_lock=0, A 進程在判斷 if (xxx_count) 時,會認為設備沒有被使用,那麼它會繼續後面的 xxx_count++ 操作,但假如這時 CPU 切換進程, A 進程還沒有來得及把 xxx_count 變成 1 的時候, B 進程開始運行,那麼 B 進程此時也會認為設備沒有被使用,它也會進行後繼操作,這樣就會出現兩個進程同時訪問設備的錯誤。
open 和 release 當然可以同時訪問,只不過在運行 spin_lock 的時候,後訪問的進程會被阻塞而已。假設有 A 進程訪問 open ,B 進程訪問 release ,你可以把這種情況理解為 A , B 進程同時訪問 open 函數,這樣或許能更好的理解這段代碼。因為 open 和 release 在使用自旋鎖的時候,方法是一樣的。
spin_lock 和 CPU 系統無關,不管是單 CPU 還是多 CPU ,運行結果都是一樣的。
這個邏輯關系比較難解釋,不知道你看懂我的意思沒。
『柒』 linux自旋鎖使用時需要注意的幾個地方
1、在內核多線程編程時,為了保護共享資源通常需要使用鎖,而使用的比較多的就是spinlock,但需要注意的是:所有臨界區代碼都需要加鎖保護,否則就達不到保護效果。也就是,訪問共享資源的多個線程需要協同工作共同加鎖才能保證不出錯。在實際寫代碼時,有時候會網掉這一點,以致出現各種稀奇古怪的問題,而且很難找到原因。
2、在出現兩個和多個自旋鎖的嵌套使用時,務必注意加鎖和解鎖的順序。
比如:在線程1中,spinlock A -> spinlock B -> spin unlock B -> spin unlock A ;那麼,在需要同步的線程2中,若需要加相同的鎖,則順序也應該保持相同,spinlock A -> spinlock B -> spin unlock B -> spin unlock A ;否則,很有可能出現死鎖。
3、spinlock保護的代碼執行時間要盡量短,若有for循環之類的代碼,則一定要確保循環可以在短時間可以退出,從而使得spinlock可以釋放。
4、spinlock所保護的代碼在執行過程中不能睡眠。比如,在spinlock和spinunlock之間不能調用kmalloc, _from_user,kthread_stop等調用,因為這些函數調用均有可能導致線程睡眠。
5、spinlock在實際使用時有如下幾種類型,spin_lock,spin_lock_bh,spin_lock_irqsave。在具體使用時,需要根據被保護臨界區鎖處的上下文選擇合適的spinlock類型。
spin_lock用於不同cpu線程間同步,spin_lock_bh和spin_lock_irqsave主要用於本cpu線程間的同步,前者關軟中斷,後者關硬中斷。
『捌』 Linux中自旋鎖是什麼
自旋鎖(Spin Lock)是一種典型的對臨界資源進行互斥訪問的手段,其名稱來源於它的工作方式。為了獲得一個自旋鎖,在某CPU上運行的代碼需先執行一個原子操作,該操作測試並設置(Test-AndSet)某個內存變數。由於它是原子操作,所以在該操作完成之前其他執行單元不可能訪問這個內存變數。如果測試結果表明鎖已經空閑,則程序獲得這個自旋鎖並繼續執行;如果測試結果表明鎖仍被佔用,程序將在一個小的循環內重復這個「測試並設置」操作,即進行所謂的「自旋」,通俗地說就是「在原地打轉」。當自旋鎖的持有者通過重置該變數釋放這個自旋鎖後,某個等待的「測試並設置」操作向其調用者報告鎖已釋放。理解自旋鎖最簡單的方法是把它作為一個變數看待,該變數把一個臨界區標記為「我當前在運行,請稍等一會」或者標記為「我當前不在運行,可以被使用。如果派肢A執行單元首先進入常式,它將持有自旋鎖;當B執行單元試圖進入同一個常式時,將獲知自旋鎖已被持有,需等到A執行單元釋放後才能進入。在ARM體系結構下,自旋鎖的實現借用了ldrex指歷羨遲令、strex指令、ARM處理器內存屏障指令dmb和dsb、wfe指令和sev指令,這類似於代碼清單7.1的邏輯肢李。可以說既要保證排他性,也要處理好內存屏障。自旋鎖主要針對SMP或單CPU但內核可搶占的情況,對於單CPU和內核不支持搶占的系統,自旋鎖退化為空操作。在單CPU和內核可搶占的系統中,自旋鎖持有期間中內核的搶占將被禁止。由於內核可搶占的單CPU系統的行為實際上很類似於SMP系統,因此,在這樣的單CPU系統中使用自旋鎖仍十分必要。另外,在多核SMP的情況下,任何一個核拿到了自旋鎖,該核上的搶占調度也暫時禁止了,但是沒有禁止另外一個核的搶占調度。盡管用了自旋鎖可以保證臨界區不受別的CPU和本CPU內的搶占進程打擾,但是得到鎖的代碼路徑在執行臨界區的時候,還可能受到中斷和底半部的影響。為了防止這種影響,就需要用到自旋鎖的衍生。
『玖』 如何實現linux下多線程之間的互斥與同步
Linux設備驅動中必須解決的一個問題是多個進程對共享資源的並發訪問,並發訪問會導致競態,linux提供了多種解決競態問題的方式,這些方式適合不同的應用場景。
Linux內核是多進程、多線程的操作系統,它提供了相當完整的內核同步方法。內核同步方法列表如下:
中斷屏蔽
原子操作
自旋鎖
讀寫自旋鎖
順序鎖
信號量
讀寫信號量
BKL(大內核鎖)
Seq鎖
一、並發與競態:
定義:
並發(concurrency)指的是多個執行單元同時、並行被執行,而並發的執行單元對共享資源(硬體資源和軟體上的全局變數、靜態變數等)的訪問則很容易導致競態(race conditions)。
在linux中,主要的競態發生在如下幾種情況:
1、對稱多處理器(SMP)多個CPU
特點是多個CPU使用共同的系統匯流排,因此可訪問共同的外設和存儲器。
2、單CPU內進程與搶占它的進程
3、中斷(硬中斷、軟中斷、Tasklet、底半部)與進程之間
只要並發的多個執行單元存在對共享資源的訪問,競態就有可能發生。
如果中斷處理程序訪問進程正在訪問的資源,則競態也會會發生。
多個中斷之間本身也可能引起並發而導致競態(中斷被更高優先順序的中斷打斷)。
解決競態問題的途徑是保證對共享資源的互斥訪問,所謂互斥訪問就是指一個執行單元在訪問共享資源的時候,其他的執行單元都被禁止訪問。
訪問共享資源的代碼區域被稱為臨界區,臨界區需要以某種互斥機制加以保護,中斷屏蔽,原子操作,自旋鎖,和信號量都是linux設備驅動中可採用的互斥途徑。
臨界區和競爭條件:
所謂臨界區(critical regions)就是訪問和操作共享數據的代碼段,為了避免在臨界區中並發訪問,編程者必須保證這些代碼原子地執行——也就是說,代碼在執行結束前不可被打斷,就如同整個臨界區是一個不可分割的指令一樣,如果兩個執行線程有可能處於同一個臨界區中,那麼就是程序包含一個bug,如果這種情況發生了,我們就稱之為競爭條件(race conditions),避免並發和防止競爭條件被稱為同步。
死鎖:
死鎖的產生需要一定條件:要有一個或多個執行線程和一個或多個資源,每個線程都在等待其中的一個資源,但所有的資源都已經被佔用了,所有線程都在相互等待,但它們永遠不會釋放已經佔有的資源,於是任何線程都無法繼續,這便意味著死鎖的發生。
二、中斷屏蔽
在單CPU范圍內避免競態的一種簡單方法是在進入臨界區之前屏蔽系統的中斷。
由於linux內核的進程調度等操作都依賴中斷來實現,內核搶占進程之間的並發也就得以避免了。
中斷屏蔽的使用方法:
local_irq_disable()//屏蔽中斷
//臨界區
local_irq_enable()//開中斷
特點:
由於linux系統的非同步IO,進程調度等很多重要操作都依賴於中斷,在屏蔽中斷期間所有的中斷都無法得到處理,因此長時間的屏蔽是很危險的,有可能造成數據丟失甚至系統崩潰,這就要求在屏蔽中斷之後,當前的內核執行路徑應當盡快地執行完臨界區的代碼。
中斷屏蔽只能禁止本CPU內的中斷,因此,並不能解決多CPU引發的競態,所以單獨使用中斷屏蔽並不是一個值得推薦的避免競態的方法,它一般和自旋鎖配合使用。
三、原子操作
定義:原子操作指的是在執行過程中不會被別的代碼路徑所中斷的操作。
(原子原本指的是不可分割的微粒,所以原子操作也就是不能夠被分割的指令)
(它保證指令以「原子」的方式執行而不能被打斷)
原子操作是不可分割的,在執行完畢不會被任何其它任務或事件中斷。在單處理器系統(UniProcessor)中,能夠在單條指令中完成的操作都可以認為是" 原子操作",因為中斷只能發生於指令之間。這也是某些CPU指令系統中引入了test_and_set、test_and_clear等指令用於臨界資源互斥的原因。但是,在對稱多處理器(Symmetric Multi-Processor)結構中就不同了,由於系統中有多個處理器在獨立地運行,即使能在單條指令中完成的操作也有可能受到干擾。我們以decl (遞減指令)為例,這是一個典型的"讀-改-寫"過程,涉及兩次內存訪問。
通俗理解:
原子操作,顧名思義,就是說像原子一樣不可再細分。一個操作是原子操作,意思就是說這個操作是以原子的方式被執行,要一口氣執行完,執行過程不能夠被OS的其他行為打斷,是一個整體的過程,在其執行過程中,OS的其它行為是插不進來的。
分類:linux內核提供了一系列函數來實現內核中的原子操作,分為整型原子操作和位原子操作,共同點是:在任何情況下操作都是原子的,內核代碼可以安全的調用它們而不被打斷。
原子整數操作:
針對整數的原子操作只能對atomic_t類型的數據進行處理,在這里之所以引入了一個特殊的數據類型,而沒有直接使用C語言的int型,主要是出於兩個原因:
第一、讓原子函數只接受atomic_t類型的操作數,可以確保原子操作只與這種特殊類型數據一起使用,同時,這也確保了該類型的數據不會被傳遞給其它任何非原子函數;
第二、使用atomic_t類型確保編譯器不對相應的值進行訪問優化——這點使得原子操作最終接收到正確的內存地址,而不是一個別名,最後就是在不同體系結構上實現原子操作的時候,使用atomic_t可以屏蔽其間的差異。
原子整數操作最常見的用途就是實現計數器。
另一點需要說明原子操作只能保證操作是原子的,要麼完成,要麼不完成,不會有操作一半的可能,但原子操作並不能保證操作的順序性,即它不能保證兩個操作是按某個順序完成的。如果要保證原子操作的順序性,請使用內存屏障指令。
atomic_t和ATOMIC_INIT(i)定義
typedef struct { volatile int counter; } atomic_t;
#define ATOMIC_INIT(i) { (i) }
在你編寫代碼的時候,能使用原子操作的時候,就盡量不要使用復雜的加鎖機制,對多數體系結構來講,原子操作與更復雜的同步方法相比較,給系統帶來的開銷小,對高速緩存行的影響也小,但是,對於那些有高性能要求的代碼,對多種同步方法進行測試比較,不失為一種明智的作法。
原子位操作:
針對位這一級數據進行操作的函數,是對普通的內存地址進行操作的。它的參數是一個指針和一個位號。
為方便其間,內核還提供了一組與上述操作對應的非原子位函數,非原子位函數與原子位函數的操作完全相同,但是,前者不保證原子性,且其名字前綴多兩個下劃線。例如,與test_bit()對應的非原子形式是_test_bit(),如果你不需要原子性操作(比如,如果你已經用鎖保護了自己的數據),那麼這些非原子的位函數相比原子的位函數可能會執行得更快些。
四、自旋鎖
自旋鎖的引入:
如 果每個臨界區都能像增加變數這樣簡單就好了,可惜現實不是這樣,而是臨界區可以跨越多個函數,例如:先得從一個數據結果中移出數據,對其進行格式轉換和解 析,最後再把它加入到另一個數據結構中,整個執行過程必須是原子的,在數據被更新完畢之前,不能有其他代碼讀取這些數據,顯然,簡單的原子操作是無能為力 的(在單處理器系統(UniProcessor)中,能夠在單條指令中完成的操作都可以認為是" 原子操作",因為中斷只能發生於指令之間),這就需要使用更為復雜的同步方法——鎖來提供保護。
自旋鎖的介紹:
Linux內核中最常見的鎖是自旋鎖(spin lock),自旋鎖最多隻能被一個可執行線程持有,如果一個執行線程試圖獲得一個被爭用(已經被持有)的自旋鎖,那麼該線程就會一直進行忙循環—旋轉—等待鎖重新可用,要是鎖未被爭用,請求鎖的執行線程便能立刻得到它,繼續執行,在任意時間,自旋鎖都可以防止多於一個的執行線程同時進入理解區,注意同一個鎖可以用在多個位置—例如,對於給定數據的所有訪問都可以得到保護和同步。
一個被爭用的自旋鎖使得請求它的線程在等待鎖重新可用時自旋(特別浪費處理器時間),所以自旋鎖不應該被長時間持有,事實上,這點正是使用自旋鎖的初衷,在短期間內進行輕量級加鎖,還可以採取另外的方式來處理對鎖的爭用:讓請求線程睡眠,直到鎖重新可用時再喚醒它,這樣處理器就不必循環等待,可以去執行其他代碼,這也會帶來一定的開銷——這里有兩次明顯的上下文切換, 被阻塞的線程要換出和換入。因此,持有自旋鎖的時間最好小於完成兩次上下文切換的耗時,當然我們大多數人不會無聊到去測量上下文切換的耗時,所以我們讓持 有自旋鎖的時間應盡可能的短就可以了,信號量可以提供上述第二種機制,它使得在發生爭用時,等待的線程能投入睡眠,而不是旋轉。
自旋鎖可以使用在中斷處理程序中(此處不能使用信號量,因為它們會導致睡眠),在中斷處理程序中使用自旋鎖時,一定要在獲取鎖之前,首先禁止本地中斷(在 當前處理器上的中斷請求),否則,中斷處理程序就會打斷正持有鎖的內核代碼,有可能會試圖去爭用這個已經持有的自旋鎖,這樣以來,中斷處理程序就會自旋, 等待該鎖重新可用,但是鎖的持有者在這個中斷處理程序執行完畢前不可能運行,這正是我們在前一章節中提到的雙重請求死鎖,注意,需要關閉的只是當前處理器上的中斷,如果中斷發生在不同的處理器上,即使中斷處理程序在同一鎖上自旋,也不會妨礙鎖的持有者(在不同處理器上)最終釋放鎖。
自旋鎖的簡單理解:
理解自旋鎖最簡單的方法是把它作為一個變數看待,該變數把一個臨界區或者標記為「我當前正在運行,請稍等一會」或者標記為「我當前不在運行,可以被使用」。如果A執行單元首先進入常式,它將持有自旋鎖,當B執行單元試圖進入同一個常式時,將獲知自旋鎖已被持有,需等到A執行單元釋放後才能進入。
自旋鎖的API函數:
其實介紹的幾種信號量和互斥機制,其底層源碼都是使用自旋鎖,可以理解為自旋鎖的再包裝。所以從這里就可以理解為什麼自旋鎖通常可以提供比信號量更高的性能。
自旋鎖是一個互斥設備,他只能會兩個值:「鎖定」和「解鎖」。它通常實現為某個整數之中的單個位。
「測試並設置」的操作必須以原子方式完成。
任何時候,只要內核代碼擁有自旋鎖,在相關CPU上的搶占就會被禁止。
適用於自旋鎖的核心規則:
(1)任何擁有自旋鎖的代碼都必須使原子的,除服務中斷外(某些情況下也不能放棄CPU,如中斷服務也要獲得自旋鎖。為了避免這種鎖陷阱,需要在擁有自旋鎖時禁止中斷),不能放棄CPU(如休眠,休眠可發生在許多無法預期的地方)。否則CPU將有可能永遠自旋下去(死機)。
(2)擁有自旋鎖的時間越短越好。
需 要強調的是,自旋鎖別設計用於多處理器的同步機制,對於單處理器(對於單處理器並且不可搶占的內核來說,自旋鎖什麼也不作),內核在編譯時不會引入自旋鎖 機制,對於可搶占的內核,它僅僅被用於設置內核的搶占機制是否開啟的一個開關,也就是說加鎖和解鎖實際變成了禁止或開啟內核搶占功能。如果內核不支持搶 占,那麼自旋鎖根本就不會編譯到內核中。
內核中使用spinlock_t類型來表示自旋鎖,它定義在:
typedef struct {
raw_spinlock_t raw_lock;
#if defined(CONFIG_PREEMPT) && defined(CONFIG_SMP)
unsigned int break_lock;
#endif
} spinlock_t;
對於不支持SMP的內核來說,struct raw_spinlock_t什麼也沒有,是一個空結構。對於支持多處理器的內核來說,struct raw_spinlock_t定義為
typedef struct {
unsigned int slock;
} raw_spinlock_t;
slock表示了自旋鎖的狀態,「1」表示自旋鎖處於解鎖狀態(UNLOCK),「0」表示自旋鎖處於上鎖狀態(LOCKED)。
break_lock表示當前是否由進程在等待自旋鎖,顯然,它只有在支持搶占的SMP內核上才起作用。
自旋鎖的實現是一個復雜的過程,說它復雜不是因為需要多少代碼或邏輯來實現它,其實它的實現代碼很少。自旋鎖的實現跟體系結構關系密切,核心代碼基本也是由匯編語言寫成,與體協結構相關的核心代碼都放在相關的目錄下,比如。對於我們驅動程序開發人員來說,我們沒有必要了解這么spinlock的內部細節,如果你對它感興趣,請參考閱讀Linux內核源代碼。對於我們驅動的spinlock介面,我們只需包括頭文件。在我們詳細的介紹spinlock的API之前,我們先來看看自旋鎖的一個基本使用格式:
#include
spinlock_t lock = SPIN_LOCK_UNLOCKED;
spin_lock(&lock);
....
spin_unlock(&lock);
從使用上來說,spinlock的API還很簡單的,一般我們會用的的API如下表,其實它們都是定義在中的宏介面,真正的實現在中
#include
SPIN_LOCK_UNLOCKED
DEFINE_SPINLOCK
spin_lock_init( spinlock_t *)
spin_lock(spinlock_t *)
spin_unlock(spinlock_t *)
spin_lock_irq(spinlock_t *)
spin_unlock_irq(spinlock_t *)
spin_lock_irqsace(spinlock_t *,unsigned long flags)
spin_unlock_irqsace(spinlock_t *, unsigned long flags)
spin_trylock(spinlock_t *)
spin_is_locked(spinlock_t *)
• 初始化
spinlock有兩種初始化形式,一種是靜態初始化,一種是動態初始化。對於靜態的spinlock對象,我們用 SPIN_LOCK_UNLOCKED來初始化,它是一個宏。當然,我們也可以把聲明spinlock和初始化它放在一起做,這就是 DEFINE_SPINLOCK宏的工作,因此,下面的兩行代碼是等價的。
DEFINE_SPINLOCK (lock);
spinlock_t lock = SPIN_LOCK_UNLOCKED;
spin_lock_init 函數一般用來初始化動態創建的spinlock_t對象,它的參數是一個指向spinlock_t對象的指針。當然,它也可以初始化一個靜態的沒有初始化的spinlock_t對象。
spinlock_t *lock
......
spin_lock_init(lock);
• 獲取鎖
內核提供了三個函數用於獲取一個自旋鎖。
spin_lock:獲取指定的自旋鎖。
spin_lock_irq:禁止本地中斷並獲取自旋鎖。
spin_lock_irqsace:保存本地中斷狀態,禁止本地中斷並獲取自旋鎖,返回本地中斷狀態。
自旋鎖是可以使用在中斷處理程序中的,這時需要使用具有關閉本地中斷功能的函數,我們推薦使用 spin_lock_irqsave,因為它會保存加鎖前的中斷標志,這樣就會正確恢復解鎖時的中斷標志。如果spin_lock_irq在加鎖時中斷是關閉的,那麼在解鎖時就會錯誤的開啟中斷。
另外兩個同自旋鎖獲取相關的函數是:
spin_trylock():嘗試獲取自旋鎖,如果獲取失敗則立即返回非0值,否則返回0。
spin_is_locked():判斷指定的自旋鎖是否已經被獲取了。如果是則返回非0,否則,返回0。
• 釋放鎖
同獲取鎖相對應,內核提供了三個相對的函數來釋放自旋鎖。
spin_unlock:釋放指定的自旋鎖。
spin_unlock_irq:釋放自旋鎖並激活本地中斷。
spin_unlock_irqsave:釋放自旋鎖,並恢復保存的本地中斷狀態。
五、讀寫自旋鎖
如 果臨界區保護的數據是可讀可寫的,那麼只要沒有寫操作,對於讀是可以支持並發操作的。對於這種只要求寫操作是互斥的需求,如果還是使用自旋鎖顯然是無法滿 足這個要求(對於讀操作實在是太浪費了)。為此內核提供了另一種鎖-讀寫自旋鎖,讀自旋鎖也叫共享自旋鎖,寫自旋鎖也叫排他自旋鎖。
讀寫自旋鎖是一種比自旋鎖粒度更小的鎖機制,它保留了「自旋」的概念,但是在寫操作方面,只能最多有一個寫進程,在讀操作方面,同時可以有多個讀執行單元,當然,讀和寫也不能同時進行。
讀寫自旋鎖的使用也普通自旋鎖的使用很類似,首先要初始化讀寫自旋鎖對象:
// 靜態初始化
rwlock_t rwlock = RW_LOCK_UNLOCKED;
//動態初始化
rwlock_t *rwlock;
...
rw_lock_init(rwlock);
在讀操作代碼里對共享數據獲取讀自旋鎖:
read_lock(&rwlock);
...
read_unlock(&rwlock);
在寫操作代碼里為共享數據獲取寫自旋鎖:
write_lock(&rwlock);
...
write_unlock(&rwlock);
需要注意的是,如果有大量的寫操作,會使寫操作自旋在寫自旋鎖上而處於寫飢餓狀態(等待讀自旋鎖的全部釋放),因為讀自旋鎖會自由的獲取讀自旋鎖。
讀寫自旋鎖的函數類似於普通自旋鎖,這里就不一一介紹了,我們把它列在下面的表中。
RW_LOCK_UNLOCKED
rw_lock_init(rwlock_t *)
read_lock(rwlock_t *)
read_unlock(rwlock_t *)
read_lock_irq(rwlock_t *)
read_unlock_irq(rwlock_t *)
read_lock_irqsave(rwlock_t *, unsigned long)
read_unlock_irqsave(rwlock_t *, unsigned long)
write_lock(rwlock_t *)
write_unlock(rwlock_t *)
write_lock_irq(rwlock_t *)
write_unlock_irq(rwlock_t *)
write_lock_irqsave(rwlock_t *, unsigned long)
write_unlock_irqsave(rwlock_t *, unsigned long)
rw_is_locked(rwlock_t *)
六、順序瑣
順序瑣(seqlock)是對讀寫鎖的一種優化,若使用順序瑣,讀執行單元絕不會被寫執行單元阻塞,也就是說,讀執行單元可以在寫執行單元對被順序瑣保護的共享資源進行寫操作時仍然可以繼續讀,而不必等待寫執行單元完成寫操作,寫執行單元也不需要等待所有讀執行單元完成讀操作才去進行寫操作。
但是,寫執行單元與寫執行單元之間仍然是互斥的,即如果有寫執行單元在進行寫操作,其它寫執行單元必須自旋在哪裡,直到寫執行單元釋放了順序瑣。
如果讀執行單元在讀操作期間,寫執行單元已經發生了寫操作,那麼,讀執行單元必須重新讀取數據,以便確保得到的數據是完整的,這種鎖在讀寫同時進行的概率比較小時,性能是非常好的,而且它允許讀寫同時進行,因而更大的提高了並發性,
注意,順序瑣由一個限制,就是它必須被保護的共享資源不含有指針,因為寫執行單元可能使得指針失效,但讀執行單元如果正要訪問該指針,將導致Oops。
七、信號量
Linux中的信號量是一種睡眠鎖,如果有一個任務試圖獲得一個已經被佔用的信號量時,信號量會將其推進一個等待隊列,然後讓其睡眠,這時處理器能重獲自由,從而去執行其它代碼,當持有信號量的進程將信號量釋放後,處於等待隊列中的哪個任務被喚醒,並獲得該信號量。
信號量,或旗標,就是我們在操作系統里學習的經典的P/V原語操作。
P:如果信號量值大於0,則遞減信號量的值,程序繼續執行,否則,睡眠等待信號量大於0。
V:遞增信號量的值,如果遞增的信號量的值大於0,則喚醒等待的進程。
信號量的值確定了同時可以有多少個進程可以同時進入臨界區,如果信號量的初始值始1,這信號量就是互斥信號量(MUTEX)。對於大於1的非0值信號量,也可稱為計數信號量(counting semaphore)。對於一般的驅動程序使用的信號量都是互斥信號量。
類似於自旋鎖,信號量的實現也與體系結構密切相關,具體的實現定義在頭文件中,對於x86_32系統來說,它的定義如下:
struct semaphore {
atomic_t count;
int sleepers;
wait_queue_head_t wait;
};
信號量的初始值count是atomic_t類型的,這是一個原子操作類型,它也是一個內核同步技術,可見信號量是基於原子操作的。我們會在後面原子操作部分對原子操作做詳細介紹。
信號量的使用類似於自旋鎖,包括創建、獲取和釋放。我們還是來先展示信號量的基本使用形式:
static DECLARE_MUTEX(my_sem);
......
if (down_interruptible(&my_sem))
{
return -ERESTARTSYS;
}
......
up(&my_sem)
Linux內核中的信號量函數介面如下:
static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);
seam_init(struct semaphore *, int);
init_MUTEX(struct semaphore *);
init_MUTEX_LOCKED(struct semaphore *)
down_interruptible(struct semaphore *);
down(struct semaphore *)
down_trylock(struct semaphore *)
up(struct semaphore *)
• 初始化信號量
信號量的初始化包括靜態初始化和動態初始化。靜態初始化用於靜態的聲明並初始化信號量。
static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);
對於動態聲明或創建的信號量,可以使用如下函數進行初始化:
seam_init(sem, count);
init_MUTEX(sem);
init_MUTEX_LOCKED(struct semaphore *)
顯然,帶有MUTEX的函數始初始化互斥信號量。LOCKED則初始化信號量為鎖狀態。
• 使用信號量
信號量初始化完成後我們就可以使用它了
down_interruptible(struct semaphore *);
down(struct semaphore *)
down_trylock(struct semaphore *)
up(struct semaphore *)
down函數會嘗試獲取指定的信號量,如果信號量已經被使用了,則進程進入不可中斷的睡眠狀態。down_interruptible則會使進程進入可中斷的睡眠狀態。關於進程狀態的詳細細節,我們在內核的進程管理里在做詳細介紹。
down_trylock嘗試獲取信號量, 如果獲取成功則返回0,失敗則會立即返回非0。
當退出臨界區時使用up函數釋放信號量,如果信號量上的睡眠隊列不為空,則喚醒其中一個等待進程。
八、讀寫信號量
類似於自旋鎖,信號量也有讀寫信號量。讀寫信號量API定義在頭文件中,它的定義其實也是體系結構相關的,因此具體實現定義在頭文件中,以下是x86的例子:
struct rw_semaphore {
signed long count;
spinlock_t wait_lock;
struct list_head wait_list;
};
『拾』 Linux進入臨界去開關中斷的幾種方式
進入中斷時清卜候關閉瞎正纖全局的中斷是為了避免程序處理中斷過程中,再進入另一個中斷打亂執行的順序,也就是為了防止中斷嵌套的情況發生。比如在irq_handler函數中首先就應該關閉中斷。或者,在某些操作順序中是不允許中斷發生打斷的情況。例如在驅動中常用的方式:
unsigned int flag;
local_irq_save(&flag);
... ... ... ...
local_irq_restore(&flag);
spin_loc_irqsave 禁止中斷(只在本地處理器)在獲得自旋鎖之前; 之前的中斷狀態保存在 flags 里. 如果你絕對確定在你的處理器上沒有禁止中斷的(或者, 換句話說, 你確信你應當在你釋放你的自旋鎖時打開中斷),你可以使用 spin_lock_irq 代替, 並且不必保持跟蹤 flags. 最後, spin_lock_bh 在獲取鎖之前禁止軟體磨仿中斷, 但是硬體中斷留作打開的。