this_cpu 操作¶
- 作者:
Christoph Lameter,2014 年 8 月 4 日
- 作者:
Pranith Kumar,2014 年 8 月 2 日
this_cpu 操作是一種最佳化訪問與 當前 執行處理器關聯的每 CPU 變數的方法。這是透過使用段暫存器(或 CPU 永久儲存特定處理器每 CPU 區域起始地址的專用暫存器)來完成的。
this_cpu 操作將每 CPU 變數偏移量新增到特定於處理器的每 CPU 基址,並將該操作編碼到對每 CPU 變數進行操作的指令中。
這意味著在偏移量的計算和資料操作之間不存在原子性問題。因此,無需停用搶佔或中斷來確保處理器在地址計算和資料操作之間不被切換。
讀-修改-寫操作尤其重要。處理器通常有特殊的低延遲指令,可以在沒有典型同步開銷的情況下執行,但仍提供某種形式的寬鬆原子性保證。例如,x86 可以執行 RMW(讀-修改-寫)指令,如 inc/dec/cmpxchg,而無需 lock 字首及相關的延遲開銷。
沒有 lock 字首的變數訪問不會被同步,但同步不是必需的,因為我們處理的是特定於當前執行處理器的每 CPU 資料。只有當前處理器才能訪問該變數,因此係統中不存在與其他處理器的併發問題。
請注意,遠端處理器對每 CPU 區域的訪問是特殊情況,可能會影響本地 RMW 操作透過 this_cpu_* 的效能和/或正確性(遠端寫入操作)。
this_cpu 操作的主要用途是最佳化計數器操作。
定義了以下具有隱式搶佔保護的 this_cpu() 操作。這些操作可以在不擔心搶佔和中斷的情況下使用。
this_cpu_read(pcp)
this_cpu_write(pcp, val)
this_cpu_add(pcp, val)
this_cpu_and(pcp, val)
this_cpu_or(pcp, val)
this_cpu_add_return(pcp, val)
this_cpu_xchg(pcp, nval)
this_cpu_cmpxchg(pcp, oval, nval)
this_cpu_sub(pcp, val)
this_cpu_inc(pcp)
this_cpu_dec(pcp)
this_cpu_sub_return(pcp, val)
this_cpu_inc_return(pcp)
this_cpu_dec_return(pcp)
this_cpu 操作的內部工作原理¶
在 x86 上,fs: 或 gs: 段暫存器包含每 CPU 區域的基址。然後可以透過簡單地使用段覆蓋來將每 CPU 相對地址重定位到處理器的正確每 CPU 區域。因此,到每 CPU 基址的重定位透過段暫存器字首編碼在指令中。
例如
DEFINE_PER_CPU(int, x);
int z;
z = this_cpu_read(x);
導致一條指令
mov ax, gs:[x]
而不是每 CPU 操作中計算地址然後從該地址獲取的序列。在 this_cpu_ops 之前,這樣的序列還需要停用/啟用搶佔,以防止核心在執行計算時將執行緒移動到不同的處理器。
考慮以下 this_cpu 操作
this_cpu_inc(x)
上述操作產生以下單條指令(沒有 lock 字首!)
inc gs:[x]
而不是在沒有段暫存器時所需的以下操作
int *y;
int cpu;
cpu = get_cpu();
y = per_cpu_ptr(&x, cpu);
(*y)++;
put_cpu();
請注意,這些操作只能用於保留給特定處理器的每 CPU 資料。在周圍程式碼不停用搶佔的情況下,this_cpu_inc() 只能保證其中一個每 CPU 計數器被正確遞增。然而,不能保證作業系統不會在 this_cpu 指令執行之前或之後直接移動程序。通常這意味著每個處理器的各個計數器的值是無意義的。所有每 CPU 計數器的總和才是唯一有意義的值。
使用每 CPU 變數是出於效能原因。如果多個處理器併發地透過相同的程式碼路徑,可以避免快取行跳動。由於每個處理器都有自己的每 CPU 變數,因此不會發生併發的快取行更新。這種最佳化所付出的代價是,當需要計數器的值時,必須將每 CPU 計數器相加。
特殊操作¶
y = this_cpu_ptr(&x)
獲取每 CPU 變數的偏移量(&x !),並返回屬於當前執行處理器的每 CPU 變數的地址。this_cpu_ptr 避免了常見的 get_cpu/put_cpu 序列所需的多個步驟。沒有可用的處理器編號。相反,本地每 CPU 區域的偏移量簡單地新增到每 CPU 偏移量中。
請注意,此操作只能在可以使用 smp_processor_id() 的程式碼段中使用,例如,已停用搶佔的地方。然後該指標用於在臨界區中訪問本地每 CPU 資料。當重新啟用搶佔時,此指標通常不再有用,因為它可能不再指向當前處理器的每 CPU 資料。
在可搶佔程式碼中獲取 per-CPU 指標的有意義的特殊情況由 raw_cpu_ptr() 處理,但此類用例需要處理兩個不同 CPU 訪問同一 per CPU 變數的情況,這很可能是第三個 CPU 的變數。這些用例通常是效能最佳化。例如,SRCU 將一對計數器實現為一對 per-CPU 變數,rcu_read_lock_nmisafe() 使用 raw_cpu_ptr() 獲取指向某個 CPU 計數器的指標,並使用 atomic_inc_long() 來處理 raw_cpu_ptr() 和 atomic_inc_long() 之間的遷移。
每 CPU 變數和偏移量¶
每 CPU 變數具有指向每 CPU 區域起始的 偏移量。它們沒有地址,儘管它們在程式碼中看起來像地址。偏移量不能直接解引用。偏移量必須新增到處理器的每 CPU 區域的基指標,才能形成有效地址。
因此,在每 CPU 操作上下文之外使用 x 或 &x 是無效的,通常會被視為 NULL 指標解引用。
DEFINE_PER_CPU(int, x);
在每 CPU 操作的上下文中,上述意味著 x 是一個每 CPU 變數。大多數 this_cpu 操作都接受一個 cpu 變數。
int __percpu *p = &x;
&x,因此 p,是每 CPU 變數的 偏移量。this_cpu_ptr() 接受每 CPU 變數的偏移量,這使得它看起來有點奇怪。
對每 CPU 結構欄位的操作¶
假設我們有一個 percpu 結構
struct s {
int n,m;
};
DEFINE_PER_CPU(struct s, p);
對這些欄位的操作很簡單
this_cpu_inc(p.m)
z = this_cpu_cmpxchg(p.m, 0, 1);
如果 struct s 有一個偏移量
struct s __percpu *ps = &p;
this_cpu_dec(ps->m);
z = this_cpu_inc_return(ps->n);
如果我們以後不使用 this_cpu 操作來操縱欄位,指標的計算可能需要使用 this_cpu_ptr()
struct s *pp;
pp = this_cpu_ptr(&p);
pp->m--;
z = pp->n++;
this_cpu 操作的變體¶
this_cpu 操作是中斷安全的。有些架構不支援這些每 CPU 本地操作。在這種情況下,操作必須替換為停用中斷的程式碼,然後執行保證原子性的操作,然後再重新啟用中斷。這樣做代價很高。如果排程器無法更改我們正在執行的處理器有其他原因,則沒有理由停用中斷。為此,提供了以下 __this_cpu 操作。
這些操作不保證防止併發中斷或搶佔。如果每 CPU 變數不在中斷上下文中使用且排程器無法搶佔,則它們是安全的。如果在操作進行中仍發生任何中斷,並且中斷也修改了變數,則無法保證 RMW 操作是安全的。
__this_cpu_read(pcp)
__this_cpu_write(pcp, val)
__this_cpu_add(pcp, val)
__this_cpu_and(pcp, val)
__this_cpu_or(pcp, val)
__this_cpu_add_return(pcp, val)
__this_cpu_xchg(pcp, nval)
__this_cpu_cmpxchg(pcp, oval, nval)
__this_cpu_sub(pcp, val)
__this_cpu_inc(pcp)
__this_cpu_dec(pcp)
__this_cpu_sub_return(pcp, val)
__this_cpu_inc_return(pcp)
__this_cpu_dec_return(pcp)
將遞增 x,並且在不能透過地址重定位和同一條指令中的讀-修改-寫操作來實現原子性的平臺上,不會回退到停用中斷的程式碼。
&this_cpu_ptr(pp)->n 與 this_cpu_ptr(&pp->n)¶
第一個操作獲取偏移量並形成一個地址,然後新增 n 欄位的偏移量。這可能導致編譯器發出兩條加法指令。
第二個操作首先將兩個偏移量相加,然後進行重定位。在我看來,第二種形式看起來更簡潔,並且更容易處理 ()。第二種形式也與 this_cpu_read() 和其友元函式的使用方式一致。
遠端訪問每 CPU 資料¶
每 CPU 資料結構旨在由一個 CPU 獨佔使用。如果按照預期使用變數,this_cpu_ops() 保證是“原子的”,因為沒有其他 CPU 可以訪問這些資料結構。
在某些特殊情況下,您可能需要遠端訪問每 CPU 資料結構。通常可以安全地進行遠端讀取訪問,這經常用於彙總計數器。遠端寫入訪問可能會有問題,因為 this_cpu 操作沒有鎖語義。遠端寫入可能會干擾 this_cpu RMW 操作。
強烈不鼓勵對 percpu 資料結構進行遠端寫入訪問,除非絕對必要。請考慮使用 IPI 喚醒遠端 CPU 並執行對其 per CPU 區域的更新。
要遠端訪問 per-cpu 資料結構,通常使用 per_cpu_ptr() 函式
DEFINE_PER_CPU(struct data, datap);
struct data *p = per_cpu_ptr(&datap, cpu);
這明確表明我們正準備遠端訪問 percpu 區域。
您還可以執行以下操作將資料偏移量轉換為地址
struct data *p = this_cpu_ptr(&datap);
但是,將透過 this_cpu_ptr 計算出的指標傳遞給其他 CPU 是不尋常的,應避免。
遠端訪問通常僅用於讀取其他 CPU 的每 CPU 資料狀態。由於 this_cpu 操作的寬鬆同步要求,寫入訪問可能會導致獨特的問題。
一個說明寫入操作相關問題示例如下:由於兩個 per CPU 變數共享一個快取行,但寬鬆同步僅應用於更新該快取行的一個程序。
考慮以下示例
struct test {
atomic_t a;
int b;
};
DEFINE_PER_CPU(struct test, onecacheline);
人們擔心,如果欄位“a”從一個處理器遠端更新,而本地處理器使用 this_cpu ops 更新欄位 b,會發生什麼。應注意避免同時訪問同一快取行內的資料。此外,可能需要昂貴的同步。在這種情況下,通常建議使用 IPI,而不是對另一個處理器的 per CPU 區域進行遠端寫入。
即使在遠端寫入很少的情況下,請記住遠端寫入會從最有可能訪問它的處理器中逐出快取行。如果處理器喚醒並發現丟失了每 CPU 區域的本地快取行,其效能和喚醒時間將受到影響。