hana_shinのLinux技術ブログ

Linuxの技術情報を掲載しています。特にネットワークをメインに掲載していきます。

カーネルモジュールの作り方

1 はじめに

様々な種類のカーネルモジュールを作って、その実行結果を確認してみます。なお、サンプルプログラムを見やすくするため、エラー処理は意図的に省略しています。

2 検証環境

CentOS版数は以下のとおりです。

[root@server ~]# cat /etc/redhat-release
CentOS Linux release 7.9.2009 (Core)

カーネル版数は以下のとおりです。

[root@server ~]# uname -r
3.10.0-1160.el7.x86_64

3 Makefile

全てのサンプルプログラム共通のMakefileです。なお、$(MAKE)とrmから行頭までの間は、空白ではなくTABです。

[root@server modules]# cat Makefile
obj-m := test.o
KDIR    := /lib/modules/$(shell uname -r)/build
VERBOSE = 0

all:
        $(MAKE) -C $(KDIR) M=$(PWD) KBUILD_VERBOSE=$(VERBOSE) CONFIG_DEBUG_INFO=y modules
clean:
        rm -f *.o *.ko *.mod.c Module.symvers modules.order

4 Hellow World

Hellow Worldをログに出力するサンプルプログラムを作成します。

[root@server modules]# cat test.c
#include <linux/module.h>

static int __init test_init(void)
{
  printk(KERN_INFO "Hello World\n");
  return 0;
}

static void __exit test_exit(void)
{
  printk(KERN_INFO "Bye!\n");
}

module_init(test_init);
module_exit(test_exit);

MODULE_AUTHOR("hana_shin");
MODULE_LICENSE("GPL v2");

makeコマンドを実行します。

[root@server modules]# make
make -C /lib/modules/3.10.0-1160.el7.x86_64/build M=/root/modules KBUILD_VERBOSE=0 CONFIG_DEBUG_INFO=y modules
make[1]: ディレクトリ `/usr/src/kernels/3.10.0-1160.el7.x86_64' に入ります
  CC [M]  /root/modules/test.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /root/modules/test.mod.o
  LD [M]  /root/modules/test.ko
make[1]: ディレクトリ `/usr/src/kernels/3.10.0-1160.el7.x86_64' から出ます

makeコマンドを実行すると、以下のファイルが作成されます。

[root@server modules]# ls -l
合計 220
-rw-r--r--. 1 root root    225  2月 16 17:00 Makefile
-rw-r--r--. 1 root root      0  2月 16 17:00 Module.symvers
-rw-r--r--. 1 root root     29  2月 16 17:00 modules.order
-rw-r--r--. 1 root root    283  2月 16 16:55 test.c
-rw-r--r--. 1 root root 101864  2月 16 17:00 test.ko
-rw-r--r--. 1 root root    856  2月 16 17:00 test.mod.c
-rw-r--r--. 1 root root  58792  2月 16 17:00 test.mod.o
-rw-r--r--. 1 root root  44616  2月 16 17:00 test.o

tailコマンドを実行して、カーネルモジュールが出力するログを確認します。

[root@server ~]# tail -f /var/log/messages

作成したカーネルモジュールをロードしてみます。

[root@server modules]# insmod test.ko

lsmodコマンドを実行します。作成したカーネルモジュールがロードされていることがわかります。

[root@server modules]# lsmod |grep test
test                   12424  0

ログを確認すると、 Hello Worldが出力されていることがわかります。

[root@server ~]# tail -f /var/log/messages
Feb 16 17:05:38 server kernel: Hello World

テストが終了したので、カーネルモジュールをアンロードします。

[root@server modules]# rmmod test.ko

lsmodコマンドを実行します。作成したカーネルモジュールがアンロードできたことがわかります。

[root@server modules]# lsmod |grep test
[root@server modules]#

5 カーネルモジュールに引数を渡す方法

カーネルモジュールに引数を渡すことができます。ここでは、数値、文字列を引数にとるサンプルプログラムを作成します。

5.1 module_paramの書式

カーネルモジュールの引数は以下のように指定します。

  • 第1引数:変数名
  • 第2引数:型名(int,long,short,charp等がある。詳細はmanを参照)
  • 第3引数:読み込み専用属性

5.2 数値を渡す方法

サンプルプログラムは以下のとおりです。

[root@server modules]# cat test.c
#include <linux/module.h>

static int cnt = 1;
module_param(cnt, int, S_IRUGO);

static int __init test_init(void)
{
  printk(KERN_INFO "Hello World,cnt=%d\n",cnt);
  return 0;
}

static void __exit test_exit(void)
{
  printk(KERN_INFO "Bye!\n");
}

module_init(test_init);
module_exit(test_exit);

MODULE_AUTHOR("hana_shin");
MODULE_LICENSE("GPL v2");

作成したカーネルモジュールをロードしてみます。

[root@server modules]# insmod test.ko cnt=5

tailコマンドを実行して、カーネルモジュールが出力するログを確認します。カーネルモジュールのロード時の引数(5)がログに出力されていることがわかります。

[root@server ~]# tail -f /var/log/messages
Feb 16 17:22:08 server kernel: Hello World,cnt=5

テストが終了したので、カーネルモジュールをアンロードします。

[root@server modules]# rmmod test.ko

5.3 文字列を渡す方法

ここではカーネルモジュールへの引数として、文字列を2つ渡してみます。
サンプルプログラムは以下のとおりです。

[root@server modules]# cat test.c
#include <linux/module.h>

static char *str1, *str2;
module_param(str1, charp, S_IRUGO);
module_param(str2, charp, S_IRUGO);

static int __init test_init(void)
{
  printk(KERN_INFO "Hello World, Input String=%s:%s\n", str1, str2);
  return 0;
}

static void __exit test_exit(void)
{
  printk(KERN_INFO "Bye!\n");
}

module_init(test_init);
module_exit(test_exit);

MODULE_AUTHOR("hana_shin");
MODULE_LICENSE("GPL v2");

作成したカーネルモジュールをロードします。このとき、モジュールへの引数として、文字列を2つ(aaとbb)渡してみます。

[root@server modules]# insmod test.ko str1=aa str2=bb

tailコマンドを実行して、カーネルモジュールが出力するログを確認します。カーネルモジュールのロード時に指定した文字列がログに出力されていることがわかります。

[root@server ~]# tail -f /var/log/messages
Feb 16 17:29:49 server kernel: Hello World, Input String=aa:bb

テストが終了したので、カーネルモジュールをアンロードします。

[root@server modules]# rmmod test.ko

6 割り込み

割り込み(IRQ=17)が発生すると、割り込みが発生した回数をログに出力するサンプルプログラムを作成してみます。

[root@server modules]# cat test.c
#include <linux/module.h>
#include <linux/interrupt.h>

#define TEST_IRQ    17
static int irq_counter=0;
static int test_dev_id;

static irqreturn_t test_interrupt(int irq, void *dev_id)
{
  printk(KERN_INFO "Interrupt Counter=%d\n",++irq_counter);
  return IRQ_HANDLED;
}

static int __init test_init(void)
{
  if(request_irq(TEST_IRQ, test_interrupt, IRQF_SHARED, "test_interrupt", &test_dev_id))
    return -1;
  printk(KERN_INFO "Loading Interrupt Handler\n");
  return 0;
}

static void __exit test_exit(void)
{
  synchronize_irq(TEST_IRQ);
  free_irq(TEST_IRQ, &test_dev_id);
  printk(KERN_INFO "Unloading Interrupt handler\n");
}

module_init(test_init);
module_exit(test_exit);

MODULE_AUTHOR("hana_shin");
MODULE_LICENSE("GPL v2");

/proc/interruptsを確認すると、CPU3に対してIRQ17が12812回発生していることがわかります。

[root@server ~]# cat /proc/interrupts |grep "17:"
            CPU0       CPU1       CPU2       CPU3
  17:      10149          0          0      12812   IO-APIC-fasteoi   ehci_hcd:usb1, ioc0

作成したカーネルモジュールをロードします。

[root@server modules]# insmod test.ko

IRQ17の割り込みが2回発生したことが分かります。

[root@server ~]# tail -f /var/log/messages
Feb 16 18:46:42 server kernel: Interrupt Counter=1
Feb 16 18:46:42 server kernel: Interrupt Counter=2

/proc/interruptsを確認すると、CPU3への割り込み回数が2回増えていることがわかります。

[root@server ~]# cat /proc/interrupts |grep "17:"
            CPU0       CPU1       CPU2       CPU3
  17:      10149          0          0      12814   IO-APIC-fasteoi   ehci_hcd:usb1, ioc0, test_interrupt

テストが終了したので、カーネルモジュールをアンロードします。

[root@server modules]# rmmod test.ko

7 スラブオブジェクト

スラブとは特定の大きさのメモリが集まった領域のことです。たとえば、128バイトのスラブとは、128バイトの大きさのメモリが集まった領域のことです。スラブの利用者は、サイズを指定して、スラブの中からメモリを獲得します。スラブの中の個々のメモリ領域はスラブオブジェクトと呼びます。

128byte slab                               64byte slab
+-----------+                              +-----------+
| 128(byte) | <-- slabオブジェクト         |  64(byte) | <-- slabオブジェクト
+-----------+                              +-----------+
|     :     |                              |     :     |
+-----------+                              +-----------+
| 128(byte) | <-- slabオブジェクト         |  64(byte) | <-- slabオブジェクト
+-----------+                              +-----------+

128バイトのスラブオブジェクトを獲得するサンプルプログラムを作成します。

[root@server modules]# cat test.c
#include <linux/module.h>
#include <linux/slab.h>

#define OBJECT_NUM_MAX  100
#define OBJECT_SIZE     128

static int num = 1;
static void *test_ptr[OBJECT_NUM_MAX];
module_param(num, int, S_IRUGO);

static int __init test_init(void)
{
  int i;

  printk(KERN_INFO "Hello!,The number of slab object is %d\n", num);

  for(i=0; i<num; i++) {
    test_ptr[i] = kmalloc(OBJECT_SIZE, GFP_ATOMIC | __GFP_ZERO);
    printk(KERN_INFO "start=0x%p, end=0x%p\n",test_ptr[i], test_ptr[i] + OBJECT_SIZE);
  }
  return 0;
}

static void __exit test_exit(void)
{
  int i;

  printk(KERN_INFO "Bye!\n");

  for(i=0; i<num; i++) {
    kfree(test_ptr[i]);
  }
}

module_init(test_init);
module_exit(test_exit);

MODULE_AUTHOR("hana_shin");
MODULE_LICENSE("GPL v2");

作成したカーネルモジュールをロードします。128バイトのスラブオブジェクトを3個獲得します。

[root@server modules]# insmod test.ko num=3

128バイトのスラブオブジェクトが3個獲得されていることがわかります。

Feb 16 19:04:24 server kernel: Hello!,The number of slab object is 3
Feb 16 19:04:24 server kernel: start=0xffff974e224cee00, end=0xffff974e224cee80
Feb 16 19:04:24 server kernel: start=0xffff974e224ce080, end=0xffff974e224ce100
Feb 16 19:04:24 server kernel: start=0xffff974e224cf480, end=0xffff974e224cf500

テストが終了したので、カーネルモジュールをアンロードします。

[root@server modules]# rmmod test.ko

8 カーネルスレッド

ユーザ空間のプロセスはシステムコールを実行すると、ユーザモードからカーネルモードに移行します。そして、システムコールの実行が完了すると、カーネルモードからユーザモードに復帰します。しかし、カーネルスレッドは、カーネルモードのみで動作するプロセスです。

ここでは、カーネルスレッドを生成するサンプルプログラムを作成します。

[root@server modules]# cat test.c
#include <linux/module.h>
#include <linux/kthread.h>

static char *name;
struct task_struct *t;
module_param(name, charp, S_IRUGO);

static int test_func(void *arg)
{
  printk(KERN_INFO "%s kthread started.PID is %d\n", t->comm, t->pid);

  while(!kthread_should_stop())
    schedule();
  return 0;
}

static int __init test_init(void)
{
  t = kthread_run(test_func, NULL, name);
  return 0;
}

static void __exit test_exit(void)
{
  kthread_stop(t);
  printk(KERN_INFO "%s kthread ended.\n", t->comm);
}

module_init(test_init);
module_exit(test_exit);

MODULE_AUTHOR("hana_shin");
MODULE_LICENSE("GPL v2");

作成したカーネルモジュールをロードします。カーネルモジュールをロードすると、test1という名前のカーネルスレッドを生成します。

[root@server modules]# insmod test.ko name=test1

ログを確認すると、PID=2380のカーネルスレッドが生成されたことがわかります。

[root@server ~]# tail -f /var/log/messages
Feb 16 19:17:43 server kernel: test1 kthread started.PID is 2380

psコマンドを実行します。PID=2380のプロセスはtest1であることがわかります。

[root@server ~]# ps -C test1 -o comm,pid,ppid,%cpu
COMMAND            PID   PPID %CPU
test1             2380      2  100

また、次のようにpsコマンドを実行すると、test1の名前の前後が[ ]で囲まれてます。カーネルスレッドの場合、プロセス名の前後が[ ]で囲まれます。

[root@server ~]# ps aux|grep test1
root       2380  100  0.0      0     0 ?        R    19:17  12:43 [test1]
root       2562  0.0  0.0 112824   972 pts/1    S+   19:30   0:00 grep --color=auto test1

テストが終了したので、カーネルモジュールをアンロードします。

[root@server modules]# rmmod test.ko

9 スピンロック

スピンロックとは、スレッドがロックを獲得できるまで単純にビジーループする方式です。ここでは、カーネルモジュールをロードするとき、スピンロックを獲得し、カーネルモジュールをアンロードするときスピンロックを解放するサンプルプログラムを作成します。

[root@server modules]# cat test.c
#include <linux/module.h>

spinlock_t test_lock;

static int __init test_init(void)
{
  spin_lock_init(&test_lock);
  printk(KERN_INFO "spin lock initialization\n");

  spin_lock(&test_lock);
  printk(KERN_INFO "lock spin lock\n");
  return 0;
}

static void __exit test_exit(void)
{
  spin_unlock(&test_lock);
  printk(KERN_INFO "unlock spin lock\n");
}

module_init(test_init);
module_exit(test_exit);

MODULE_AUTHOR("hana_shin");
MODULE_LICENSE("GPL v2");

作成したカーネルモジュールをロードします。

[root@server modules]# insmod test.ko

カーネルモジュールをロードすると、ログに”lock spin lock”というメッセージが出力されることがわかります。

[root@server ~]# tail -f /var/log/messages
Feb 16 19:56:08 server kernel: lock spin lock

作成したカーネルモジュールをアンロードします。

[root@server modules]# rmmod test.ko

カーネルモジュールをアンロードすると、ログに”unlock spin lock”というメッセージが出力されることがわかります。

[root@server ~]# tail -f /var/log/messages
Feb 16 19:56:14 server kernel: unlock spin lock

10 タイマ

定期的にタイマを起動するサンプルプログラムを作成します。

[root@server modules]# cat test.c
#include <linux/module.h>

static int sec = 1;
module_param(sec, int, S_IRUGO);
static struct timer_list test_timer;

static void test_func(int sec)
{
  printk(KERN_INFO "Timer expired\n");
  mod_timer(&test_timer, jiffies + sec * HZ);
}

static int __init test_init(void)
{
  init_timer(&test_timer);

  test_timer.data = sec;
  test_timer.function = (void *)&test_func;
  test_timer.expires = jiffies + sec * HZ;
  add_timer(&test_timer);

  printk(KERN_INFO "Hello. Time interval is %d second\n", sec);
  return 0;
}

static void __exit test_exit(void)
{
  printk(KERN_INFO "Bye\n");
  del_timer(&test_timer);
}

module_init(test_init);
module_exit(test_exit);

MODULE_AUTHOR("hana_shin");
MODULE_LICENSE("GPL v2");

カーネルモジュールをロードします。このとき、カーネルモジュールの引数にタイマの起動間隔(2秒)を指定します。

[root@server modules]# insmod test.ko sec=2

ログを確認すると、2秒おきにタイマが起動されていることがわかります。

[root@server ~]# tail -f /var/log/messages
Feb 16 20:01:53 server kernel: Hello. Time interval is 2 second
Feb 16 20:01:55 server kernel: Timer expired
Feb 16 20:01:57 server kernel: Timer expired
Feb 16 20:01:59 server kernel: Timer expired

テストが終了したので、カーネルモジュールをアンロードします。

[root@server modules]# rmmod test.ko

11 sysfs

sysfsとは、ユーザープロセスから、デバイスやドライバーなどに関するカーネル情報にアクセスするためのインターフェイスです。/sys/kernel配下にtest/statディレクトリを作成し、そのディレクトにjiffiesとtimeという名前のファイルを作成するサンプルプログラムを作成します。

[root@server modules]# cat test.c
#include <linux/module.h>

static struct kobject *test_kobj;

static ssize_t test1_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
  return sprintf(buf, "%lu\n", get_seconds());
}

static ssize_t test2_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
  return sprintf(buf, "%lu\n", jiffies);
}

static struct kobj_attribute test1_attr = __ATTR(time, 0444, test1_show, NULL);
static struct kobj_attribute test2_attr = __ATTR(jiffies, 0444, test2_show, NULL);

static struct attribute *test_attrs[] = {
  &test1_attr.attr,
  &test2_attr.attr,
  NULL,
};

static struct attribute_group test_attr_group = {
  .name = "stat",
  .attrs = test_attrs,
};

static int test_init(void)
{
  int ret;

  printk(KERN_INFO "Hello\n");

  test_kobj = kobject_create_and_add("test", kernel_kobj);
  ret = sysfs_create_group(test_kobj, &test_attr_group);
  return ret;
}

static void test_exit(void)
{
  kobject_put(test_kobj);
  printk(KERN_INFO "Bye!\n");
}

module_init(test_init);
module_exit(test_exit);

MODULE_AUTHOR("hana_shin");
MODULE_LICENSE("GPL v2");

作成したカーネルモジュールをロードします。

[root@server modules]# insmod test.ko

ディレクトリを/sys/kernel/test/statに移動します。

[root@server modules]# cd /sys/kernel/test/stat/
[root@server stat]# pwd
/sys/kernel/test/stat

/sys/kernel/test/stat配下に作成したファイルを確認します。

[root@server stat]# ls -l
合計 0
-r--r--r--. 1 root root 4096  2月 16 20:29 jiffies
-r--r--r--. 1 root root 4096  2月 16 20:29 time

jiffiesファイルを読み出します。jiffiesはカーネル起動時からの時刻です。1ミリ秒単位で+1増加します。

[root@server stat]# cat jiffies
4299535090

timeファイルを読み出します。

[root@server stat]# cat time
1645011290

12 デバイスの登録、削除

/dev配下に"test_device"という名前でのデバイスを作成するサンプルプログラムを作成します。

[root@server modules]# cat test.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>

static struct file_operations test_dev_fops = {
    .owner = THIS_MODULE,
};

static struct miscdevice test_dev = {
    .name = "test_device",
    .minor = MISC_DYNAMIC_MINOR,
    .fops = &test_dev_fops,
};

static __init int test_init(void)
{
    printk(KERN_INFO "Device registered!\n");
    misc_register(&test_dev);
    return 0;
}

static __exit void test_exit(void)
{
    printk(KERN_INFO "Device unegistered!\n");
    misc_deregister(&test_dev);
}

module_init(test_init);
module_exit(test_exit);

MODULE_AUTHOR("hana_shin");
MODULE_LICENSE("GPL v2");

カーネルモジュールをロードします。

[root@server modules]# insmod test.ko

カーネルモジュールをロードすると、/dev配下に"test_device"という名前のデバイスが作成されます。

[root@server modules]# ls -l /dev/test_device
crw-------. 1 root root 10, 55  2月 16 20:42 /dev/test_device

カーネルモジュールをアンロードします。

[root@server modules]# rmmod test.ko

カーネルモジュールをアンロードすると、/dev配下に"test_device"という名前のデバイスが削除されたことがわかります。

[root@server modules]# ls -l /dev/test_device
ls: /dev/test_device にアクセスできません: そのようなファイルやディレクトリはありません

Z 参考情報

私が業務や記事執筆で参考にした書籍を以下のページに記載します。
Linux技術のスキルアップをしよう! - hana_shinのLinux技術ブログ