MIRACLE
メールサービス申込 ユーザー登録 パートナー情報
お問い合わせ FAQ サイトマップ
MIRACLE LINUXの特長 製品紹介 サービス案内 購入 サポート 技術フォーラム

プロフィール

コアテクノロジー部

ミラクル・リナックスのOS開発やサポートを担う、技術部の精鋭陣が交代で担当します。

ミラクル関連リンク

採用情報

サイト検索

2009年10月

        1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31

スピンロックその6

月イチ更新と化したこのスピンロック連載、おそらく固定読者はいないと思いますが細々と続けてみます。
前回の最後に「次は(c)のケースだ」などと予告しましたが、(b)のケースとあまり変わり映えしないので思い切って省略します。ふりかえれば、その2でロック取得を追い始めて以来早くも3か月経ってしまいました。いい加減にロックを解放しなきゃどんなシステムも止まるわな、てなわけではありませんが話題を spin_unlock() に移します。
その2で確認したタスク・スケジューラの task_rq_lock() に対応する解放処理は、その名も task_rq_unlock() です。

kernel/sched.c

static inline void task_rq_unlock(runqueue_t *rq, unsigned long *flags)
        __releases(rq->lock)
{
        spin_unlock_irqrestore(&rq->lock, *flags);
}

関数のインタフェース宣言行と処理本体との間に見慣れない行がありますね。これはロックの取得・解放の数をカウントするツールのためのマクロだそうです([linux-pm] __releases and __acquires macros in compiler.h)。定義からしても通常のビルドでは消えますね。

#ifdef __CHECKER__
# define __acquires(x)  __attribute__((context(0,1)))
# define __releases(x)  __attribute__((context(1,0)))
#else
# define __acquires(x)
# define __releases(x)
#endif

さて task_rq_unlock() の実体は spin_unlock_irqrestore() であることが分りましたが、これもまたマクロです。

include/linux/spinlock.h

#define spin_unlock_irqrestore(lock, flags) \
                                        _spin_unlock_irqrestore(lock, flags)

この _spin_unlock_irqrestore() の定義は Uni-Processor と SMP とで異なります。
まずは簡単そうな UP から見ていきましょう。

include/linux/spinlock_api_up.h

#define _spin_unlock_irqrestore(lock, flags) __UNLOCK_IRQRESTORE(lock, flags)
#define __UNLOCK_IRQRESTORE(lock, flags) \
  do { local_irq_restore(flags); __UNLOCK(lock); } while (0)

この local_irq_restore() は割り込みの回復ですが、これは後回しにして、実体と思われる__UNLOCK() から先に確認しましょう。

include/linux/spinlock_api_up.h

#define __UNLOCK(lock) \
  do { preempt_enable(); __release(lock); (void)(lock); } while (0)

この preempt_enable() は include/linux/preempt.h で定義されていますが、これはマクロが続く分かりにくい構造なので、諦めて gcc に展開させることにします。
次のような C 言語ソースファイル a.c を準備。

#include <linux/preempt.h>
preempt_enable();

カーネルのソースツリーの include ディレクトリを -I で指定した cc -E でプリプロセッサに掛けます。

$ cc -E -I ./linux-2.6.16.18/include a.c
[関係ないところは省略]
do { } while (0);
$

空っぽになるんですね。
次に CONFIG_PREEMPT を define してコンパイルすると、

$ cc -E -I ./linux-2.6.16.18/include -DCONFIG_PREEMPT a.c
[関係ないところは省略]
do { do { barrier(); do { (current_thread_info()->preempt_count) -= (1); } while (0); } while (0); barrier(); do { if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) preempt_schedule(); } while (0); } while (0);
$

あまりに読みにくい。。。 indent コマンドで整形してみますか。

$ cc -E -I ./linux-2.6.16.18/include -DCONFIG_PREEMPT a.c | indent
[関係ないところは省略]
do
  {
    do
      {
        barrier ();
        do
          {
            (current_thread_info ()->preempt_count) -= (1);
          }
        while (0);
      }
    while (0);
    barrier ();
    do
      {
        if (unlikely (test_thread_flag (TIF_NEED_RESCHED)))
          preempt_schedule ();
      }
    while (0);
  }
while (0);
$

余計な do {} while() を削ると、つまりこーゆーことですね。

barrier ();
(current_thread_info ()->preempt_count) -= (1);
barrier ();
if (unlikely (test_thread_flag (TIF_NEED_RESCHED)))
  preempt_schedule ();

タスク毎の preempt_count というカウンタから1を引いています。その前後にはメモリバリアですね。最後は、もしも休止すべきタスクならば、ここで休止させると。
なんだかタスク・スケジューラの方に脱線しそうなので、ここで一旦休憩です。

切りがいいので今回はここまでとさせてください。

X のコアダンプ

本日は時間の関係で、「Linux 一口メモ」にさせていただきます。

アプリケーションがクラッシュした際、どのように原因を調査しますか?
吐き出されたコアダンプを gdb 等を用いてデバッグ方もいると思います。
しかし、X Window がクラッシュした場合、そのままではコアダンプを吐かないため、以下の設定をする必要があります。

1. /etc/X11/xorg.conf に以下を追加する。
------------------------------------------------------------
Section "ServerFlags"
    Option "NoTrapSignals"
EndSection
------------------------------------------------------------
2. root で実行 (必須)
3. SEGV が発生すると core.$$ を吐くようになります。

スピンロックその5

前回の_raw_spin_trylock()の紹介では、CONFIG_PREEMPとCONFIG_SMPとCONFIG_DEBUG_SPINLOCKのすべてが定義されているケースの実装における

if (ret)
      debug_spin_lock_after(lock);

について触れていませんでした。そこを補足します。

変数 ret が真になるのは、ロックを獲得できた場合です。この時、デバッグ用途のロック情報を保存するためdebug_spin_lock_after()が実行されます。

static inline void debug_spin_lock_after(spinlock_t *lock)
{
     lock->owner_cpu = raw_smp_processor_id();
     lock->owner = current;
}

typedef struct {
     raw_spinlock_t raw_lock;
#if defined(CONFIG_PREEMPT)&& defined(CONFIG_SMP)
     unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
     unsigned int magic, owner_cpu;
     void *owner;
#endif
} spinlock_t;

ロックを取得したタスクおよびその時のCPUが記録されるわけです。ダンプ情報でこれを確認したブログ「kernel-2.6.17でのspinlockのオーナー情報」も参照して下さい。

さて、その2からスピンロック取得処理の実装を追ってきましたが、今回でようやく(b)のケースが終わりです。次回は最後の(c)のケースです。

(a) CONFIG_SMPとCONFIG_DEBUG_SPINLOCKがともに未定義の場合、すなわちデバッグ無しのユニプロセッサ・カーネルの場合。

(b) CONFIG_PREEMPとCONFIG_SMPがともに定義されている場合、すなわちプリエンプションなマルチプロセッサ・カーネルの場合。

(c) それ以外の場合。

スピンロックその4

多忙のため、1ヶ月ほど間があいてしまいましたが、何事も無かったように平静を装って続けます。

今回は

(b) CONFIG_PREEMP と CONFIG_SMP がともに定義されている場合、すなわちプリエンプションなマルチプロセッサ・カーネルの場合。

のケースで _raw_spin_trylock() の確認からです。これは CONFIG_DEBUG_SPINLOCK が定義されているかどうかに応じて 2種類ありました。

  • CONFIG_DEBUG_SPINLOCK が定義されている場合

int _raw_spin_trylock(spinlock_t *lock)
{
        int ret = __raw_spin_trylock(&lock->raw_lock);

        if (ret)
                debug_spin_lock_after(lock);
#ifndef CONFIG_SMP
        /*
         * Must not happen on UP:
         */
        SPIN_BUG_ON(!ret, lock, "trylock failure on UP");
#endif

        return ret;
}

  • CONFIG_DEBUG_SPINLOCK が定義されていない場合

# define _raw_spin_trylock(lock)        __raw_spin_trylock(&(lock)->raw_lock)

ということで、両者を比較すると、上で定義されている場合の赤いエリアが省略された状態ですね(もちろん、間に関数呼び出しが入ってるとか違いはありますけど)。というわけで、上のほうのコードを確認しましょう。

まずはロックの中心の処理 __raw_spin_trylock() です。

static inline int __raw_spin_trylock(raw_spinlock_t *lock)
{
        char oldval;
        __asm__ __volatile__(
                "xchgb %b0,%1"
                :"=q" (oldval), "=m" (lock->slock)
                :"0" (0) : "memory");
        return oldval > 0;
}

ようやく出てきたインラインアセンブリ構文です。gcc (というより gas?) と縁の無い人にはサッパリ不明な構文ですが、A. SAITOH さんの「GCCでインラインアセンブリを使用する方法と留意点等 for x86」 あたりを読めば楽勝です。

"xchgb %b0,%1" が実行されるアセンブリコード、"=q" (oldval), "=m" (lock->slock) が出力オペランド、"0" (0) が入力オペランドですね。

"q" で自動割当されたレジスタ (eax,ebx,ecx,edx のいずれか) に 0 を代入して xchgb の第1引数として渡し、第2引数としてメモリ上の slock 変数を渡して xchgb を実行しています。そして実行後のレジスタの値を oldval 変数で受け取っています。ここの xchgb の b は byte の b ですね。

ちょっと分かりにくいでしょうか? 具体的なアセンブリコードも見たほうがいいですね。cc -O2 -c でコンパイルして objdump -D で逆アセンブルすると以下の結果になりました。右端に簡単なコメントも付けてみます。

00000000 <__raw_spin_trylock>:
  0: 55        push   %ebp            EBPを退避
  1: 89 e5     mov    %esp,%ebp       ESPをEBPにコピー
  3: 31 c0     xor    %eax,%eax       EAXを0に設定
  5: 8b 55 08  mov    0x8(%ebp),%edx  アドレスをEDXに
  8: 86 02     xchg   %al,(%edx)      ALとslockを交換
  a: 84 c0     test   %al,%al         ALとALの論理積
  c: 0f 9f c0  setg   %al             AL > 0を判定
  f: 0f b6 c0  movzbl %al,%eax        ALをEAXに拡張
 12: c9        leave                  EBPを復旧
 13: c3        ret                    リターン

8ビットの AL レジスタが使われているのが分かりますね。

まとめると __raw_spin_trylock(lock) は、lock->slock の値が 0(LOCKED) なら偽(0) を返し、1(UNLOCKED) 以上なら真(1) を返します。さらに lock->slock の値を 0(LOCKED) に設定します。

volatile unsigned int 型の slock を char 型の oldval で受け取るところはスピンロックその3でみた slock の char キャストと似た扱いですね。

なお xchg などの命令についての詳細は"IA-32 IntelR Architecture Software Developer's Manual Volume 2B: Instruction Set Reference, N-Z" を参照してください。

今回はここまでにします。
更新が遅いせいもあってか、なかなか進みがよくありませんね。もっと飛ばしてサクサク進んだほうがよい、または今のペースでもいいので逐一追ったほうがよい、などリクエストがあればコメントにお願いします。

Qlogicドライバ failoverの話

雑談のネタがないので、技術系の雜話をします。どっちも雑ですが。。。

さて、Linuxを趣味で使用する方に、無縁かもしれませんが、エンタープライズにLinuxを使用すると、性能以外に、拡張性、耐故障性などが求められます。今回は、Qlogicのfailoverについて触れます。

ファイバーチャンネルでQlogic社のHBAのQlogicを使われる方が多いと思います。ポートが二つ以上あると、多チャンネル構成が出来ますので、ドライバのバージ>ョンによってI/Oのfailoverやloadbalanceが可能になります。最新のバージョンは両方できるようになっています。

2ポート以上の構成は見たことがないので、2ポートについての動作を書きます。この場合、チャンネルはそれぞれアクティブパス(Active Path)、スタンバイパス(Standby Path)を構成します。この構成がシステムのブート時に決まります。

Linuxブートプロセスで、PCIスキャンの時に最初に見つかったHBAのポートのチャンネルがディフォルトのActive Pathになります。その後見つかったものがStandby Pathになります。ここで、最初に見つかったHBAを0、次に見つかったHBAを1とします。

まず、カードが認識されていることをprocファイルから確認します。
次のprocファイルが存在します。

#ls /proc/scsi/qla2300/
/proc/scsi/qla2300/0
/proc/scsi/qla2300/1

次に、カードの情報を確認します。
#cat /proc/scsi/qla2300/0

QLogic PCI to Fibre Channel Host Adapter for QLA2340         :
        Firmware version:  3.03.06, Driver version 7.03.00-fo
              :
              :
Number of retries for empty slots = 0
Number of reqs in pending_q= 0, retry_q= 0, done_q= 0, scsi_retry_q= 0
Number of reqs in failover_q= 0
Host adapter:loop state= <READY>, flags= 0x860a33  <--この行に注目(1)
              :
              :
SCSI LUN Information:  <---- この情報に注目(2)
(Id:Lun)  * - indicates lun is not registered with the OS.
( 0: 0): Total reqs 12, Pending reqs 0, flags 0x0, 0:0:00,
( 0: 1): Total reqs 5, Pending reqs 0, flags 0x0, 0:0:00,
( 0: 2): Total reqs 5, Pending reqs 0, flags 0x0, 0:0:00,
( 0: 3): Total reqs 5, Pending reqs 0, flags 0x0, 0:0:00,

loop stateには、<READY>, <DOWN>, <DEAD>があります。それぞれは
<READY> =>  チャンネルのリンクが正常
<DOWN>  =>  チャンネルのリンクが異常 (リトライ中)
<DEAD>  =>  チャンネルのリンクが断線

上記(1)の情報から、HBA0のチャンネルが正常で、(2)からHBA0にディフォルのアクティブパスがあるということが分ります。

#cat /proc/scsi/qla2300/1
QLogic PCI to Fibre Channel Host Adapter for QLA2340
        Firmware version:  3.03.06, Driver version 7.03.00-fo
             :
             :
Host adapter:loop state= <READY>, flags= 0x860a33  <-- リンクOK。
             :
             :
SCSI LUN Information:  <---- なにもない。
(Id:Lun)  * - indicates lun is not registered with the OS.

SCSI LUN Information には、OSに登録されたSCSI ID - LUNの情報があります。起動時にディフォルトのアクティブパスから読み取った情報がカーネルに登録されます。スタンバイパスから読み取ったSCSI ID - LUN情報は、ドライバ内部で先に登録された情報にマージされます。このため、RAID上でLUNが4つがある場合、4 x 2 = 8個のディスクが見える筈ですが、failoverモードを使用すると、その半分の4つのディスクに構成されます。dupしたディスクが1つになったわけです。

ちょっとした実験ですが、アクティブパスのファイバーを抜いて、暫くして差し戻すと以下のdmesgの出力を確認出来ます。

#dmesg
             :
             :
qla2x00: FAILOVER device 0 from 5006xxxxxxxx7910 -> 5006xxxxxxxx7912 - LUN 00, reason=0x1
qla2x00: FROM HBA 1 to HBA 0
             :
             :
qla2x00: FAILBACK device 0 -> 5006xxxxxxxx7910 LUN 00
qla2x00: FROM HBA 0 to HBA 1
             :
             :

尚、このブログに書いた情報はMIRACLE LINUX 3.0 , kernel-2.4.21-20.29AXsmpの環境でQlogic 2340カードを使った時のものです。

Linux の割り込み処理 【第1回】

本日 「Linux の割り込み処理 【第1回】」 ということで書きたいと思います。
以下の内容は Intel 8086 系です。他のアーキは別の機会ということで。

「割り込み」 と一口に言っても、IRQ を介したハード割り込みや、CPU が発生させる例外など、色々あります。
各割り込みには 0 から 255 までの番号が付けられており、それぞれを 「割り込みベクタ」 と呼んでいます。
それらのベクタは idt_table (Interrupt Descriptor Table) というものに書かれており、ベクタの種類や割り込みに対する処理などを知ることができます。

crash> rd idt_table 512
c03cc000:  00605250 c02d8f00 006052e0 c02d8e00
c03cc010:  00605330 c02d8e00 006053c8 c02dee00
c03cc020:  006053f8 c02def00 00605404 c02def00
c03cc030:  00605410 c02d8f00 006052a8 c02d8f00

(中略)

c03cc7e0:  00605170 c02d8e00 00605190 c02d8e00
c03cc7f0:  006051f0 c02d8e00 00605210 c02d8e00

ベクタはサイズが 8 byte で 256 個ありますので、合計 2048 byte です。

そして、各割り込みの使用目的は以下のようになっています。

  • 0 から 31 までのベクタは例外と non maskable 割り込み
  • 32 から 47 までのベクタは IRQ による割り込み
  • 48 から 255 までのベクタはソフトウェア割り込み

例として、0 番について見ます。

crash> rd idt_table 2
c03cc000:  00605250 c02d8f00

40 - 43 bit がこのベクタの種類を表しており、

  • 0101 ならタスクゲート (最後が 500)
  • 1110 なら割り込みゲート (最後が e00)
  • 1111 ならトラップゲート (最後が f00)

となります。これはトラップゲートです。

次にベクタが呼ばれた場合の処理ですが、0 - 15 bit が low-part, 48 - 63 bit が high-part となる 32 bit のアドレスが使用されます。
この場合、アドレスは 0xc02d5250 となります。
これは以下のように、除算エラーの例外ハンドラです。

crash> sym c02d5250
c02d5250 (T) divide_error

次に、14 番について見ます。

crash> rd idt_table+14*8 2
c03cc070:  00605464 c02d8e00

40 - 43 bit が e00 なので、割り込みゲートです。
0 - 15 bit を low-part, 48 - 63 bit を high-part として組み合わせると、アドレスは 0xc02d5464 となります。
これは以下のように、ページフォールトの例外ハンドラです。

crash> sym c02d5464
c02d5464 (T) page_fault

今回はここまでにしますが、次回 (があれば) IRQ の割り込みや、システムコールについて書きたいと思います。

スピンロックその3

前回に続き、スピンロック取得処理の定義を追います。今回は(b)のケースです。

(a) CONFIG_SMP と CONFIG_DEBUG_SPINLOCK がともに未定義の場合、すなわちデバッグ無しのユニプロセッサ・カーネルの場合。

(b) CONFIG_PREEMP と CONFIG_SMP がともに定義されている場合、すなわちプリエンプションなマルチプロセッサ・カーネルの場合。

(c) それ以外の場合。

(b) が定義されているのは kernel/spinlock.c の中央付近です。ここでは _spin_lock() だけでなく、

/*
*         _[spin|read|write]_lock()
*         _[spin|read|write]_lock_irq()
*         _[spin|read|write]_lock_irqsave()
*         _[spin|read|write]_lock_bh()
*/

の計12個の関数がマクロを駆使して一括定義されています。
BUILD_LOCK_OPS(spin, spinlock) によって定義される _spin_lock() はマクロを置換すると以下の関数になります。

void __lockfunc _spin_lock(spinlock_t *lock)
{
        preempt_disable();
        for (;;) {
                if (likely(_raw_spin_trylock(lock)))
                        break;
                preempt_enable();
                if (!(lock)->break_lock)
                        (lock)->break_lock = 1;
                while (!spin_can_lock(lock) && (lock)->break_lock)
                        cpu_relax();
                preempt_disable();
        }
        (lock)->break_lock = 0;
}

EXPORT_SYMBOL(_spin_lock);

始めに preempt_disable() でプリエンプションを禁止しているのは (a) のケースと同様ですね。実際にロックの取得を試みる処理は、その次の for ループです。_raw_spin_trylock() でロック取得に成功したら真を返してループから抜け出します。ロックできなかったらプリエンプションを許可し、次のチャンスを待ちます。すぐに _raw_spin_trylock() を再実行したりせず、while ループで cpu_relax() を実行するというわけです。cpu_relax() はアーキテクチャによって実装は違いますが、ビジーウェイトでの CPU 負荷を下げるための関数です。i386 だと HT テクノロジの環境で効果がありそうなところですね。
cpu_relax() をやめて _raw_spin_trylock() を再実行するための条件は、以下のいずれかが満たされた時です。

(i) spin_can_lock(lock) が真である。
(ii) (lock)->break_lock が 0 になった。

まず (i) の定義を確認してみましょう。
include/linux/spinlock.h

#define spin_can_lock(lock)     (!spin_is_locked(lock))
#define spin_is_locked(lock)    __raw_spin_is_locked(&(lock)->raw_lock)

include/asm-i386/spinlock.h

#define __raw_spin_is_locked(x) \
                (*(volatile signed char *)(&(x)->slock) <= 0)

これらをまとめると、 (i) が満たされるのは

*(volatile signed char *)(&(&(lock)->raw_lock)->slock) > 0

すなわち、その1で確認した slock 変数の値が 1 以上の時になります。UNLOCKED 状態が 1 ですから、ここで 0 (以下) ならばロックされている状態であろうことが推測できますね。なお slock のアドレスを (volatile signed char *) でキャストしていますが、slock の型はそもそも volatile unsigned int です。この不一致はどのような意味があるのでしょうか。signed は 0 を下回る時 (これは実際に起こるのでしょうか?) に備えたのでしょうけれど、char とした理由はわかりません。

次は (ii) が満たされる場合ですが、ここは自分自身では直前に break_lock を 1 に設定しているため、自力では満たされません。ということは、同じロックを狙って競合していた他のカーネルスレッドが先にロックを獲得して _spin_lock() を終了した時になります。

まとめると (i),(ii) は次のように言い替えることができます。

(i) ロックが解放されたタイミング。ロックが解放されていないのにロック取得を試みる無駄を防ぐためのもの。
(ii) ロックが解放されたが、他のカーネルスレッドが先にロックを獲得してしまった。

条件 (ii) で使用されている break_lock 変数は、SMP カーネルでロックを取得したスレッドのスケジューリングを改良するために http://linux.bkbits.net:8080/linux-2.6/cset@1.1966.39.47 で導入されたものです。ちょっと難しいかもしれませんが、頑張って読んでみて下さい。

次回は _raw_spin_trylock() を確認します。

スピンロックその2

もうすっかり忘れられているかもしれませんが、スピンロックその1の続編です。

今回はロックの取得処理を追っていきましょう。前回に続き、プロセス・スケジューラ kernel/sched.c を徘徊してみます。。。

/*
* task_rq_lock - lock the runqueue a given task resides on and disable
* interrupts.  Note the ordering: we can safely lookup the task_rq without
* explicitly disabling preemption.
*/
static inline runqueue_t *task_rq_lock(task_t *p, unsigned long *flags)
        __acquires(rq->lock)
{
        struct runqueue *rq;

repeat_lock_task:
        local_irq_save(*flags);
        rq = task_rq(p);
        spin_lock(&rq->lock);

指定されたタスクが所属しているランキューをロックする関数 task_rq_lock() がありました。この関数は、例えばタスクを別なランキューに移動させる前などに実行してランキューをロックします。ロックの中心処理は、そのものずばり spin_lock() です。この実装を追ってみましょう。

spin_lock() の定義を遡っていくと、カーネルのオプションに応じて、大きく分けて以下の 3通りの定義がありました。

(a) CONFIG_SMP と CONFIG_DEBUG_SPINLOCK がともに未定義の場合、すなわちデバッグ無しのユニプロセッサ・カーネルの場合。

(b) CONFIG_PREEMP と CONFIG_SMP がともに定義されている場合、すなわちプリエンプションなマルチプロセッサ・カーネルの場合。

(c) それ以外の場合。

プロセッサが単一なのかそれとも複数なのかでも実装が異なるということですね。
まずは一番シンプルな (a) の場合を見ていきましょう。

include/linux/spinlock.h

#define spin_lock(lock)                 _spin_lock(lock)

include/linux/spinlock_api_up.h

#define _spin_lock(lock)                        __LOCK(lock)
#define __LOCK(lock) \
  do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)

さらに __LOCK(lock) の定義の直前にはこんなコメントが。

/*
* In the UP-nondebug case there's no real locking going on, so the
* only thing we have to do is to keep the preempt counts and irq
* flags straight, to supress compiler warnings of unused lock
* variables, and to add the proper checker annotations:
*/

ということで実際のロック処理は行っていないとのこと。定義も preempt_disable() でプリエンプション禁止しているだけですね。__acquire() は普通のカーネル (__CHECKER__ マクロは未定義ですよね?) なら空定義です。ユニプロセッサで割り込まれることが無い状態ならば、ロックが不要という扱いのようです。逆に、spin_lock() でロックを獲得したまま schedule() で処理を明け渡すようなコードがあったら排他処理にはならないということでしょうか。

次回は(b)を追ってみることにします。

スピンロックその1

今回は趣向を変えて、少しだけカーネル内部に触れてみたいと思います。

みなさん「スピンロック」という言葉に馴染みがあるでしょうか。そのメカニズムを詳しく知ってる方も多いかもしれませんし、例えばどこかで「スピンロック待ちの『ビジーウェイト』でCPUにロスが発生した」のような会話を小耳に挟む機会などもあるかもしれません。しかし、Linux カーネルにおけるスピンロックの実装部分を読んだことのある方は、もしかして意外に多くないかもしれません。ということで、今回はそのあたり(ただし i386 アーキテクチャのみ)を追ってみたいと思います。スピンロックの概念についての説明は省きます。

いきなりソースコードに飛び込んでみましょう(これがミラクル流です)。C言語に馴染みのない方はごめんなさい。弊社MIRACLE LINUXでもいいのですが、ここでは本家の http://www.kernel.org/pub/linux/kernel/v2.6/ に置いてある一番新しそうな linux-2.6.16.20.tar.bz2 を拾ってきて解凍しました。Linux カーネルは更新が速く、コードがどんどん変わっています。異なるバージョンでは以下の文章とは違う実装になっていると思いますので、そのへんは注意して下さい。

スピンロックはカーネル内の色々なところで使われています。どこでもよいのですが、ここではプロセス・スケジューラ kernel/sched.c でも覗いてみましょうか。。。

void __init sched_init(void)
{
        runqueue_t *rq;
        int i, j, k;

        for_each_cpu(i) {
                prio_array_t *array;

                rq = cpu_rq(i);
                spin_lock_init(&rq->lock);
                rq->nr_running = 0;

スケジューラを初期化する関数 sched_init() の中で、ランキューのスピンロック情報を保持する lock メンバを spin_lock_init() で初期化している処理が見つかりました。この lock メンバは spinlock_t 型で、これは include/linux/spinlock_types.h で次のように定義されています。

typedef struct {
        raw_spinlock_t raw_lock;
#if defined(CONFIG_PREEMPT) && defined(CONFIG_SMP)
        unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
        unsigned int magic, owner_cpu;
        void *owner;
#endif
} spinlock_t;

メインは raw_spinlock_t 型の raw_lock メンバのようです。この型は i386 では include/asm-i386/spinlock_types.h で次のように定義されています。

typedef struct {
        volatile unsigned int slock;
} raw_spinlock_t;

この slock 変数の値がロックの状態を保持するんですね。

データ構造が分かったところで、スピンロックの初期化処理に戻りましょう。 spinlock_t 型の &rq->lock メンバを初期化する spin_lock_init() は実は関数ではなくマクロで、 include/linux/spinlock.h で定義されていました。

#define spin_lock_init(lock)    do { *(lock) = SPIN_LOCK_UNLOCKED; } while (0)

この SPIN_LOCK_UNLOCKED は、普通のカーネル (CONFIG_DEBUG_SPINLOCK が未定義のカーネル) では、次のように定義されています。

include/linux/spinlock_types.h

# define SPIN_LOCK_UNLOCKED \
        (spinlock_t)    {       .raw_lock = __RAW_SPIN_LOCK_UNLOCKED }

include/asm-i386/spinlock_types.h

#define __RAW_SPIN_LOCK_UNLOCKED        { 1 }

ということで、slock に 1 が代入された状態が初期状態、すなわち UNLOCKED 状態であることがわかりました。

区切りが良いので、今回はここまでです。ロックが取得されることで、この slock の値が 1 からどのように変化するのか、あるいはどんなコードで実装されているのか、それらは次回、またコードを追ってみることにします。

会社情報 採用情報 個人情報保護方針 商標等取り扱い事項 English
Copyright(c)2000-2006 MIRACLE LINUX CORPORATION. All Rights Reserved.