Linux進程管理核心機制:從調度到RCU的底層原理
每天在 Linux 用ps -ef看進程、top盯 CPU 時,你或許會好奇:多進程搶 CPU,為何瀏覽器不卡、數據庫能響應?高并發下內核讀寫共享數據,怎沒因 “鎖” 拖慢速度?答案藏在 Linux 進程管理的兩大核心機制里:一是進程調度機制,像 “交通警察” 分配 CPU 資源,決定進程運行優先級與時長,直接影響系統響應;二是RCU 機制,似 “高并發數據管家”,解決多線程讀寫共享數據的 “快與安全” 難題,多核場景下至關重要。
不管你做后端、運維,還是想深入內核,懂這兩大機制都是 “看透 Linux 本質” 的關鍵 —— 比如明白調度策略,就懂nice命令調優先級的原理;搞懂 RCU,就理解高并發 “無鎖訪問” 的實現。接下來會從實際問題出發,不堆砌晦澀源碼,拆解進程調度的優先級邏輯、調度策略,以及 RCU 的 “讀 - 復制 - 更新” 核心思路,幫你搞懂內核如何高效管理資源與數據。
一、RCU 機制是什么?
RCU,全稱 Read - Copy - Update,即讀 - 拷貝 - 更新,是 Linux 內核中一種用于實現高效并發控制的同步機制,特別適用于讀多寫少的場景。與傳統的鎖機制(如互斥鎖、讀寫鎖等)不同,RCU 通過獨特的設計,盡可能減少讀操作的開銷,實現讀操作的無鎖化,從而大大提高系統在高并發讀取情況下的性能。
在操作系統中,數據一致性訪問是一個非常重要的部分,通常我們可以采用鎖機制實現數據的一致性訪問。例如,semaphore、spinlock機制,在訪問共享數據時,首先訪問鎖資源,在獲取鎖資源的前提下才能實現數據的訪問。這種原理很簡單,根本的思想就是在訪問臨界資源時,首先訪問一個全局的變量(鎖),通過全局變量的狀態來控制線程對臨界資源的訪問。但是,這種思想是需要硬件支持的,硬件需要配合實現全局變量(鎖)的讀-修改-寫,現代CPU都會提供這樣的原子化指令。采用鎖機制實現數據訪問的一致性存在如下兩個問題:
- 效率問題。鎖機制的實現需要對內存的原子化訪問,這種訪問操作會破壞流水線操作,降低了流水線效率。這是影響性能的一個因素。另外,在采用讀寫鎖機制的情況下,寫鎖是排他鎖,無法實現寫鎖與讀鎖的并發操作,在某些應用下會降低性能。
- 擴展性問題。當系統中CPU數量增多的時候,采用鎖機制實現數據的同步訪問效率偏低。并且隨著CPU數量的增多,效率降低,由此可見鎖機制實現的數據一致性訪問擴展性差。
圖片
- 讀者無鎖訪問:在 RCU 機制下,讀者線程在訪問被保護的共享數據時,不需要獲取任何鎖。這意味著多個讀者線程可以同時并發地訪問共享數據,而不會因為鎖競爭而產生等待和性能損耗 。例如,在一個多線程的文件系統中,當多個線程需要讀取文件目錄結構時,如果采用 RCU 機制,這些讀操作可以并行進行,極大提高了讀取效率。
- 寫者復制更新:當寫者線程需要修改共享數據時,不會直接在原數據上進行操作。相反,寫者會首先創建一個原數據的副本,然后在這個副本上進行修改。修改完成后,通過一個原子操作,將指向原數據的指針更新為指向新的修改后的副本。這樣做的好處是,在寫者進行修改的過程中,讀者線程仍然可以繼續訪問原數據,不會受到寫操作的影響。比如,在更新網絡設備的配置信息時,寫者先復制當前配置數據,修改副本后再更新指針,讀配置信息的線程不會被打斷。
- 寬限期(Grace Period):在寫者完成數據更新并切換指針后,并不會立即釋放舊數據的內存空間。這是因為可能還有一些讀者線程在寫操作開始前就已經進入臨界區,正在訪問舊數據。只有當所有在寫操作開始前進入臨界區的讀者都退出臨界區后,舊數據才會被安全地釋放。從寫者完成更新到舊數據被釋放的這段時間,就稱為寬限期。寬限期的實現依賴于內核中對 CPU 上下文切換等事件的監測,當所有 CPU 都經歷了一次上下文切換(表示之前的讀操作都已完成),寬限期結束,舊數據可以被回收。
- 發布 - 訂閱模式(Publish-Subscribe Pattern):為了確保讀者和寫者之間的數據一致性,RCU 機制引入了發布 - 訂閱模式。寫者在完成數據修改并準備切換指針時,通過特定的操作(如rcu_assign_pointer)“發布” 新的數據。讀者在訪問共享數據時,使用rcu_dereference操作 “訂閱” 數據。這些操作結合內存屏障(Memory Barrier)技術,保證了寫者發布新數據的操作對讀者可見,同時防止指令重排序導致的數據不一致問題。例如,在路由表更新場景中,寫者更新路由表后發布新表,讀者通過訂閱獲取最新且一致的路由信息用于數據轉發。
RCU的關鍵思想有兩個:①復制后更新;②延遲回收內存。典型的RCU更新時序如下:
- 復制:將需要更新的數據復制到新內存地址;
- 更新:更新復制數據,這時候操作的新的內存地址;
- 替換:使用新內存地址指針替換舊數據內存地址指針,
- 此后舊數據將無法被后續讀者訪問;
- 等待,所有訪問舊數據的讀者進入靜默期,即訪問舊數據完成;
- 回收:當沒有任何持有舊數據結構引用的讀者后,安全地回收舊數據內存。
二、RCU 機制如何工作?
RCU 機制的工作原理主要圍繞讀操作和寫操作展開,同時涉及寬限期的管理,以確保數據一致性和高效的并發訪問。
2.1讀操作流程
- 進入臨界區:讀者線程在訪問被 RCU 保護的共享數據前,調用rcu_read_lock函數。這個函數的主要作用是標記讀操作的開始,同時通過preempt_disable關閉內核搶占(防止在讀取過程中被其他高優先級任務搶占,導致數據訪問不一致),但允許中斷發生。例如,在一個多線程的數據庫查詢場景中,當一個線程調用rcu_read_lock后,它就開始了對數據庫表結構(共享數據)的讀取操作,此時不會因為內核調度其他任務而被打斷讀取流程。
- 數據訪問:進入臨界區后,讀者線程使用rcu_dereference操作來安全地獲取指向共享數據的指針并訪問數據。rcu_dereference結合內存屏障技術,確保讀者線程能夠看到最新的、一致的數據。比如在讀取網絡配置參數時,rcu_dereference能保證讀取到的是完整且最新的配置信息,而不會因為寫操作正在進行而讀到部分更新或不一致的數據。
- 離開臨界區:完成數據訪問后,讀者線程調用rcu_read_unlock函數,標記讀操作的結束,并通過preempt_enable重新開啟內核搶占。這樣,系統又可以正常調度其他任務,不會因為本次讀操作而影響系統的整體調度。
#include <linux/rculist.h>
#include <linux/sched.h>
#include <linux/module.h>
// 定義一個被RCU保護的共享數據結構(示例:簡單鏈表節點)
struct rcu_demo_node {
int data;
struct list_head list;
};
// 全局共享鏈表(被RCU保護)
static LIST_HEAD(rcu_demo_list);
static DEFINE_SPINLOCK(rcu_demo_lock); // 寫操作時使用的鎖
// RCU讀操作示例函數
static void rcu_reader_example(void)
{
struct rcu_demo_node *node;
// 1. 進入RCU讀臨界區:禁用搶占,標記讀操作開始
rcu_read_lock();
// 2. 安全訪問共享數據:通過rcu_dereference獲取指針并遍歷
list_for_each_entry_rcu(node, &rcu_demo_list, list) {
// 訪問數據(此時即使有寫操作,也能看到一致的舊版本或新版本數據)
pr_info("RCU Reader: Read data = %d\n", node->data);
}
// 3. 退出RCU讀臨界區:恢復搶占,標記讀操作結束
rcu_read_unlock();
}
// 模塊初始化函數(示例:啟動一個讀操作)
static int __init rcu_demo_init(void)
{
pr_info("RCU demo module loaded\n");
rcu_reader_example(); // 執行RCU讀操作
return 0;
}
// 模塊退出函數
static void __exit rcu_demo_exit(void)
{
pr_info("RCU demo module unloaded\n");
}
module_init(rcu_demo_init);
module_exit(rcu_demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("RCU Read Operation Example");- 進入臨界區:rcu_read_lock() 函數會禁用當前線程的內核搶占(通過 preempt_disable),同時將當前線程標記為活躍的 RCU 讀者。這一步確保讀操作不會被內核調度打斷,避免延長寫操作的等待時間。
- 數據訪問:使用 list_for_each_entry_rcu 宏遍歷鏈表(內部封裝了 rcu_dereference),確保通過 RCU 安全機制獲取指針。該宏會插入內存屏障,防止 CPU 指令重排序,保證讀取到的數據是一致的快照。
- 退出臨界區:rcu_read_unlock() 函數恢復內核搶占(preempt_enable),并解除當前線程的 RCU 讀者標記。此時若有寫操作等待回收舊數據,系統會在所有讀者退出后進行清理。
注意:此代碼為內核態示例,RCU 是 Linux 內核的同步機制,用戶態程序通常不直接使用。實際使用中,還需要配合寫操作的 RCU 機制(如 rcu_assign_pointer、call_rcu 等)才能完整工作。
2.2寫操作流程
- 復制數據:當寫者線程需要修改共享數據時,首先分配一塊新的內存空間,用于存放數據的副本。例如,在更新一個鏈表節點的數據時,寫者會創建一個新的節點,其結構與原節點相同。然后將原數據的內容完整地復制到新的副本中。以更新文件系統的元數據為例,寫者會復制當前的元數據結構到新的內存區域,確保新副本包含原數據的所有信息。
- 修改副本:在新的副本上進行數據修改操作。由于這是對副本進行修改,不會影響正在被讀者線程訪問的原數據,保證了讀操作的連續性和一致性。比如在修改路由表項時,寫者在副本上更新目標地址、下一跳等信息,整個修改過程對讀路由表的線程透明。
- 指針替換:完成修改后,寫者使用一個原子操作(如rcu_assign_pointer)將指向原數據的指針更新為指向新的修改后的副本。這個原子操作保證了指針切換的原子性,避免了讀者線程看到不一致的指針狀態。例如,在更新系統的設備列表時,通過原子操作切換指針,使得新的設備列表信息能夠立即被后續的讀操作獲取到,同時保證了當前正在進行的讀操作不會受到影響。
- 注冊回調函數:寫者注冊一個回調函數,用于在寬限期結束后釋放舊數據的內存空間。這個回調函數會被加入到 RCU 的回調函數隊列中,等待寬限期結束后執行。
#include <linux/rculist.h>
#include <linux/sched.h>
#include <linux/module.h>
#include <linux/slab.h>
// 定義帶RCU頭的共享數據結構
struct rcu_demo_node {
int data;
struct list_head list;
struct rcu_head rcu; // 用于RCU回調回收
};
// 全局共享鏈表及寫鎖
static LIST_HEAD(rcu_demo_list);
static DEFINE_SPINLOCK(rcu_demo_lock);
// 舊數據回收回調函數
static void rcu_node_free(struct rcu_head *rcu)
{
struct rcu_demo_node *node = container_of(rcu, struct rcu_demo_node, rcu);
kfree(node); // 寬限期結束后釋放舊節點
pr_info("RCU Writer: Old node freed\n");
}
// RCU寫操作示例函數(修改指定節點數據)
static void rcu_writer_example(int old_val, int new_val)
{
struct rcu_demo_node *old_node, *new_node;
unsigned long flags;
// 1. 查找需要修改的舊節點(簡化示例,實際需遍歷查找)
spin_lock_irqsave(&rcu_demo_lock, flags);
list_for_each_entry(old_node, &rcu_demo_list, list) {
if (old_node->data == old_val) {
// 2. 復制數據:分配新節點并復制舊數據
new_node = kmalloc(sizeof(*new_node), GFP_KERNEL);
if (!new_node) {
spin_unlock_irqrestore(&rcu_demo_lock, flags);
return;
}
*new_node = *old_node; // 復制舊節點數據
// 3. 修改副本:在新節點上修改數據
new_node->data = new_val;
pr_info("RCU Writer: Modified data from %d to %d\n", old_val, new_val);
// 4. 指針替換:原子替換鏈表節點
list_replace_rcu(&old_node->list, &new_node->list);
// 5. 注冊回調:寬限期后釋放舊節點
call_rcu(&old_node->rcu, rcu_node_free);
break;
}
}
spin_unlock_irqrestore(&rcu_demo_lock, flags);
}
// 初始化函數:添加測試節點并執行寫操作
static int __init rcu_demo_init(void)
{
struct rcu_demo_node *node;
unsigned long flags;
// 初始化測試節點
node = kmalloc(sizeof(*node), GFP_KERNEL);
node->data = 100;
spin_lock_irqsave(&rcu_demo_lock, flags);
list_add_rcu(&node->list, &rcu_demo_list);
spin_unlock_irqrestore(&rcu_demo_lock, flags);- 復制數據:通過kmalloc分配新節點,使用*new_node = *old_node完整復制舊節點數據,確保新副本包含所有原始信息。
- 修改副本:直接在新節點new_node上修改目標字段(new_node->data = new_val),此操作完全獨立于舊節點,不影響讀者對舊數據的訪問。
- 指針替換:使用list_replace_rcu宏(內部封裝rcu_assign_pointer)原子性替換鏈表節點,保證讀者要么看到舊指針,要么看到新指針,不會出現中間狀態。
- 注冊回調:通過call_rcu注冊rcu_node_free回調函數,RCU 機制會在所有活躍讀者退出臨界區(寬限期結束)后自動執行該函數,安全釋放舊節點內存。
此示例完整展示了 RCU"讀無鎖、寫復制" 的核心思想,寫操作不會阻塞讀操作,讀操作也不會阻塞寫操作,大幅提升了高并發場景下的性能。
2.3寬限期的作用與實現
在 RCU(Read-Copy-Update)機制中,寬限期(Grace Period)是保障數據安全回收的核心機制,其設計直接決定了 RCU 在并發場景下的正確性。
三、RCU 機制的優勢
3.1性能提升
在高并發讀取場景下,RCU 機制通過允許讀者無鎖訪問共享數據,顯著減少了鎖競爭和同步開銷,從而極大地提升了系統性能。在傳統的鎖機制中,當多個讀者線程試圖同時訪問共享數據時,會因為鎖的存在而產生競爭。例如,使用互斥鎖時,每次只能有一個線程獲取鎖并訪問數據,其他線程必須等待鎖的釋放,這會導致大量的上下文切換和等待時間,嚴重降低系統的并發處理能力。
而在 RCU 機制下,讀者線程無需獲取鎖即可直接訪問共享數據。以一個多線程的數據庫查詢系統為例,假設有大量的查詢線程(讀者)需要讀取數據庫中的用戶信息表(共享數據)。在高并發情況下,如果使用傳統鎖機制,線程之間會頻繁競爭鎖資源,導致查詢操作的延遲增加。但采用 RCU 機制后,這些查詢線程可以同時無鎖地讀取用戶信息表,大大提高了查詢的并發處理能力,減少了響應時間 。
3.2擴展性好
RCU 機制在多核系統中展現出良好的擴展性,不會隨著 CPU 數量的增加而導致性能下降。隨著硬件技術的發展,多核處理器被廣泛應用,系統中 CPU 核心數量不斷增多。在這種情況下,傳統的同步機制面臨著嚴峻的挑戰。例如,自旋鎖在多核環境下,如果多個 CPU 核心同時競爭同一個鎖,會導致大量的 CPU 時間浪費在自旋等待上,隨著 CPU 數量的增加,這種競爭會更加激烈,從而嚴重影響系統性能。而 RCU 機制的設計理念使其天然適合多核環境。在多核系統中,每個 CPU 核心上的線程都可以作為讀者無鎖地訪問共享數據,不會因為 CPU 數量的增加而產生額外的鎖競爭開銷。
例如,在一個具有多個 CPU 核心的服務器系統中,網絡路由表(共享數據)需要被頻繁讀取和偶爾更新。使用 RCU 機制,各個 CPU 核心上的網絡處理線程可以高效地讀取路由表,而寫者線程在更新路由表時,也不會影響其他 CPU 核心上的讀操作,系統的整體性能能夠隨著 CPU 核心數量的增加而線性提升 。
3.3無死鎖風險
死鎖是多線程編程中常見的問題,當多個線程相互等待對方釋放鎖資源時,就會陷入死鎖狀態,導致程序無法繼續執行。傳統的鎖機制,如互斥鎖、讀寫鎖等,如果使用不當,很容易出現死鎖問題。例如,線程 A 持有鎖 1 并試圖獲取鎖 2,而線程 B 持有鎖 2 并試圖獲取鎖 1,此時就會發生死鎖。而 RCU 機制能有效避免死鎖問題。
在 RCU 中,讀者線程在訪問共享數據時不需要獲取鎖,這就從根本上消除了因為讀者和寫者之間或讀者之間的鎖依賴而導致的死鎖可能性。寫者線程雖然在更新數據時需要進行一些同步操作,但由于其采用復制更新和寬限期的策略,也不會與讀者線程形成死鎖關系。例如,在一個多線程的文件系統實現中,使用 RCU 機制來保護文件元數據的訪問。讀者線程在讀取文件元數據時無需鎖,寫者線程在更新元數據時,先復制數據進行修改,然后等待寬限期結束后才替換舊數據,整個過程中不存在鎖的循環等待情況,確保了系統的穩定性和可靠性 。
四、RCU 機制的局限性
4.1寫操作開銷大
RCU 機制雖然在提升讀操作性能方面表現出色,但寫操作卻存在較大開銷。寫者在更新數據時,需要進行數據復制操作,這不僅消耗額外的內存資源,還增加了時間開銷。以更新一個包含大量元素的數組為例,寫者需要分配新的內存空間,并將原數組的所有元素逐一復制到新的副本中,這個過程會占用較多的內存帶寬和 CPU 時間。
此外,寫者還需要等待寬限期結束后才能釋放舊數據的內存空間,這意味著在寬限期內,系統需要維護新舊兩份數據,進一步增加了內存的使用壓力。如果寫操作頻繁發生,這些開銷可能會對系統的整體性能產生顯著影響,導致系統響應變慢、內存利用率降低等問題 。
4.2適用場景有限
RCU 機制主要適用于讀多寫少的場景,在這種場景下,其無鎖讀的特性能夠充分發揮優勢,提高系統的并發性能。然而,在寫操作頻繁的場景中,RCU 機制的性能表現可能并不理想。由于寫者在更新數據時需要復制數據和等待寬限期,這會導致寫操作的延遲增加。例如,在一個實時數據庫系統中,如果寫操作頻繁,RCU 機制可能無法滿足系統對寫操作的實時性要求。
此外,對于那些需要頻繁進行數據一致性更新的場景,RCU 機制可能也不太適用。因為 RCU 機制在更新數據時,存在一定的時間窗口,期間讀者可能會讀取到舊數據,這在一些對數據一致性要求極高的場景(如金融交易系統)中是不可接受的 。
4.3實現復雜
RCU 機制的實現依賴于底層的內存屏障和原子操作等技術,這使得其實現和理解都相對復雜。內存屏障用于確保內存操作的順序性和可見性,防止 CPU 或編譯器的優化導致數據不一致問題。原子操作則用于保證數據更新的原子性,避免并發訪問時的數據沖突。例如,在 x86 架構下,rcu_assign_pointer函數中會使用特定的內存屏障指令(如mfence)來確保指針更新的可見性和順序性。這些底層技術對于開發者來說,需要深入了解硬件和操作系統的原理才能正確運用。此外,RCU機制的調試也比較困難,因為其涉及到復雜的并發控制和寬限期管理。當出現數據不一致或性能問題時,很難快速定位和解決問題,需要開發者具備豐富的經驗和深入的知識 。
五、RCU 機制的應用場景
5.1內核數據結構管理
在 Linux 內核中,鏈表和哈希表是常用的數據結構,用于管理各種系統資源和信息。RCU 機制在這些數據結構的管理中發揮著重要作用,極大地提高了內核在多線程環境下的并發性能。
以鏈表為例,在傳統的鏈表操作中,如果多個線程同時對鏈表進行讀寫操作,需要使用鎖機制來保證數據的一致性和完整性。例如,當一個線程要遍歷鏈表(讀操作)時,另一個線程可能正在刪除鏈表中的節點(寫操作),如果沒有鎖的保護,讀操作可能會訪問到已經被刪除的節點,導致程序崩潰或數據錯誤。
而使用 RCU 機制,讀者線程在遍歷鏈表時不需要獲取鎖,可以無鎖并發地訪問鏈表。當寫者線程要刪除鏈表中的節點時,先將節點從鏈表中移除,但并不立即釋放該節點的內存。而是等待寬限期結束,確保所有可能訪問該節點的讀者線程都已完成訪問后,再安全地釋放節點內存。這樣,既保證了讀操作的高效性,又確保了寫操作不會影響正在進行的讀操作 。
對于哈希表,RCU 機制同樣能提升其并發性能。哈希表常用于快速查找數據,在 Linux 內核中,如網絡協議棧中的路由表就常以哈希表的形式實現。當多個線程需要查找哈希表中的數據(讀操作)時,RCU 機制允許它們無鎖地進行并發查找,提高了查找效率。而當寫者線程要更新哈希表中的數據(如添加或刪除一個路由表項)時,先創建一個新的哈希表副本,在副本上進行修改,然后通過原子操作將指向原哈希表的指針更新為指向新的副本。在寬限期內,舊的哈希表仍然保留,以確保正在進行的讀操作可以繼續正常進行。這種方式避免了傳統鎖機制下讀寫操作相互等待的問題,提高了哈希表在高并發環境下的性能和穩定性 。
5.2文件系統
在文件系統中,文件元數據包含了文件的各種屬性信息,如文件大小、創建時間、所有者等,對文件元數據的高效讀寫對于文件系統的性能至關重要。RCU 機制通過其獨特的設計,有效地提升了文件系統對文件元數據的處理能力。
當多個線程需要讀取文件元數據時,RCU 機制允許這些讀操作無鎖并發進行。例如,在一個多用戶的服務器系統中,多個用戶可能同時查看同一個目錄下的文件列表,每個用戶的操作都涉及讀取文件元數據。使用 RCU 機制,這些讀操作可以并行執行,大大提高了文件系統的響應速度,減少了用戶等待時間。
而當寫者線程需要更新文件元數據時,如修改文件的權限或所有者信息,寫者首先復制當前的文件元數據結構,在副本上進行修改。完成修改后,通過原子操作將指向原文件元數據的指針更新為指向新的修改后的副本。在寬限期內,舊的文件元數據仍然可供讀者線程訪問,確保了讀操作的連續性。只有當寬限期結束,所有可能訪問舊文件元數據的讀者線程都完成訪問后,舊的文件元數據才會被安全地釋放。這種方式避免了傳統鎖機制下讀寫操作相互阻塞的問題,提高了文件系統在處理大量并發讀寫請求時的性能和穩定性 。
5.3網絡協議棧
在網絡協議棧中,路由表用于存儲網絡路由信息,指導數據包的轉發。路由表的查詢操作非常頻繁,而更新操作相對較少,這使得 RCU 機制成為優化路由表操作的理想選擇。
當網絡設備接收到一個數據包時,需要查詢路由表來確定數據包的轉發路徑。在高并發的網絡環境中,可能有大量的數據包同時到達,需要頻繁查詢路由表。使用 RCU 機制,多個查詢線程(讀者)可以無鎖并發地訪問路由表,大大提高了查詢效率,確保數據包能夠快速轉發,減少網絡延遲。
當網絡拓撲發生變化或新的路由信息加入時,需要更新路由表(寫操作)。寫者線程在更新路由表時,首先創建一個新的路由表副本,在副本上進行修改,如添加、刪除或修改路由表項。修改完成后,通過原子操作將指向原路由表的指針更新為指向新的副本。在寬限期內,舊的路由表仍然保留,以確保正在進行的查詢操作可以繼續正常進行。這樣,既保證了路由表查詢操作的高效性,又確保了路由表更新操作不會影響網絡數據包的正常轉發,提高了網絡通信的效率和穩定性 。
六、如何使用 RCU 機制
6.1相關 API 介紹
如果指針ptr指向被RCU保護的數據結構,直接反引用指針是被禁止的,首先必須調用rcu_dereference(ptr),然后反引用返回的結果,需要使用rcu_read_lock和rcu_read_unlock調用來進行保護。
rcu_read_lock()
rcu_read_unlock()
synchronize_rcu()/call_rcu()
rcu_assign_pointer()
rcu_dereference()①rcu_read_lock():用于標記讀操作的開始,關閉內核搶占,確保在讀取共享數據期間不會因為內核調度而被打斷,從而保證數據訪問的一致性。例如在文件系統中讀取文件目錄結構時,調用rcu_read_lock()可以防止在讀取過程中被其他高優先級任務搶占,導致目錄結構讀取不完整。它的實現原理主要是通過preempt_disable來禁止內核搶占,在一些不支持搶占的內核中,可能僅僅是執行一條內存屏障指令 。
void rcu_read_lock(void);讀者讀取受RCU保護的數據結構時使用,通知回收者讀者進入了RCU的讀端臨界區。在RCU讀端臨界區訪問的任何受RCU保護的數據結構都會保證在臨界區期間保持未回收狀態。另外,引用計數可以與RCU一起使用,以維護對數據結構的長期引用。在RCU讀側臨界區阻塞是非法的。rcu_read_lock的實現非常簡單,是關閉搶占:
static inline void __rcu_read_lock(void)
{
preempt_disable();
}②rcu_read_unlock():與rcu_read_lock()成對使用,標記讀操作的結束,重新開啟內核搶占。例如在完成對文件目錄結構的讀取后,調用rcu_read_unlock(),系統就可以正常調度其他任務,不會因為本次讀操作而影響系統的整體調度。它通過preempt_enable來實現重新開啟內核搶占的功能。
void rcu_read_unlock(void);讀者結束讀取后使用,用于通知回收者其退出了讀端臨界區。RCU的讀端臨界區可能被嵌套或重疊。rcu_read_unlock的實現是開發搶占。
static inline void __rcu_read_unlock(void)
{
preempt_enable();
}③synchronize_rcu():寫者調用該函數,等待寬限期結束,即等待所有在寫操作開始前進入臨界區的讀者都退出臨界區。在更新網絡設備的配置信息時,寫者完成配置數據的修改并切換指針后,調用synchronize_rcu(),確保所有可能訪問舊配置信息的讀者都已經完成訪問,然后才進行后續的操作,如釋放舊數據的內存空間,保證了數據的一致性和安全性 。
void synchronize_rcu(void);synchronize_rcu 函數的關鍵思想是等待。確保讀者完成對舊結構體的操作后釋放舊結構體。synchronize_rcu 的調用點標志著“更新者代碼的結束”和“回收者代碼的開始”。它通過阻塞來做到這一點,直到所有cpu上所有預先存在的RCU讀端臨界區都完成。
需要注意的是,synchronize_rcu()只需要等待調用它之前的讀端臨界區完成,不需要等待調用它之后開始的讀取者完成。另外,synchronize_rcu()不一定在最后一個預先存在的RCU讀端臨界區完成之后立即返回。具體實現中可能會有延時調度。同時,為了提高效率,許多RCU實現請求批量處理,這可能會進一步延遲 synchronize_rcu() 的返回。
④call_rcu():寫者使用這個函數注冊一個回調函數,該回調函數會在寬限期結束后被調用,通常用于釋放舊數據的內存空間。例如在刪除鏈表節點時,寫者調用call_rcu()注冊一個釋放節點內存的回調函數,當寬限期結束,所有可能訪問該節點的讀者都已完成訪問后,系統會自動調用這個回調函數,安全地釋放節點內存 。
在上面的例子中,rcu_st_update阻塞直到一個寬限期結束。這很簡單,但在某些情況下,人們不能等這么久——可能還有其他高優先級的工作要做。 在這種情況下,使用call_rcu()而不是synchronize_rcu()。call_rcu() API如下:
void call_rcu(struct rcu_head * head, void (*func)(struct rcu_head *head));此函數在寬限期過后調用func(heda)。此調用可能發生在softirq或進程上下文中,因此不允許阻止該函數。rcu_st結構需要添加一個rcu-head結構,可能如下所示:
struct foo {
int a;
char b;
long c;
struct rcu_head rcu;
};foo_update_a()函數示例如下:
/*
* Create a new struct foo that is the same as the one currently
* * pointed to by gbl_foo, except that field "a" is replaced
* * with "new_a". Points gbl_foo to the new structure, and
* * frees up the old structure after a grace period. *
* Uses rcu_assign_pointer() to ensure that concurrent readers
* * see the initialized version of the new structure.
* * Uses call_rcu() to ensure that any readers that might have
* * references to the old structure complete before freeing the * old structure.
* */
void foo_update_a(int new_a) {
struct foo *new_fp = NULL;
struct foo *old_fp = NULL;
new_fp = kmalloc(sizeof(*new_fp), GFP_KERNEL);
spin_lock(&foo_mutex);
old_fp = rcu_dereference_protected(gbl_foo, lockdep_is_held(&foo_mutex));
*new_fp = *old_fp;
new_fp->a = new_a;
rcu_assign_pointer(gbl_foo, new_fp);
spin_unlock(&foo_mutex);
/* 掛接釋放函數 */
call_rcu(&old_fp->rcu, foo_reclaim);
}
// The foo_reclaim() function might appear as follows:
void foo_reclaim(struct rcu_head *rp)
{
struct foo *fp = container_of(rp, struct foo, rcu);
foo_cleanup(fp->a);
kfree(fp);
}container_of() 原語是一個宏,給定指向結構的指針,結構的類型以及結構內的指向字段,該宏將返回指向結構開頭的指針。
使用 call_rcu() 可使 foo_update_a() 的調用方立即重新獲得控制權,而不必擔心新近更新的元素的舊版本。 它還清楚地顯示了更新程序 foo_update_a()和回收程序 foo_reclaim() 之間的RCU區別。
在從受RCU保護的數據結構中刪除數據元素之后,請使用call_rcu()-以注冊一個回調函數,該函數將在所有可能引用該數據項的RCU讀取側完成后調用。如果call_rcu()的回調除了在結構上調用kfree()之外沒有做其他事情,則可以使用kfree_rcu()代替call_rcu()來避免編寫自己的回調:kfree_rcu(old_fp,rcu)
⑤rcu_assign_pointer():寫者在完成數據修改并準備切換指針時,使用這個函數將指向原數據的指針更新為指向新的修改后的副本。這個函數結合內存屏障技術,保證了指針更新操作的原子性和可見性,確保讀者能夠看到最新的、一致的數據。比如在更新系統的設備列表時,寫者通過 rcu_assign_pointer() 將指向舊設備列表的指針更新為指向新的設備列表,使得新的設備列表信息能夠立即被后續的讀操作獲取到,同時保證了當前正在進行的讀操作不會受到影響 。
voidrcu_assign_pointer(p,typeof(p)v);rcu_assign_pointer()通過宏實現。將新指針賦給RCU結構體,賦值前的讀者看到的還是舊的指針。更新者使用這個函數為受rcu保護的指針分配一個新值,以便安全地將更新的值更改傳遞給讀者。 此宏不計算rvalue,但它執行某CPU體系結構所需的內存屏障指令。保證內存屏障前的指令一定會先于內存屏障后的指令被執行。
它用于記錄:哪些指針受 RCU 保護以及給定結構可供其他CPU訪問的點,rcu_assign_pointer()最常通過_rcu列表操作原語(例如list_add_rcu())間接使用。
⑥rcu_dereference():讀者使用這個函數來安全地獲取指向共享數據的指針并訪問數據。它結合內存屏障技術,確保讀者能夠看到最新的、一致的數據,避免了因為讀寫并發導致的數據不一致問題。例如在讀取網絡配置參數時,讀者通過rcu_dereference()獲取指向配置數據的指針,能保證讀取到的是完整且最新的配置信息,而不會因為寫操作正在進行而讀到部分更新或不一致的數據 。
typeof(p) rcu_dereference(p);與rcu_assign_pointer()類似,rcu_dereference()也必須通過宏實現。讀者通過rcu_dereference()獲取受保護的RCU指針,該指針返回一個可以安全解除引用的值。 請注意,rcu_dereference()實際上并未取消對指針的引用,相反,它保護指針供以后取消引用。 它還針對給定的CPU體系結構執行任何所需的內存屏障指令。
常見的編碼實踐是使用rcu_dereference() 將一個受rcu保護的指針復制到一個局部變量,然后解引用這個局部變量,例如:
p = rcu_dereference(head.next);
return p->data;然而,上述情況可以整合成如下一句:
return rcu_dereference(head.next)->data;6.2代碼示例
下面是一個使用 RCU 機制保護鏈表的代碼示例,包括添加節點、刪除節點和遍歷節點的操作:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/rcupdate.h>
#include <linux/list.h>
// 定義鏈表節點結構
struct my_node {
int data;
struct list_head list;
struct rcu_head rcu;
};
// 定義鏈表頭
static LIST_HEAD(my_list);
// 添加節點函數
void add_node(int new_data) {
struct my_node *new_node = kmalloc(sizeof(struct my_node), GFP_KERNEL);
if (!new_node) {
return;
}
new_node->data = new_data;
// 使用RCU機制添加節點
list_add_rcu(&new_node->list, &my_list);
}
// 刪除節點函數
void remove_node(struct my_node *node) {
// 使用RCU機制刪除節點
list_del_rcu(&node->list);
// 注冊回調函數,在寬限期結束后釋放節點內存
call_rcu(&node->rcu, (void (*)(struct rcu_head *))kfree);
}
// 遍歷節點函數
void traverse_list(void) {
struct my_node *entry;
// 進入RCU讀臨界區
rcu_read_lock();
list_for_each_entry_rcu(entry, &my_list, list) {
printk(KERN_INFO "Node data: %d\n", entry->data);
}
// 離開RCU讀臨界區
rcu_read_unlock();
}
static int __init my_module_init(void) {
add_node(10);
add_node(20);
traverse_list();
struct my_node *node_to_remove = list_entry(my_list.next, struct my_node, list);
remove_node(node_to_remove);
traverse_list();
return 0;
}
static void __exit my_module_exit(void) {
struct my_node *entry, *tmp;
// 確保所有RCU操作完成
synchronize_rcu();
list_for_each_entry_safe_rcu(entry, tmp, &my_list, list) {
list_del_rcu(&entry->list);
kfree(entry);
}
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("RCU Linked List Example");- add_node函數用于向鏈表中添加新節點,通過kmalloc分配內存,然后使用list_add_rcu將新節點添加到鏈表中。
- remove_node函數用于從鏈表中刪除指定節點,首先使用list_del_rcu刪除節點,然后通過call_rcu注冊一個回調函數kfree,在寬限期結束后釋放節點的內存。
- traverse_list函數用于遍歷鏈表,在遍歷之前調用rcu_read_lock進入 RCU 讀臨界區,遍歷結束后調用rcu_read_unlock離開臨界區,確保在遍歷過程中鏈表不會被修改,保證數據的一致性 。
- 在模塊初始化函數my_module_init中,先添加兩個節點,然后遍歷鏈表,接著刪除一個節點,再次遍歷鏈表以驗證刪除操作的正確性。
- 在模塊退出函數my_module_exit中,先調用synchronize_rcu等待所有 RCU 操作完成,確保所有可能訪問鏈表節點的讀者都已完成訪問,然后安全地釋放鏈表中剩余節點的內存 。

































