iT邦幫忙

2021 iThome 鐵人賽

DAY 8
0
Software Development

閱讀 Linux Kernel 文件系列 第 8

# Day 8 Why the “volatile” type class should not be used

今天想要來記錄這篇文件 Why the “volatile” type class should not be used,契機是用 ${linux}/scripts 做 checkpatch.pl 的時候,只要有用到 volatile 這個修飾詞,一定會吐出如下的訊息:Use of volatile is usually wrong: see Documentation/volatile-considered-harmful.txt

我們接著看下去~

文件

:Original: :ref:`Documentation/process/volatile-considered-harmful.rst
           <volatile_considered_harmful>`

爲什麼不應該使用「volatile」類型
================================

C 語言程式設計師通常認爲 volatile 表示某個變數可以在當前執行緒之外被改變;
因此,在核心程式中用到共享的資料結構時,常常會有 C 語言程式設計師使用 volatile。
換句話說,他們經常會把 volatile 類型當作簡易的原子變數,當然它們不是。
在核心程式碼中使用 volatile 幾乎總是錯誤的;本文件將解釋為何如此。

理解 volatile 的關鍵是知道它的目的是用來消除優化,實際上很少有人真正需要這樣的應用。
在核心中,程式設計師必須防止不預期的並行存取,破壞共享的資料結構,這其實是一個完全不同的應用。
用來防止不預期並行存取的保護措施,可以更加有效率地避免大多數優化相關的問題。

像 volatile 一樣,核心提供了很多內建功能來保證並行存取時的資料安全
(自旋鎖、互斥旗標、記憶體屏障等等),同樣可以防止意外的優化。
如果可以正確使用這些核心功能,那麼就沒有必要再使用 volatile。
如果仍然必須使用 volatile,那麼幾乎可以肯定在程式碼的某處有一個bug。
在正確設計的核心程式碼中,volatile 能帶來的僅僅是使事情變慢。

思考一下這段典型的核心程式碼::

    spin_lock(&the_lock);
    do_something_on(&shared_data);
    do_something_else_with(&shared_data);
    spin_unlock(&the_lock);

如果所有的程式碼都遵循加鎖規則,當持有 the_lock 的時候,不可能意外的改變 shared_data 的值。
任何可能存取該資料的其他程式碼都會在這個鎖上等待。自旋鎖跟記憶體屏障一樣 
—— 它們被顯式地使用來達成這個目的 —— 意味著資料存取不會被優化。
所以本來編譯器認爲他能推斷出在 shared_data 裡面將有什麼資料,
但是因爲使用 spin_lock() 跟記憶體屏障一樣,會強制編譯器忘記它所推斷的一切。
那麼在存取這些資料時不會進行優化。

如果 shared_data 被聲名爲 volatile,鎖操作仍然是必須的。
編譯器也將無法對臨界區內 shared_data 的存取進行優化,就算我們知道沒有其他人正在使用它。
在鎖有效的同時,shared_data 不是 volatile 的。
在處理共享資料的時候,適當的鎖操作可以讓使用 volatile 變得不必要 —— 甚至是有潛在危害的。

volatile 的存儲類型最初是爲那些記憶體映射的 I/O 暫存器而定義。
在核心中,暫存器存取也應該被鎖保護,但是人們也不希望編譯器「優化」臨界區內的暫存器訪問。
核心內 I/O 的記憶體存取是通過存取函數完成的;
通過指標對 I/O 記憶體直接存取,是極不能被接受,且不是在所有架構上都能正常運作。
那些存取函數正是爲了防止不預期的優化而寫的,因此,再次,volatile 類型不是必需的。

另一種讓使用者可能想使用 volatile 的情況是,當處理器正 busy-waiting 一個變數。
正確執行一個 busy-waiting 的方法是::

    while (my_variable != what_i_want)
        cpu_relax();

cpu_relax() 會降低 CPU 的能量消耗或者轉交給超執行緒的另一個執行緒;它像是記憶體屏障一樣,
所以,再次,volatile 不是必需的。當然,busy-waiting 一開始就是一種反常規的做法。

核心中,在很少數的情況下 volatile 仍然是有意義的:

  - 在一些架構上,允許直接的 I/O 記憶體存取,那麼前面提到的存取函數可以使用 volatile。
    基本上,每一個存取函數呼叫,它自己都是一個小的臨界區域並且保證按程式設計師的期望做存取。

  - 某些會改變記憶體狀態的內嵌組合語言,雖然沒有其他明顯的作用,但是有被 GCC 優化掉的可能。
    在內嵌組語中加上 volatile 關鍵字可以防止這種優化。

  - Jiffies 變數是一種特殊情況,在於每次讀取它的時候都可以有不同的值,
    但讀取 jiffies 變數時不需要任何特殊的加鎖保護。
    所以 jiffies 變數可以使用 volatile,但是不贊成其他跟 jiffies 相同類型變數使用 volatile。
    Jiffies被認爲是一種"愚蠢的遺留物"(Linus 說的),因爲解決這個問題比保持現狀要麻煩的多。

  - 由於某些 I/O 設備可能會修改一致性記憶體,
    所以有時指向連續一致性記憶體資料結構的指標需要正確的使用 volatile。
    網路卡使用的 ring buffer 是這類情形的一個例子,
    其中網路卡用改變指標來表示哪些描述子已經處理過了。

對於大多程式碼,上述幾種可以使用 volatile 的情況都不適用。
所以,使用 volatile 是一種 bug 並且需要對這樣的程式碼額外仔細檢查。
那些試圖使用 volatile 的開發人員需要好好思考他們真正想實現的是什麼。

非常歡迎刪除 volatile 變量的 patch - 只要證明這些 patch 完整的考慮了並行問題。

參考文件
-------

[1] https://lwn.net/Articles/233481/
[2] https://lwn.net/Articles/233482/

致謝
----

最初由 Randy Dunlap 推動並作初步研究
由 Jonathan Corbet 撰寫
參考 Satyam Sharma,Johannes Stezenbach,Jesper Juhl,Heikki Orsila,
H. Peter Anvin,Philipp Hahn 和 Stefan Richter 的意見改善了本文件。

我的理解

  • 結論就是不確定自己在做什麼就不要用 volatile XD
  • 上述提到較合理使用 volatile 的實際使用情境如下:(不過 ring buffer 的還沒有找到)
    • accessor function、inline asm
      • 在參考文件中,Linus 有提到 "並不是變數是 volatile,而是要存取的這個動作是 volatile",所以這裡可以看見,函數的參數都有被冠上 volatile 的修飾詞。
    • jiffies

同場加映

在 checkpatch 的時候,還有遇到其他幾種不同的 error/warning,也一併記錄起來:

後記

今天作為,在一個系列結束和下一個系列開始的,緩衝日,
明天就會正式進入 Cache and TLB Flushing Under Linux 的系列囉!
我們明天見!


上一篇
# Day 7 Supporting PMUs on RISC-V platforms (三)
下一篇
# Day 9 Cache and TLB Flushing Under Linux (一)
系列文
閱讀 Linux Kernel 文件30

1 則留言

0
高魁良
iT邦新手 4 級 ‧ 2021-09-17 23:02:11

延伸問題:RISC-V 是否是可以直接進行 I/O 記憶體存取?

我要留言

立即登入留言