Linux kernel の見取り図

Linux_bigmap_for_driver_developer.png

標準ライブラリ

Linux kernel で提供される標準ライブラリはおおよそアプリケーション向けの glibc に近い機能が提供されています。加えてビット操作と kernel 内ならではのアルゴリズム群があります。アルゴリズム群は「こんなのも有るのか」程度に知っておくと良いでしょう。アプリケーション向けに作られたライブラリが移植されていることもあるので、欲しいと思ったら Linux kernel ソースコード検索 から探してみるのも良いでしょう。

要注意なことは、アプリケーションプログラムでは普通に除算 '/'、剰余 '%' 演算子を用いた式を long long 型に対して書けば計算可能なのに対し Linux kernel 内では linux/math64.h からインクルードされる関数群を使用して関数呼び出し(あるいはマクロ呼び出し)の形で計算する必要があることです。

機能単位主なヘッダやソース備考
型定義linux/types.h
コンパイラは inttypes.h, stdint.h を提供しています。
定数、最大、最小linux/kernel.h
linux/stddef.h
linux/limits.h
linux/sizes.h
uapi/linux/limits.h は linux/limits.h で include します。
構造体オフセットlinux/stddef.h
linux/kernel.h
メンバーを指すポインタからそれを保持する構造体を指すポインタへ変換するマクロ container_of() もここに含まれます。
文字linux/ctype.hlinux/ctype.h は1バイト文字(いわゆるASCII)のみ対応です。
文字列・メモリlinux/string.h
linux/glob.h
linux/string_helpers.h
linux/textsearch.h
linux/textsearch_fsm.h
linux/ihex.h
文字列変換linux/kernel.h
linux/parser.h
linux/kernel.h の中で strtoxxx は kstrtoxxx という関数になっています。たとえば strtoul() は kstrtoul() という関数になっています。
書式フォーマットlinux/kernel.hscnprintf() などの関数が定義されています。書式文字列は Documentation/printk-formats.txt を参考にしてください。おおよそ glibc の printf 書式と同じです。%p によるポインタ表示、%s に対して NULL を渡した場合の表示など共通の書式でありながら、挙動が違う場合もあります。
計算(数値)linux/kernel.h
linux/math64.h
asm/div64.h
linux/gcd.h
linux/lcm.h
linux/log2.h
linux/mpi.h
asm/div64.h は linux/math64.h から include されます。プロセッサ語長が 32 bit の場合、64bit (long long) 型に対して除算または剰余演算子を使うとリンクに失敗します。64bit 除算ライブラリ do_div() などを使用して下さい。
計算(ビット)linux/bcd.h
linux/bitops.h
asm/bitops.h
linux/bitrev.h
linux/bitmap.h
asm-generic/bitops/const_hweight.h
asm-generic/bitops/ffs.h
asm-generic/bitops/fls.h
asm-generic/bitops/find.h
linux/crc-ccitt.h
linux/crc-itu-t.h
linux/crc-t10dif.h
linux/crc16.h
linux/crc32.h
linux/crc32c.h
linux/crc7.h
linux/crc8.h
asm/bitops.h, asm-generic/bitops/const_hweight.h, asm-generic/bitops/ffs.h, asm-generic/bitops/fls.h, asm-generic/bitops/find.h は linux/bitops.h で include されます。直接 include しないで下さい。
bitops.h はプロセッサ毎に定義されています。
asm-generic/bitops/const_hweight.h はビットを数え上げる計算です。ファイル名に const がついていますが、即値と変数両方に使えるマクロです。gcc の組み込み関数 __builtin_constant_p() を使用して静的な定数と変数に対する計算をそれぞれ最適化しています。
エンディアン変換linux/byteorder/generic.h
linux/swab.h
linux/byteorder/generic.h は関数一覧として挙げてあります。実際は各アーキテクチャに最適な変換処理を利用するので asm/byteorder.h をインクルードします。
アルゴリズムlinux/average.h
linux/bch.h
linux/bpf.h
linux/bsearch.h
linux/btree.h
linux/circ_buf.h
linux/cordic.h
linux/kfifo.h
linux/plist.h
linux/range.h
linux/rbtree.h
linux/rslib.h
linux/sort.h
linux/crypto.h
linux/cryptohash.h
linux/cryptouser.h
linux/hash.h
linux/hashtable.h
linux/jhash.h
圧縮linux/lz4.h
linux/lzo.h
linux/xz.h
linux/zlib.h
おそらくデバイスドライバで積極的に圧縮処理を使うことは無いと思います。圧縮処理が必要だったとして kernel とアプリケーションの処理分担として適切か検討してみることを勧めます。

リンクリスト

双方向リンクリストを操作するマクロです。リストを辿るループ処理を構成する list_for_each_entry() などの巧妙なマクロが定義されています。

機能単位主なヘッダやソース備考
リスト操作linux/list.hリンクリストメンバーからそれを保持する構造体へ変換するマクロ list_entry()container_of() と同じです。リスト操作に関連するならば list_entry() を使用して下さい。

アトミック操作

整数とビットに対するアトミック操作の機能群です。Tasklet, Work Queue などを含み、複数のスレッドや割り込みコンテキストと共有する変数、お互いの同期を取るための状態変数を操作するために使用します。ソースコード中で a++ や a |= FLAG_MASK といった式を書いたときに 変数 a は複数スレッドや割り込みコンテキストから参照・更新されるのか常に気にする必要が有ります。もし、参照・更新されるのなら spin_lock やアトミック操作が必要である可能性が高いです。

機能単位主なヘッダやソース備考
整数linux/atomic.h
asm/atomic.h
asm/atomic.h はプロセッサ毎に定義されています。asm/atomic.h は linux/atomic.h で include されます。
ビットasm/bitops.hasm/bitops.h はプロセッサ毎に定義されています。ヘッダファイルに atomic かどうか書かれています。atomic な関数・マクロは set_bit(), clear_bit(), clear_bit_unlock(), change_bit(), test_and_set_bit(), test_and_set_bit_lock(), test_and_clear_bit(), test_and_change_bit(), test_bit() atomic 関数と混在可 です。asm/bitops.h は linux/bitops.h で include されます。

バリア指示・同期

バリア指示 barrier() とバリア同期 mb(), rmb(), wmb() の関数またはマクロ定義です。バリア指示はコンパイラに対して、barrier() を超えてメモリアクセス順を変更しないようにします。バリア同期は同期関数の前までに行ったメモリ操作 mb(), メモリ読みだし rmb(), メモリ書き込み wmb() を済ませるよう指示します。インラインアセンブラなどによってロート・ストアキューまたはキャッシュに保留されたトランザクションをメモリに反映します。Documentation/memory-barriers.txt を参照して下さい。

バリア同期を行ったのにデバイスの制御が不完全になっていると思われる場合、(1) レジスタの読み書きにリカバリータイム(連続アクセスする場合の待ち時間)が規定されていないか、(2) バスブリッジにトランザクションが滞留していないか 確認してください。さらに他にも原因はあるかもしれません。ここで (2) の場合について触れます。プラットホーム(SoC 内部に構成された回路ブロックやバス、基板・バックプレーン・インターコネクトで接続されたプロセッサ、チップセット、バスブリッジなどで構成されるアーキテクチャ)によっては各回路(特にバスブリッジやインターコネクト)のリクエストキューにバストランザクションがバリア指示・同期を行っても滞留する場合もあります。プラットホームのデータシートを参照して意図通りにバリア指示・同期ができているか確認して下さい。

滞留したバストランザクションの掃き出し

プラットホームのデータシートを参照すれば何らかの方法でバスブリッジに滞留したトランザクションを吐き出す(完了するまで待つ)方法が書かれていると思います。方法が見つからない場合、制御しようとするデバイスのバージョンレジスタ、コンフィギュレーションレジスタなどデバイスの内部状態を変えないレジスタを読みだしてみて、滞留したトランザクションを吐き出せないか実験してみてください。先行した書き込みを追い越して読み出しのトランザクションが実行されることは恐らく無いと思われます。あったら、バスあるいはバスブリッジの設計が破綻しています。

機能単位主なヘッダやソース備考
バリアlinux/compiler.h
asm/barrier.h
ドキュメントは Documentation/memory-barriers.txt です。linux/compiler.h は linux/kernel.h より include されます。asm/barrier.h は linux/atomic.h から include されます。

排他制御

複数の処理を一貫して行いたい場合に使う関数とマクロ群です。Spin Lock は複数のバリエーションがあり、割り込みコンテキストと共に使える機能もあります。ロックを獲得できるまで CPU が Spin (空走ループ)します。短時間で済む処理をロックして行うのに適しています。空走時間が長くなる場合 CPU の処理能力を無駄に消費します。

Semaphore, Mutex は呼び出したプロセスやスレッドなどを中断して他に実行を譲れる状況で使用します。割り込みコンテキストの中から使うことは出来ません。ロックを獲得できないならば、他に実行を譲ります。

機能単位主なヘッダやソース備考
Spin Locklinux/spinlock.h使い分けのドキュメントは Documentation/locking/spinlocks.txt, Documentation/DocBook/kernel-locking/index.html にあります。ドキュメントの冒頭にあるように spin_lock_irqsave(), spin_unlock_irqrestore() の組が最も汎用的にどんな場所でも使えます。使い分けが難しいと感じるならばこの組み合わせから使ってみて下さい。
Semaphorelinux/semaphore.hDEFINE_SEMAPHORE()sema_init() で初期化します。down() あるいはそのバリエーションで排他獲得、up() で排他解放します。
Mutexlinux/mutex.hDocumentation/locking/mutex-design.txt, Documentation/locking/rt-mutex-design.txt にドキュメントがあります。

同期制御、状態変化待ち

条件が成立するまで待つ機能、完了を待つ機能があります。別スレッドや work queue に処理を任せたあと完了を待ったり、割り込みが発生を条件に処理を進める場合に使用します。

機能単位主なヘッダやソース備考
Conditionlinux/wait.hドキュメントは Wait queues and Wake events です。説明の中に Wait Queue を初期化する関数 init_waitqueue_head() の説明がありません。これはソースコードを見て用法を理解して下さい。初期化が済んだ後の典型的な流れは wait_event_interruptible() で条件待ちをし、条件成立をさせたならば wake_up() で起床させます。wait_event_*, wake_up_* とも変化型が多くあります目的に合う関数またはマクロを選んで下さい。
Completionlinux/completion.hドキュメントは Documentation/scheduler/completion.txt です。古いドライバでは linux/semaphore.h にある down(), up() を使って完了待ち合わせを実現しています。今はこのような実装は書き直されているはずです。

割り込みハンドラ登録・設定

デバイスから来る割り込みを受ける関数を登録するための定義群です。ドライバやデバイスが外された時は登録抹消します。割り込みコントローラの制御は SoC ベンダーあるいはボードベンダーによって実装されています。割り込みハンドラに必要最低限の処理は割り込み要因を解消することです。解消しない場合は割り込み処理が再割り込みによって永遠に継続したり、2 度目の割り込みが発生しないなどの事態になります。

機能単位主なヘッダやソース備考
IRQ classlinux/interrupt.hドキュメントは Documentation/DocBook/genericirq/index.html です。デバイスドライバでよく使われる request_irq(), free_irq()Documentation/driver-model/design-patterns.txt にデザインパターンとして出ています。多くのドライバソースコードを読んで用法を習得できるでしょう。
割り込み番号(IRQ number)は SoC ベンダーが開発・実行環境を整備しているならば gpio_to_irq() 関数を使ってチップのピン番号から IRQ 番号へ変換します。linux/gpio.h をインクルードすれば gpio_to_irq() を使えるようになります。GPIO (汎用入出力ピン)を識別するマクロ定義は各プラットホーム毎に違うので arch/processor 以下の開発対象に関係したソースを読んでみてマクロ定義の一覧を含んだヘッダファイルあるいはデバイスのピン番号とソースコード中の GPIO ピン番号の対応を把握して下さい。

時間待ち、タイマー

制御対象のデバイスはプロセッサの処理速度に比べ動作が遅いことがあります。コマンド発行間隔、要求してから結果が得られるまでの待ち時間が必要な場合が有ります。Sleep, Delay, Timer はこれらのタイミングを合わせるための機能です。注意点はこれらの待ち時間は長くなる方向でぶれます。平行して隣接する機能ブロックを操作している場合、処理や制御の重なりで思わぬ挙動を示す場合が有ります。

機能単位主なヘッダやソース備考
Sleeplinux/delay.hsleep の代表的な関数は msleep(), usleep_range() です。実行中のタスクを再スケジュールします。CPU 資源を無駄にしません。割り込み処理中では使えません。
単に別の task に実行を渡す(いわゆる 0 秒待ちをする)場合は schedule() を使用します。
Delaylinux/delay.hdelay 関数は ndelay(), udelay(), mdelay() があります。これらは CPU を空走させて時間待ちを実現します。割り込みハンドラの中でも使えます。しかし、割り込み応答性能を悪化させます。
Timerlinux/timer.h
linux/hrtimer.h
ドキュメントは Delaying, scheduling, and timer routines です。代表的な関数は add_timer(), mod_timer(), del_timer_sync() です。
Delayed Worklinux/workqueue.h遅延実行機能付きの Work Queue です。指定時間が経過した後に呼び出される関数の中で msleep() などのコンテキストスイッチの可能性を伴う関数を使用できるので Timer より使い勝手が良いです。INIT_DELAYED_WORK(), schedule_delayed_work() などの用例を参考にして下さい。

時刻

kernel 内で刻まれている時刻を取得する機能群です。jiffies はプリエンプション周期である 1/HZ 秒単位 で進む時刻です。ミリ秒オーダーの精度です。より高精度の時刻を必要とする場合は、local_clock(), sched_clock() を使用して下さい。これらは kernel が起動されてから刻まれる時刻です。現実世界の時刻と相対的な時間差があります。CPU が完全に停止した suspend 中には刻まれず止まっています。

機能単位主なヘッダやソース備考
jiffieslinux/jiffies.hjiffies の時間的前後関係を判定する場合は、linux/jiffies.h に定義された比較関数・マクロを使用して下さい。jiffies と時間を変換するには HZ を直接使うより msecs_to_jiffies(), jiffies_to_msecs() などを使用することをお勧めします。マイクロ秒、ナノ秒単位への変換関数 linux/jiffies.hに定義されています。
Timelinux/time.h
linux/timekeeping.h
linux/sched.h
linux/sched.h の中に local_clock(), sched_clock() などの時刻関数が入っています。

メモリ確保

アプリケーションで使用している malloc(), free() の代わりになる機能です。動的なメモリ確保を必要とする場合、ほぼ kzalloc(), kfree() の 2 つで十分に対応できます。

生成したデバイスコンテキスト、デバイスに対するコマンドパケットなどの生存期間はデバイスが突然外されたり、コンテキストが複数のリンクリスト等に属する場合、特定の操作に合わせた単純な allocate - free の関係では扱いきれない場合が有ります。リファレンスカウンタ (ドキュメントは Documentation/kref.txt、ヘッダファイルは linux/kref.h) が使えないか、検討して下さい。

機能単位主なヘッダやソース備考
任意サイズlinux/slab.hアロケーターは SLAB, SLUB, SLOB の 3 つがあります。どれを選んでもインクルードするファイルは linux/slab.h です。一つの構造体を複数回確保する様な状況(固定した任意サイズ x N 個)に対して kmem_cache_create(), kmem_cache_init(), kmem_cache_alloc(), kmem_cache_free() 等とこれらの周辺機能を使うと、キャッシュラインを意識しアクセス速度を早めた領域を確保できます。
ページ単位linux/gfp.h
linux/mm.h
ページ確保: linux/gfp.h:alloc_page() 、ページ・アドレス取得: linux/mm.h:page_address() となっています。
DMA 領域確保linux/dma-mapping.h
linux/gfp.h
ドキュメントは Documentation/DMA-API.txt です。kzalloc() (対は kfree()), alloc_page() (対は free_page()), alloc_pages() (対は free_pages()) に GFP_DMA を付けて領域を確保する方法と、dma_alloc_coherent() (対の関数は dma_free_coherent()) を使う方法があります。ここに挙げたの一部です。dma_alloc_coherent() を使うと Consistent memory (プロセッサと周辺デバイスにとって内容が一貫しているメモリ)が割り当てられます。Documentaion/DMA-API.txt では Consistent memory であっても プロセッサの書き込みバッファを掃き出す必要性があることを注意しています。一文を取り上げると "You may however need to make sure to flush the processor's write buffers before telling devices to read that memory." とあります。

仮想-物理アドレス・マップ、DMA 転送、コピー

おそらく DMA 転送を主導するようなドライバを書く機会は少ないと思います。ほぼこのような処理は SoC デバイスメーカーや開発プラットホームを作ったメーカーが提供しています。最も使うのは user space - kernel copy だと思います。ioctl(), read(), write() に対応する処理を実装すると、アプリケーションプログラムへ(から)データを転送する処理が伴います。このデータ転送は(実質的な実装がそうであっても) memcpy() を使ってはいけません。

機能単位主なヘッダやソース備考
user space - kernel copyasm/uaccess.h
linux/uaccess.h
ドキュメントは User Space Memory Access です。asm/uaccess.h は linux/uaccess.h で include されます。copy_to_user(), copy_from_user() などはアーキテクチャに最適な実装がされることがあります。ioctl() の実装などで構造体の各メンバーを順にアクセスする場合は、構造体全体のアクセス可能性を access_ok() で確認してから進めるのが良いでしょう。
Virt - Phy map convertasm/io.h
linux/io.h
mach/hardware.h
ドキュメントは Public Functions Provided(題名が変ですが) です。asm/io.h は linux/io.h で include されます。virt_to_phys(),phys_to_virt() などはアーキテクチャやプラットホームに最適な実装がされることがあります。
ioremap(), iounmap() などを使って物理アドレスと仮想アドレスの対応付けをします。一部のプラットホームでは内蔵された機能ブロックの仮想アドレス割り付けを単純化していて、軽量な実装の IO_ADDRESS() マクロなどを経由して物理アドレスから仮想アドレスへ変換出来るよう便宜が図られています。arch/processor/ 以下に格納されたプラットホーム毎の初期化処理やデバイス登録処理を読んでみて機能ブロックに対する仮想アドレスの扱いかたを把握しておくと良いでしょう。
DMA mapping, Address convert, Scatter Gatherasm/dma-mapping.h
asm-generic/dma-mapping-common.h
ドキュメントは Documentation/DMA-API.txt, Documentation/DMA-API-HOWTO.txt です。asm/dma-mapping.h, asm-generic/dma-mapping-common.h は linux/dma-mapping.h で include されます。
よく使うのは dma_map_single(), dma_unmap_single(), dma_map_page(), dma_unmap_page(), dma_map_sg(), dma_unmap_sg() です。アーキテクチャ、プラットホームによっては DMA アドレスと物理アドレスが違っています。phys_to_dma(), dma_to_phys() 関数が用意されています。

メモリ・マップド・デバイス・アクセス

SoC 内蔵の機能ブロック、PCI 接続のカードに乗っているデバイスはメモリにマップされたレジスタやメモリ空間があります。これらをアクセスするために用意された関数、あるいはマクロ群です。C 言語の '*' (間接演算子) を使ったアクセスは禁止されていません。しかし、関数やマクロには volatile を付加したり、メモリバリアを行ったりデバイスをアクセスする作法として必要な処理が盛り込まれています。

機能単位主なヘッダやソース備考
Read-Writeasm/io.hドキュメントは Accessing the device です。asm/io.h は linux/io.h で include されます。readb(), writeb(), readw(), writew(), readl(), writel() などメモリにマップされたデバイスをアクセスするための関数がアーキテクチャやプラットホームに最適化されています。

スレッド・軽量処理

「ユーザーランドの動作と独立して kernel 内でデバイスの変化に合わせデバイスを制御する。」、「デバイスが起こした割り込みに対応する。」等の処理をするためスレッド(kthread)、work queue、tasklet を利用できます。割り込み処理は request_irq() で登録した割り込みハンドラ(関数)で行います。割り込みハンドラはアトミックなので(他のスレッドやプロセスに実行を譲らないので)使用できる kernel 内の API は限られます。ハンドラは割り込み要因をクリアする事と、割り込み時に必要な処理を起床させることに専念し軽量な実装にして、割り込み発生時に必要な処理は kthread, work queue で行うのがドライバで多く見られる実装です。

スレッドか work queue か?work queue を使う方が実装量が少なく簡単です。たとえばデバイスのステータスを読み取り、ドライバで確保したデバイスコンテキストに格納するだけのような単純な処理であれば work queue を使うのが適切でしょう。ただし、work queue は手軽な代わりに複雑な処理を盛り込もうとすると、難易度が高くなります。たとえばワークを実行する関数内で再び自らを work queue に投入する様な処理可能です。しかし、何かの事象が発生し別のコンテキストが work queue に投入する処理を行う場合もあることを忘れないで下さい。

ポーリング処理が必要なデバイスであれば、kthread を使うのが適切でしょう。thread 内で wait_event_timeout() あるいは類似の関数を使用し、事象を待ちつつタイムアウトを併用して定期的なポーリングを実現します。

デバイスを制御するのに複数のステップを必要とする場合も、kthread を検討して下さい。スレッドとして動作する関数にステートマシンを実装して対応できます。

機能単位主なヘッダやソース備考
kthreadlinux/kthread.h主に使う関数は kthread_create(), kthread_stop(), kthread_should_stop() です。すこし高度な使い方が Internal Functions として書かれています。
kthread_create() で作成した thread は wake_up_process() で起床すするまでは止まった状態です。kthread_stop()KTHREAD_SHOULD_STOP フラグを 1 にするだけです。kthread_should_stop()KTHREAD_SHOULD_STOP フラグの値を読みます。
kthread はアプリケーションで使われる pthread と違い、終了待ち機能 pthread_join に相当するを提供していません。必要であれば実装して下さい。
kthread の中では copy_to_user(), copy_from_user() は使えません。open 処理の中で kthread_create() を使ったとしても、ユーザーランドの仮想アレス割り付けは kthread に継承されません。
work queuelinux/workqueue.hドキュメントは Workqueues and KeventsDocumentation/workqueue.txt です。API が多くあるうち、良く使用するのは DECLARE_WORK(), INIT_WORK(), INIT_WORK_ONSTACK()schedule_work(), cancel_work_sync() です。独自に work queue を作成する場合、加えて alloc_workqueue(), destroy_workqueue(), queue_work() です。
work queue と delayed work を一つの work queue で併用する興味深い実装が drivers/ata/libata-sff.c に見られます。
schedule_work() を使って system work queue (system_wq) を使う場合は要注意です。work queue に投入されたワーク work_struct は順次実行されます。他のドライバ、モジュールも system work queue にワークを投入します。投入したワークがどの程度時間が経過してから実行されるのかは、先行して投入されたワークの実行時間に依存します。自分が投入したワークの後に続いたワークは、自分のワーク実行に続けて実行されます。ワークの処理時間が長い場合、(見も知らない)お互いの実行時間干渉に注意が必要です。
taskletlinux/interrupt.hヘッダファイルは linux/interrupt.h です。この中の tasklet_init(), tasklet_schedule(), tasklet_kill() をはじめとする関数群です。tasklet で実行される関数は softirq (TASKLET_SOFTIRQ) コンテキストで実行されます。msleep() の様に実行が他のスレッドに移る可能性がある関数は tasklet 関数の中から使えません。tasklet_schedule() で実行を予定すると即座に実行されるか、次のタイムティック(プリエンプション)で実行されます。ただし、Documentation/kernel-per-CPU-kthreads.txt の TASKLET_SOFTIRQ にあるように多用すると、他のドライバの割り込み応答時間が伸びる方向でぶれを生じやすくなります。ぶれを抑えるために work queue を使用する様に推奨されています。

接続切断通知

デバイスの状態変化を通知する uevent とコネクタの状態変化を通知する External Connector (extcon, Android においては switch) が有ります。

uevent は struct device のメンバーに含まれている (struct kobject) kobj 構造体を通してデバイスの状態を通知します。

External Connector (extcon) は Android 向けに拡張された Linux で Switch として実装された class を取り込んだ物です。組み込み機器向けの機能です。主にコネクタにプラグが「接続された|切断された」事象を伝えるために使われます。キーボードに分類されないようなボタンやドアスイッチの状態を伝達するためにも使えます。

機能単位主なヘッダやソース備考
ueventlinux/kobject.hkobject_uevent(), kobject_uevent_env() 関数で kobject (ユーザーランド視点で /sys/ 以下のノード) から uevent を発行します。
/sys/some-levels-device-path/uevent ノードに書き込むと意図的に uevent を発行できます。uevent_store() 関数と続いて DRIVER_ATTR_WO(), DEVICE_ATTR_RW() で宣言された uevent 変数で実装されています。
External Connector (Switch)linux/extcon.hAndroid で switch と呼ばれていた抽象化デバイスです。ドキュメントは Documentation/extcon/porting-android-switch-class です。Android で switch と呼ばれていた class からの移植を主眼に書かれています。Android も extcon に移行しています。コネクタの接続切断、キーボードとして扱わないボタン押しなどを検出して uevent で通知します。現在の状態は /sys/class/extcon/external-connector-name/state から読み出せます。

リファレンスカウンタ

kernel API の中で *_alloc(), *_get(), *_put() のという接尾辞が付いた API の組の根幹をなすライブラリです。間接的に kobject, kmap を通して使われることもあります。接尾辞は多少の変化があります。ほぼ *_alloc() で参照カウンタ 1 でインスタンスを生成します。*_get() で参照カウンタを +1、*_put() で参照カウンタを -1 します。参照カウンタが 0 になったら、kref_put() の引数に指定された関数 *_release() が呼び出され、多くの場合インスタンスを開放(メモリから削除)する処理をします。

機能単位主なヘッダやソース備考
kreflinux/kref.hドキュメントは Documentation/kref.txt です。Reference Counter を構成するライブラリです。生成したインスタンスの生存期間を参照の有無によって決定する場合に使います。kref_init(), kref_get(), kref_put() が代表的関数です。

Kernel Object

機能単位主なヘッダやソース備考
KObject, Ksetlinux/kobject.hkobject(kernel object) のインスタンスを直接生成する機会は殆ど無いと思います。kobject を利用する場面は、kobject を参照するポインタを使って /sys (sysfs) の下にあるデバイスやドライバのノードに特殊なノードを作ったり、uevent を発行するのが主な場面になるでしょう。

ノード形成

典型的なデバイスドライバは read(), write(), ioctl(), poll() などを通してデバイスをアクセスできるようにノードを作ります。伝統的かつ基本的な方法は character device または block device として VFS に {major, minor} 番号の組として表現したノードを登録します。登録はクラスデバイスとして登録したときにクラスドライバが代行する場合も有りますし、作成したデバイスドライバが自らする場合もあります。ユーザーランドから mknod コマンド(system call) で {major, minor} 番号の組に対応するデバイスパスを作成し、open, close できるようにします。

新しい方法、あるいは比較的軽量な方法として sysfs に(/sys 以下のノード)にデバイスの状態を read, write するためのノードを作成する方法があります。ノードの _show, _store メソッドをテキストで入出力する「気が利いた」実装にすれば、コマンドラインで cat や echo コマンドを使ってデバイスの状態を確認したり、設定することができます。

機能単位主なヘッダやソース備考
VFS(character)linux/fs.h
linux/cdev.h
ドキュメントは Char devices です。alloc_chrdev_region(), register_chrdev(), unregister_chrdev_region(), cdev_alloc(), cdev_init(), cdev_add(), cdev_del() 辺りから使用例を探ると良いでしょう。
VFS(block)linux/fs.h
linux/genhd.h
ドキュメントは Block Devices です。register_blkdev(), unregister_blkdev(), blk_register_region(), blk_unregister_region(), alloc_disk(), put_disk(), add_disk(), del_gendisk() 辺りから使用例を探ると良いでしょう。
SCSIscsi/scsi.h
scsi/scsi_host.h
scsi/scsi_driver.h
scsi/scsi_device.h
block device として登録するならば SCSI device として登録した方が良い場合もあります。SCSI 周りは記述が多くなるので別記検討中です。
procfslinux/proc_fs.hprocfs は任意のディレクトリ構造が作りやすいファイルシステムです。積極的に推奨されないせいもあり、詳細なドキュメントがありません。proc_mkdir(), proc_create(), remove_proc_entry(), proc_remove() の使用例を参考に使って見て下さい。
procfs (と debugfs)は linux/seq_file.h と共に使われることが多いです。seq_file の ドキュメントは Documentation/filesystems/seq_file.txt に有ります。ノードを read() するとテキストが得られるようにしたい場合に便利な実装です。バイナリも seq_write() で対応できます。
sysfslinux/sysfs.h
linux/device.h
linux/moduleparam.h
デバイスに属性を付与する DEVICE_ATTR() は linux/device.h にあります。ドキュメントは Documentation/driver-model/device.txt の Attributes 節です。
module_param() は linux/moduleparam.h にあります。kernel boot parameter からスタティックリンクされたモジュールの module_param() に値を渡すことができます。Documentation/kernel-parameters.txt を参考にして下さい。
debugfslinux/debugfs.hドキュメントは Documentation/filesystems/debugfs.txtThe debugfs filesystem です。ディストリビューター、SoC メーカー、ボードメーカーから提供された環境によっては debugfs が /sys/kernel/debug にマウントされていないことがあります。debugfs を使うには Documentation/filesystems/debugfs.txt を参考にマウントする必要があります。
名前の通りデバッグ目的のファイルシステムです。アプリケーション向けの API として常用したい場合は、sysfs か procfs へ移行するかデバイスアクセス用のノードを作成することを進めます。

モジュールロード、アンロード、シンボル解決

恐らくデバイスドライバ自身がここにある API を使うことは稀だと思います。insmod, rmmod, modprobe をしたときに問題が起きた場合、何が起きているのか追跡するために参照することが多いでしょう。シンボル解決機能は興味深い機能の一つでしょう。

機能単位主なヘッダやソース備考
Module insmod rmmodkernel/module.cSystem Call として関数は実装されています。kernel 内部から呼び出すには symbol を リンクできるように修正する必要が有ります。
Symbol lookuplinux/kallsyms.h
linux/license.h
linux/module.h
シンボルのアドレスを取得する symbol_get()symbol_put() は linux/module.h に有ります。シンボルの解決を使うと static link された kernel から動的にロードしたモジュールの関数を呼び出すことができるようになります。ただし、GPL の抜け穴になるような作り込みは慎みましょう。

基本的なドライバ

/dev/null, /dev/zero, /dev/full はハードウエアとして存在するデバイスのドライバではありません。しかし、参考になります。機能が単純なため struct file_operations のメソッド(関数テーブル)に登録する read, write といったデバイスドライバの基本的な機能実装をどのようにすれば良いか見通し良く理解できる実装になっています。

機能単位主なヘッダやソース備考
Null, Zero, Full, Mem driversdrivers/char/mem.c/dev/null, /dev/zero, /dev/full, /dev/mem の実装です。単純なキャラクタ型デバイスを実装したドライバです。単純ながら興味深い実装になっています。

基本的なファイルシステムノード

select(poll), ioctl を実装するのに参考になるソースを上げておきます。pipe, eventfd はデバイスから読み出すデータをキューイングする方法の参考にもなるでしょう。

機能単位主なヘッダやソース備考
pipefs/pipe.cpoll(select)の実装、ioctl FIONREAD の実装 は単純な挙動で特別なデバイスも無い環境でも試せるので参考になるでしょう。アプリケーション向けのドキュメントは man 7 pipe です。
eventfdfs/eventfd.ceventfd, signalfd, timerfd それぞれで system call を実装し、file descriptor を user space API として提供しています。独自の file descriptor を使用する API を必要としているならば読んでみるのも良いでしょう。アプリケーション向けのドキュメントは man eventfd です。
signalfdfs/signalfd.cアプリケーション向けのドキュメントは man signalfd です。
timerfdfs/timerfd.cアプリケーション向けのドキュメントは man timerfd です。

デバイス登録

Linux ではデバイスとドライバをそれぞれ管理しています。システムに存在するデバイスの管理と、システムに存在するドライバの管理をしています。登録されたデバイスとドライバは drivers/base を核として対応付けされます。起動処理時の特殊な状況が終わった後の対応付けは device_attach(), bus_for_each_drv()driver_attach(), bus_for_each_dev() で行われています。対応付けの処理によりデバイスとドライバのどちらが先に存在しても(後から付け足されても)デバイスに対して適合するドライバを見つけ probe 処理が始まります。

組み込み開発で良く扱うバスとそこに接続するデバイスの登録処理を挙げます。platform(SoC チップ内蔵機能、基板上でメモリ、IO 空間に固定的に割り付けられた周辺回路), I2C, SPI バス接続デバイスは固定された構成としてデバイスを登録する必要があります。PCI, USB, MMC(SD card slot) の様に動的な接続・切断に合わせてデバイスを登録・削除する処理が既に実装されている場合もあります。

機能単位主なヘッダやソース備考
platformlinux/platform_device.hplatform_device_register(), platform_device_unregister() にてプラットホーム・デバイスを登録、削除します。プラットホームデバイスは SoC に内蔵された IP ブロックが主に対象となります。ただし、suspend、resume、接続、切断 動作に関して何らかの上流バスの支配あるいは管轄下にある場合は、そのバスのデバイスとして登録します。
I2Clinux/i2c.hI2C 関連の基礎的な用語 Algorithm, Adapter, Driver, Client については Documentation/i2c/summary にて解説されています。全般的なドキュメントは Documentation/i2c/ 以下のファイルと API の解説 Documentation/DocBook/device-drivers/i2c.html を参照してください。
多くの場合 i2c_register_board_info() を使ってプラットホームの初期化処理にてデバイスを登録します。動的な登録と削除は i2c_new_device(), i2c_unregister_device() で行います。
PCIlinux/pci.hPCI 接続のデバイス登録は SoC メーカーあるいは開発環境を提供したメーカーがすでに初期化処理の中で行うよう実装しているはずです。pci_common_init(), pcibios_scan_root(), pci_bus_add_devices() 辺りからコードを追跡して確認してください。それでも意図して登録する場合は pci_bus_add_device(), リファレンスカウンタを減らす pci_dev_put() の用例を参考にしてください。
SDIOlinux/mmc/host.hSDIO デバイスは自動でデバイスが登録されます。登録の流れは mmc_detect_change(), (delayed work を挟み) mmc_rescan(), mmc_rescan_try_freq(), mmc_attach_sdio(), mmc_attach_sd(), mmc_attach_mmc(), mmc_add_card() の様になります。
SPIlinux/spi/spi.hドキュメントは Serial Peripheral Interface です。spi_register_board_info() を使ってプラットホームの初期化処理にてデバイスを登録します。動的な登録は spi_alloc_device(), spi_add_device() で行います。spi_alloc_device(), spi_add_device() の流れで登録した SPI デバイスはリファレンスカウンタを減らす spi_dev_put() で削除します。
USBlinux/usb.h
linux/usb/hcd.h
接続処理(デバイス登録)は自動で行われます。usb_new_device(), hub_port_connect(), usb_hub_create_port_device() とその呼び出し元のコードを探索してみてください。

ドライバ登録

ドライバ登録はデバイス登録と対になります。デバイス登録のところに書いたようにどちらを先に登録してもデバイスとドライバは対応付けされます。

機能単位主なヘッダやソース備考
platformlinux/platform_device.hドキュメントは Device Drivers Baseです。ドライバの登録は platform_driver_register(), module_platform_driver() で行います。ドライバ削除は platform_driver_register() の対になる platform_driver_unregister() で行います。module_platform_driver() はマクロの中で登録と削除が対になっています。
ただし、ドライバ削除は稀です。プラットホームに初めから備わっているデバイスが切断されるというのは考えられていない場合が多いと思います。正しく機能するかどうか十分にテストされていない可能性も考えてください。プラットホームに備わった機能を使わずドライバが不要な場合は、機能を停止する初期化が必要か検討してください。回路ブロックに供給するクロックを停止し、電源を切ることで消費電力を低減することができます。
I2Clinux/i2c.hドキュメントは Documentation/i2c/writing-clients です。ドライバ同録は i2c_add_driver(), module_i2c_driver() です。ドライバ削除は i2c_add_driver() の対として i2c_del_driver() があります。module_i2c_driver() はマクロの中に登録と削除が組み込まれています。
PCIlinux/pci.hドキュメントは PCI Support Library, How To Write Linux PCI Drivers です。この中ではドライバ登録関数として __pci_register_driver() を紹介しています。この館数は接頭辞に __ が付いています。PCI バス操作を主たる目的とするライブラリの中から使うことを前提とした API です。なるべくなら pci_register_driver() を使うことをお勧めします。もう一つのお手軽登録関数は module_pci_driver() です。ドライバ削除は pci_register_driver() の対になる pci_unregister_driver() です。module_pci_driver() はマクロの中で登録処理と削除処理を対にして組み込んでいます。
SDIOlinux/mmc/mmc.h
linux/mmc/core.h
linux/mmc/card.h
linux/mmc/sdio.h
linux/mmc/sdio_ids.h
linux/mmc/sdio_func.h
ドライバ登録は sdio_register_driver() で行います。ドライバ削除は sdio_unregister_driver() です。左の列に挙げたヘッダファイルは一部だけ使用する場合もあります。実装する機能と照らし合わせ include/linux/mmc の下のヘッダファイル群を取り込んで下さい。
SPIlinux/spi/spi.hドキュメントは Serial Peripheral Interface です。ドライバ登録は spi_register_driver(), module_spi_driver() で行います。ドライバ削除は spi_register_driver() の対として spi_unregister_driver() があります。module_spi_driver() はマクロの中で登録処理と削除処理を対にして組み込んでいます。
USBlinux/usb.hドキュメントは Writing USB Device Driversです。ドライバ登録は usb_register(), module_usb_driver() で行います。ドライバ削除は module_usb_driver() の対として usb_deregister() で行います。module_usb_driver() はマクロの中で登録処理と削除処理を対にして組み込んでいます。

クラスデバイス登録

クラスデバイスは種類別に分類されたデバイスです。種類は発揮できる機能により決まります。たとえば LED (linux/leds.h) は「発光」する種類になります。前出の「デバイス登録」でいうデバイスは回路的な機能ブロックやバスに接続されたチップを意味しています。どんな機能かという情報は含まれていません。

ドライバにより機能ブロックやチップが確かに存在し、少なくとも初期化に成功する程度に動作すると確認できたならば、デバイスを操作するためのノードを作ります。ノードは独自に作る方法と、クラスデバイスとして(分類可能であれば)登録しクラスデバイスのコアモジュールにノードを作らせる方法があります。コアモジュールは汎用的な API を提供しているでしょう。

汎用的な API は複雑に見えるかもしれません。その複雑さを追うと、kernel 内での連携、アプリケーションに対する便利なアクセス方法、汎用 API を使った実用的なアプリケーションも見つかるでしょう。多少の難しい学習をすれば周辺の開発とデバッグが大幅に楽になることがあります。

新しく Devres - Managed Device Resource が導入されつつあります。devres はデバイスコンテキスト struct devicedevres_add()(内部関数は add_dr())で確保・占有するリソースを追加し、デバイス削除時に自動的に解放する機能です。devres 機能付きクラスデバイスは追加関数に接頭辞 devm_ が付きます。一覧は Devres - Managed Device Resource の後ろの方にあります。

機能単位主なヘッダやソース備考
Input, HIDlinux/input.h
linux/hid.h
マトリックスキーボード、電圧・電流レベル識別キーボード、シリアル接続のキーボードなどを使った構成の場合
ドキュメントは Documentation/DocBook/device-drivers/input_subsystem.html です。 input_allocate_device(), input_register_device() を使います。対の関数は input_unregister_device() または input_free_device() で、使い分けの詳細は関数のコメントにあるように input_register_device() を使った後ならば input_unregister_device() を使います。関数名はデバイス登録を思わせる名前です。実質はドライバ登録に当たります。同じドライバソースの中にキー入力を報告(伝達)する関数 input_report_key(), input_sync() が見つかると思います。代表的な PS/2 インターフェースのドライバは drivers/input/keyboard/atkbd.c です。
HID 系の(Report Descriptor, (押されているキーの一覧を通知する)Report 形式の)入力デバイスの場合
HID のドキュメントは Documentation/hid 以下にあります。全体的な説明は Documentation/hid/hid-transport.txt を参照してください。別ページに読解を進めたときの記録 HID class 調査 を書きました。デバイスの登録、ドライバの登録、デバイスとドライバの照合と突き合わせは巧妙な構造になっています。
デバイスの登録は hid_allocate_device(), hid_add_device()、対の削除は hid_destroy_device()) です。
HID Class ドライバの登録と削除は hid_register_driver(), module_hid_driver(), hid_unregister_driver() です。
LEDlinux/leds.h汎用的な LED Class Device の場合
デバイスの登録は led_classdev_register()、削除は led_classdev_unregister() です。LED Class Device として登録すると LED 制御用の API が sysfs(/sys/class/leds) にノードとして作られます。CONFIG_LEDS_TRIGGERS を .config に定義しておき、struct led_classdev.default_trigger に LED トリガ名を設定しておくと、kernel 内の様々な事象に対して LED を点灯する API led_trigger_register(), led_trigger_register_simple(), led_trigger_event(), led_trigger_unregister(), led_trigger_unregister_simple() と対応づけられます。
GPIO 接続に特化した LED Class Device の場合
GPIO 接続 LED class device を参照して下さい。
USB (汎用)linux/usb.hドキュメントは Documentation/DocBook/usb/index.html の "5. USB Core APIs" です。 デバイスの登録は usb_register_dev()、対になる削除は usb_deregister_dev() です。登録と削除関数に渡す struct usb_class_driverstruct file_operations 構造体を指すメンバ fops があります。一式の read(), write(), ioctl(), poll() API で十分に実装できるデバイスに適しています。
misclinux/miscdevice.hドキュメントは Documentation/DocBook/kernel-api/miscdev.html です。デバイスの登録は misc_register()、対になる削除は misc_deregister() です。登録と削除関数に渡す struct miscdevicestruct file_operations 構造体を指すメンバ fops があります。「その他何でも」的なキャラクタデバイスのクラスです。ノードの Major 番号は MISC_MAJOR です。Minor 番号の一部は既に特定機能のデバイスに割り振られています。linux/miscdevice.h にある *_MINOR を参照して下さい。 一式の read(), write(), ioctl(), poll() API で十分に実装できるデバイスに適しています。Minor 番号を動的に振る場合は MISC_DYNAMIC_MINORstruct miscdevice の minor メンバに設定して下さい。

Class device は他にもたくさんあります

Class device は他にも沢山の種類が存在します。cd /sys/class;ls と入力してみて出できたノード一覧がほぼ class device の一覧になります。おおよそ機能の予測がつく名前になっています。自分が作ろうとするドライバが機能として似ているならば、class device (class driver) として振る舞うように(正直に言えば、コード借りて実装負担が減るように)した方が良いでしょう。ソースコードツリーの root または drivers ディレクトリをカレントディレクトリにして、出てきたノード名を grep -r '"class-device-node-name"' * の様にして探し、実装場所を見つけてみて下さい。


添付ファイル: fileLinux_bigmap_for_driver_developer.png 787件 [詳細] fileLKDQPicture.xlsx 182件 [詳細]

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