00. 目次
記事ディレクトリ
01. 概要
Linux システムでは、デバイス ドライバーはカーネル モジュールの形式で表示されます。ドライバー開発の前提条件として、Linux カーネル モジュールのプログラミングを学習する必要があります。
Linux は、多くのデバイスをサポートするクロスプラットフォーム オペレーティング システムであり、Linux カーネル ソース コードのコードの 50% 以上がデバイス ドライバーに関連しています。Linux はマクロカーネルアーキテクチャを採用しているため、すべての機能を有効にするとカーネルが非常に肥大化します。カーネル モジュールは、特定の機能を実装するカーネル コードの一部であり、カーネルの実行プロセス中にコードのこの部分をカーネルにロードすることで、カーネルの機能を動的に向上させることができます。この特徴を利用して、デバイスドライバーを開発する際には、カーネル全体をコンパイルすることなく、該当するドライバーコードのみをコンパイルするだけで、カーネルモジュールの形でデバイスドライバーを記述します。
カーネル モジュールの導入は、システムの柔軟性を向上させるだけでなく、開発者に大きな利便性をもたらします。デバイスドライバーの開発プロセスでは、テスト対象のドライバーをカーネルに自由に追加または削除でき、カーネルモジュールのコードを変更するたびにカーネルを再起動する必要がありません。開発ボードでは、不必要な記憶領域の占有を避けるために、カーネル モジュール プログラムやデバイス ドライバーの ELF ファイルを開発ボードに保存する必要はありません。カーネルモジュールをロードする必要がある場合、NFSサーバーをマウントすることで、他のデバイスに格納されているカーネルモジュールを開発ボードにロードできます。特定の状況においては、現在の環境をより適切に提供するために、必要に応じてシステムのカーネル モジュールをロード/アンロードできます。
02. 最初のカーネルモジュール
test.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk(KERN_EMERG "Hello Module Init\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_EMERG "Hello Module Exit\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("uplooking");
MODULE_DESCRIPTION("hello module");
MODULE_ALIAS("test_module");
03. コードフレームワーク分析
Linux カーネル モジュールのコード フレームワークは通常、次の部分で構成されます。
- モジュールロード関数 (必須): insmod または modprobe コマンドを通じてカーネルモジュールがロードされると、モジュールのロード関数がカーネルによって自動的に実行され、このモジュールに関連する初期化作業が完了します。
- モジュールのアンインストール機能 (必須): rmmod コマンドを実行してモジュールをアンインストールすると、モジュールのアンインストール機能がカーネルによって自動的に実行され、関連するクリーンアップ作業が完了します。
- モジュール ライセンス ステートメント (必須):ライセンス ステートメントには、カーネル モジュールの権限が記述されています。モジュールが宣言されていない場合は、モジュールのロード時にカーネルが汚染されているという警告が表示されます。
- モジュールパラメータ:モジュールパラメータは、モジュールがロードされるときにモジュールに渡すことができるパラメータです。
- モジュールはシンボルをエクスポートします:モジュールは、他のカーネル モジュールから呼び出せるように、準備された変数または関数をシンボルとしてエクスポートできます。
- モジュールに関するその他の関連情報:モジュールの作成者およびその他の情報を宣言できます。
上記の例の hello モジュール プログラムには、上記の 3 つの必要な部分とモジュールのその他の情報の宣言のみが含まれています。
ヘッダー ファイルには <linux/init.h> と <linux/module.h> が含まれており、カーネル モジュールを作成する際にはこれら 2 つのヘッダー ファイルを含める必要があります。モジュール初期化関数 hello_init は printk 関数を呼び出しますが、カーネルモジュールの実行プロセス中は C ライブラリ関数に依存できないため、printf 関数は使用できず、別途印刷出力関数 printk を使用する必要があります。この関数の使用法は printf 関数と似ています。モジュールの初期化関数が完了したら、マクロ module_init を呼び出して、初期化に hello_init 関数を使用するようにカーネルに指示する必要があります。モジュール アンインストール関数は、printk 関数を使用して文字列を出力し、マクロ module_exit を使用してモジュールのアンインストール関数をカーネルに登録します。最後に、モジュールがライセンスに基づいて使用されることを宣言する必要があります。ここでは GPL2 に設定します。
04. ヘッダーファイル
私たちは以前に Linux アプリケーション プログラミングを経験しており、Linux ヘッダー ファイルが /usr/include に保存されていることを学びました。カーネル モジュールの作成に必要なヘッダー ファイルは、上記のディレクトリではなく、Linux カーネル ソース コードの include フォルダーにあります。
- #include <linux/module.h>:カーネルのロード module_init()/アンロード module_exit() 関数とカーネル モジュール情報関連関数の宣言が含まれます。
- #include <linux/init.h>:一部のカーネル モジュール関連セクションのマクロ定義が含まれています
- #include <linux/kernel.h>: printk など、カーネルによって提供されるさまざまな関数が含まれています。
ヘッダーファイルのパス
deng@local:~/a72/x3399/kernel/include/linux$ pwd
/home/deng/a72/x3399/kernel/include/linux
deng@local:~/a72/x3399/kernel/include/linux$
次の 2 つのヘッダー ファイルは、カーネル モジュールを作成するときによく使用されます: <linux/init.h> と <linux/module.h>。ヘッダー ファイルの前にフォルダー名 linux があることもわかりますが、これは include の下の linux フォルダーに対応します。このフォルダーに移動して、これら 2 つのヘッダー ファイルの内容を確認します。
init.h ヘッダー ファイル (カーネル ソース コード/include/linux/init.h にあります)
/* These macros are used to mark some functions or
* initialized data (doesn't apply to uninitialized data)
* as `initialization' functions. The kernel can take this
* as hint that the function is used only during the initialization
* phase and free up used memory resources after
*
* Usage:
* For functions:
*
* You should add __init immediately before the function name, like:
*
* static void __init initme(int x, int y)
* {
* extern int z; z = x * y;
* }
*
* If the function has a prototype somewhere, you can also add
* __init between closing brace of the prototype and semicolon:
*
* extern int initialize_foobar_device(int, int, int) __init;
*
* For initialized data:
* You should insert __initdata or __initconst between the variable name
* and equal sign followed by value, e.g.:
*
* static int init_variable __initdata = 0;
* static const char linux_logo[] __initconst = { 0x32, 0x36, ... };
*
* Don't forget to initialize data not at file scope, i.e. within a function,
* as gcc otherwise puts the data into the bss section and not into the init
* section.
*/
/* These are for everybody (although not all archs will actually
discard it in modules) */
#define __init __section(.init.text) __cold notrace __noretpoline
#define __initdata __section(.init.data)
#define __initconst __constsection(.init.rodata)
#define __exitdata __section(.exit.data)
#define __exit_call __used __section(.exitcall.exit)
#define __exit __section(.exit.text) __exitused __cold notrace
init.h ヘッダー ファイルには主にカーネル モジュールで使用されるいくつかのマクロ定義が含まれているため、カーネル モジュールのプログラミングに関与する限り、このヘッダー ファイルを追加する必要があります。
module.h ヘッダー ファイル (カーネル ソース コード/include/linux/module.h にあります)
/**
* module_init() - driver initialization entry point
* @x: function to be run at kernel boot time or module insertion
*
* module_init() will either be called during do_initcalls() (if
* builtin) or at module insertion time (if a module). There can only
* be one per module.
*/
#define module_init(x) __initcall(x);
/**
* module_exit() - driver exit entry point
* @x: function to be run when driver is removed
*
* module_exit() will wrap the driver clean-up code
* with cleanup_module() when used with rmmod when
* the driver is a module. If the driver is statically
* compiled into the kernel, module_exit() has no effect.
* There can only be one per module.
*/
#define module_exit(x) __exitcall(x);
/* Generic info of form tag = "info" */
#define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info)
#define MODULE_ALIAS(_alias) MODULE_INFO(alias, _alias)
#define MODULE_LICENSE(_license) MODULE_INFO(license, _license)
#define MODULE_AUTHOR(_author) MODULE_INFO(author, _author)
上記のコードには、カーネル モジュールのロードおよびアンロード関数の宣言が含まれており、module.h ファイル内のいくつかのマクロ定義もリストされています。これらのマクロ定義の一部は必須ではありませんが、MODULE_LICENSE はカーネル モジュールを指定します。ライセンスは必須です。 。
注:このチュートリアルで使用されているカーネルの 4.x バージョンでは、module_init
およびmodule_exit
関数がファイル内で宣言されていますが/include/linux/module.h
、古いバージョンのカーネルでは、これら 2 つの関数が/include/linux/init.h
ファイル内で宣言されています。
05. モジュールロード・アンロード機能
5.1 module_init 関数
/**
* module_init() - driver initialization entry point
* @x: function to be run at kernel boot time or module insertion
*
* module_init() will either be called during do_initcalls() (if
* builtin) or at module insertion time (if a module). There can only
* be one per module.
*/
#define module_init(x) __initcall(x);
戻り値:
- 0:モジュールが正常に初期化され、モジュール名を持つ新しいディレクトリが /sys/module の下に作成されることを示します。
- 0 以外:モジュールの初期化が失敗したことを示します
マクロ定義 module_init は、モジュールの初期化時に初期化にどの関数を使用するかをカーネルに通知するために使用されます。対応するセクションに関数アドレスが追加されるため、起動時にモジュールが自動的にロードされるようになります。
[root@rk3399:/]# cd /sys/module/
[root@rk3399:/sys/module]# ls
8250 firmware_class pstore sr_mod
8250_core ftdi_sio ramoops stmmac
asix fuse rcupdate sunrpc
auth_rpcgss hci_vhci rcutree suspend
bfusb hid rfcomm sysrq
block hidp rfkill tc35874x
bluetooth i2c_algo_bit rk817_battery tcp_cubic
brd i2c_hid rk817_charger tpm
btbcm input_polldev rk_vcodec tpm_i2c_infineon
btintel ipv6 rndis_wlan uinput
btmrvl kernel rng_core usb_storage
btmrvl_sdio keyboard rockchip_pwm_remotectl usbcore
btrtl libertas_tf rtc_pcf8563 usbhid
btusb lkdtm sbs_battery usbtouchscreen
cdc_ncm lockd scsi_mod uvcvideo
cec loop sdhci v4l2_mem2mem
cfg80211 mac80211 sierra video_rkisp1
configfs mali snd videobuf2_core
cpuidle midgard_kbase snd_pcm videobuf2_dma_sg
devres mmcblk snd_seq videobuf_core
dns_resolver module snd_seq_dummy vt
drm mpp_dev_common snd_seq_midi workqueue
drm_kms_helper nfs snd_soc_rk817 xhci_hcd
dynamic_debug ntfs snd_timer xz_dec
ehci_hcd nvme snd_usb_audio
elan_i2c pcie_aspm spidev
fiq_debugger printk spurious
[root@rk3399:/sys/module]#
参考例
static int __init func_init(void)
{
}
module_init(func_init);
func_init 関数は、カーネル モジュールで初期化関連の作業も実行します。
C 言語における static キーワードの機能は次のとおりです。
- static によって変更された静的ローカル変数は、プログラムの実行が終了するまで解放されず、ローカル変数のライフサイクルが延長されます。
- 静的に変更されたグローバル変数には、このファイル内でのみアクセスでき、他のファイルからはアクセスできません。
- static によって変更された関数は、このファイル内でのみ呼び出すことができ、他のファイルから呼び出すことはできません。
カーネル モジュールのコードは実際にはカーネル コードの一部です。カーネル モジュールで定義された関数がカーネル ソース コード内の関数で繰り返される場合、コンパイラはエラーを報告し、コンパイルが失敗します。そのため、次のコードを追加します。 static 修飾子を使用すると、このエラーを回避できます。
__init、__initdata マクロ定義 (カーネル ソース コード/include/linux/init.h にあります)
#define __init __section(.init.text) __cold __latent_entropy __noinitretpoline
#define __initdata __section(.init.data)
上記のコードの __init および __initdata マクロ定義 (カーネル ソース コード/linux/init.h にあります) では、__init は関数の変更に使用され、__initdata は変数の変更に使用されます。__init 修飾子を使用すると、関数が実行可能ファイルの __init セクションに配置されることを意味します。このセクションの内容は、モジュールの初期化フェーズでのみ使用できます。初期化フェーズが完了すると、この部分の内容はリリースされます。
5.2 module_exit
カーネル モジュール アンロード関数 func_exit は、カーネル ロード関数とは対照的に、主に初期化プロセスの逆のプロセスである初期化フェーズで割り当てられたメモリ、割り当てられたデバイス番号などを解放するために使用されます。
/**
* module_exit() - driver exit entry point
* @x: function to be run when driver is removed
*
* module_exit() will wrap the driver clean-up code
* with cleanup_module() when used with rmmod when
* the driver is a module. If the driver is statically
* compiled into the kernel, module_exit() has no effect.
* There can only be one per module.
*/
#define module_exit(x) __exitcall(x);
__exit、__exitdata マクロ定義 (カーネル ソース コード/include/linux/init.h にあります)
#define __exit __section(.exit.text) __exitused __cold notrace
#define __exitdata __section(.exit.data)
モジュール読み込み関数と同様に、__exit は関数を変更するために使用され、__exitdata は変数を変更するために使用されます。マクロ定義 module_exit は、モジュールがアンロードされるときにどの関数を呼び出す必要があるかをカーネルに伝えるために使用されます。
参考例
static void __exit func_exit(void)
{
}
module_exit(func_exit);
func_init 関数との違いは、この関数の戻り値が void 型であることと、修飾子が異なることです。ここで使用される __exit は、関数が実行ファイルの __exit セクションに配置されることを意味します。完了すると、この領域のスペースは自動的に解放されます。
06. モジュール情報
テーブルカーネルモジュール情報宣言関数
関数 | 効果 |
---|---|
MODULE_LICENSE() | モジュール コードによって受け入れられたソフトウェア ライセンス契約を示します。Linux カーネルは、GPL V2 オープン ソース契約に従います。カーネル モジュールは、Linux カーネルとの一貫性のみを必要とします。 |
MODULE_AUTHOR() | モジュールを説明する作成者情報 |
MODULE_DESCRIPTION() | モジュールの簡単な紹介 |
MODULE_ALIAS() | モジュールのエイリアスを設定する |
6.1 ライセンス
Linux は GPL ライセンスを採用した無料のオペレーティング システムであり、ユーザーはソース コードを自由に変更できます。GPL契約の主な内容は、特定のGPL契約製品が提供するライブラリをソフトウェア製品で使用し、新しい製品を派生させる場合でも、そのソフトウェア製品はGPL契約を採用しなければならない、つまりオープンソースでなければならないというものです。 GPL 契約が伝染することがわかります。そのため、Linuxではさまざまなフリーソフトウェアを使用することができます。今後 Linux を学習する過程で、インストールするソフトウェアには 30 日間の試用期間がなかったり、アクティベーション コードが必要だったりすることに気づくかもしれません。
/*
* The following license idents are currently accepted as indicating free
* software modules
*
* "GPL" [GNU Public License v2 or later]
* "GPL v2" [GNU Public License v2]
* "GPL and additional rights" [GNU Public License v2 rights and more]
* "Dual BSD/GPL" [GNU Public License v2
* or BSD license choice]
* "Dual MIT/GPL" [GNU Public License v2
* or MIT license choice]
* "Dual MPL/GPL" [GNU Public License v2
* or Mozilla license choice]
*
* The following other idents are available
*
* "Proprietary" [Non free products]
*
* There are dual licensed components, but when running with Linux it is the
* GPL that is relevant so this is a non issue. Similarly LGPL linked with GPL
* is a GPL combined work.
*
* This exists for several reasons
* 1. So modinfo can show license info for users wanting to vet their setup
* is free
* 2. So the community can ignore bug reports including proprietary modules
* 3. So vendors can do likewise based on their own policies
*/
#define MODULE_LICENSE(_license) MODULE_INFO(license, _license)
カーネル モジュール ライセンスには、「GPL」、「GPL v2」、「GPL および追加の権利」、「Dual SD/GPL」、「Dual MPL/GPL」、「Proprietary」などが含まれます。
6.2 著者情報
カーネル モジュール作成者マクロ定義 (カーネル ソース コード/include/linux/module.h にあります)
/*
* Author(s), use "Name <email>" or just "Name", for multiple
* authors use multiple MODULE_AUTHOR() statements/lines.
*/
#define MODULE_AUTHOR(_author) MODULE_INFO(author, _author)
先ほどmodinfoで出力したモジュール情報の「作成者」情報はマクロ定義MODULE_AUTHORからのものです。このマクロ定義は、モジュールの作成者を宣言するために使用されます。
6.3 モジュールの説明情報
モジュールの説明情報 (カーネル ソース コード/include/linux/module.h にあります)
/* What your module does. */
#define MODULE_DESCRIPTION(_description) MODULE_INFO(description, _description)
モジュール情報の「説明」情報は、モジュールの機能を説明するために使用されるマクロ MODULE_DESCRIPTION から取得されます。
6.4 モジュールのエイリアス
カーネル モジュール エイリアス マクロ定義 (カーネル ソース コード/inlcude/linux/module.h にあります)
/* For userspace: you can also call me... */
#define MODULE_ALIAS(_alias) MODULE_INFO(alias, _alias)
モジュール情報の「エイリアス」情報は、マクロ定義 MODULE_ALIAS から取得されます。このマクロ定義は、カーネル モジュールのエイリアスを作成するために使用されます。モジュールのエイリアスを使用する場合、モジュールを /lib/modules/kernel source/ にコピーし、コマンド depmod を使用してモジュールの依存関係を更新する必要があることに注意してください。そうでない場合、Linux カーネルは、このモジュールが別の名前を持つことをどのようにして認識するのでしょうか。
07. 出力機能
printk関数
/**
* printk - print a kernel message
* @fmt: format string
*
* This is printk(). It can be called from any context. We want it to work.
*
* We try to grab the console_lock. If we succeed, it's easy - we log the
* output and call the console drivers. If we fail to get the semaphore, we
* place the output into the log buffer and return. The current holder of
* the console_sem will notice the new output in console_unlock(); and will
* send it to the consoles before releasing the lock.
*
* One effect of this deferred printing is that code which calls printk() and
* then changes console_loglevel may break. This is because console_loglevel
* is inspected when the actual printing occurs.
*
* See also:
* printf(3)
*
* See the vsnprintf() documentation for format string extensions over C99.
*/
asmlinkage __visible int printk(const char *fmt, ...)
- printf: glibc によって実装された印刷関数。ユーザー空間で動作します。
- printk: カーネル モジュールは glibc ライブラリ関数を使用できません。これはカーネル自体によって実装される printf に似た関数ですが、印刷レベルを指定する必要があります。
- #define KERN_EMERG "<0>" は通常、システムがクラッシュする前の情報です
- #define KERN_ALERT "<1>" すぐに処理する必要があるメッセージ
- #define KERN_CRIT "<2>" 重大な状態
- #define KERN_ERR "<3>" エラー条件
- #define KERN_WARNING 「<4>」 問題のある状況
- #define KERN_NOTICE「<5>」メモ
- #define KERN_INFO “<6>” 通常メッセージ
- #define KERN_DEBUG “<7>” デバッグ情報
現在のシステムの printk 印刷レベルを表示します。cat /proc/sys/kernel/printk
左から右に、現在のコンソール ログ レベル、デフォルトのメッセージ ログ レベル、最小コンソール レベル、およびデフォルトのコンソール ログ レベルに対応します。
[root@rk3399:/sys/module]# cat /proc/sys/kernel/printk
7 4 1 7
すべてのカーネル印刷情報を出力します: dmesg. カーネル ログ バッファ サイズには制限があり、バッファ データは上書きされる可能性があることに注意してください。
08. メイクファイル
カーネル モジュールの場合、これはカーネルに属するコードの一部ですが、カーネル ソース コードには含まれません。このため、コンパイル時にはカーネルソースディレクトリでコンパイルする必要があります。カーネル モジュールのコンパイルに使用される Makefile は、前に C コードのコンパイルに使用した Makefile とほぼ同じです。これは、Linux カーネルのコンパイルに使用される Kbuild システムによるものです。したがって、カーネル モジュールをコンパイルするときは、次のことも必要です。環境変数 ARCH および CROSS_COMPILE の値を指定します。
KERNEL_DIR=/home/deng/a72/x3399/kernel
CROSS_COMPILE=aarch64-linux-gnu-gcc
obj-m := test.o
all:
$(MAKE) -C $(KERNEL_DIR) M=`pwd` modules
.PHONE:clean
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean
上記のコードは、カーネル モジュールをコンパイルするための Makefile を提供します。
- 行 1: この Makefile は、カーネル ソース コードのディレクトリを保存する変数 KERNEL_DIR を定義します。
- 行 2: ツールチェーンの指定
- 行 3: 変数 obj-m は、モジュールにコンパイルする必要があるターゲット ファイルの名前を保持します。
- 4 行目: '$(MAKE)modules' は、実際には、Linux のトップレベル Makefile を実行する疑似ターゲット モジュールです。オプション「-C」を使用すると、make ツールがソース ディレクトリにジャンプして最上位の Makefile を読み取ることができます。「M=$(CURDIR)」は、現在のディレクトリに戻り、現在のディレクトリ内の Makefile を読み取って実行し、カーネル モジュールのコンパイルを開始することを示します。pwd は現在のディレクトリに設定されます。
コンパイルコマンドの説明
次のコマンドを入力してドライバー モジュールをコンパイルします。
deng@local:~/code/test/1module$ make
make -C /home/deng/a72/x3399/kernel M=`pwd` modules
make[1]: 进入目录“/home/deng/a72/x3399/kernel”
CC [M] /home/deng/a72/code/1module/test.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/deng/a72/code/1module/test.mod.o
LD [M] /home/deng/a72/code/1module/test.ko
make[1]: 离开目录“/home/deng/a72/x3399/kernel”
deng@local:~/code/test/1module$
コンパイルが成功すると、「test.ko」という名前のドライバー モジュール ファイルがディレクトリに生成されます。
09. カーネルモジュールのロードとアンロード
カーネルモジュールをロードする
[root@rk3399:/mnt/a72/code/1module]# insmod test.ko
[16099.893741] Hello Module Init
カーネルモジュールをアンインストールする
[root@rk3399:/mnt/a72/code/1module]# rmmod test
[16103.132570] Hello Module Exit
[root@rk3399:/mnt/a72/code/1module]#