Linux Kernel Driver Quest

本ページの Linux kernel version は linux-4.1.27 です。

はじめに

「自分が Linux kernel driver を書いてきた経験を整理したい」という目的でこのページを書く予定です。既に世の中には数多くの Linux kernel driver を書くための参考書があります。このページがこれらの参考書より優れた記述になる自信はありません。Linux kernel driver を書こうとするとき、様々な疑問や気になることをフラフラしながら書いていく予定です。

driver を書く上で知っていれば良い範囲で Linux kernel を読んでいきます。多くの kernel 解説書で触れているような 起動処理、排他制御、リソース管理(メモリ、ディスク、他)、プロセス・スレッド管理/スケジューリング、ファイルシステム等は軽く触れるだけにします。

このページの進め方

全体を通して ソースコードを追跡できる環境 を使って Linux kernel を読み進め、理解を深めるやり方で進めます。ソースコードを追跡する環境は web page として用意します。説明の各所でソースコード中のシンボルとそれに関連するソースコートを読めるリンクを張ります。一々リンクを辿るのは面倒かもしれません。それでも、ソースコードを読んでいくことを強く希望します。自分は Linux kernel はソース自体が仕様書だと思っています。1 行、1 行、あるいはもっと細かく 1 演算子に相当する処理であっても、なぜその順で処理していくのか。いくつかの計算方法から、なぜその方法を選んだのか意図が込められている箇所があります。人間の言葉では書ききれない仕様です。

使用する書式・表現

文字の字体や記号、段落の背景色を使い文書の意図や背景を表現する箇所があります。記述が進むにつれ、表現は追加され、継続して読むのに問題が起きない範囲で若干の修正を加えるかもしれません。

文字装飾

字体と記号を使って、単語や文字列の由来や表現する内容を次のようにします。

表現内容表現例説明
キーボード操作[Ctrl] + [Alt] + [Del]キーボードを押す操作を表現します。
GUI コントロールxtermGUI の ウインドウ・タイトル、テキスト・ボックス、チェック・ボックスの様なコントロールを表現します。
可変要素major_number環境や状況によって変化する内容を表現します。
コンソール入力gcc -Wall -O -o main main.cコンソールの入力を表現します。
コンソール出力Hello world.コンソールの出力を表現します。
コンソール入力の可変要素down_loaded_directoryコンソールの入力の可変要素を表現します。
コンソール出力の可変要素4.1.27コンソール出力の可変要素のうち入力や次の作業に再利用する箇所を示します。
コンソール出力注目箇所See hereコンソール出力の可変要素のうち注目が必要な部分を表現します。
コード断片if (likely(condition))文章中のコード断片を表現します。関数名のようなシンボルはフォントを変えず、そのまま表記します。

囲み記事

本文の流れに付け加えるような参考になる情報、注意を要する事、深刻な問題を起こすような事項は囲みを使って表現します。表現内容が短い場合は、背景色だけ違う段落を用いて表現します。

このページの背景

ページ全体、あるいはその一部の背景を説明します。表示しているソースやシンボルのリンク先 kernel version を示すことが多いです。

ノート

付け加えて参考になる情報を緑色の囲みで書きます。応用できる事項、関連する事項、詳細や背景を説明します。

注意

注意が必要な情報を黄色の囲みで書きます。予測に反して起きる事柄、応用や修正が思ったほどには上手く行かない場合、深くソースコードの読解や挙動の把握が必要な場合を説明します。

警告

ほぼ禁止事項に関する内容を赤色の囲みで書きます。復旧困難な問題、追跡・解析が難しい問題、動作に不具合を起こす問題など、深刻な問題について説明します。

ちょっとした考え事

自分が考えている事について書きます。解決したところで大きく何かが変わるわけでもない疑問、Linux kernel 内のしきたりから外れていること、広く一般的に同意が得られないことについて書きます。

期待している経験値

手短に言えば linux あるいは POSIX 相当環境で、C 言語によるアプリケーション開発経験があると期待しています。最低限 2,000 行程度のアプリケーションを 4, 5 本は書いたという程度です。man page section 2 にある system call を使った経験があるのが望ましいです。たとえば open() (kernel 内 SYSCALL_DEFINE3 open), read() (kernel 内 SYSCALL_DEFINE3 read), write() (kernel 内 SYSCALL_DEFINE3 write), lseek() (kernel 内 SYSCALL_DEFINE3 lseek), ioctl() (kernel 内 SYSCALL_DEFINE3 ioctl), close() (kernel 内 SYSCALL_DEFINE1 close)など低水準な入出力関数です。

fileOpenGrok(linux-4.1.27/fs/open.c:1028)
Expand allFold all
1028
1029
1030
1031
1032
1033
1034
 
-
|
|
|
|
!
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
        if (force_o_largefile())
                flags |= O_LARGEFILE;
 
        return do_sys_open(AT_FDCWD, filename, flags, mode);
}

今までと違うコーディング・ルール・スタイル

恐らく仕事でコーディングルールが決まっている。あるいは教育機関で「良い」とされるコーディングルールを習ったと思います。Linux kernel にもコーディング・スタイル /Documentation/CodingStyle があります。おおよそあり得るルールだと思います。

では Linux kernel のコーディング・スタイルに従う必要が有るのか?

正直に言えば、CodingStyle に書かれている一部のルールはバグを誘引しやすいルールを含んでいると考えています。特に if () 周りの中括弧の使い方は追跡困難なバグを作りやすいと思っています。 いい方向で、なるべく違和感が無い程度のルール変更はありだと思っています。
自分が書いた Linux kernel に組み込むデバイス・ドライバは main line にマージされるのか?なさそうであれば、気にせず独自のコーディング・スタイルでと思っています。デバイス・ドライバはファイル・システム、ブロック IO エレベータ、メモリ管理、スケジューラー、ネットワーク・フィルター、セキュリティ等とは違いテストが可能な環境は限られています。kernel 内 API の大胆な変更に伴いデバイス・ドライバも修正を受けますが、それが他者によってテストされることは期待できないでしょう。

CodingStyle ファイルに書かれていないルールはどうなっているのでしょうか? いくつかはアプリケーションプログラマにとって衝撃的な現状でしょう。いくつかの例を見ていきます。

  • あたかも関数のようなマクロ wake_up_* マクロ, spin_lock_* マクロ
    全て小文字のマクロが多用されています。見た目で関数だと思って引数を渡し、思わぬ副作用や実行効率低下を招く可能性に注意してください。spin_lock_irqsave マクロ の第 2 引数 flags はマクロ内で代入先、あるいは参照渡しになっていることも特徴です。wake_up_* はタスク間(プロセス、スレッド間)同期に使われます。spin_lock_* は排他制御に使います。
    fileOpenGrok(linux-4.1.27/include/linux/wait.h:165)
    Expand allFold all
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    
     
     
     
     
     
     
     
     
     
     
     
    -
    |
    !
     
     
     
     
     
     
     
     
    
    #define wake_up(x)                      __wake_up(x, TASK_NORMAL, 1, NULL)
    #define wake_up_nr(x, nr)               __wake_up(x, TASK_NORMAL, nr, NULL)
    #define wake_up_all(x)                  __wake_up(x, TASK_NORMAL, 0, NULL)
    #define wake_up_locked(x)               __wake_up_locked((x), TASK_NORMAL, 1)
    #define wake_up_all_locked(x)           __wake_up_locked((x), TASK_NORMAL, 0)
     
    #define wake_up_interruptible(x)        __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
    #define wake_up_interruptible_nr(x, nr) __wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)
    #define wake_up_interruptible_all(x)    __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)
    #define wake_up_interruptible_sync(x)   __wake_up_sync((x), TASK_INTERRUPTIBLE, 1)
     
    /*
     * Wakeup macros to be used to report events to the targets.
     */
    #define wake_up_poll(x, m)                                              \
            __wake_up(x, TASK_NORMAL, 1, (void *) (m))
    #define wake_up_locked_poll(x, m)                                       \
            __wake_up_locked_key((x), TASK_NORMAL, (void *) (m))
    #define wake_up_interruptible_poll(x, m)                                \
            __wake_up(x, TASK_INTERRUPTIBLE, 1, (void *) (m))
    #define wake_up_interruptible_sync_poll(x, m)                           \
            __wake_up_sync_key((x), TASK_INTERRUPTIBLE, 1, (void *) (m))
    fileOpenGrok(linux-4.1.27/include/linux/spinlock.h:345)
    Expand allFold all
    345
    346
    347
    348
    
     
    -
    |
    !
    
    #define spin_lock_irqsave(lock, flags)                          \
    do {                                                            \
            raw_spin_lock_irqsave(spinlock_check(lock), flags);     \
    } while (0)
  • あたかも変数のようなマクロ current マクロ
    current は現在のタスク・コンテキストを指すポインタです。変数のように見えますが、その実態は多くのプロセッサでスタック・ポインタから計算した場所を指しています。

    ノート

    current マクロ のもう一つの特徴はプロセッサごとに実装が違うことです。ソースコードを追跡している途中で、このように多くの実装箇所を見つけた場合は、ターゲットに使っているプロセッサ、プラットホーム、.config ファイルに書かれた条件と一致するコードを追いかけます。

  • 多用される goto PC キーボードドライバ i8042.cSATA/PATA ドライバ・コア libata-core.c
    ドライバのソースコードを読むと goto が多用されて気になるかもしれません。多くの場合、エラー発生や不調などにより確保していたリソースを解放したり、変更した状態を元に戻す等の復旧処理へ流れを移す、あるいは再試行のため処理の始めへ戻ることをしています。goto ラベルを見ると out_*, fail_*, recover_*, err*, retry* など異常事象の流れで有ることを示しています。goto を使わず同様な流れをコピーしたかのように書くよりは、goto を使って復旧処理へ飛ぶ書き方が好まれます。
    fileOpenGrok(linux-4.1.27/drivers/input/serio/i8042.c:1373)
    Expand allFold all
    1373
    1374
    1375
    1376
    1377
    1378
    1379
    1380
    1381
    1382
    1383
    1384
    1385
    1386
    1387
    1388
    1389
    1390
    1391
    1392
    1393
    1394
    1395
    1396
    1397
    1398
    1399
    1400
    1401
    1402
    1403
    1404
    1405
    1406
    1407
    1408
    1409
    1410
    1411
    1412
    
     
    -
    |
    |
    |
    |
    |
    |
    |
    -
    |
    |
    |
    |
    -
    -
    |
    |
    |
    !
    |
    !
    |
    |
    |
    |
    |
    |
    |
    |
    |
    |
    |
    |
    |
    |
    |
    |
    |
    !
    
    static int __init i8042_setup_aux(void)
    {
            int (*aux_enable)(void);
            int error;
            int i;
     
            if (i8042_check_aux())
                    return -ENODEV;
     
            if (i8042_nomux || i8042_check_mux()) {
                    error = i8042_create_aux_port(-1);
                    if (error)
                            goto err_free_ports;
                    aux_enable = i8042_enable_aux_port;
            } else {
                    for (i = 0; i < I8042_NUM_MUX_PORTS; i++) {
                            error = i8042_create_aux_port(i);
                            if (error)
                                    goto err_free_ports;
                    }
                    aux_enable = i8042_enable_mux_ports;
            }
     
            error = request_irq(I8042_AUX_IRQ, i8042_interrupt, IRQF_SHARED,
                                "i8042", i8042_platform_device);
            if (error)
                    goto err_free_ports;
     
            if (aux_enable())
                    goto err_free_irq;
     
            i8042_aux_irq_registered = true;
            return 0;
     
     err_free_irq:
            free_irq(I8042_AUX_IRQ, i8042_platform_device);
     err_free_ports:
            i8042_free_aux_ports();
            return error;
    }

環境

仮想環境を使って Linux kernel に組み込むドライバの基本的な事柄を探っていきます。ある程度なれたら、実働ターゲットを使い実践的なドライバを作っていく予定です。

仮想環境を使ったチュートリアル

仮想環境を使い、Linux kernel の基礎を理解していく予定です。driver を書き始める前に理解し、慣れることが多くあります。あまりにも多いので飽きてしまったり、実務のために必要な時間が少なくなってしまうかもしれません。

パソコン

仮想環境が十分にハードウエアをエミュレートしていれば使わなくて済む様に進める予定です。もし、Linux をインストールした PC を用意できるならば、仮想環境より実感がある経験が得られるでしょう。ms (ミリ秒) オーダーのタイミングで変化する状態などはデバイスやドライバが意図通り変化しているのか、判断・分析するための手がかりになることがあります。

実働ターゲットを使った実践

raspberry pi または beagle bone black と実際のデバイスを使用して driver を開発していく予定です。

Use the source, Luke!

Linux kernel 対する理解を深めるにはソースを読むことです。このページで説明を加えようとしているのは自己矛盾かもしれません。ソースコードは膨大な量です。何処を読めばよいのか、grep でシンボルを探しても見つからない。といった理解を困難にする問題もできる限り解決できるようにしていきます。

デバイス・ドライバを作る上で、読む・修正する箇所はおおよそ次の場所です。関数の機能を探るためもっと他の場所も読むこともあるでしょう。

場所主な内容説明
/Documentation技術資料00-INDEX を索引ページとして Linux kernel に関するドキュメントが格納されています。一通り読むのは大変です。grep で関数をはじめとするシンボルを検索して見つかったファイルを読んでみると、理解が進み用法が解ってくることがあります。
/includeヘッダ・ファイル主に kernel source code で使用するヘッダファイルが格納されています。
/driversデバイス・ドライバ殆どのデバイス・ドライバがここに格納されています。おそらく、新しいデバイスために 1 からドライバを書くのではなく、似たようなデバイス・ドライバを手本に、デバイス固有部分を書き直す場合が多いでしょう。
/soundサウンド・デバイス・ドライバサウンド・デバイス・ドライバを書く、あるいは修正する場合に読む必要があります。サウンド・デバイス・ドライバとその抽象化レイヤ (ALSA API) はここに格納されています。DocBook/writing-an-alsa-driver.tmpl を参照してください。おそらく多くのプラットホームでサウンド・デバイス・ドライバは完成された状態で SoC メーカーから提供されているはずです。変更するとしたら DAC, ADC, Volime, Mixer を好みの音がするチップに変える場合でしょう。
/arch 以下
processor/mach-your_platform | plat-your_platform
初期化コード・コード、プラットホーム固有コード、ヘッダファイルプラットホーム(PC で言えばマザーボードに乗っている CPU と チップセットの組)に固有のコードです。/arch ディレクトリ以下は各プロセッサ、各プラットホーム(System On Chip device, 評価用ボード) によって構成はまちまちです。PC では /arch/x86 の下にまとめられていて、デバイスを初期化するコードの一部は kernel の下に入っています。ARM の様に多様なプラットホームが存在するとサブ・ディレクトリ mach-your_platform か plat-your_platform が作られ、この中に各プラットホームに固有なコードやヘッダファイルが格納されます。
デバイス・ドライバ開発でこの付近のコードを修正する機会は、kernel 起動時にハードウエアの初期化が必要な場合、共用のグローバル変数領域が必要で初期化をする場合、SoC 内に組み込まれた IP ブロックをデバイスとして組み込む platform_device_register()、I2C device を登録 i2c_register_board_info() するなどの処理です。
Linux kernel 起動中に LED 等の表示デバイスを点灯・点滅させるなど、凝った処理を要求される場合も、修正を加える所になります。
*_defconfig の一覧
/arch/processor/configs/*-defconfig
make, コンパイラが参照するマクロ値各プラットホームに適した make, compile 時に参照されるマクロの値定義を格納したファイルです。make platform-defconfig コマンドでこのファイルから、ソース・コード・ツリーのルートに .config ファイルを作成します。

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2017-10-14 (土) 11:14:02 (2388d)