Linux Kernelの簡単なCharacter Deviceを作成する方法(Linked List APIの使用方法サンプル)

前書き

本記事では、Linux KernelにおけるCharacter Device向けのDevice Driverを作成する方法を示します。専用のHardware(例:シリアルデバイスのUARTなど)を用いず、メモリ上のデータ読み書きのみを行います。そのため、擬似デバイス(/dev/nullや/dev/zeroなど)を操作するDriverと同等です。

Character Deviceは、少量のデータを管理する低速のデバイスを指し、該当するデバイスはキーボード、マウス、シリアルポート等です。これらのデバイスを読み書き(Read/Write)操作する際、バイト単位で順次操作します。

本記事で作成するDevice Driverでは、Open/Close、Read/Writeのシステムコールのみを実装します(仕様概要は下表)。

システムコール 仕様説明
Write

ユーザが文字列を書き込む度に(Writeシステムコールを発行する度に)、Listに要素を追加します。増やした要素に、ユーザが書き込んだ文字列を保持します。

Read ユーザが書き込んだ文字列を管理するListを操作し、ユーザが指定したByte数だけ文字列をユーザに返します。
Open ユーザが書き込む文字列を管理するためのListを初期化します。
Close Read/Writeシステムコールで使用したListや文字列保持用のメモリを解放します。

                  

検証環境

検証は、Debian10(buster)環境で実施しました。他のディストリビューションでもDevice Driverが作成可能ですが、Debian系(Ubuntu、Kaliなど)を使用した方が作業手順に差異が少ないと思われます。

                                          

前準備

Linux Kernel用のDevice Driverを作成するには、環境構築が必要になります。環境構築およびKernelモジュールの雛形を作成するまでの手順は、以下の記事に記載してあります。本記事の手順を実施する前に、確認して下さい。

また、本記事で使用するコードは、GitHubに格納してあります。

環境構築: Linux Kernelモジュールの作成準備

                             

Device DriverのLoad処理の作成

Device DriverのLoad時(手動の場合はinsmodコマンド実行時)は、以下の内容を実施します。詳細な説明は、コードの後に記載しています。

  • デバイスのメジャー番号(デバイスの種類を表す番号)を動的取得
  • /sys/class/以下にデバイスクラスを登録
  • Character Deviceを操作するためのシステムコールを登録
  • Character DeviceをKernelに登録
  • /dev以下にデバイスノード(今回は/dev/debimate)を追加

Load用の関数(debimate_init())全体で注意すべき点は、各登録処理(class_create())の途中で失敗した場合、それまでに成功していた登録処理を解除する事です。そのため、各処理の異常系では、登録解除処理を行う位置までgoto文を用いてジャンプします。

異常系処理で用いられるIS_ERR()やPTR_ERR()は、NULLポインタのエラー原因を特定するためのKernel APIです。このKernel特有のエラーハンドリングに関しては、以下の記事にまとめてあります。

Linux Kernel: NULLポインタエラーハンドリング(ERR_PTR, IS_ERR, PTR_ERR)

                    

debimate_init()の最初で実行しているalloc_chrdev_region()では、メジャー番号を動的に取得しています。引数のMINOR_NR_BASE、MAX_MINOR_NRは、マイナー番号用の設定です。メジャー番号はデバイスの種類を表し、マイナー番号は同じ種類のデバイス(複数個)を識別するため値です。

今回の例では、debimate(デバイス)用のメジャー番号を動的に取得し、マイナー番号は0〜1まで割り当てるという設定をしています。メジャー番号とデバイスの対応は、以下のように/proc/devicesで確認できます。

次のclass_create()は、/sys/class/以下にデバイスを登録します。/sys/class以下には、クラスで分類されたデバイスの親子関係をディレクトリ階層(サブディレクトリ)で表します。今回の例では、/sys以下に関する設定をしないため、「登録しただけ」という形になります。生成されるディレクトリは、以下の通りです。

cdev_init()では、Charcter Deviceを操作するために、file_operations構造体に各システムコール(ReadやWriteなど)用の関数ポインタを登録します。関数ポインタを登録していないシステムコールは、NULL扱いなため、Kernel内部で使用されません。今回の例では、以下の関数をセットします。メンバ変数owner(module構造体)は、どのようなDevice DriverであってもTHIS_MODULEを指定します。コンパイル時に、メンバ変数ownerに値が自動的にセットされます。

cdev_add()ではKernelにCharacter Deviceを登録し、device_create()では/dev以下にデバイスファイル(デバイスノード)を生成します。ここまでが、Load処理となります。

                   

Device DriverのUnload処理の作成

Unload処理は、Load時に実行した登録処理と逆の順番で、各登録の解除処理を実施します。ここでの各登録の解除処理は、Load時の異常系(goto文を用いた解除処理)に似た流れで実施します。

  • /dev以下からデバイスノード(今回は/dev/debimate)を削除
  • Character DeviceをKernelから削除
  • /sys/class/以下からデバイスクラスを削除
  • Character Deviceが使用していたメジャー番号の登録を解除

                                                                 

Device DriverのOpen処理の作成

Open処理では、同じデバイスファイルを複数プロセスから同時に開かれた場合に備えて、関数内でメモリを確保します。つまり、グローバル変数を用いて、複数プロセス用に使い回す事はしません。グローバル変数を共有する場合は、データの競合が発生しないように注意が必要です。

関数内で確保したメモリを他関数(ReadやWrite)で使用するために、Open関数の引数として渡されるfile構造体を用います。file構造体のメンバ変数private_data(void型ポインタ)に、Device Driver内で使用するメモリを渡しておけば、ReadやWriteでもprivate_dataを使用できます。

今回のOpen処理では、独自に定義したstr_list構造体のメモリ確保、初期化、private_dataへの登録を行います。エラーが発生するのは、メモリ確保に失敗した場合のみです。

(Linked) List操作の方法(API)に関しては、別記事でまとめています。これから説明するWrite、Read、Closeでは、List操作を知らないと理解できない内容のため、自身がない方は確認して下さい。

Linux Kernel: List構造を操作するためのAPI(Listの使い方)

                   

                   

Device DriverのWrite処理の作成

Write処理では、以下の処理を行います。

  • ユーザが書き込んだ文字列を管理するList用メモリを確保
  • ユーザが書き込んだ文字列をコピーするためのメモリを確保
  • ユーザが書き込んだ文字列(User空間メモリ)をKernel空間メモリにコピー
  • Open関数で作成したリストの末尾に、本関数内で作成したListを挿入

Write関数の引数は順番に、Open関数で作成したListにアクセスするためのfile構造体、User空間からデータを受け取るための変数buf、ユーザが書き込んだデータサイズを表すcount、書き込み位置オフセットの変数f_posです。

Write処理で用いている関数の中で、Kernel特有の関数はstrncpy_from_user()です。User空間とKernel空間では、使用するメモリ領域が違います。strncpy_from_user()は、その事を意識して、文字列をコピーしてくれます。

                          

Device DriverのRead処理の作成

Read処理では、ユーザが読み込みたいByte数をListから取り出し、User空間に文字列をコピーします。

Read関数の引数は、Write関数とほぼ同じです。Open関数で作成したListにアクセスするためのfile構造体、User空間にデータを渡すための変数buf、ユーザが読み込みたいデータサイズを表すcount、読み込み位置オフセットの変数f_posです。

今回の仕様では、Listの各要素が何Byteの文字列を保持しているか分かりません。そのため、Listの先頭から順番に文字列のサイズを確認します。その後、ユーザが読み込みたいByte数の分だけ、Listを探索し、Listの要素(文字列)を変数strにコピーします。最後に、Kernel空間からUser空間にメモリをコピーするためのcopy_to_user()を使用します。なお、copy_to_user()の返り値は、読み込んだByte数ではなく、読み込めなかったByte数のため、注意が必要です。

                            

Device DriverのClose処理の作成

Close処理では、「文字列格納用メモリ(Listの要素)の解放」、「Open関数で作成したListから順番に全てのListを削除」、「List用メモリの解放」を行います。

list_for_each_entry_safe(Listの先頭から順にListを辿るためのAPI)を使用すれば、iterator自身のメモリを開放する事ができるため、以下のように

  1. 文字列用バッファの解放
  2. Listから要素を削除
  3. List用メモリを解放

を順番に行う事ができます。なお、list_for_each_entry(別API)では、iteratorのメモリが開放できません。

                        

Device Driverのテストプログラム

今回作成したDevice Driverのテストプログラムとして、ユーザ空間から”/dev/debimate” にアクセスし、”abcde”および”fghij”を書き込みます。その後、10Byteのデータを”/dev/debimate” から読み出し、表示します。

テストプログラム(test.cおよびMakefile)は、以下の通りです。

以下に、実行例を示します。

                                    

おすすめ