- 1. lockdepとは
- 2. 前提
- 3. 各種コンセプトの整理
- 4. 検知対象の種別とlockdep splatの対応
- 5. 更に捕捉範囲を広げる為に利用可能な各種API
- 6. 動作メカニズム詳解
- 7. オーバヘッドについて
- 8. 偽陽性・偽陰性について
- 9. まとめ
執筆者 : 田 幸一郎
1. lockdepとは
lockdepは、Linux Kernelのlocking primitiveの誤使用に因るdeadlock検知・予測をはじめ、 RCU primitiveの誤使用検知、ww-mutex(wait/wound mutex)API誤使用検知、また最近では多重ロック獲得時の各ロックが想定するsleepable性の妥当性 のチェックに至るまで、synchronization primitiveを誤った形で使用している"bad code"を幅広く検知する為の機構です。 筆者も普段の業務の中で、Linux Kernel Moduleやlivepatchの開発の際には当lockdep機構に 少なからず助けられてきました。一方で巷にはそのメカニズム、つまり何をどのようにして検知するのか?に関して解説したものはどうも見当たらないように伺えます。 そこで本記事では、この「縁の下の力持ち」にスポットライトを当て、その実装にも踏み込みながら解説してみたいと思います。
2. 前提
lockdep公式ドキュメント(Documentation/locking/lockdep-design.txt) は一度目を通していることが望ましいです。また、本稿は、以下を前提としています。
Linux Kernel: v5.8.0 CONFIG_LOCKDEP=y CONFIG_LOCKDEP_SMALL=n CONFIG_PROVE_LOCKING=y CONFIG_PROVE_RAW_LOCK_NESTING=y
3. 各種コンセプトの整理
幾つか、次項以降に進む前に確認しておくべきコンセプトを、図を混じえながら解説していきます。
3-1. lock class
lock class(構造体名 lock_class
)は、lockdepのvalidation対象オブジェクトの最小粒度です。lock classの概要については、公式ドキュメント(Documentation/locking/lockdep-design.txt)の冒頭に記述されているので省略しますが、lockdepはlock classとlock instanceを明確に区別しています。lock instanceは、初期化された1つのlocking primitiveを指します。
lock instanceをlock classにマッピングするのは、lockdep_map
構造体の役割です。
lock class
の固有性はつまるところ、classhash_table
に格納された lock_class
を引くキーの固有性に依ります。
例えば公式ドキュメントで言及されているinode構造体を取り上げてみます。inode.i_lock
においては、inode_init_always() -> spin_lock_init()
において、static local変数なkeyをベースにしている為、結果として異なるlockdep_mapが、1つの lock_class
に対応していることが分かります。
逆に言うと lock_class
の粒度は、locking primitiveの、"普通"使用すると考えられるAPIの実装に依ることが多いと言えるでしょう。
以下図では、例としてspin_lock、rw_semaphoreのケースを取り上げます。
それぞれ、マクロ展開先であるlock instance作成箇所がソースコードにおける特定の関数内である場合、
それらは1つの lock_class
に集約されます。
一方、必要に応じてkeyの集約の粒度(lock_class
の粒度)を後付けで調整する事も出来ます。
例として uart_port->lock
のキー port_lock_key
による lock_class
の集約を挙げてみましょう。
uart_port_spin_lock_init
は2箇所で展開され得ますが、それらを同一の lock_class
として取り扱いたいのは自然なことです。
[drivers/tty/serial/serial_core.c] 1923 static inline void uart_port_spin_lock_init(struct uart_port *port) 1924 { 1925 if (uart_console(port)) 1926 return; 1927 1928 spin_lock_init(&port->lock); --> 1929 lockdep_set_class(&port->lock, &port_lock_key); 1930 }
こうして定義されたlock class単位を元に、lockdepはvalidationを実行します(主要な物はdeadlock avoidance、及びdeadlock detectionと言って差し支えないでしょう)。 なお、lock classは、Resource Allocation Graph(RAG)で言うところのResourceとは意味合い、粒度が異なっていることに注意して下さい。
3-2. lock chain
lock_chain
は、1つのコンテキスト1において積み上がった(=ロックしたままの)
ロックに対応するlock classのチェーンを指します。
lock instance単位でのrecursion、inversionの検知・予測は、lock class単位での検知・予測範囲に内包されますので、
lock class粒度での情報を保持する1つの lock_chain
の、
(a). それ自体の妥当性、(b). 更新後の依存グラフ2の妥当性、のチェックは一度行なえば、再現率(recall)の観点では十分です。
その為、一度validateした lock_chain
はキャッシュされ、以後標準的なO(N2)のvalidationの大部分を省略することが出来ます。
なお、キャッシュを更新・参照するキーは、lock_chain
を構成するclass id群3により一意に決まります。
3-3. chain_hlocks
chain_hlocks
は、validationのパフォーマンスを向上させる為の機構です。
より具体的には、 lock_chain
を構成するlock class群のidを記録するリストです。つまり、class id情報は lock_chain
から外出しされて別途 chain_hlocks
で管理されています。
lock_chain
構造体自体はシンプルなハッシュテーブル(chainhash_table
)を通じて頻繁にサーチが走る対象であり、
コンパクトな構造体です。先述の通り、一度validationを通した lock_chain
は以後validationを省略することが出来ます。
一方、いざ誤使用検知がなされた場合(=異常事態)、lockdep splatに関係するlock class群の情報を出力する為に、
chain_hlocks
内の記録された該当class id群が参照されます。
以下図では、システムのブート後すぐ、まっさらな chain_hlocks
が使われ始める様子を示します。
初期段階では、chain_block_buckets
の先頭エントリ(#0)に紐づく空きリストを先頭の方から消費していく形をとりますが、
これはもはや chain_hlocks
全体です。
ここで注意して頂きたいのは、1つのタスク・コンテキストが続けて複数ロックを獲得していった時、
1つ目のロックを獲得した時点の lock_chain
(以下図内、パターン#1)、2つ目のロックを獲得した
時点での lock_chain
(以下図内、パターン#2)、のように、全てのパターンを記憶しようとする点です。
このパターンが、例えば別のコンテキスト(別タスクであったり、或いは同一プロセスコンテキスト中のハード割込み後IRQ処理コンテキスト)において再発した場合、
その lock_chain
を依りどころにしたvalidationは再度実行する必要が無くなるということになります。
その後、バケット#0の末尾の空きスペースが残り少なくなったり、lock class自体の撤去("zapping")が発生したりなどを
経て、バケット#Nも使用されている状況下で、新たなパターン(lock_chain
)が追加されるときの様子も以下図に示しておきます。
3-4. debug_locks
基本的にlockdep splatは最初の1つしか出力されず、すぐにlockdepが無効状態に移行します4 。 今まさにシステムにおいてlockdepが有効になっているかどうかについては、 本稿執筆時点ではsysfs/procfsの類を通じて確認する事は出来ませんが、若干行儀が悪いものの例えば以下のワンライナーで確認することは出来ます。
$ sudo gdb -q -ex 'p (int)(*0x'$(sudo grep -oP '.*(?= D debug_locks$)' /proc/kallsyms)')' -ex q -c /proc/kcore [...] # もし以下のように0が出力されれば、それは最早現時点でlock debuggingが無効化されてしまっていることを示します。 # dmesg等で、システムのboot後から現時点までのいずれかのタイミングで # 何らかのlockdep splatが出力されていないか確認すると良いでしょう。 # (※ 勿論、そもそも使用kernelがlockdepを有効化したビルドである前提) $1 = 0 # もし以下のように1が出力されれば、lock debuggingが現時点で有効です。 $1 = 1
「どう考えてもlockdep splatが出るはずなのに、何も出力されない!」といった場合、実は既に無効化(=debug_locks_off()
)された後だったというだけかもしれません。
上記ワンライナー等で確認してみると良いでしょう。
4. 検知対象の種別とlockdep splatの対応
4-1. deadlock
deadlock avoidance、deadlock detectionに関して、多様なlockdep splatが存在しますが、それらは以下のようにシンプルに分類出来ます5。
(a). recursion deadlock
これは暗に、単一のlock classに閉じたdeadlockを意味します。
(a)-1. 純粋に同一lockを多重に獲得しようとするケース
current->held_locks
に閉じたvalidationです。言い換えると、同一タスクにおける、現時点で取得中の全てのロックを確認する、シンプルなvalidationです。対応するWARNING冒頭は以下のようなものです:
============================================ WARNING: possible recursive locking detected -------------------------------------------- {curr->comm}/{its-pid} is trying to acquire lock: but task is already holding lock:
(a)-2. irq-safe <-> irq-unsafeの依存性検知による、潜在的多重獲得の検知
lock_class
のusage_mask
ベースのvalidationです。言い換えると、システム全体におけるあらゆるタスク、コンテキストでのブート後の使用履歴全ての情報を元にvalidationを行います。
================================ WARNING: inconsistent lock state -------------------------------- inconsistent {%s} -> {%s} usage.
(b). lock inversion deadlock
これは暗に、異なるlock class間のdeadlockを意味します。
(b)-1. circular dependency検知
====================================================== WARNING: possible circular locking dependency detected ------------------------------------------------------ {curr->comm}/{its-pid} is trying to acquire lock: but task is already holding lock:
(b)-2. irq-safe -> irq-unsafeの依存性検知による、潜在的lock inversionの検知
(b)-2-(i). 依存グラフ間の接続に伴う、潜在的lock inversionの検知
===================================================== WARNING: %s-safe -> %s-unsafe lock order detected -----------------------------------------------------
(b)-2-(ii). 特定のlock classに対する、新規のusageパターンの出現に伴う、潜在的lock inversionの検知
========================================================
WARNING: possible irq lock inversion dependency detected
--------------------------------------------------------
4-2. wait-type checks
比較的最近(Linux Kernel v5.7以降)導入された機構です6。
誤解を恐れずに言うと、ロックを多重に獲得する時、より内側で(獲得順で言うと、より後で)
獲得されたロックが、外側のロックよりも"sleepable"であってはならないという要求のチェックをします。
典型的には、(通常のkernelにおけるsleepableではない)spinlockの内側でsleepableなmutexを獲得すると、
外側のspinlockの解放遅延がunboundedになってしまって問題である、という例をイメージすれば良いでしょう。
なお、RCU lockはどんなコンテキストでも獲得(rcu_read_lock(|_bh|_sched)
)出来る一方、
完全にsleepableなロックを内側で獲得する事は許さないこともあり、
wait_type_inner
、wait_type_outer
を取り回す形でwate-type checksは実装されています。
詳細は脚注のCommitメッセージをご覧ください。
WARNING出力冒頭は以下のようなものです。
============================= [ BUG: Invalid wait context ] -----------------------------
余談ですが、現時点のpreempt-rt kernelにおいては、最新のv5.6.19-rt11でもwait-type checksは取り込まれていません。
4-3. wait/wound mutex API misuse checks
wait/wound mutex自体がdeadlock avoidance機構ですが、wait/wound mutexのAPI自体幾分複雑だからでしょうか、 そのAPIの誤使用検知機構が豊富に提供されています。詳細は Documentation/locking/ww-mutex-design.txt にまとまっている為、本稿では省略します。
4-4. その他
WARNINGは出るものの、debug_locks
が無効化されない物としては以下、ユーザモードへのreturn時に何らかのカーネル空間におけるロックを保持したままであった場合に出力される物を1つ取り上げます。
================================================
WARNING: lock held when returning to user space!
5. 更に捕捉範囲を広げる為に利用可能な各種API
5-1. assertions及びheld判定
debug locking有効状態においては、基本的に各種validationが勝手に走りますが、その他にも開発者に対して 幾つかの便利なAPIが提供されています。
# それぞれ関数名が全てを物語っています。 lockdep_assert_held() lockdep_assert_held_write() lockdep_assert_held_read() lockdep_assert_held_once() lockdep_assert_irqs_enabled() lockdep_assert_irqs_disabled() lockdep_assert_irqs_in_irq() lockdep_assert_RT_in_threaded_ctx() # 以下は、別の判定条件と組み合わて独自にpr_warn等したいときなどに便利です。 lock_is_held() lock_is_held_type()
5-2. lock pinning
想定外のパスでlockを解放してしまっているケースを捕捉する為のAPIです。 想定外のcrossrelease(つまり、予期せぬ別コンテキストによるrelease)に限られる訳でもなく、 またついでに言えばKernel v4.14 upstreamに含まれていたcrossreleaseサポートとも関係がありません。 詳細は以下のCommitメッセージに記載されています。
Commit a24fc60d63da ("lockdep: Implement lock pinning")
5-3. その他
debug_show_all_locks() debug_show_held_locks(struct task_struct *task) debug_check_no_locks_freed(const void *from, unsigned long len) debug_check_no_locks_held() etc.
6. 動作メカニズム詳解
6-1. validation発火の基本パターン
各種locking primitiveの獲得時
最も標準的なパターンです。タスク単位の現時点での獲得中ロック情報(
current->held_locks
)の管理は、典型的にはロックの獲得時/解放時にpush/popされ、 その一方でlock_class間の依存関係は累積で積み重なっていく形です(つまりロック解放時、依存グラフには何らかの削除更新はなされません)。なお、解放するロックが現時点の最も内側の物、つまり一番最後に獲得された物では無いときpopではなくなりますが、この時解放対象よりも 内側のロックに関して、lockdep文脈における擬似的な再獲得がなされることに注意して下さい。nested lockの場合、"merge"の発生可能性もあります。
IRQ/SoftIRQ有効化時
最新のlock chain(IRQ/SoftIRQ無効化状態で獲得され、そのままのロック群)は強制的にhardirq-unsafe/softirq-unsafeだと 認識を改めなくてはなりません。よってここでも一部のvalidationが発火します。 具体的には次項で解説するメカニズムにおいて、Phase 1及びPhase 2が実施されることになりますが、 Phase 3以降が不必要なことは明らかでしょう。なぜなら、IRQ/SoftIRQ有効化自体がrecursion deadlockを新たに発生させることは無いし、 また依存グラフのトポロジ自体にも変更は加わらない為です。
補足: IRQ/SoftIRQ無効化時はvalidationは不要ですが、その後のdisabled期間中のlockdepの挙動に幾つかの変化が及びます。
IRQ/SoftIRQ無効化状態で新たに獲得されたロックはirq-safeである為、 irq-safe->irq-unsafeパターンは気にする必要がなくなります。 usage_maskにENABLED、IN-USEいずれのフラグも立たなくなります。 よって後述のvalidation Step 1及び2は不要であり、走らなくなります。 なお、Step 3(recursion deadlock detection)、Step 4(circular dependency detection)及びStep 5は依然必要ですが、これは説明するまでもないでしょう。
6-2. 各validationの動作メカニズム
ここでは、lockdepのvalidationの根幹部分をPhase 1 ~ Phase 5に分けて図解します。 なお、ソースコードに「Phase N」といった記述は無く、筆者が独自に説明の為に分解したに過ぎません。 更に、将来lockdepのソースコードに対する変更により、下記の記述が obsolete になり得ることには十分注意してください。
なお、Phase 1 ~ Phase 5全ての図に登場するdirectedなlock class依存グラフは、 Resource Allocation Graph (RAG) からProcessをそぎ落としたような物、或いはRAGからwait-for graphを引いたような物と捉えることが出来るでしょう。 但し先述の通り、lock classはRAGにおけるResourceと一対一とは限りません。
Phase 1.
各lock_classはusage_maskに、ブート後からの全ての使用コンテキストパターンを記憶しています。 acquire時、それが新しいコンテキストパターンだった場合、usage_maskの該当bitに1がセットされ、他方release時には変化はありません。 usage bit、usage maskの関係性は以下の図を参照ください。 Phase 1ではこのusage_maskを見て、過去に(まさに現在獲得しようとしている新しいheld_lockに対する)逆direction(USED_IN or ENABLED)の 非readロック獲得が無かったかどうかを検証します。(=inversion deadlock avoidance)
Phase 2.
ロックのdirection(ENABLED or USED-IN)次第でlock_class依存グラフを辿り、irq-safe -> irq-unsafeパターンが新たに生じてしまわないかを BFS(Breadth-First Search)でチェックします(=inversion deadlock avoidance)。
もし対象とする新しいheld_lockのlock_classが「USED-IN」だった場合check_usage_forward()
、
「ENABLED」だった場合、check_usage_backward()
です。余談ですが、ここでは後述Phase 4と同様のBFS実装が使用されているものの、
当Phase 2においてはそもそも全ノードを探索する必要がある為、BFSである必然性はないと言えるでしょう。
Phase 3.
自タスクのheld_locksに既に当該lock_classが存在していないかチェックします(=recursion deadlock detection)。 機構自体がシンプルで直感的ですのでここではこれ以上の説明は不要そうです。
Phase 4.
自タスクのheld_locks内に、新なheld_lockが加わる事でcircular dependencyを生み出してしまわないかチェックします(=recursion deadlock avoidance)。 これは、RAGベースのdeadlock avoidanceにおけるcircle検知に近いものです。 DFS(Depth-First Search)ではなくBFS(Breadth-First Search)なのは、最小のcircular dependencyを検出する為です。
Phase 5.
新しく2つの依存グラフが接続される事で、irq-safe -> irq-unsafeのパターンが生まれてしまわないかチェックします(=inversion deadlock avoidance)。
7. オーバヘッドについて
7-1. 一連のvalidationにおける計算コスト
標準的なvalidationをcall pathに沿って図示してみます。
# 以下図内大文字アルファベット略記について: M : current->lockdep_depth E : #. of edges in the relevant lock dependency graph V : #. of vertices in the relevant lock dependency graph
7-2. procfs
echo 0 > /proc/sys/kernel/prove_locking
これは先述の標準的なvalidation(phase 1 ~ phase 5)を、原則全て無効化するに等しいです。 一方でRCU primitiveの誤使用検知をはじめとした、lockdepの周辺機能群は依然利用出来ます。 assertionをはじめとした機構は有効化しつつ、lockdepの根幹のオーバヘッドが気になる時に使用すると良いかもしれません。
8. 偽陽性・偽陰性について
8-1. 偽陽性
そもそもlockdepのWARNINGにおいて「Possible...」とあるように、必ずしもdeadlockが発生せずともWARNINGは出ます。RAGベースのdeadlock avoidanceに近い物と捉えることができます。一方で、RAGベースのdeadlock avoidanceにおいては「safe」であるはずのパターンに関しても一部、lockdep splatに結び付きdebug_locks==0となってしまい得る事に関しては注意すべきでしょう。依存グラフのvertexはlock_classのみであった事を思い出して下さい。
例えば以下のように、100% deadlockが発生しないようなケースにおいても、先述のvalidationにおける「phase 4」に引っ掛かってしまい、後続の真のdeadlockを捕捉しそびれ得ます。このケースは、厳密にRAGベースでのdeadlock avoidanceであればそもそもcycleとして認知されないので、問題無かった(「safe」であった)はずの物です。そうは言っても、仮にRAGベースの純粋なdeadlock avoidanceを行おうとすれば、メモリ使用量、計算コストといった観点で現実的ではないはずです。
適当なkernel module
#include <linux/kthread.h> #include <linux/module.h> #include <linux/spinlock.h> static spinlock_t lock_1; static spinlock_t lock_2; static int thread_fn(void *priv) { // lock_1, lock_2でdeadlockが起きる確率は0です。 // (kernel thread "test1"の)process contextにおいてのみ // lock/unlockされ得る為, _irqsave/_irqrestoreは不要です。 spin_lock(&lock_1); spin_lock(&lock_2); spin_unlock(&lock_2); spin_unlock(&lock_1); spin_lock(&lock_2); spin_lock(&lock_1); spin_unlock(&lock_1); spin_unlock(&lock_2); return 0; } static int __init test_init(void) { struct task_struct *thread; spin_lock_init(&lock_1); spin_lock_init(&lock_2); thread = kthread_create(thread_fn, NULL, "test1"); if (IS_ERR(thread)) return -1; wake_up_process(thread); return 0; } static void __exit test_exit(void) { return; } module_init(test_init) module_exit(test_exit) MODULE_AUTHOR("Test Shitaro"); MODULE_LICENSE("Dual BSD/GPL"); MODULE_DESCRIPTION("test");
上記kernel moduleのinsmod後たちまち吐かれるmessage
[ 5224.449556] ====================================================== [ 5224.450401] WARNING: possible circular locking dependency detected [ 5224.452055] 5.8.0 #22 Tainted: G OE [ 5224.453055] ------------------------------------------------------ [ 5224.453851] test1/2913 is trying to acquire lock: [ 5224.454482] ffffffffa06e14d8 (&lock_1){+.+.}-{3:3}, at: thread_fn+0x4d/0x68 [test1] [ 5224.455603] [ 5224.455603] but task is already holding lock: [ 5224.456379] ffffffffa06e1478 (&lock_2){+.+.}-{3:3}, at: thread_fn+0x41/0x68 [test1] [ 5224.457596] [ 5224.457596] which lock already depends on the new lock. [ 5224.457596] [ 5224.458723] [ 5224.458723] the existing dependency chain (in reverse order) is: [ 5224.459816] [ 5224.459816] -> #1 (&lock_2){+.+.}-{3:3}: [ 5224.460614] _raw_spin_lock+0x34/0x80 [ 5224.461235] thread_fn+0x1d/0x68 [test1] [ 5224.461875] kthread+0x134/0x180 [ 5224.462448] ret_from_fork+0x22/0x30 [ 5224.462962] [ 5224.462962] -> #0 (&lock_1){+.+.}-{3:3}: [ 5224.463673] __lock_acquire+0x14fa/0x26e0 [ 5224.464302] lock_acquire+0xc7/0x3b0 [ 5224.464762] _raw_spin_lock+0x34/0x80 [ 5224.465263] thread_fn+0x4d/0x68 [test1] [ 5224.465760] kthread+0x134/0x180 [ 5224.466179] ret_from_fork+0x22/0x30 [ 5224.466669] [ 5224.466669] other info that might help us debug this: [ 5224.466669] [ 5224.467580] Possible unsafe locking scenario: [ 5224.467580] [ 5224.468268] CPU0 CPU1 [ 5224.468772] ---- ---- [ 5224.469403] lock(&lock_2); [ 5224.469978] lock(&lock_1); [ 5224.470656] lock(&lock_2); [ 5224.471288] lock(&lock_1); [ 5224.471612] [ 5224.471612] *** DEADLOCK *** [ 5224.471612] [ 5224.472384] 1 lock held by test1/2913: [ 5224.472803] #0: ffffffffa06e1478 (&lock_2){+.+.}-{3:3}, at: thread_fn+0x41/0x68 [test1] [...]
8-2. 偽陰性
原理的には存在しませんが、そもそもlockdepがカバーしていないlockに関しては当然捕捉できません。例えば、Linux Kernel v4.14において一時的にupstreamでサポートされていた7、cross-releaseのサポートは本記事執筆時点の環境においては既に撤去されている為、当然それら(page lock, completion, etc.)はもうカバー出来ません。
9. まとめ
lockdepが何をどのようにして検知しようとしているものか、またどういった実装になっているのか、初めて理解する必要が出た際にでも、 本稿が少しでもお役に立てることが出来れば幸いです。
-
IRQコンテキスト、SoftIRQコンテキスト、通常のタスクコンテキストそれぞれでも切り替わります。コンテキストが切り替わると、
current->held_locks
自体は積み上がり続けるものの、lock chainは切り替わります。一方で、単にlocal_irq_save
やlocal_bh_disable
の類では、lock chainは切り替わりません。対向する個別のheld stack同士としてvalidationを走らせる必要が無い為です。↩ -
lock class間の依存を示すグラフを指しています。lock class Aのinstanceを獲得後、続けてlock class Bのinstanceを獲得した場合、
lock class A -> lock class B
となります。↩ -
class idのシーケンスにより
lock_chain
は一意に決まる為、class idの順番も当然重要です。↩ -
lockdepを無効化しない類のWARNINGも多種存在します。次項後半でも一部取り上げています。↩
-
最小単位のlocking primitive自体は基本的に、deadlock条件の内「Mutual Exclusion」、「Hold and Wait」、「No Preemption」の3点に関して、そのprimitiveの使用方法の如何によって避けられるような物でもないので、必然的に「Circular Wait」のみに着目したdeadlock “avoidance” ないしdeadlock “detection” が行われることになります。その為、このシンプルさは当然といえば当然でしょう。内部的にlocking primitiveを使用したsynchronization primitive、例えばそもそもdeadlockを回避する為の機構であるwait/wound mutex(
ww_mutex
)は、一つ上のレイヤーで「No Preemption」条件を排除する機構である為、1つのwait/wound mutexのコンテキスト単体においては、「Circular Wait」に着目した典型的なlockdep splatが見られることはありません。ところで、(a)、(b)の分類は一見CSP(Communicating Sequential Process)文脈における、1プロセスのdeadlock("STOP")と並行プロセス間のdeadlock("ungranted requests“)に対応しているように見えるかもしれませんが、全く別次元の話です。↩ -
関連Commits: Commit b09be676e0ff “locking/lockdep: Implement the ‘crossrelease’ feature”, Commit e966eaeeb623 “locking/lockdep: Remove the cross-release locking checks”↩