Linux kernel : 從 spinlock 到 atomic 到 Ameba

收錄在 AIoT Ameba 2020 - Just for Fun

--
緣由:

當我們對一個變數做計算時, CPU 通常的動作是:
  (1) 從 memory 將變數值讀到暫存器
  (2) CPU 通過暫存器做計算
  (3) 將值寫回 memory

這也就是所謂的 read - modify - write 的動作
但這若在多工環境下, 可能會有問題, 例如: github-atomic1

我們做兩個 thread, 一個對 sum 加 1, 一個對 sum 減 1, 兩個都做三次, sum 初始值為 0.
正常來說最後 sum 值應該還是 0.

我們故意在計算完後, 呼叫 schedule() 強迫切換 thread,
可以發現, 這樣執行起來, sum 的值並非預期. 最後變成了 -2, 而非 0

[90334.635512] hello_init: pid=14949
[90334.635699] hello_init: pid=14949, before sum=0
[90334.635714] work_handler1: pid=12886, sum=1
[90334.635717] work_handler1: pid=12886, sum=2
[90334.635719] work_handler2: pid=13336, sum=0
[90334.635722] work_handler1: pid=12886, sum=1
[90334.635724] work_handler2: pid=13336, sum=-1
[90334.635727] work_handler2: pid=13336, sum=-2
[90334.635773] hello_init: pid=14949, after sum=-2


1. interrupt disable
    我們用在 MCU 常用的手法, 把 interrupt 關閉看看 - github-atomic2
    可以使用 raw_local_irq_save() / raw_local_irq_restore()


2. UP v.s SMP
    Linux 上的 UP 指的是 uni-processor 單個 CPU,
   相對於 SMP - symmetric multi-processors 多個 CPU 而言.

    上述 禁制中斷 這方式, 在多核的 cpu 上會有問題 (SMP)
    這是因為我們雖然在該 CPU 上禁制了 interrupt, 該 cpu不會執行別的 thread,
    但其他 CPU 仍可執行

    所以如果我們把次數三次放大到  十萬次, 來增加它發生的機率,
    可以發現執行結果 sum 不是 0

    [104815.844974] hello_init: pid=19042, before sum=0
    [104815.850770] hello_init: pid=19042, after sum=-20814

3. spin_lock v.s. spin_lock_irqsave()
   差別在於 spin_lock_irqsave() 會禁制中斷,
   這問題發生在, 如果這 lock 會在 中斷函式 中使用的話,
   有可能當 thread 執行 spin_lock 時, 中斷來臨, 跳到中斷函式也執行 spin_lock
   這樣就會變成雙層請求死鎖

   因為我們這範例只有在 thread 中使用 lock, 所以可以用 spin_lock - github-atomic3



  可以發現, 即便次數放大到一百萬次, 最後 sum 結果為 0

4. ticket spin_lock

   Linux 的 spin lock 使用的是 ticket spin_lock 的方式,
   將 slock 分成 now_serving (owner) 和 next_ticket (next)
   一開始初始化, slock 為 0, 當做 lock() 時, 會將 next +1 給下一個 thread,
   並判斷 owner 和 next 值是否相同, 如果相同代表該 thread 有權拿到 lock,
   如果不相同就繼續等待.


    例如 :

    (1) thread 1 一開始 owner:0 , next: 0, 先把 next 變成 1,
             並比較 原 next(0) 和 owner 值 是否相同, 因為都是 0, 所以拿到 lock,
    (2) thread 2 做 lock 時, owner :0 , next:1 -> 2, 便繼續等待.
    (3) 當 thread 1做完 unlock() 時, 會將 owner 值 +1, 此時 thread 2 比較 owner / 原 next 均為 1,
          則拿到 lock.


5. atomic

    可以發現, lock 計算時, 也是做 +1 動作, 那怎麼確保在做 +1 的時候, 其他 CPU 不會執行呢.
    也就是確保 read - modify - write 這一系列動作的完整性, 中間不會被插隊
    這就是 atomic operation 的原由. atom 原子的意思, 也就代表不可分割.
    而這仰賴 CPU 的指令支援.

    (1) 在 x86, 用的是 lock bus 的方式,

   可以發現, 在 assembly, 會在動作上多一個 LOCK#, 來告訴 CPU 這 load / store 讀寫記憶體,
   需要搭配 bus lock, 讓其他 CPU 不能在這時候做 load / store

    細節可以參考這篇:
    https://xem.github.io/minix86/manual/intel-x86-and-64-manual-vol3/o_fe12b1e2a880e0ce-259.html

    PS: cache 的部分, 會透過 cache coherence 的機制來達成一致.

   (2) Ameba 的 CPU 是 ARM cortex-m, ARM 是透過 ldrex / strex 來達成
         這邊的  ex 指的就是 exclusive , 例如我們看 ARM mbed 的 RTOS - RTX 做法:

https://github.com/ARM-software/CMSIS_5/blob/develop/CMSIS/RTOS2/RTX/Source/rtx_core_cm.h


LDREX 和 STREX 是配對使用, ARM cpu 會讓第二次執行 STREX 失敗, 只讓第一次的 STREX 成功

可以參考之前 Ameba 這篇: ARM Cortex-M Instruction Set Architecture
   的 synchronization / lock acquire


所以原本程式可以簡化如下, 執行結果一樣是正確的
github-atomic4





留言

熱門文章