導航:首頁 > 編程系統 > linux高並發編程

linux高並發編程

發布時間:2024-06-19 06:43:44

❶ 如何實現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;
};

❷ 楂樻ц兘寮傛io鏈哄埗錛歩o_uring

楂樻ц兘寮傛I/O澶勭悊鏈哄埗錛歩o_uring鐨勯潻鍛芥х獊鐮

闅忕潃Linux 5.10鐗堟湰鐨勯潻鏂幫紝io_uring寮傛IO鎺ュ彛搴旇繍鑰岀敓錛屽畠閫氳繃宸у欑殑鐢ㄦ埛絀洪棿鍐呭瓨鏄犲皠鍜屾棤閿佺幆褰㈤槦鍒楄捐★紝鏋佸ぇ鍦版彁鍗囦簡鏁版嵁澶勭悊鐨勬晥鐜囥俰o_uring浠ュ叾鐙鐗圭殑璁捐★紝灝嗕換鍔℃彁浜や笌緇撴灉榪斿洖鏃犵紳鏁村悎錛屽噺灝戜簡鍐呭瓨鎷瘋礉鐨勫紑閿錛屽睍鐜板嚭鍗撹秺鐨勬ц兘銆



io_uring鐨勬牳蹇冩満鍒跺湪浜庡叾鍙屽悜闃熷垪緇撴瀯錛屽寘鎷琒Q錛圫ubmit Queue錛夊拰CQ錛圕ompletion Queue錛夈係Q璐熻矗鎺ユ敹鐢ㄦ埛鐨処O璇鋒眰錛岃孋Q鍒欒礋璐i氱煡鐢ㄦ埛璇鋒眰鐨勫畬鎴愮姸鎬併傚畠浠閫氳繃鍐呭瓨灞忛殰鎿嶄綔淇濇寔鍚屾ワ紝鏃犻渶閿佹満鍒訛紝浠庤岄伩鍏嶄簡甯歌佺殑絝炴佹潯浠躲



緋葷粺璋冪敤鐨勫叧閿鐜鑺




涓轟簡鐩磋傚湴灞曠ずio_uring鐨勫▉鍔涳紝鎴戜滑閫氳繃瀹炴垬婕旂ず錛氫緥濡傦紝uring_cat紼嬪簭錛屽畠鍒╃敤io_uring灝佽呬簡鏂囦歡鎿嶄綔錛屽疄鐜板湪紓佺洏I/O鏂歸潰鐨勬樉钁楁ц兘鎻愬崌銆傚湪瀹為檯嫻嬭瘯涓錛屽規瘮寮傛ユā寮忥紙io_uring錛岃揪鍒版儕浜虹殑19.0k IOPS錛夊拰鍚屾ユā寮忥紙8k IOPS錛夛紝io_uring鍦ㄧ佺洏I/O鎬ц兘涓婂崰鎹浜嗘樉钁椾紭鍔褲



姝ゅ栵紝rust_echo_bench鏈嶅姟鍣ㄦ祴璇曡繘涓姝ヨ瘉瀹炰簡io_uring鐨勪紭瓚婃э紝瀹冨湪澶勭悊澶ч噺騫跺彂璇鋒眰鏃訛紝灞曠幇鍑哄崜瓚婄殑鍚炲悙閲忓拰鍝嶅簲閫熷害銆傛繁鍏ョ悊瑙io_uring鐨勬牳蹇冪郴緇熻皟鐢ㄦ帴鍙o紝鑳藉熷府鍔╁紑鍙戣呮洿鏈夋晥鍦板埄鐢ㄨ繖涓寮哄ぇ鐨勫伐鍏楓



瀹炶返搴旂敤涓庡︿範璧勬簮


瑕佹繁鍏ユ帉鎻io_uring錛孌PDK鏁欑▼鏄涓涓鏋佸ソ鐨勫︿範璧風偣銆傞氳繃瀹冿紝寮鍙戣呭彲浠ヤ簡瑙e備綍鍒╃敤io_uring榪涜岄珮鏁堢殑緗戠粶鍜孖/O鎿嶄綔錛屼互鍙婂備綍浼樺寲緋葷粺鎬ц兘銆傜珛鍗寵㈤槄錛岃笍涓奿o_uring鐨勯珮鎬ц兘涔嬫棶錛



灝界io_uring鎻愪緵浜嗗己澶х殑鎬ц兘鎻愬崌錛屼絾鍏朵嬌鐢ㄤ篃瑕佹眰寮鍙戣呭叿澶囦竴瀹氱殑緋葷粺緙栫▼鐭ヨ瘑鍜屽唴瀛樼$悊鎶宸с傞氳繃緇嗚嚧鐨勪唬鐮佺ず渚嬪拰瀹炶返錛屼綘灝嗚兘澶熼嗙暐io_uring鐨勫唴鍦ㄩ瓍鍔涳紝涓哄簲鐢ㄧ▼搴忓甫鏉ュ墠鎵鏈鏈夌殑閫熷害涓庢晥鐜囥

❸ 關於 linux 驅動中並發控制的方法有哪些

需要一定的努力才可以學好:
Linux設備驅動是linux內核的一部分,是用來屏蔽硬體細節,為上層提供標准介面的一種技術手段。為了能夠編寫出質量比較高的驅動程序,要求工程師必須具備以下幾個方面的知識:
1、 熟悉處理器的性能
如:處理器的體系結構、匯編語言、工作模式、異常處理等。對於初學者來說,在還不熟悉驅動編寫方法的情況下,可以先不把重心放在這一項上,因為可能因為它的枯燥、抽象而影響到你對設備驅動的興趣。隨著你不斷地熟悉驅動的編寫,你會很自然的意識到此項的重要性。
2、掌握驅動目標的硬體工作原理及通訊協議
如:串口控制器、顯卡控制器、硬體編解碼、存儲卡控制器、I2C通訊、SPI通訊、USB通訊、SDIO通訊、I2S通訊、PCI通訊等。編寫設備驅動的前提就是需要了解設備的操作方法,所以這些內容的重要程度不言而喻。但不是說要把所有設備的操作方法都熟悉了以後才可以寫驅動,你只需要了解你要驅動的硬體就可以了。
一、掌握硬體的控制方法
如:中斷、輪詢、DMA 等,通常一個硬體控制器會有多種控制方法,你需要根據系統性能的需要合理的選擇操作方法。初學階段以實現功能為目的,掌握的順序應該是,輪詢->中斷->DMA。隨著學習的深入,需要綜合考慮系統的性能需求,採取合適的方法。
二、良好的GNU C語言編程基礎
如:C語言的指針、結構體、內存操作、鏈表、隊列、棧、C和匯編混合編程等。這些編程語法是編寫設備驅動的基礎,無論對於初學者還是有經驗者都非常重要。
三、 良好的linux操作系統概念
如:多進程、多線程、進程調度、進程搶占、進程上下文、虛擬內存、原子操作、阻塞、睡眠、同步等概念及它們之間的關系。這些概念及方法在設備驅動里的使用是linux設備驅動區別單片機編程的最大特點,只有理解了它們才會編寫出高質量的驅動。
四、掌握linux內核中設備驅動的編寫介面
如:字元設備的cdev、塊設備的gendisk、網路設備的net_device,以及基於這些基本介面的framebuffer設備的fb_info、mtd設備的mtd_info、tty設備的tty_driver、usb設備的usb_driver、mmc設備的mmc_host等。

❹ linux騫跺彂紼嬪簭璁捐 瀹炶灝忕粨錛屾ユ眰錛侊紒錛侊紒瀛楁暟500~600

瀹為獙6 Linux榪涚▼騫跺彂紼嬪簭璁捐

1 瀹為獙鐩鐨勶細
鎺屾彙Linux鐜澧冧笅鐨勮繘紼嬪苟鍙戠▼搴忓強綆¢亾搴旂敤紼嬪簭鐨勭紪鍐欒佺偣銆

2 瀹為獙鍐呭瑰拰瀹為獙姝ラわ細
錛1錛 璋冭瘯騫惰繍琛3.10鐨勫苟鍙戠▼搴忚捐″疄渚嬶紝鏄劇ず緇撴灉鏄浠涔堬紝騫跺垎鏋愪箣銆

閫氳繃pipeline.c榪欎釜鏂囦歡錛岃皟鐢╟hild1.c鍜宖ather1.c榪欎袱涓紼嬪簭錛宖ather1.c鍐欏叆綆$悊錛岀劧鍚庡啀閫氳繃child1.c璇葷¢亾錛屽洜姝よ緭鍑轟負浠ヤ笂緇撴灉銆

錛2錛 緙栧啓涓涓騫跺彂紼嬪簭錛岀埗榪涚▼鎵撳嵃鈥淭he Parent is running鈥濓紱瀛愯繘紼嬫墦鍗扳淭he Child is running鈥濓紱

#include<stdio.h>

#include<unistd.h>

main()

{

int p1;

while((p1=fork())==-1);

if(p1>0)

{

wait(0);

printf("The Parent is running.\n");

}

else

{

printf("The Child is running.\n");

exit(0);

}

}

緇撴灉涓猴細

The Child is running.

The Parent is running.

錛3錛 緙栧啓涓涓綆¢亾搴旂敤紼嬪簭錛岀埗榪涚▼閫氳繃綆¢亾鎻愪緵瀛楃︿覆鈥減ut the string into the pipe.鈥濈粰瀛愯繘紼嬶紝瀛愯繘紼嬮氳繃綆¢亾鎺ユ敹榪欐潯淇℃伅錛岀劧鍚庢墦鍗拌緭鍑恆

#include<stdio.h>

#include<unistd.h>

main()

{

int p1,fd[2];

char outpipe[50]; //瀹氫箟璇葷紦鍐插尯

char inpipe[50]="put the string into the pipe."; //瀹氫箟鍐欑紦鍐插尯

pipe(fd); //鍒涘緩鏃犲悕綆¢亾fd

while((p1=fork())==-1);

if (p1>0) //鐖惰繘紼嬭繑鍥

{

write(fd[1],inpipe,50); //鍐欎俊鎮鍒扮¢亾

wait(0);

}

else //瀛愯繘紼嬭繑鍥

{

read(fd[0],outpipe,50); //浠庣¢亾璇諱俊鎮鍒拌葷紦鍐插尯

printf("%s\n",outpipe); //鏄劇ず璇誨埌鐨勪俊鎮

exit(0);

}

}

緇撴灉涓猴細

put the string into the pipe.

3錛庡疄楠岃佹眰錛氬啓鍑哄疄楠屾姤鍛婂苟灝嗙粨鏋滀笂浼犲埌FTP SERVER涓婅嚜宸辯殑浣滀笟鐩褰曘

❺ 高性能網路伺服器編程:為什麼linux下epoll

基本的IO編程過程(包括網路IO和文件IO)是,打開文件描述符(windows是handler,java是stream或channel),多路捕獲(Multiplexe,即select和poll和epoll)IO可讀寫的狀態,而後可以讀寫的文件描述符進行IO讀寫,由於IO設備速度和CPU內存比速度會慢,為了更好的利用CPU和內存,會開多線程,每個線程讀寫一個文件描述符。
但C10K問題,讓我們意識到在超大數量的網路連接下,機器設備和網路速度不再是瓶頸,瓶頸在於操作系統和IO應用程序的溝通協作的方式。
舉個例子,一萬個socket連接過來,傳統的IO編程模型要開萬個線程來應對,還要注意,socket會關閉打開,一萬個線程要不斷的關閉線程重建線程,資源都浪費在這上面了,我們算建立一個線程耗1M內存,1萬個線程機器至少要10G內存,這在IA-32的機器架構下基本是不可能的(要開PAE),現在x64架構才有可能舒服點,要知道,這僅僅是粗略算的內存消耗。別的資源呢?
所以,高性能的網路編程(即IO編程),第一,需要松綁IO連接和應用程序線程的對應關系,這就是非阻塞(nonblocking)、非同步(asynchronous)的要求的由來(構造一個線程池,epoll監控到有數的fd,把fd傳入線程池,由這些worker thread來讀寫io)。第二,需要高性能的OS對IO設備可讀寫(數據來了)的通知方式:從level-triggered notification到edge-triggered notification,關於這個通知方式,我們稍後談。
需要注意非同步,不等於AIO(asynchronous IO),Linux的AIO和java的AIO都是實現非同步的一種方式,都是渣,這個我們也接下來會談到。
針對前面說的這兩點,我們看看select和poll的問題
這兩個函數都在每次調用的時候要求我們把需要監控(看看有沒有數據)的文件描述符,通過數組傳遞進入內核,內核每次都要掃描這些文件描述符,去理解它們,建立一個文件描述符和IO對應的數組(實際內核工作會有好點的實現方式,但可以這么理解先),以便IO來的時候,通知這些文件描述符,進而通知到進程里等待的這些select、poll。當有一萬個文件描述符要監控的時候呢(一萬個網路連接)?這個工作效率是很低的,資源要求卻很高。
我們看epoll
epoll很巧妙,分為三個函數,第一個函數創建一個session類似的東西,第二函數告訴內核維持這個session,並把屬於session內的fd傳給內核,第三個函數epoll_wait是真正的監控多個文件描述符函數,只需要告訴內核,我在等待哪個session,而session內的fd,內核早就分析過了,不再在每次epoll調用的時候分析,這就節省了內核大部分工作。這樣每次調用epoll,內核不再重新掃描fd數組,因為我們維持了session。
說道這里,只有一個字,開源,贊,眾人拾柴火焰高,贊。
epoll的效率還不僅僅體現在這里,在內核通知方式上,也改進了,我們先看select和poll的通知方式,也就是level-triggered notification,內核在被DMA中斷,捕獲到IO設備來數據後,本來只需要查找這個數據屬於哪個文件描述符,進而通知線程里等待的函數即可,但是,select和poll要求內核在通知階段還要繼續再掃描一次剛才所建立的內核fd和io對應的那個數組,因為應用程序可能沒有真正去讀上次通知有數據後的那些fd,應用程序上次沒讀,內核在這次select和poll調用的時候就得繼續通知,這個os和應用程序的溝通方式效率是低下的。只是方便編程而已(可以不去讀那個網路io,方正下次會繼續通知)。
於是epoll設計了另外一種通知方式:edge-triggered notification,在這個模式下,io設備來了數據,就只通知這些io設備對應的fd,上次通知過的fd不再通知,內核不再掃描一大堆fd了。
基於以上分析,我們可以看到epoll是專門針對大網路並發連接下的os和應用溝通協作上的一個設計,在linux下編網路伺服器,必然要採用這個,nginx、PHP的國產非同步框架swool、varnish,都是採用這個。
注意還要打開epoll的edge-triggered notification。而java的NIO和NIO.2都只是用了epoll,沒有打開edge-triggered notification,所以不如JBoss的Netty。
接下來我們談談AIO的問題,AIO希望的是,你select,poll,epoll都需要用一個函數去監控一大堆fd,那麼我AIO不需要了,你把fd告訴內核,你應用程序無需等待,內核會通過信號等軟中斷告訴應用程序,數據來了,你直接讀了,所以,用了AIO可以廢棄select,poll,epoll。
但linux的AIO的實現方式是內核和應用共享一片內存區域,應用通過檢測這個內存區域(避免調用nonblocking的read、write函數來測試是否來數據,因為即便調用nonblocking的read和write由於進程要切換用戶態和內核態,仍舊效率不高)來得知fd是否有數據,可是檢測內存區域畢竟不是實時的,你需要在線程里構造一個監控內存的循環,設置sleep,總的效率不如epoll這樣的實時通知。所以,AIO是渣,適合低並發的IO操作。所以java7引入的NIO.2引入的AIO對高並發的網路IO設計程序來說,也是渣,只有Netty的epoll+edge-triggered notification最牛,能在linux讓應用和OS取得最高效率的溝通。

閱讀全文

與linux高並發編程相關的資料

熱點內容
word2007層次結構 瀏覽:456
去掉文件名的數字 瀏覽:713
word公司 瀏覽:710
淘寶店數據包怎麼上傳 瀏覽:341
pbt文件 瀏覽:204
HX基礎編程怎麼改變字體 瀏覽:876
怎麼開網路教學 瀏覽:915
630升級工程武器 瀏覽:936
用換機助手接收的軟體文件在哪找 瀏覽:282
閱達app一教一輔五年級有哪些 瀏覽:7
win10系統用f2調節音量 瀏覽:19
壓縮文件密碼器 瀏覽:840
線下活動數據分析有哪些 瀏覽:314
助聽器插片式編程線如何連接 瀏覽:293
怎麼刪除系統休眠文件 瀏覽:914
搜索文件內容中包含的文字並替換 瀏覽:542
微信相冊程序圖標 瀏覽:714
win8怎麼顯示文件格式 瀏覽:547
文件伺服器中毒 瀏覽:721
如何修改網站訪問次數 瀏覽:518

友情鏈接