㈠ linux 中斷 下半部 處理時間過長 怎麼辦
一、中斷處理為什麼要下半部?
Linux在中斷處理中間中斷處理分了上半部和下半部,目的就是提高系統的響應能力和並發能力。通俗一點來講:當一個中斷產生,調用該中斷對應的處理程序(上半部)然後告訴系統,對應的後半部可以執行了。然後中斷處理程序就返回,下半部會在合適的時機有系統調用。這樣一來就大大的減少了中斷處理所需要的時間。
二、那些工作應該放在上半部,那些應該放在下半部?
沒有嚴格的規則,只有一些提示:
1、對時間非常敏感,放在上半部。
2、與硬體相關的,放在上半部。
3、不能被其他中斷打斷的工作,放在上半部。
以上三點之外的,考慮放在下半部。
三、下半部機制在Linux中是怎麼實現的?
下半部在Linux中有以下實現機制:
1、BH(在2.5中刪除)
2、任務隊列(task queue,在2.5刪除)
3、軟中斷(softirq,2.3開始。本文重點)
4、tasklet(2.3開始)
5、工作隊列(work queue,2.5開始)
四、軟中斷是怎麼實現的(以下代碼出自2.6.32)?
軟中斷不會搶占另外一個軟中斷,唯一可以搶占軟中斷的是中斷處理程序。
軟中斷可以在不同CPU上並發執行(哪怕是同一個軟中斷)
1、軟中斷是編譯期間靜態分配的,定義如下:
struct softirq_action { void (*action)(struct softirq_action *); };
/*
* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
* frequency threaded job scheling. For almost all the purposes
* tasklets are more than enough. F.e. all serial device BHs et
* al. should be converted to tasklets, not to softirqs.
*/
enum {
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
/*
* map softirq index to softirq name. update 'softirq_to_name' in * kernel/softirq.c when adding a new softirq.
*/
extern char *softirq_to_name[NR_SOFTIRQS];
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
說明:
(1)、軟中斷的個數書上說是32,看來到這個版本已經發生變化了。
(2)、void (*action)(struct softirq_action *);傳遞整個結構體指針在於當結構體成員發生變化是,介面不變。
2、系統執行軟中斷一個注冊的軟中斷必須被標記後才會執行(觸發軟中斷),通常中斷處理程序會在返回前標記它的軟中斷。在下列地方,待處理的軟中斷會被執行:
(1)、從一個硬體中斷代碼處返回。
(2)、在ksoftirqd內核線程。
(3)、在那些顯示檢查和執行待處理的軟中斷代碼中。
ksoftirqd說明:
每個處理器都有一個這樣的線程。所有線程的名字都叫做ksoftirq/n,區別在於n,它對應的是處理器的編號。在一個雙CPU的機器上就有兩個這樣的線程,分別叫做ksoftirqd/0和ksoftirqd/1。為了保證只要有空閑的處理器,它們就會處理軟中斷,所以給每個處理器都分配一個這樣的線程。
執行軟中斷的代碼如下:
asmlinkage void __do_softirq(void)
{
struct softirq_action *h;
__u32 pending;
int max_restart = MAX_SOFTIRQ_RESTART;
int cpu;
pending = local_softirq_pending();
account_system_vtime(current);
__local_bh_disable((unsigned long)__builtin_return_address(0));
lockdep_softirq_enter();
cpu = smp_processor_id();
restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0);
local_irq_enable();
h = softirq_vec;
do {
if (pending & 1) {
int prev_count = preempt_count();
kstat_incr_softirqs_this_cpu(h - softirq_vec);
trace_softirq_entry(h, softirq_vec);
h->action(h);
trace_softirq_exit(h, softirq_vec);
if (unlikely(prev_count != preempt_count())) {
printk(KERN_ERR "huh, entered softirq %td %s %p"
"with preempt_count %08x,"
" exited with %08x?\n", h - softirq_vec,
softirq_to_name[h - softirq_vec],
h->action, prev_count, preempt_count());
preempt_count() = prev_count;
}
rcu_bh_qs(cpu);
}
h++;
pending >>= 1;
} while (pending);
local_irq_disable();
pending = local_softirq_pending();
if (pending && --max_restart)
goto restart;
if (pending)
wakeup_softirqd();
lockdep_softirq_exit();
account_system_vtime(current);
_local_bh_enable();
}
3、編寫自己的軟中斷
(1)、分配索引,在HI_SOFTIRQ與NR_SOFTIRQS中間添加自己的索引號。
(2)、注冊處理程序,處理程序:open_softirq(索引號,處理函數)。
(3)、觸發你的軟中斷:raise_softirq(索引號)。
4、軟中斷處理程序注意
(1)、軟中斷處理程序執行的時候,允許響應中斷,但自己不能休眠。
(2)、如果軟中斷在執行的時候再次觸發,則別的處理器可以同時執行,所以加鎖很關鍵。
㈡ 如何實現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用戶和內核空間中進行動態跟蹤
你不記得如何在代碼中插入探針點了嗎? 沒問題!了解如何使用uprobe和kprobe來動態插入它們吧。 基本上,程序員需要在源代碼匯編指令的不同位置插入動態探針點。
探針點
探針點是一個調試語句,有助於探索軟體的執行特性(即,執行流程以及當探針語句執行時軟體數據結構的狀態)。printk是探針語句的最簡單形式,也是黑客用於內核攻擊的基礎工具之一。
因為它需要重新編譯源代碼,所以printk插入是靜態的探測方法。內核代碼中重要位置上還有許多其他靜態跟蹤點可以動態啟用或禁用。 Linux內核有一些框架可以幫助程序員探測內核或用戶空間應用程序,而無需重新編譯源代碼。Kprobe是在內核代碼中插入探針點的動態方法之一,並且uprobe在用戶應用程序中執行此操作。
使用uprobe跟蹤用戶空間
可以通過使用thesysfs介面或perf工具將uprobe跟蹤點插入用戶空間代碼。
使用sysfs介面插入uprobe
考慮以下簡單測試代碼,沒有列印語句,我們想在某個指令中插入探針:
[source,c\n.test.c
#include <stdio.h>\n#include <stdlib.h>\n#include <unistd.h>
編譯代碼並找到要探測的指令地址:
# gcc -o test test.\n# objmp -d test
假設我們在ARM64平台上有以下目標代碼:
0000000000400620 <func_1>: 400620\t90000080\tadr\tx0, 410000 <__FRAME_END__+0xf6f8>
並且我們想在偏移量0x620和0x644之間插入探針。執行以下命令:
# echo 'p:func_2_entry test:0x620' > /sys/kernel/debug/tracing/uprobe_event\n# echo 'p:func_1_entry test:0x644' >> /sys/kernel/debug/tracing/uprobe_event\n# echo 1 > /sys/kernel/debug/tracing/events/uprobes/enable# ./test&
在上面的第一個和第二個echo語句中,p告訴我們這是一個簡單的測試。(探測器可以是簡單的或返回的。)func_n_entry是我們在跟蹤輸出中看到的名稱,名稱是可選欄位,如果沒有提供,我們應該期待像p_test_0x644這樣的名字。test 是我們要插入探針的可執行二進制文件。如果test 不在當前目錄中,則需要指定path_to_test / test。
0x620或0x640是從程序啟動開始的指令偏移量。請注意>>在第二個echo語句中,因為我們要再添加一個探針。所以,當我們在前兩個命令中插入探針點之後,我們啟用uprobe跟蹤,當我們寫入events/ uprobes / enable時,它將啟用所有的uprobe事件。程序員還可以通過寫入在該事件目錄中創建的特定事件文件來啟用單個事件。一旦探針點被插入和啟用,每當執行探測指令時,我們可以看到一個跟蹤條目。
讀取跟蹤文件以查看輸出:
# cat /sys/kernel/debug/tracing/trac\n# tracer: no\n\n# entries-in-buffer/entries-written: 8/8\n#P:\n\n# _-----=> irqs-of\n# / _----=> need-resche\n# | / _---=> hardirq/softir\n# || / _--=> preempt-dept\n# ||| / dela\n# TASK-PID CP\n# |||| TIMESTAMP FUNCTION# | | | |||| | |
我們可以看到哪個CPU完成了什麼任務,什麼時候執行了探測指令。
返回探針也可以插入指令。當返回該指令的函數時,將記錄一個條目:
# echo 0 > /sys/kernel/debug/tracing/events/uprobes/enabl\n# echo 'r:func_2_exit test:0x620' >> /sys/kernel/debug/tracing/uprobe_event\n# echo 'r:func_1_exit test:0x644' >> /sys/kernel/debug/tracing/uprobe_event\n# echo 1 > /sys/kernel/debug/tracing/events/uprobes/enable
這里我們使用r而不是p,所有其他參數是相同的。請注意,如果要插入新的探測點,需要禁用uprobe事件:
test-3009 [002] .... 4813.852674: func_1_entry: (0x400644)
上面的日誌表明,func_1返回到地址0x4006b0,時間戳為4813.852691。
# echo 0 > /sys/kernel/debug/tracing/events/uprobes/enabl\n# echo 'p:func_2_entry test:0x630' > /sys/kernel/debug/tracing/uprobe_events count=%x\n# echo 1 > /sys/kernel/debug/tracing/events/uprobes/enabl\n# echo > /sys/kernel/debug/tracing/trace# ./test&
當執行偏移量0x630的指令時,將列印ARM64 x1寄存器的值作為count =。
輸出如下所示:
test-3095 [003] .... 7918.629728: func_2_entry: (0x400630) count=0x1
使用perf插入uprobe
找到需要插入探針的指令或功能的偏移量很麻煩,而且需要知道分配給局部變數的CPU寄存器的名稱更為復雜。 perf是一個有用的工具,用於幫助引導探針插入源代碼中。
除了perf,還有一些其他工具,如SystemTap,DTrace和LTTng,可用於內核和用戶空間跟蹤;然而,perf與內核配合完美,所以它受到內核程序員的青睞。
# gcc -g -o test test.c# perf probe -x ./test func_2_entry=func_\n# perf probe -x ./test func_2_exit=func_2%retur\n# perf probe -x ./test test_15=test.c:1\n# perf probe -x ./test test_25=test.c:25 numbe\n# perf record -e probe_test:func_2_entry -e\nprobe_test:func_2_exit -e probe_test:test_15\n-e probe_test:test_25 ./test
如上所示,程序員可以將探針點直接插入函數start和return,源文件的特定行號等。可以獲取列印的局部變數,並擁有許多其他選項,例如調用函數的所有實例。 perf探針用於創建探針點事件,那麼在執行./testexecutable時,可以使用perf記錄來探測這些事件。當創建一個perf探測點時,可以使用其他錄音選項,例如perf stat,可以擁有許多後期分析選項,如perf腳本或perf報告。
使用perf腳本,上面的例子輸出如下:
# perf script
使用kprobe跟蹤內核空間
與uprobe一樣,可以使用sysfs介面或perf工具將kprobe跟蹤點插入到內核代碼中。
使用sysfs介面插入kprobe
程序員可以在/proc/kallsyms中的大多數符號中插入kprobe;其他符號已被列入內核的黑名單。還有一些與kprobe插入不兼容的符號,比如kprobe_events文件中的kprobe插入將導致寫入錯誤。 也可以在符號基礎的某個偏移處插入探針,像uprobe一樣,可以使用kretprobe跟蹤函數的返回,局部變數的值也可以列印在跟蹤輸出中。
以下是如何做:
; disable all events, just to insure that we see only kprobe output in trace\n# echo 0 > /sys/kernel/debug/tracing/events/enable; disable kprobe events until probe points are inseted\n# echo 0 > /sys/kernel/debug/tracing/events/kprobes/enable; clear out all the events from kprobe_events\n to insure that we see output for; only those for which we have enabled
[root@pratyush ~\n# more /sys/kernel/debug/tracing/trace# tracer: no\n\n# entries-in-buffer/entries-written: 9037/9037\n#P:8\n# _-----=> irqs-of\n# / _----=> need-resche\n# | / _---=> hardirq/softirq#\n|| / _--=> preempt-depth#\n ||| / delay# TASK-PID CPU#\n |||| TIMESTAMP FUNCTION#\n | | | |||| | |
使用perf插入kprobe
與uprobe一樣,程序員可以使用perf在內核代碼中插入一個kprobe,可以直接將探針點插入到函數start和return中,源文件的特定行號等。程序員可以向-k選項提供vmlinux,也可以為-s選項提供內核源代碼路徑:
# perf probe -k vmlinux kfree_entry=kfre\n# perf probe -k vmlinux kfree_exit=kfree%retur\n# perf probe -s ./ kfree_mid=mm/slub.c:3408 \n# perf record -e probe:kfree_entry -e probe:kfree_exit -e probe:kfree_mid sleep 10
使用perf腳本,以上示例的輸出:
關於Linux命令的介紹,看看《linux就該這么學》,具體關於這一章地址3w(dot)linuxprobe/chapter-02(dot)html
㈣ Linux環境變數順序
內核啟動的時候,各個驅動初始化的工作在文件init/main.c中的do_basic_setup()函數中做.
------------------------------------------------------------------------------------------------------
static void __init do_basic_setup(void)
{
/* drivers will send hotplug events */
init_workqueues();
usermodehelper_init();
driver_init();
#ifdef CONFIG_SYSCTL
sysctl_init();
#endif
/* Networking initialization needs a process context */
sock_init();
do_initcalls();
}
------------------------------------------------------------------------------------------------------
其中的driver_init()做一些核心的初始化,看看代碼就明白了.
相應的驅動程序的初始化在do_initcalls()中做.
------------------------------------------------------------------------------------------------------
static void __init do_initcalls(void)
{
initcall_t *call;
int count = preempt_count();
for (call = __initcall_start; call < __initcall_end; call++) {
char *msg;
if (initcall_debug) {
printk(KERN_DEBUG "Calling initcall 0x%p", *call);
print_fn_descriptor_symbol(": %s()", (unsigned long) *call);
printk("\n");
}
(*call)();
msg = NULL;
if (preempt_count() != count) {
msg = "preemption imbalance";
preempt_count() = count;
}
if (irqs_disabled()) {
msg = "disabled interrupts";
local_irq_enable();
}
if (msg) {
printk(KERN_WARNING "error in initcall at 0x%p: "
"returned with %s\n", *call, msg);
}
}
/* Make sure there is no pending stuff from the initcall sequence */
flush_scheled_work();
}
------------------------------------------------------------------------------------------------------
這個__initcall_start是在文件 arch/xxx/kernel/vmlinux.lds.S (其中的xxx 是你的體系結構的名稱,例如i386)
這個文件是內核ld的時候使用的.其中定義了各個sectioin,看看就明白了。
在這個文件中有個.initcall.init, 代碼如下:
------------------------------------------------------------------------------------------------------
__initcall_start = .;
.initcall.init : {
*(.initcall1.init)
*(.initcall2.init)
*(.initcall3.init)
*(.initcall4.init)
*(.initcall5.init)
*(.initcall6.init)
*(.initcall7.init)
}
------------------------------------------------------------------------------------------------------
這里有7個初始化的優先順序,內核會按照這個優先順序的順序依次載入.
這些優先順序是在文件include/linux/init.h 中定義的. 你注意一下宏 __define_initcall的實現就明白了.
相關代碼如下:
#define __define_initcall(level,fn) \
static initcall_t __initcall_##fn __attribute_used__ \
__attribute__((__section__(".initcall" level ".init"))) = fn
#define core_initcall(fn) __define_initcall("1",fn)
#define postcore_initcall(fn) __define_initcall("2",fn)
#define arch_initcall(fn) __define_initcall("3",fn)
#define subsys_initcall(fn) __define_initcall("4",fn)
#define fs_initcall(fn) __define_initcall("5",fn)
#define device_initcall(fn) __define_initcall("6",fn)
#define late_initcall(fn) __define_initcall("7",fn)
我們可以看到,我們經常寫的設備驅動程序中常用的mole_init其實就是對應了優先順序6:
#define __initcall(fn) device_initcall(fn)
#define mole_init(x) __initcall(x);
文章出處:http://www.diybl.com/course/6_system/linux/Linuxjs/2008628/128990.html
請採納。
㈤ 銆怢inux鍐呮牳|榪涚▼綆$悊銆0鍙風嚎紼媠wapper綆浠
鍦↙inux鍐呮牳鐨勪笘鐣岄噷錛0鍙風嚎紼嬶紙閫氬父縐頒負init_task鎴swapper錛夋壆婕旂潃鑷沖叧閲嶈佺殑瑙掕壊錛岀壒鍒鏄鍦ㄥ唴鏍稿垵濮嬪寲鐨勬棭鏈熼樁孌點傝╂垜浠娣卞叆鎺㈣ㄨ繖涓鏍稿績緇勪歡錛屼簡瑙e畠濡備綍椹卞姩鏁翠釜緋葷粺鍚鍔ㄦ祦紼嬨
棣栧厛錛岃╂垜浠浠嶭inux-6.1鍐呮牳鐗堟湰璇磋搗銆傚傛灉浣犲圭幆澧冩惌寤烘劅鍏磋叮錛屽彲浠ヤ粠鎴戠殑涓浜轟粨搴撳紑濮嬫帰緔錛歔鐜澧冩惌寤鴻剼鏈琞(https://gitee.com/kingdix10/envsetupeel)錛屼富浠撳簱[[涓諱粨搴撻摼鎺]]錛屾敼鍔ㄤ粨搴揫[鏀瑰姩浠撳簱閾炬帴]]錛岄┍鍔ㄧず渚媅[椹卞姩紺轟緥閾炬帴]]錛屼互鍙婅緟鍔╁伐鍏穂[杈呭姪宸ュ叿閾炬帴]]銆傝繖浜涜祫婧愬皢甯鍔╀綘鏇村ソ鍦扮悊瑙e拰瀹炶返銆
鍦ˋRM64鏋舵瀯涓錛屼竴鍒囧嬩簬0鍙風嚎紼嬬殑鍒濆嬪寲銆傚畠棣栧厛鍚鍔錛屽壋寤1鍙峰拰2鍙風嚎紼嬶紝鐒跺悗榪涘叆涓涓絳夊緟鐘舵侊紝榪欓氬父琚縐頒負idle鐘舵併傝繖涓榪囩▼鐢kernel_init鍜kernel_execve寮曞礆紝鍚庤呭紩瀵肩敤鎴鋒佺殑init紼嬪簭錛岃繘涓姝ュ壋寤哄叾浠栬繘紼嬬嚎紼嬨
鍦ㄦ傚康鍜屽垵濮嬪寲闃舵碉紝init_task鏄涓涓闈欐佺粨鏋勪綋錛屾槸鍐呮牳鍚鍔ㄧ殑璧風偣銆傚畠涓task_struct緔у瘑鐩歌繛錛屽垵濮嬪寲鏃惰劇疆浜嗗叧閿緇勪歡錛屽init_mm鍐呭瓨綆$悊緋葷粺銆佽皟搴﹀櫒銆佸畾鏃跺櫒銆佷腑鏂澶勭悊銆丳ID綆$悊鍜屽唴鏍哥紦瀛樼瓑銆
奼囩紪闃舵碉紝start_kernel鍚鍔ㄤ簡鍐呮牳鐨勬墽琛岋紝闆嗕腑綺懼姏浜庡垵濮嬪寲璋冨害鍣錛岃劇疆瀹氭椂鍣錛屼互鍙婂垵濮嬪寲cred錛堢敤鎴鋒爣璇嗭級鍜宖ork絳夋牳蹇冨姛鑳姐傚湪姝よ繃紼嬩腑錛屼腑鏂鐘舵佸緱鍒扮$悊錛岀郴緇熸椂閽熷拰寤惰繜璁$畻涔熷緱浠ヨ劇疆銆
榪涘叆arch_call_rest_init()錛岀郴緇熻繘涓姝ュ垵濮嬪寲rest閮ㄥ垎錛岄槻姝㈠熬璋冪敤浼樺寲錛屽苟涓烘瘡涓澶勭悊鍣ㄥ壋寤簊wapper綰跨▼銆傚湪1鍙風嚎紼嬭繍琛屽悗錛宻mp_init浼氳皟鐢╥dle_threads_init錛屼負闈瀊oot CPU鍒涘緩0鍙風嚎紼嬨
褰0鍙風嚎紼嬬粨鏉燂紝緋葷粺寮濮嬪壋寤1鍙峰拰2鍙風嚎紼嬶紝榪涜屽叏闈㈢殑璋冨害銆傚湪rest_init涓錛宨nit_task錛圥ID=1錛夎浼樺厛鍒涘緩錛岀『淇濆叾鍦╧threadd涔嬪墠榪愯岋紝騫惰劇疆緋葷粺鐘舵佷負SYSTEM_SCHEDULING銆傛ゆ椂錛schele()棣栨℃墽琛岋紝涓哄悗緇鐨勮繘紼嬭皟搴﹀犲畾浜嗗熀紜銆
鍦╥dle綰跨▼鐨勫驚鐜涓錛屾垜浠鐪嬪埌鍏抽敭鎿嶄綔濡備笅錛