Linux kernel: BUG scheduling while atomic 分析

收錄在 AIoT Ameba 2020 - Just for Fun


===

最近實作 rtw88-usb, 遇到了一個 BUG: scheduling while atomic
https://github.com/neojou/rtw88-usb/issues/23

在多人開發時, 如果沒有規劃好, 有時很難避免一些 Linux kernel 上的誤用,
例如這個問題, 即是上層是其他人開發用到 spin lock,
而底層 hci 是另一個人開發, 所用的 USB kernel API - usb_control_msg(),
kernel 內核會做 schedule waiting 的動作而沒注意到
( usb_start_wait_urb() -> wait_for_completion_timeout()
因此產生 BUG: scheduling while atomic. 分析如下:

我們先 trace code 看看這個 bug 是怎麼產生的:

首先先開啟 Linux kernel preemption
https://devarea.com/understanding-linux-kernel-preemption/

在 Linux kernel 的
General -> Preemption Model

有三種 mode:

(1) CONFIG_PREEMPT_NONE : 沒有 preemption
(2) CONFIG_PREEMPT_VOLUNTARY : 額外檢查後才能使用
(3) CONFIG_PREEMPT : kernel 支援 preemption

當選擇第一種 none 時, linux kernel 會盡量減少rescheduling 和 context switch,
所以 throughput 可能會比較高, 但相對的 latency 會比較大.

當選擇第二種時, 如果 flag need_resched 有被設定的話, scheduler 會被呼叫

選擇用第三種: CONFIG_PREEMPT 時, 當 interrupt 來時, 只要 kernel 不是在 atomic mode,
會立刻做 scheduling, 這樣的狀況下, latency 可以最小.

這個問題我們要用第三種 CONFIG_PREEMPT,
這樣編譯好的 kernel 執行下, uname -a 可以看到 PREEMPT 字眼

--
atomic context 有底下幾種:
1. 最直觀的就是 interrupt handler
2. spin lock 或 rcu
3. preempt_disable() 到 preempt_enable() 間
4. interrupt disable (IRQ off)


所以 spin lock 在 preempt Linux kernel 是屬於 atomic context.
這是因為 spin lock 是一種 busy wait 的 mutex.

接著我們繼續看產生這 BUG 訊息的 kernel code
這是在 kernel 的 sched/core.c 這邊 產生的



在 schedule_debug(): 


static inline void schedule_debug(struct task_struct *prev, bool preempt)
{
...
 if (unlikely(in_atomic_preempt_off())) {
  __schedule_bug(prev);
  preempt_count_set(PREEMPT_DISABLED);
 }

在 __schedule_bug() 顯示



所以主要判斷在這個 函式: in_atomic_preempt_off()


可以發現, Linux kernel 巧妙地用 preempt_count() 來判斷 atomic context.
當執行 preempt_disable() 時, 這個 count 會加 1.
而當執行 preempt_enable() 時, 這個 count 會減 1.


而 spin_lock 屬於 atomic context, lock acquired 後, 並不會做 preempt_enable(), 直到 unlock
但 mutex_lock  function 回傳前, 會做 preempt_enable()



例如這個範例 :
在這個 sched_atomic1 我們用 spin_lock
https://github.com/neojou/new_ldd/blob/master/scheduling/sched_atomic1.c

而在 sched_atomic2 我們用 mutex_lock
https://github.com/neojou/new_ldd/blob/master/scheduling/sched_atomic2.c

可以發現用 spin_lock 的, 在 lock 之後, preempt_count() 值為 1 非 0.
而用 mutex_lock 的, 在 lock 之後, preempt_count() 為 0.

而如果在 atomic context 中, 我們有做其他 schedule waiting 的動作的話,
e.g. msleep() 或 像這 issue 中呼叫的 wait_for_completion_timeout()
這邊會再呼叫 preempt_disable() 而讓 preempt count 值增加超過 1.

( 回顧一下這個 in_atomic_preempt_off(), PREEMPT_DIABLE_OFFSET 值為 1. )


這時候 in_atomic_preempt_off() 為 (2 != 1) 為 true,
所以會發生 scheduling while atomic

例如我們在上述的範例中加入 msleep(1);

可以發現 mutex lock 的沒有問題.


但用 spin lock 就會發生 kernel bug - scheduling while atomic



--
Ref: Mastering Embedded Linux Programming, 2nd Edition


留言

熱門文章