Linux Kernel: mutex APIによるロック(排他)方法
前書き:mutexとは
Linux Kernelに限らず、様々なプログラミング言語やライブラリはロック機構を提供しています。ロック機構は、複数のプロセスが同時に共有データを書き換え、意図しないデータ状態となる事を防ぎます。代表的なロック機構には、
- spinlock
- semaphore
- mutex
があります。
今回説明するmutex(Mutal Exclusion)は、一つのプロセス(スレッド)だけが共有リソースにアクセスする事を許可するロック機構です。Device Driverで用いるロック機構ではデファクトスタンダード扱いです。その理由は、他のロック機構がデメリット(以下)があるからです。
- spinlockはロック解放待ち(ビジーウェイト)でCPUリソースを消費
- ただし、割り込みコンテキストではspinlockを使用(mutexはデッドロックが発生する可能性あり)
- semaphoreは共有データに複数プロセスが同時アクセス可能(ロック獲得条件が緩い)
本記事では、mutexを使用する上での注意点を述べた後に、Linux Kernelのmutex APIについて説明していきます。
mutexを使用する上での注意点
Linux Kernelソースコードのinclude/linux/mutex.hには、mutexを使用する上での注意点(以下の箇条書き)が記載されています。User空間でも同じようなノウハウ(注意点)があると思いますが、赤字部分はHardwareを考慮した注意事点(Kernel空間ならではの注意点)です。
- 一度に、一つのタスクだけがmutexを保持可能
- mutex所有者のみがロックを解除可能
- 複数のロック解除は許可されていない
- 再帰的なロックは許可されていない
- mutexオブジェクト(mutex用の構造体)は、API経由で初期化する事
- mutexオブジェクトは、memset()やコピーによる初期化をしてはいけない
- タスク(プロセス)は、mutex(ロック)を保持した状態で終了してはいけない
- ロック獲得したままのメモリ領域は、解放(free)してはいけない
- 獲得されたmutex(使用中のmutex)は、再初期化してはいけない
- mutexは、taskletsやtimerのようなHW/SWのIRQ(割り込み)コンテキストで使用不可
- 理由:割り込みハンドラがロック中のmutexを獲得しようとするとデッドロック発生するため
Linux Kernel mutex構造体
mutex構造体は、include/linux/mutex.hに定義が存在します。mutex APIを使う上でmutex内部の仕組みを知る必要はありませんが、簡単に説明します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
struct mutex { /* 1: unlocked, 0: locked, negative: locked, possible waiters */ atomic_t count; spinlock_t wait_lock; struct list_head wait_list; #if defined(CONFIG_DEBUG_MUTEXES) || defined(CONFIG_MUTEX_SPIN_ON_OWNER) struct task_struct *owner; #endif #ifdef CONFIG_MUTEX_SPIN_ON_OWNER struct optimistic_spin_queue osq; /* Spinner MCS lock */ #endif #ifdef CONFIG_DEBUG_MUTEXES void *magic; #endif #ifdef CONFIG_DEBUG_LOCK_ALLOC struct lockdep_map dep_map; #endif }; |
mutex構造体のメンバ変数 | 役割 |
atomic_t count | atomic_t構造体(中身はint型のcounter)としてmutexの状態を管理します。1=アンロック状態、0=ロック状態。 |
spinlock_t wait_lock | mutex内部で、mutex獲得待ちのタスクを起床(wake up)させる場合などに、spinlockを使用します。 |
struct list_head wait_list | mutex獲得待ちのタスクをList形式で管理します。Linux Kernel内のListの仕組みに関しては、本サイトに説明記事があります。 |
struct task_struct *owner; | mutex構造体を管理(使用)するタスクを意味します。 |
struct optimistic_spin_queue osq | Mutex獲得時のMCSロック(スピン)機能が有効の場合、使用します。MCSロック機能では、1)他のCPUがmutexの所有者、2)タスクの再スケジューリングが不要、の条件を共に満たした場合、ロック獲得(※)のためにスピンして待ち続けます 。※他のCPUが処理実行中のため、直ぐにロックが解放される可能性があります。 |
void *magic | mutexのセマンティックス違反やデッドロックを検知する場合に使用します。 |
struct lockdep_map dep_map | lockdep機能(ロック獲得順番などの正当性チェッカ)で用いるロック依存関係マップです。 |
Linux Kernel mutexの初期化(動的)
Linux Kernel内で、動的にmutexを初期化する場合、mutex_initマクロを使用します。初期化例は、以下の通りです。
1 2 |
struct mutex __mutex; mutex_init(&__mutex); |
mutex_initマクロは、以下の実装の通り、各APIの初期化関数をコールしているだけです。途中で登場するlock_class_key構造体は、lockdep機能におけるロッククラス(ロック規則に関して論理的に同じロックのグループ)に対して、一意(ユニーク)のアドレスを割り当てるために存在します。
1 2 3 4 5 6 |
# define mutex_init(mutex) \ do { \ static struct lock_class_key __key; \ \ __mutex_init((mutex), #mutex, &__key); \ } while (0) |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void __mutex_init(struct mutex *lock, const char *name, struct lock_class_key *key) { atomic_set(&lock->count, 1); spin_lock_init(&lock->wait_lock); INIT_LIST_HEAD(&lock->wait_list); mutex_clear_owner(lock); #ifdef CONFIG_MUTEX_SPIN_ON_OWNER osq_lock_init(&lock->osq); #endif debug_mutex_init(lock, name, key); } |
Linux Kernel mutexの初期化(静的)
Linux Kernel内で、静的にmutexを初期化する場合、DEFINE_MUTEXマクロを使用します。ここでの静的とは、mutexの変数宣言時という意味です。DEFINE_MUTEXマクロをコールすると、新しいmutex構造体(__mutex)が作成され、初期化済みになります。初期化例は、以下の通りです。
1 |
DEFINE_MUTEX(__mutex); |
DEFINE_MUTEXマクロの定義は、include/linux/mutex.hにあり、以下の通りです。mutexの初期化処理としては、sponlockやlistなどの各APIにおける静的初期化処理をコールしているだけです。
1 2 3 4 5 6 7 8 9 |
#define __MUTEX_INITIALIZER(lockname) \ { .count = ATOMIC_INIT(1) \ , .wait_lock = __SPIN_LOCK_UNLOCKED(lockname.wait_lock) \ , .wait_list = LIST_HEAD_INIT(lockname.wait_list) \ __DEBUG_MUTEX_INITIALIZER(lockname) \ __DEP_MAP_MUTEX_INITIALIZER(lockname) } #define DEFINE_MUTEX(mutexname) \ struct mutex mutexname = __MUTEX_INITIALIZER(mutexname) |
mutex(ロック)獲得およびmutex(ロック)解放
mutex獲得にはmutex_lock()、mutex解放にはmutex_unlock()を用います。mutex_lock()はmutexを獲得できれば処理を継続しますが、mutexを獲得できなければ獲得できる状態になるまでスリープします。どちらのケースも、mutex_lock()の処理が終了した後では、mutexを獲得した状態になっています。
以下に実行例を示します。
1 2 3 4 5 6 |
struct mutex __mutex; mutex_init(&__mutex); mutex_lock(&__mutex); /* ここに共有リソースへのアクセス処理を記載 */ mutex_unlock(&__mutex); |
mutex(ロック)の状態をチェック
mutexの獲得はせず、mutex(ロック)の状態を調べたい場合、mutex_is_locked()を使用します。返り値が1の場合は他のタスクがmutexを獲得中(ロック状態)、0の場合はどのタスクもmutexを獲得していません(アンロック状態)。
以下に実行例を示します。
1 2 3 4 5 6 7 8 |
struct mutex __mutex; mutex_init(&__mutex); if(mutex_is_locked(&__mutex)) { /* ロック中 */ } else { /* アンロック中 */ } |
mutex獲得を試み、獲得失敗時はスリープしない
mutexの獲得失敗時、mutex_lock()のようにスリープ状態に移行せず、そのまま処理を継続するには、mutex_trylock()を使用します。mutex_trylock()は、mutexの獲得成功時には1を返し、獲得失敗時は0を返します。
以下に実行例を示します。
1 2 3 4 5 6 7 8 |
struct mutex __mutex; mutex_init(&__mutex); if(mutex_trylock(&__mutex)) { /* ロック中 */ } else { /* ロック獲得に失敗したため、別の処理を実行 */ } |
割り込み可能なスリープでmutex獲得を待機
mutex(ロック)獲得中に割り込みが発生し、mutex(ロック)獲得処理の呼び出し元がスリープ状態になる場合、mutex_lock_interruptible()を使用します。mutex_lock_interruptible()は、mutexの獲得成功時には1を返し、ロック獲得を試みている最中にシグナルによって中断された場合は-EINTRを返します。
ユーザコンテキストで処理が制御される場合、ユーザが急に処理を中断(“Ctrl+C”=”SIGINT”)する可能性があるため、mutex_lock()ではなくmutex_lock_interruptible()を使用します。mutex_lock()は、Ctrl+Cなどのシグナルが発生した場合も、スリープし続けます。
以下に実行例を示します。
1 2 3 4 5 6 7 8 |
struct mutex __mutex; mutex_init(&__mutex); if(!mutex_lock_interruptible(&__mutex)) { /* ロック中 */ } else { /* シグナルにより、mutex獲得が中断 */ } |
mutex獲得待機タスクのKILLを検出
mutex獲得待機タスクがKILLされた事を検出するには、mutex_lock_killable()を使用します。前述のmutex_lock_interruptible()は複数のシグナルを検出しますが、その一方でmutex_lock_killable()はタスク(プロセス)を終了させるSIGKILL(および他の重要なシグナル)を検出します(Ctrl+Cは、SIGKILLではなくSIGINT)。
mutex_lock_interruptible()は、mutex 獲得待ちの間に重要なシグナルを受信した場合、シグナル処理を優先するため、現在実行中のmutex 獲得待ちタスクをmutex獲得待ちリストから削除します。返り値は、mutexが獲得成功時に0、それ以外のケースは状況に応じたERROR番号(-EALREADY、-EINTR、-EDEADLK)です。
以下に実行例を示します。
1 2 3 4 5 6 7 8 |
struct mutex __mutex; mutex_init(&__mutex); if(!mutex_lock_killable(&__mutex)) { /* ロック中 */ } else { /* シグナルにより、mutex獲得が中断 */ } |
ロシア人と国際結婚した地方エンジニア。
小学〜大学院、就職の全てが新潟。
大学の専攻は福祉工学だったのに、エンジニアとして就職。新卒入社した会社ではOS開発や半導体露光装置ソフトを開発。現在はサーバーサイドエンジニアとして修行中。HR/HM(メタル)とロシア妻が好き。サイトに関するお問い合わせやTwitterフォローは、お気軽にどうぞ。